選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

collation.go 6.8KB

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