| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- package vrtxsql
- // GORM 兼容回归测试 (报告 #1/#2/#3 同根因):
- //
- // GORM 把时间族字段的值【内联成 SQL 字符串字面量】而不是 ? 参数, 零值
- // gorm.DeletedAt 被序列化成 "{0001-01-01 00:00:00 +0000 UTC false}" 垃圾串。
- // 若驱动把它当普通文本存进 DATETIME 列, 该行 deleted_at 就不是 NULL,
- // `deleted_at IS NULL` 恒 0 行 → GORM 软删除作用域下的 UPDATE/DELETE/First
- // 全部失效。本文件覆盖四个修复: 时间字面量解析、LIMIT/OFFSET 占位符、
- // SELECT 无 FROM 的探活语句、ON CONFLICT 友好报错。
- import (
- "database/sql"
- "strings"
- "testing"
- "time"
- )
- // TestGormDateTimeLiteral 修复 1: DATETIME 列的内联时间字面量。
- // 非零字面量 → 毫秒正确落库; 零时间 (含 GORM 垃圾串) → NULL;
- // UPDATE SET 零时间 → 列变 NULL; WHERE dt IS NULL 按 NULL 位图命中。
- func TestGormDateTimeLiteral(t *testing.T) {
- db := openTestDB(t)
- if _, err := db.Exec(`CREATE TABLE tasks (
- id BIGINT PRIMARY KEY,
- name VARCHAR(32),
- deleted_at DATETIME
- )`); err != nil {
- t.Fatalf("create: %v", err)
- }
- // GORM 内联非零时间字面量 (MySQL 格式, 带毫秒) —— 不再是 ? 参数
- ts := time.Date(2026, 9, 30, 0, 8, 59, 839*int(time.Millisecond), time.Local)
- if _, err := db.Exec(`INSERT INTO tasks (id, name, deleted_at) VALUES (1, 'a', '2026-09-30 00:08:59.839')`); err != nil {
- t.Fatalf("insert 时间字面量: %v", err)
- }
- // ? 参数 time.Time 路径保持好用 (回归)
- if _, err := db.Exec(`INSERT INTO tasks (id, name, deleted_at) VALUES (?, ?, ?)`,
- 2, "b", ts.Add(24*time.Hour)); err != nil {
- t.Fatalf("insert time.Time 参数: %v", err)
- }
- // GORM 零值 DeletedAt 的垃圾串 → 必须是 NULL 而不是垃圾文本
- if _, err := db.Exec(`INSERT INTO tasks (id, name, deleted_at) VALUES (3, 'c', '{0001-01-01 00:00:00 +0000 UTC false}')`); err != nil {
- t.Fatalf("insert 零时间垃圾串: %v", err)
- }
- // 不带 {} 的裸零时间字面量同样按 NULL
- if _, err := db.Exec(`INSERT INTO tasks (id, name, deleted_at) VALUES (4, 'd', '0001-01-01 00:00:00')`); err != nil {
- t.Fatalf("insert 裸零时间: %v", err)
- }
- // 非零字面量回读毫秒正确
- var got time.Time
- if err := db.QueryRow(`SELECT deleted_at FROM tasks WHERE id = 1`).Scan(&got); err != nil {
- t.Fatalf("回读 id=1: %v", err)
- }
- if !got.Equal(ts) {
- t.Fatalf("id=1 时间=%v, 想要 %v", got, ts)
- }
- // 垃圾串行 deleted_at 必须是 NULL
- var nd sql.NullTime
- if err := db.QueryRow(`SELECT deleted_at FROM tasks WHERE id = 3`).Scan(&nd); err != nil {
- t.Fatalf("回读 id=3: %v", err)
- }
- if nd.Valid {
- t.Fatalf("id=3 deleted_at 应为 NULL, 实际 %v", nd.Time)
- }
- if err := db.QueryRow(`SELECT deleted_at FROM tasks WHERE id = 4`).Scan(&nd); err != nil {
- t.Fatalf("回读 id=4: %v", err)
- }
- if nd.Valid {
- t.Fatalf("id=4 deleted_at 应为 NULL, 实际 %v", nd.Time)
- }
- // WHERE 时间字面量等值 (字面量 → time.Time → 引擎按列编码比较)
- var id int64
- if err := db.QueryRow(`SELECT id FROM tasks WHERE deleted_at = '2026-09-30 00:08:59.839'`).Scan(&id); err != nil {
- t.Fatalf("WHERE 时间字面量: %v", err)
- }
- if id != 1 {
- t.Fatalf("WHERE 字面量命中 id=%d, 想要 1", id)
- }
- // IS NULL / IS NOT NULL 命中数
- var cnt int
- if err := db.QueryRow(`SELECT COUNT(*) FROM tasks WHERE deleted_at IS NULL`).Scan(&cnt); err != nil || cnt != 2 {
- t.Fatalf("IS NULL: cnt=%d err=%v, 想要 2", cnt, err)
- }
- if err := db.QueryRow(`SELECT COUNT(*) FROM tasks WHERE deleted_at IS NOT NULL`).Scan(&cnt); err != nil || cnt != 2 {
- t.Fatalf("IS NOT NULL: cnt=%d err=%v, 想要 2", cnt, err)
- }
- // UPDATE SET 零时间垃圾串 → 列变 NULL (软删除恢复/置空语义)
- res, err := db.Exec(`UPDATE tasks SET deleted_at = '{0001-01-01 00:00:00 +0000 UTC false}' WHERE id = 1`)
- if err != nil {
- t.Fatalf("UPDATE 零时间: %v", err)
- }
- if n, _ := res.RowsAffected(); n != 1 {
- t.Fatalf("UPDATE 零时间 RowsAffected=%d, 想要 1", n)
- }
- if err := db.QueryRow(`SELECT deleted_at FROM tasks WHERE id = 1`).Scan(&nd); err != nil {
- t.Fatalf("回读 UPDATE 后 id=1: %v", err)
- }
- if nd.Valid {
- t.Fatalf("UPDATE 后 id=1 deleted_at 应为 NULL, 实际 %v", nd.Time)
- }
- // UPDATE SET 非零时间字面量 → 正常写入
- t2 := time.Date(2027, 1, 15, 10, 30, 0, 123*int(time.Millisecond), time.Local)
- if _, err := db.Exec(`UPDATE tasks SET deleted_at = '2027-01-15 10:30:00.123' WHERE id = 3`); err != nil {
- t.Fatalf("UPDATE 非零时间: %v", err)
- }
- if err := db.QueryRow(`SELECT deleted_at FROM tasks WHERE id = 3`).Scan(&got); err != nil {
- t.Fatalf("回读 UPDATE 后 id=3: %v", err)
- }
- if !got.Equal(t2) {
- t.Fatalf("UPDATE 后 id=3 时间=%v, 想要 %v", got, t2)
- }
- }
- // TestGormSoftDeleteScope 修复 1+2: GORM 软删除作用域的 UPDATE 走 database/sql
- // 全链路 (GORM 矩阵同款: WHERE id=? AND deleted_at IS NULL)。
- // 零 DeletedAt 行必须命中且 RowsAffected=1; 已软删行必须不命中。
- func TestGormSoftDeleteScope(t *testing.T) {
- db := openTestDB(t)
- if _, err := db.Exec(`CREATE TABLE users (
- id BIGINT PRIMARY KEY,
- name VARCHAR(32),
- deleted_at DATETIME
- )`); err != nil {
- t.Fatalf("create: %v", err)
- }
- // GORM Create: 零 DeletedAt 内联成垃圾串 → deleted_at 必须是 NULL,
- // 否则后续软删除作用域全失效 (报告 #1/#2/#3 的根因)。
- if _, err := db.Exec(`INSERT INTO users (id, name, deleted_at) VALUES (?, ?, ?)`,
- 1, "alice", "{0001-01-01 00:00:00 +0000 UTC false}"); err != nil {
- t.Fatalf("insert: %v", err)
- }
- // GORM 软删除 UPDATE 作用域: UPDATE ... WHERE id=? AND deleted_at IS NULL
- res, err := db.Exec(`UPDATE users SET name = ? WHERE id = ? AND deleted_at IS NULL`, "alice2", 1)
- if err != nil {
- t.Fatalf("软删除作用域 UPDATE: %v", err)
- }
- if n, _ := res.RowsAffected(); n != 1 {
- t.Fatalf("RowsAffected=%d, 想要 1 (deleted_at 垃圾串会让它变 0)", n)
- }
- // GORM Delete (软删): SET deleted_at='<当前时间>' WHERE ... AND deleted_at IS NULL
- now := time.Now().Truncate(time.Millisecond)
- if _, err := db.Exec(`UPDATE users SET deleted_at = ? WHERE id = ? AND deleted_at IS NULL`, now, 1); err != nil {
- t.Fatalf("软删: %v", err)
- }
- // 已软删 → 同作用域再 UPDATE 必须 0 行
- res, err = db.Exec(`UPDATE users SET name = ? WHERE id = ? AND deleted_at IS NULL`, "alice3", 1)
- if err != nil {
- t.Fatalf("已软删后再 UPDATE: %v", err)
- }
- if n, _ := res.RowsAffected(); n != 0 {
- t.Fatalf("已软删行 RowsAffected=%d, 想要 0", n)
- }
- var cnt int
- if err := db.QueryRow(`SELECT COUNT(*) FROM users WHERE deleted_at IS NULL`).Scan(&cnt); err != nil || cnt != 0 {
- t.Fatalf("软删后 IS NULL 行数=%d err=%v, 想要 0", cnt, err)
- }
- }
- // TestLimitOffsetPlaceholders 修复 2: LIMIT/OFFSET 接受 ? 占位符。
- func TestLimitOffsetPlaceholders(t *testing.T) {
- db := openTestDB(t)
- if _, err := db.Exec(`CREATE TABLE t (id BIGINT PRIMARY KEY)`); err != nil {
- t.Fatal(err)
- }
- for i := 1; i <= 10; i++ {
- if _, err := db.Exec(`INSERT INTO t (id) VALUES (?)`, i); err != nil {
- t.Fatal(err)
- }
- }
- // LIMIT ? OFFSET ?
- var got []int64
- rows, err := db.Query(`SELECT id FROM t ORDER BY id LIMIT ? OFFSET ?`, 3, 2)
- if err != nil {
- t.Fatalf("LIMIT ? OFFSET ?: %v", err)
- }
- for rows.Next() {
- var id int64
- if err := rows.Scan(&id); err != nil {
- t.Fatal(err)
- }
- got = append(got, id)
- }
- rows.Close()
- if len(got) != 3 || got[0] != 3 || got[1] != 4 || got[2] != 5 {
- t.Fatalf("LIMIT ? OFFSET ? 得 %v, 想要 [3 4 5]", got)
- }
- // MySQL 写法 LIMIT offset,count 全占位
- got = got[:0]
- rows, err = db.Query(`SELECT id FROM t ORDER BY id LIMIT ?, ?`, 2, 3)
- if err != nil {
- t.Fatalf("LIMIT ?,?: %v", err)
- }
- for rows.Next() {
- var id int64
- if err := rows.Scan(&id); err != nil {
- t.Fatal(err)
- }
- got = append(got, id)
- }
- rows.Close()
- if len(got) != 3 || got[0] != 3 || got[1] != 4 || got[2] != 5 {
- t.Fatalf("LIMIT ?,? 得 %v, 想要 [3 4 5]", got)
- }
- // 字面量路径兼容 (回归)
- var one int64
- if err := db.QueryRow(`SELECT id FROM t ORDER BY id LIMIT 1`).Scan(&one); err != nil || one != 1 {
- t.Fatalf("字面量 LIMIT: one=%d err=%v", one, err)
- }
- // 非整数参数必须报错
- if _, err := db.Query(`SELECT id FROM t LIMIT ?`, 1.5); err == nil {
- t.Fatalf("LIMIT 1.5 应报错")
- }
- if _, err := db.Query(`SELECT id FROM t LIMIT ?`, "abc"); err == nil {
- t.Fatalf("LIMIT 'abc' 应报错")
- }
- // Prepare 路径 (stmt.NumInput 必须把占位符计入)
- stmt, err := db.Prepare(`SELECT id FROM t ORDER BY id LIMIT ? OFFSET ?`)
- if err != nil {
- t.Fatalf("prepare: %v", err)
- }
- defer stmt.Close()
- got = got[:0]
- srows, err := stmt.Query(2, 4)
- if err != nil {
- t.Fatalf("stmt query: %v", err)
- }
- for srows.Next() {
- var id int64
- if err := srows.Scan(&id); err != nil {
- t.Fatal(err)
- }
- got = append(got, id)
- }
- srows.Close()
- if len(got) != 2 || got[0] != 5 || got[1] != 6 {
- t.Fatalf("prepare LIMIT ? OFFSET ? 得 %v, 想要 [5 6]", got)
- }
- }
- // TestSelectNoFrom 修复 3: SELECT 无 FROM 的字面量值清单 (探活语句)。
- func TestSelectNoFrom(t *testing.T) {
- db := openTestDB(t)
- // SELECT 1 —— 最常见的探活语句
- var n int64
- if err := db.QueryRow(`SELECT 1`).Scan(&n); err != nil || n != 1 {
- t.Fatalf("SELECT 1: n=%d err=%v", n, err)
- }
- // 值清单: 数字 / 字符串 / NULL 混排
- rows, err := db.Query(`SELECT 'a', 1, NULL`)
- if err != nil {
- t.Fatalf("SELECT 值清单: %v", err)
- }
- defer rows.Close()
- cols, err := rows.Columns()
- if err != nil {
- t.Fatal(err)
- }
- if len(cols) != 3 {
- t.Fatalf("列数=%d, 想要 3", len(cols))
- }
- if !rows.Next() {
- t.Fatal("值清单应恰有一行")
- }
- var s []byte
- var i int64
- var nu sql.NullInt64
- if err := rows.Scan(&s, &i, &nu); err != nil {
- t.Fatalf("scan: %v", err)
- }
- if string(s) != "a" || i != 1 || nu.Valid {
- t.Fatalf("值=%q %d %v, 想要 a 1 NULL", s, i, nu)
- }
- if rows.Next() {
- t.Fatal("值清单应只有一行")
- }
- // 浮点字面量
- var f float64
- if err := db.QueryRow(`SELECT 1.5`).Scan(&f); err != nil || f != 1.5 {
- t.Fatalf("SELECT 1.5: f=%v err=%v", f, err)
- }
- // 表达式/函数不支持 → 明确错误而非静默做错
- if _, err := db.Query(`SELECT 1 + 1`); err == nil {
- t.Fatalf("SELECT 1 + 1 应报错 (不支持表达式)")
- }
- if _, err := db.Query(`SELECT COUNT(*)`); err == nil {
- t.Fatalf("SELECT COUNT(*) 无 FROM 应报错")
- }
- }
- // TestOnConflictFriendlyError 修复 4: UPSERT 语法给出明确报错文案。
- func TestOnConflictFriendlyError(t *testing.T) {
- db := openTestDB(t)
- if _, err := db.Exec(`CREATE TABLE t (id BIGINT PRIMARY KEY, v INT)`); err != nil {
- t.Fatal(err)
- }
- cases := []string{
- `INSERT INTO t (id, v) VALUES (1, 2) ON CONFLICT (id) DO UPDATE SET v = 3`,
- `INSERT INTO t (id, v) VALUES (1, 2) ON DUPLICATE KEY UPDATE v = 3`,
- }
- for _, q := range cases {
- _, err := db.Exec(q)
- if err == nil {
- t.Fatalf("%q 应报错", q)
- }
- if !strings.Contains(err.Error(), "不支持 UPSERT") ||
- !strings.Contains(err.Error(), "ON CONFLICT/ON DUPLICATE KEY UPDATE") {
- t.Fatalf("报错文案不对: %v", err)
- }
- }
- }
|