schema_align_test.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. package fixture
  2. import (
  3. "database/sql"
  4. "os"
  5. "path/filepath"
  6. "strings"
  7. "testing"
  8. )
  9. // 本文件单测结构对齐工具的**纯函数部分**:ALGORITHM 子句、DDL 解析、MODIFY 语句构造。
  10. // 真库比对与执行由 cmd/ac15fixture align 完成(需连库,不进单测)。
  11. func TestAlgorithmSuffix(t *testing.T) {
  12. cases := map[string]string{
  13. "instant": ", ALGORITHM=INSTANT",
  14. "INSTANT": ", ALGORITHM=INSTANT",
  15. "inplace": ", ALGORITHM=INPLACE, LOCK=NONE",
  16. "copy": ", ALGORITHM=COPY",
  17. "none": "",
  18. "": "",
  19. "garbage": "",
  20. }
  21. for in, want := range cases {
  22. if got := algorithmSuffix(in); got != want {
  23. t.Errorf("algorithmSuffix(%q) = %q, want %q", in, got, want)
  24. }
  25. }
  26. }
  27. // TestBuildModifySQLKeepsOtherAttributes 是本工具最关键的约束:
  28. // MODIFY 只许改 DEFAULT(与 ON UPDATE),type / NULL 性 / 注释必须**照抄实库**。
  29. func TestBuildModifySQLKeepsOtherAttributes(t *testing.T) {
  30. live := liveColumn{
  31. Table: "t_x", Column: "created_at", ColumnType: "datetime(3)",
  32. Nullable: true, Comment: "创建时间",
  33. }
  34. ddl := ddlColumn{Table: "t_x", Column: "created_at", NotNull: true, HasDefault: true}
  35. // 默认(不修 NULL 性):NULL 性必须保持实库的"可空"
  36. got := buildModifySQL(live, ddl, false, "none")
  37. want := "ALTER TABLE `t_x` MODIFY COLUMN `created_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间'"
  38. if got != want {
  39. t.Errorf("不修 NULL 性时语句不符:\n got=%q\nwant=%q", got, want)
  40. }
  41. // 修 NULL 性:只多一个 NOT NULL,其它不变
  42. got = buildModifySQL(live, ddl, true, "none")
  43. want = "ALTER TABLE `t_x` MODIFY COLUMN `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间'"
  44. if got != want {
  45. t.Errorf("修 NULL 性时语句不符:\n got=%q\nwant=%q", got, want)
  46. }
  47. // updated_at 必须带 ON UPDATE,且位于 DEFAULT 之后、COMMENT 之前
  48. live2 := liveColumn{Table: "t_x", Column: "updated_at", ColumnType: "datetime(3)"}
  49. ddl2 := ddlColumn{Table: "t_x", Column: "updated_at", NotNull: true, HasDefault: true, OnUpdate: true}
  50. got = buildModifySQL(live2, ddl2, false, "copy")
  51. want = "ALTER TABLE `t_x` MODIFY COLUMN `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), ALGORITHM=COPY"
  52. if got != want {
  53. t.Errorf("updated_at 语句不符:\n got=%q\nwant=%q", got, want)
  54. }
  55. }
  56. // TestBuildModifySQLEscapesComment 证明注释里的单引号会被转义(不破坏语句)。
  57. func TestBuildModifySQLEscapesComment(t *testing.T) {
  58. live := liveColumn{Table: "t", Column: "created_at", ColumnType: "datetime(3)", Comment: "it's fine"}
  59. ddl := ddlColumn{Table: "t", Column: "created_at", NotNull: true, HasDefault: true}
  60. got := buildModifySQL(live, ddl, false, "none")
  61. if !strings.Contains(got, "COMMENT 'it''s fine'") {
  62. t.Errorf("注释未正确转义:%q", got)
  63. }
  64. }
  65. // TestParseDDLFile 用一个小样本验证解析口径(NOT NULL / DEFAULT / ON UPDATE / COMMENT)。
  66. func TestParseDDLFile(t *testing.T) {
  67. src := `
  68. -- 注释行应被忽略
  69. SET NAMES utf8mb4;
  70. CREATE TABLE ` + "`t_demo`" + ` (
  71. ` + "`id`" + ` BIGINT NOT NULL AUTO_INCREMENT,
  72. ` + "`created_at`" + ` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
  73. ` + "`updated_at`" + ` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
  74. ` + "`update_time`" + ` DATETIME(3) DEFAULT NULL COMMENT '最后修改时间',
  75. PRIMARY KEY (` + "`id`" + `)
  76. ) ENGINE=InnoDB;
  77. `
  78. dir := t.TempDir()
  79. p := filepath.Join(dir, "demo.sql")
  80. if err := os.WriteFile(p, []byte(src), 0o600); err != nil {
  81. t.Fatal(err)
  82. }
  83. cols, err := ParseDDLFile(p, []string{"created_at", "updated_at", "update_time"})
  84. if err != nil {
  85. t.Fatal(err)
  86. }
  87. if len(cols) != 3 {
  88. t.Fatalf("应解析到 3 列,实际 %d:%+v", len(cols), cols)
  89. }
  90. byName := map[string]ddlColumn{}
  91. for _, c := range cols {
  92. byName[c.Column] = c
  93. }
  94. if c := byName["created_at"]; !c.NotNull || !c.HasDefault || c.OnUpdate {
  95. t.Errorf("created_at 解析不符:%+v", c)
  96. }
  97. if c := byName["updated_at"]; !c.OnUpdate || !c.NotNull {
  98. t.Errorf("updated_at 解析不符:%+v", c)
  99. }
  100. // update_time 的 DDL 是 DEFAULT NULL —— HasDefault 会被识别成 true,
  101. // 但 NotNull=false,所以分类时应落到 ddl-no-default 之外的分支由 NULL 性判断先拦住。
  102. if c := byName["update_time"]; c.NotNull {
  103. t.Errorf("update_time DDL 是可空,不该解析成 NOT NULL:%+v", c)
  104. }
  105. }
  106. // TestCreateTimeAssertionNeedsMarker 证明「没有运行标记时断言不放行」
  107. // (不允许"没标记就当通过"这种自欺)。
  108. func TestCreateTimeAssertionNeedsMarker(t *testing.T) {
  109. // 用一个必然查不到的 db 指针不行(会 panic),这里只验证函数签名与语义说明存在:
  110. // 真正的 marker 语义由 cmd/ac15fixture verify 的集成路径覆盖。
  111. var _ = sql.NullString{}
  112. if RunMarkerAction == "" {
  113. t.Error("RunMarkerAction 不应为空")
  114. }
  115. }