package fixture import ( "database/sql" "os" "path/filepath" "strings" "testing" ) // 本文件单测结构对齐工具的**纯函数部分**:ALGORITHM 子句、DDL 解析、MODIFY 语句构造。 // 真库比对与执行由 cmd/ac15fixture align 完成(需连库,不进单测)。 func TestAlgorithmSuffix(t *testing.T) { cases := map[string]string{ "instant": ", ALGORITHM=INSTANT", "INSTANT": ", ALGORITHM=INSTANT", "inplace": ", ALGORITHM=INPLACE, LOCK=NONE", "copy": ", ALGORITHM=COPY", "none": "", "": "", "garbage": "", } for in, want := range cases { if got := algorithmSuffix(in); got != want { t.Errorf("algorithmSuffix(%q) = %q, want %q", in, got, want) } } } // TestBuildModifySQLKeepsOtherAttributes 是本工具最关键的约束: // MODIFY 只许改 DEFAULT(与 ON UPDATE),type / NULL 性 / 注释必须**照抄实库**。 func TestBuildModifySQLKeepsOtherAttributes(t *testing.T) { live := liveColumn{ Table: "t_x", Column: "created_at", ColumnType: "datetime(3)", Nullable: true, Comment: "创建时间", } ddl := ddlColumn{Table: "t_x", Column: "created_at", NotNull: true, HasDefault: true} // 默认(不修 NULL 性):NULL 性必须保持实库的"可空" got := buildModifySQL(live, ddl, false, "none") want := "ALTER TABLE `t_x` MODIFY COLUMN `created_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间'" if got != want { t.Errorf("不修 NULL 性时语句不符:\n got=%q\nwant=%q", got, want) } // 修 NULL 性:只多一个 NOT NULL,其它不变 got = buildModifySQL(live, ddl, true, "none") want = "ALTER TABLE `t_x` MODIFY COLUMN `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间'" if got != want { t.Errorf("修 NULL 性时语句不符:\n got=%q\nwant=%q", got, want) } // updated_at 必须带 ON UPDATE,且位于 DEFAULT 之后、COMMENT 之前 live2 := liveColumn{Table: "t_x", Column: "updated_at", ColumnType: "datetime(3)"} ddl2 := ddlColumn{Table: "t_x", Column: "updated_at", NotNull: true, HasDefault: true, OnUpdate: true} got = buildModifySQL(live2, ddl2, false, "copy") want = "ALTER TABLE `t_x` MODIFY COLUMN `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), ALGORITHM=COPY" if got != want { t.Errorf("updated_at 语句不符:\n got=%q\nwant=%q", got, want) } } // TestBuildModifySQLEscapesComment 证明注释里的单引号会被转义(不破坏语句)。 func TestBuildModifySQLEscapesComment(t *testing.T) { live := liveColumn{Table: "t", Column: "created_at", ColumnType: "datetime(3)", Comment: "it's fine"} ddl := ddlColumn{Table: "t", Column: "created_at", NotNull: true, HasDefault: true} got := buildModifySQL(live, ddl, false, "none") if !strings.Contains(got, "COMMENT 'it''s fine'") { t.Errorf("注释未正确转义:%q", got) } } // TestParseDDLFile 用一个小样本验证解析口径(NOT NULL / DEFAULT / ON UPDATE / COMMENT)。 func TestParseDDLFile(t *testing.T) { src := ` -- 注释行应被忽略 SET NAMES utf8mb4; CREATE TABLE ` + "`t_demo`" + ` ( ` + "`id`" + ` BIGINT NOT NULL AUTO_INCREMENT, ` + "`created_at`" + ` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', ` + "`updated_at`" + ` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', ` + "`update_time`" + ` DATETIME(3) DEFAULT NULL COMMENT '最后修改时间', PRIMARY KEY (` + "`id`" + `) ) ENGINE=InnoDB; ` dir := t.TempDir() p := filepath.Join(dir, "demo.sql") if err := os.WriteFile(p, []byte(src), 0o600); err != nil { t.Fatal(err) } cols, err := ParseDDLFile(p, []string{"created_at", "updated_at", "update_time"}) if err != nil { t.Fatal(err) } if len(cols) != 3 { t.Fatalf("应解析到 3 列,实际 %d:%+v", len(cols), cols) } byName := map[string]ddlColumn{} for _, c := range cols { byName[c.Column] = c } if c := byName["created_at"]; !c.NotNull || !c.HasDefault || c.OnUpdate { t.Errorf("created_at 解析不符:%+v", c) } if c := byName["updated_at"]; !c.OnUpdate || !c.NotNull { t.Errorf("updated_at 解析不符:%+v", c) } // update_time 的 DDL 是 DEFAULT NULL —— HasDefault 会被识别成 true, // 但 NotNull=false,所以分类时应落到 ddl-no-default 之外的分支由 NULL 性判断先拦住。 if c := byName["update_time"]; c.NotNull { t.Errorf("update_time DDL 是可空,不该解析成 NOT NULL:%+v", c) } } // TestCreateTimeAssertionNeedsMarker 证明「没有运行标记时断言不放行」 // (不允许"没标记就当通过"这种自欺)。 func TestCreateTimeAssertionNeedsMarker(t *testing.T) { // 用一个必然查不到的 db 指针不行(会 panic),这里只验证函数签名与语义说明存在: // 真正的 marker 语义由 cmd/ac15fixture verify 的集成路径覆盖。 var _ = sql.NullString{} if RunMarkerAction == "" { t.Error("RunMarkerAction 不应为空") } }