ddl.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. package fixture
  2. import (
  3. "context"
  4. "database/sql"
  5. "fmt"
  6. "os"
  7. "regexp"
  8. "strings"
  9. )
  10. // 本文件把 02-data-model/core-model-ddl.sql 安全地套到目标库上,规则只有三条:
  11. //
  12. // 1. **只执行 CREATE TABLE**(外加 DDL 自带的 INSERT IGNORE 种子与两个 SET)。
  13. // `DROP TABLE IF EXISTS` 一律**丢弃**——DDL 文件是「干净起步」口径写的,
  14. // 而目标库已经有 28 张表(27 academic + t_campus),照抄会删库。
  15. // 2. `CREATE TABLE x` 一律改写成 `CREATE TABLE IF NOT EXISTS x`,
  16. // 已存在的表(如 t_campus)跳过而不报错中断。
  17. // 3. 绝不 ALTER / TRUNCATE / DROP 任何已有表;不碰 e2e_test。
  18. // defaultCoreDDLPath 是 core DDL 的默认位置(可用 AC15_CORE_DDL 覆盖)。
  19. const defaultCoreDDLPath = `C:\Users\Run\WorkBuddy\项目合并\unified\02-data-model\core-model-ddl.sql`
  20. // DDLAction 描述一条语句的实际处置结果。
  21. type DDLAction struct {
  22. Kind string // create / seed / set / skipped-drop
  23. Target string // 表名或说明
  24. Executed bool
  25. ExistedBefore bool
  26. Err string
  27. }
  28. // DDLReport 是一次补齐操作的结果。
  29. type DDLReport struct {
  30. Path string
  31. Actions []DDLAction
  32. TablesBefore int
  33. TablesAfter int
  34. Created []string
  35. Skipped []string
  36. }
  37. var (
  38. createTableRe = regexp.MustCompile("(?i)^CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?`?([A-Za-z0-9_]+)`?")
  39. insertIntoRe = regexp.MustCompile("(?i)^INSERT\\s+(?:IGNORE\\s+)?INTO\\s+`?([A-Za-z0-9_]+)`?")
  40. backtickAfterCreate = regexp.MustCompile("(?i)^CREATE\\s+TABLE\\s+`")
  41. )
  42. // SplitStatements 去掉纯注释行后按 `;` 切分成语句列表。
  43. //
  44. // DDL 文件里的注释全部是**整行注释**(行首 `--`),没有行尾注释,
  45. // 因此「先剔行再切分」是安全的;一旦将来出现行尾注释,这里会立刻在解析阶段暴露(语句变非法)而不是悄悄漏建表。
  46. func SplitStatements(src string) []string {
  47. var body strings.Builder
  48. for _, line := range strings.Split(src, "\n") {
  49. t := strings.TrimSpace(line)
  50. if t == "" || strings.HasPrefix(t, "--") {
  51. continue
  52. }
  53. body.WriteString(line)
  54. body.WriteString("\n")
  55. }
  56. var out []string
  57. for _, stmt := range strings.Split(body.String(), ";") {
  58. if s := strings.TrimSpace(stmt); s != "" {
  59. out = append(out, s)
  60. }
  61. }
  62. return out
  63. }
  64. // countTables 统计某个 schema 的表数。
  65. func countTables(ctx context.Context, db *sql.DB, schema string) (int, error) {
  66. var n int
  67. err := db.QueryRowContext(ctx,
  68. `SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = ?`, schema).Scan(&n)
  69. return n, err
  70. }
  71. // tableExists 判断表是否已存在。
  72. func tableExists(ctx context.Context, db *sql.DB, schema, table string) (bool, error) {
  73. var n int
  74. err := db.QueryRowContext(ctx,
  75. `SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = ? AND table_name = ?`,
  76. schema, table).Scan(&n)
  77. return n > 0, err
  78. }
  79. // ApplyCoreDDL 把 core DDL 补齐到目标库。
  80. //
  81. // ddlPath 为空时用 defaultCoreDDLPath(环境变量 AC15_CORE_DDL 优先)。
  82. func ApplyCoreDDL(ctx context.Context, cfg Config, ddlPath string) (*DDLReport, error) {
  83. if ddlPath == "" {
  84. ddlPath = os.Getenv("AC15_CORE_DDL")
  85. }
  86. if ddlPath == "" {
  87. ddlPath = defaultCoreDDLPath
  88. }
  89. raw, err := os.ReadFile(ddlPath)
  90. if err != nil {
  91. return nil, fmt.Errorf("读取 core DDL 失败(%s):%w", ddlPath, err)
  92. }
  93. if !strings.Contains(strings.ToUpper(string(raw)), "CREATE TABLE") {
  94. return nil, fmt.Errorf("core DDL 文件内容异常(无 CREATE TABLE):%s", ddlPath)
  95. }
  96. db, err := cfg.OpenTarget()
  97. if err != nil {
  98. return nil, err
  99. }
  100. defer db.Close()
  101. before, err := countTables(ctx, db, cfg.TargetDB)
  102. if err != nil {
  103. return nil, err
  104. }
  105. rep := &DDLReport{Path: ddlPath, TablesBefore: before}
  106. for _, stmt := range SplitStatements(string(raw)) {
  107. upper := strings.ToUpper(stmt)
  108. // 1) DROP TABLE —— 一律跳过(目标是「补齐」,不是「重建」)。
  109. if strings.HasPrefix(upper, "DROP TABLE") {
  110. rep.Actions = append(rep.Actions, DDLAction{Kind: "skipped-drop", Target: firstLine(stmt), Executed: false})
  111. continue
  112. }
  113. // 2) SET ... —— 会话级开关,照执行(不改变任何数据)。
  114. if strings.HasPrefix(upper, "SET ") {
  115. if _, err := db.ExecContext(ctx, stmt); err != nil {
  116. rep.Actions = append(rep.Actions, DDLAction{Kind: "set", Target: firstLine(stmt), Err: err.Error()})
  117. return rep, fmt.Errorf("执行 SET 失败:%w", err)
  118. }
  119. rep.Actions = append(rep.Actions, DDLAction{Kind: "set", Target: firstLine(stmt), Executed: true})
  120. continue
  121. }
  122. // 3) CREATE TABLE —— 改写成 IF NOT EXISTS,已存在则跳过。
  123. if m := createTableRe.FindStringSubmatch(stmt); m != nil {
  124. table := m[1]
  125. exists, err := tableExists(ctx, db, cfg.TargetDB, table)
  126. if err != nil {
  127. return rep, err
  128. }
  129. if exists {
  130. rep.Skipped = append(rep.Skipped, table)
  131. rep.Actions = append(rep.Actions, DDLAction{Kind: "create", Target: table, Executed: false, ExistedBefore: true})
  132. continue
  133. }
  134. fixed := stmt
  135. if backtickAfterCreate.MatchString(fixed) {
  136. idx := strings.Index(fixed, "`")
  137. fixed = fixed[:idx] + "IF NOT EXISTS " + fixed[idx:]
  138. }
  139. if _, err := db.ExecContext(ctx, fixed); err != nil {
  140. rep.Actions = append(rep.Actions, DDLAction{Kind: "create", Target: table, Err: err.Error()})
  141. return rep, fmt.Errorf("建表 %s 失败:%w", table, err)
  142. }
  143. rep.Created = append(rep.Created, table)
  144. rep.Actions = append(rep.Actions, DDLAction{Kind: "create", Target: table, Executed: true})
  145. continue
  146. }
  147. // 4) INSERT IGNORE 种子 —— 幂等(IGNORE + 主键冲突不动),照执行。
  148. if m := insertIntoRe.FindStringSubmatch(stmt); m != nil {
  149. if _, err := db.ExecContext(ctx, stmt); err != nil {
  150. rep.Actions = append(rep.Actions, DDLAction{Kind: "seed", Target: m[1], Err: err.Error()})
  151. return rep, fmt.Errorf("插入种子 %s 失败:%w", m[1], err)
  152. }
  153. rep.Actions = append(rep.Actions, DDLAction{Kind: "seed", Target: m[1], Executed: true})
  154. continue
  155. }
  156. // 5) 其它语句一律拒绝执行(白名单策略:没预期到的语句不跑)。
  157. rep.Actions = append(rep.Actions, DDLAction{Kind: "rejected", Target: firstLine(stmt), Executed: false,
  158. Err: "不在允许的语句类型(CREATE TABLE / INSERT IGNORE / SET)内"})
  159. return rep, fmt.Errorf("DDL 中出现未预期的语句,拒绝执行:%s", firstLine(stmt))
  160. }
  161. after, err := countTables(ctx, db, cfg.TargetDB)
  162. if err != nil {
  163. return rep, err
  164. }
  165. rep.TablesAfter = after
  166. return rep, nil
  167. }
  168. // firstLine 取语句首行(多为 60~80 字符),仅用于报告展示。
  169. func firstLine(s string) string {
  170. if i := strings.IndexByte(s, '\n'); i >= 0 {
  171. return strings.TrimSpace(s[:i])
  172. }
  173. return strings.TrimSpace(s)
  174. }