|
| 1 | +// Copyright 2023 The Gitea Authors. All rights reserved. |
| 2 | +// SPDX-License-Identifier: MIT |
| 3 | + |
| 4 | +package db |
| 5 | + |
| 6 | +import ( |
| 7 | + "errors" |
| 8 | + "fmt" |
| 9 | + "strings" |
| 10 | + |
| 11 | + "code.gitea.io/gitea/modules/container" |
| 12 | + "code.gitea.io/gitea/modules/log" |
| 13 | + "code.gitea.io/gitea/modules/setting" |
| 14 | + |
| 15 | + "xorm.io/xorm" |
| 16 | + "xorm.io/xorm/schemas" |
| 17 | +) |
| 18 | + |
| 19 | +type CheckCollationsResult struct { |
| 20 | + ExpectedCollation string |
| 21 | + AvailableCollation container.Set[string] |
| 22 | + DatabaseCollation string |
| 23 | + IsCollationCaseSensitive func(s string) bool |
| 24 | + CollationEquals func(a, b string) bool |
| 25 | + |
| 26 | + InconsistentCollationColumns []string |
| 27 | +} |
| 28 | + |
| 29 | +func findAvailableCollationsMySQL(x *xorm.Engine) (ret container.Set[string], err error) { |
| 30 | + var res []struct { |
| 31 | + Collation string |
| 32 | + } |
| 33 | + if err = x.SQL("SHOW COLLATION WHERE (Collation = 'utf8mb4_bin') OR (Collation LIKE '%\\_as\\_cs%')").Find(&res); err != nil { |
| 34 | + return nil, err |
| 35 | + } |
| 36 | + ret = make(container.Set[string], len(res)) |
| 37 | + for _, r := range res { |
| 38 | + ret.Add(r.Collation) |
| 39 | + } |
| 40 | + return ret, nil |
| 41 | +} |
| 42 | + |
| 43 | +func findAvailableCollationsMSSQL(x *xorm.Engine) (ret container.Set[string], err error) { |
| 44 | + var res []struct { |
| 45 | + Name string |
| 46 | + } |
| 47 | + if err = x.SQL("SELECT * FROM sys.fn_helpcollations() WHERE name LIKE '%\\_CS\\_AS%'").Find(&res); err != nil { |
| 48 | + return nil, err |
| 49 | + } |
| 50 | + ret = make(container.Set[string], len(res)) |
| 51 | + for _, r := range res { |
| 52 | + ret.Add(r.Name) |
| 53 | + } |
| 54 | + return ret, nil |
| 55 | +} |
| 56 | + |
| 57 | +func CheckCollations(x *xorm.Engine) (*CheckCollationsResult, error) { |
| 58 | + dbTables, err := x.DBMetas() |
| 59 | + if err != nil { |
| 60 | + return nil, err |
| 61 | + } |
| 62 | + |
| 63 | + res := &CheckCollationsResult{} |
| 64 | + res.CollationEquals = func(a, b string) bool { return a == b } |
| 65 | + |
| 66 | + var candidateCollations []string |
| 67 | + if x.Dialect().URI().DBType == schemas.MYSQL { |
| 68 | + if _, err = x.SQL("SELECT @@collation_database").Get(&res.DatabaseCollation); err != nil { |
| 69 | + return nil, err |
| 70 | + } |
| 71 | + res.IsCollationCaseSensitive = func(s string) bool { |
| 72 | + return s == "utf8mb4_bin" || strings.HasSuffix(s, "_as_cs") |
| 73 | + } |
| 74 | + candidateCollations = []string{"utf8mb4_0900_as_cs", "uca1400_as_cs", "utf8mb4_bin"} |
| 75 | + res.AvailableCollation, err = findAvailableCollationsMySQL(x) |
| 76 | + if err != nil { |
| 77 | + return nil, err |
| 78 | + } |
| 79 | + res.CollationEquals = func(a, b string) bool { |
| 80 | + // MariaDB adds the "utf8mb4_" prefix, eg: "utf8mb4_uca1400_as_cs", but not the name "uca1400_as_cs" in "SHOW COLLATION" |
| 81 | + // At the moment, it's safe to ignore the database difference, just trim the prefix and compare. It could be fixed easily if there is any problem in the future. |
| 82 | + return a == b || strings.TrimPrefix(a, "utf8mb4_") == strings.TrimPrefix(b, "utf8mb4_") |
| 83 | + } |
| 84 | + } else if x.Dialect().URI().DBType == schemas.MSSQL { |
| 85 | + if _, err = x.SQL("SELECT DATABASEPROPERTYEX(DB_NAME(), 'Collation')").Get(&res.DatabaseCollation); err != nil { |
| 86 | + return nil, err |
| 87 | + } |
| 88 | + res.IsCollationCaseSensitive = func(s string) bool { |
| 89 | + return strings.HasSuffix(s, "_CS_AS") |
| 90 | + } |
| 91 | + candidateCollations = []string{"Latin1_General_CS_AS"} |
| 92 | + res.AvailableCollation, err = findAvailableCollationsMSSQL(x) |
| 93 | + if err != nil { |
| 94 | + return nil, err |
| 95 | + } |
| 96 | + } else { |
| 97 | + return nil, nil |
| 98 | + } |
| 99 | + |
| 100 | + if res.DatabaseCollation == "" { |
| 101 | + return nil, errors.New("unable to get collation for current database") |
| 102 | + } |
| 103 | + |
| 104 | + res.ExpectedCollation = setting.Database.CharsetCollation |
| 105 | + if res.ExpectedCollation == "" { |
| 106 | + for _, collation := range candidateCollations { |
| 107 | + if res.AvailableCollation.Contains(collation) { |
| 108 | + res.ExpectedCollation = collation |
| 109 | + break |
| 110 | + } |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + if res.ExpectedCollation == "" { |
| 115 | + return nil, errors.New("unable to find a suitable collation for current database") |
| 116 | + } |
| 117 | + |
| 118 | + allColumnsMatchExpected := true |
| 119 | + for _, table := range dbTables { |
| 120 | + for _, col := range table.Columns() { |
| 121 | + if col.Collation != "" { |
| 122 | + allColumnsMatchExpected = allColumnsMatchExpected && res.CollationEquals(col.Collation, res.ExpectedCollation) |
| 123 | + if !res.IsCollationCaseSensitive(col.Collation) || !res.CollationEquals(col.Collation, res.DatabaseCollation) { |
| 124 | + res.InconsistentCollationColumns = append(res.InconsistentCollationColumns, fmt.Sprintf("%s.%s", table.Name, col.Name)) |
| 125 | + } |
| 126 | + } |
| 127 | + } |
| 128 | + } |
| 129 | + // if all columns match expected collation, then it could also be considered as "consistent" |
| 130 | + if allColumnsMatchExpected { |
| 131 | + res.InconsistentCollationColumns = nil |
| 132 | + } |
| 133 | + return res, nil |
| 134 | +} |
| 135 | + |
| 136 | +func CheckCollationsDefaultEngine() (*CheckCollationsResult, error) { |
| 137 | + return CheckCollations(x) |
| 138 | +} |
| 139 | + |
| 140 | +func alterDatabaseCollation(x *xorm.Engine, collation string) error { |
| 141 | + if x.Dialect().URI().DBType == schemas.MYSQL { |
| 142 | + _, err := x.Exec("ALTER DATABASE CHARACTER SET utf8mb4 COLLATE " + collation) |
| 143 | + return err |
| 144 | + } else if x.Dialect().URI().DBType == schemas.MSSQL { |
| 145 | + // MSSQL has many limitations on changing database collation, it could fail in many cases |
| 146 | + _, err := x.Exec("ALTER DATABASE CURRENT COLLATE " + collation) |
| 147 | + return err |
| 148 | + } |
| 149 | + return errors.New("unsupported database type") |
| 150 | +} |
| 151 | + |
| 152 | +func preprocessDatabaseCollation(x *xorm.Engine) { |
| 153 | + // check database & table collation, and alter the database collation if needed |
| 154 | + r, err := CheckCollations(x) |
| 155 | + if err != nil { |
| 156 | + log.Error("Failed to check database collation: %v") |
| 157 | + } |
| 158 | + if r == nil { |
| 159 | + return // no check result means the database doesn't need to do such check/process (at the moment ....) |
| 160 | + } |
| 161 | + |
| 162 | + // try to alter database collation to expected, it might fail in some cases (and it isn't necessary to succeed) |
| 163 | + if !r.CollationEquals(r.DatabaseCollation, r.ExpectedCollation) { |
| 164 | + if err = alterDatabaseCollation(x, r.ExpectedCollation); err != nil { |
| 165 | + log.Error("Failed to change database collation to %q: %v", r.ExpectedCollation, err) |
| 166 | + } else { |
| 167 | + if r, err = CheckCollations(x); err != nil { |
| 168 | + log.Fatal("Failed to check database collation again after altering: %v", err) // impossible case |
| 169 | + } |
| 170 | + log.Warn("Current database has been altered to use collation %q", r.DatabaseCollation) |
| 171 | + } |
| 172 | + } |
| 173 | + |
| 174 | + // check column collation, and show warning/error to end users -- no need to fatal and do not block the startup |
| 175 | + if !r.IsCollationCaseSensitive(r.DatabaseCollation) { |
| 176 | + log.Warn("Current database is using a case-insensitive collation %q, although Gitea could work with it, there might be some rare cases which don't work as expected.", r.DatabaseCollation) |
| 177 | + } |
| 178 | + |
| 179 | + if len(r.InconsistentCollationColumns) > 0 { |
| 180 | + log.Error("There are %d table columns have inconsistent collation, they should use %q. Please go to admin panel Self Check page or refer to Gitea document", len(r.InconsistentCollationColumns), r.DatabaseCollation) |
| 181 | + } |
| 182 | +} |
0 commit comments