gorm_compat_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. package vrtxsql
  2. // GORM 兼容回归测试 (报告 #1/#2/#3 同根因):
  3. //
  4. // GORM 把时间族字段的值【内联成 SQL 字符串字面量】而不是 ? 参数, 零值
  5. // gorm.DeletedAt 被序列化成 "{0001-01-01 00:00:00 +0000 UTC false}" 垃圾串。
  6. // 若驱动把它当普通文本存进 DATETIME 列, 该行 deleted_at 就不是 NULL,
  7. // `deleted_at IS NULL` 恒 0 行 → GORM 软删除作用域下的 UPDATE/DELETE/First
  8. // 全部失效。本文件覆盖四个修复: 时间字面量解析、LIMIT/OFFSET 占位符、
  9. // SELECT 无 FROM 的探活语句、ON CONFLICT 友好报错。
  10. import (
  11. "database/sql"
  12. "strings"
  13. "testing"
  14. "time"
  15. )
  16. // TestGormDateTimeLiteral 修复 1: DATETIME 列的内联时间字面量。
  17. // 非零字面量 → 毫秒正确落库; 零时间 (含 GORM 垃圾串) → NULL;
  18. // UPDATE SET 零时间 → 列变 NULL; WHERE dt IS NULL 按 NULL 位图命中。
  19. func TestGormDateTimeLiteral(t *testing.T) {
  20. db := openTestDB(t)
  21. if _, err := db.Exec(`CREATE TABLE tasks (
  22. id BIGINT PRIMARY KEY,
  23. name VARCHAR(32),
  24. deleted_at DATETIME
  25. )`); err != nil {
  26. t.Fatalf("create: %v", err)
  27. }
  28. // GORM 内联非零时间字面量 (MySQL 格式, 带毫秒) —— 不再是 ? 参数
  29. ts := time.Date(2026, 9, 30, 0, 8, 59, 839*int(time.Millisecond), time.Local)
  30. if _, err := db.Exec(`INSERT INTO tasks (id, name, deleted_at) VALUES (1, 'a', '2026-09-30 00:08:59.839')`); err != nil {
  31. t.Fatalf("insert 时间字面量: %v", err)
  32. }
  33. // ? 参数 time.Time 路径保持好用 (回归)
  34. if _, err := db.Exec(`INSERT INTO tasks (id, name, deleted_at) VALUES (?, ?, ?)`,
  35. 2, "b", ts.Add(24*time.Hour)); err != nil {
  36. t.Fatalf("insert time.Time 参数: %v", err)
  37. }
  38. // GORM 零值 DeletedAt 的垃圾串 → 必须是 NULL 而不是垃圾文本
  39. 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 {
  40. t.Fatalf("insert 零时间垃圾串: %v", err)
  41. }
  42. // 不带 {} 的裸零时间字面量同样按 NULL
  43. if _, err := db.Exec(`INSERT INTO tasks (id, name, deleted_at) VALUES (4, 'd', '0001-01-01 00:00:00')`); err != nil {
  44. t.Fatalf("insert 裸零时间: %v", err)
  45. }
  46. // 非零字面量回读毫秒正确
  47. var got time.Time
  48. if err := db.QueryRow(`SELECT deleted_at FROM tasks WHERE id = 1`).Scan(&got); err != nil {
  49. t.Fatalf("回读 id=1: %v", err)
  50. }
  51. if !got.Equal(ts) {
  52. t.Fatalf("id=1 时间=%v, 想要 %v", got, ts)
  53. }
  54. // 垃圾串行 deleted_at 必须是 NULL
  55. var nd sql.NullTime
  56. if err := db.QueryRow(`SELECT deleted_at FROM tasks WHERE id = 3`).Scan(&nd); err != nil {
  57. t.Fatalf("回读 id=3: %v", err)
  58. }
  59. if nd.Valid {
  60. t.Fatalf("id=3 deleted_at 应为 NULL, 实际 %v", nd.Time)
  61. }
  62. if err := db.QueryRow(`SELECT deleted_at FROM tasks WHERE id = 4`).Scan(&nd); err != nil {
  63. t.Fatalf("回读 id=4: %v", err)
  64. }
  65. if nd.Valid {
  66. t.Fatalf("id=4 deleted_at 应为 NULL, 实际 %v", nd.Time)
  67. }
  68. // WHERE 时间字面量等值 (字面量 → time.Time → 引擎按列编码比较)
  69. var id int64
  70. if err := db.QueryRow(`SELECT id FROM tasks WHERE deleted_at = '2026-09-30 00:08:59.839'`).Scan(&id); err != nil {
  71. t.Fatalf("WHERE 时间字面量: %v", err)
  72. }
  73. if id != 1 {
  74. t.Fatalf("WHERE 字面量命中 id=%d, 想要 1", id)
  75. }
  76. // IS NULL / IS NOT NULL 命中数
  77. var cnt int
  78. if err := db.QueryRow(`SELECT COUNT(*) FROM tasks WHERE deleted_at IS NULL`).Scan(&cnt); err != nil || cnt != 2 {
  79. t.Fatalf("IS NULL: cnt=%d err=%v, 想要 2", cnt, err)
  80. }
  81. if err := db.QueryRow(`SELECT COUNT(*) FROM tasks WHERE deleted_at IS NOT NULL`).Scan(&cnt); err != nil || cnt != 2 {
  82. t.Fatalf("IS NOT NULL: cnt=%d err=%v, 想要 2", cnt, err)
  83. }
  84. // UPDATE SET 零时间垃圾串 → 列变 NULL (软删除恢复/置空语义)
  85. res, err := db.Exec(`UPDATE tasks SET deleted_at = '{0001-01-01 00:00:00 +0000 UTC false}' WHERE id = 1`)
  86. if err != nil {
  87. t.Fatalf("UPDATE 零时间: %v", err)
  88. }
  89. if n, _ := res.RowsAffected(); n != 1 {
  90. t.Fatalf("UPDATE 零时间 RowsAffected=%d, 想要 1", n)
  91. }
  92. if err := db.QueryRow(`SELECT deleted_at FROM tasks WHERE id = 1`).Scan(&nd); err != nil {
  93. t.Fatalf("回读 UPDATE 后 id=1: %v", err)
  94. }
  95. if nd.Valid {
  96. t.Fatalf("UPDATE 后 id=1 deleted_at 应为 NULL, 实际 %v", nd.Time)
  97. }
  98. // UPDATE SET 非零时间字面量 → 正常写入
  99. t2 := time.Date(2027, 1, 15, 10, 30, 0, 123*int(time.Millisecond), time.Local)
  100. if _, err := db.Exec(`UPDATE tasks SET deleted_at = '2027-01-15 10:30:00.123' WHERE id = 3`); err != nil {
  101. t.Fatalf("UPDATE 非零时间: %v", err)
  102. }
  103. if err := db.QueryRow(`SELECT deleted_at FROM tasks WHERE id = 3`).Scan(&got); err != nil {
  104. t.Fatalf("回读 UPDATE 后 id=3: %v", err)
  105. }
  106. if !got.Equal(t2) {
  107. t.Fatalf("UPDATE 后 id=3 时间=%v, 想要 %v", got, t2)
  108. }
  109. }
  110. // TestGormSoftDeleteScope 修复 1+2: GORM 软删除作用域的 UPDATE 走 database/sql
  111. // 全链路 (GORM 矩阵同款: WHERE id=? AND deleted_at IS NULL)。
  112. // 零 DeletedAt 行必须命中且 RowsAffected=1; 已软删行必须不命中。
  113. func TestGormSoftDeleteScope(t *testing.T) {
  114. db := openTestDB(t)
  115. if _, err := db.Exec(`CREATE TABLE users (
  116. id BIGINT PRIMARY KEY,
  117. name VARCHAR(32),
  118. deleted_at DATETIME
  119. )`); err != nil {
  120. t.Fatalf("create: %v", err)
  121. }
  122. // GORM Create: 零 DeletedAt 内联成垃圾串 → deleted_at 必须是 NULL,
  123. // 否则后续软删除作用域全失效 (报告 #1/#2/#3 的根因)。
  124. if _, err := db.Exec(`INSERT INTO users (id, name, deleted_at) VALUES (?, ?, ?)`,
  125. 1, "alice", "{0001-01-01 00:00:00 +0000 UTC false}"); err != nil {
  126. t.Fatalf("insert: %v", err)
  127. }
  128. // GORM 软删除 UPDATE 作用域: UPDATE ... WHERE id=? AND deleted_at IS NULL
  129. res, err := db.Exec(`UPDATE users SET name = ? WHERE id = ? AND deleted_at IS NULL`, "alice2", 1)
  130. if err != nil {
  131. t.Fatalf("软删除作用域 UPDATE: %v", err)
  132. }
  133. if n, _ := res.RowsAffected(); n != 1 {
  134. t.Fatalf("RowsAffected=%d, 想要 1 (deleted_at 垃圾串会让它变 0)", n)
  135. }
  136. // GORM Delete (软删): SET deleted_at='<当前时间>' WHERE ... AND deleted_at IS NULL
  137. now := time.Now().Truncate(time.Millisecond)
  138. if _, err := db.Exec(`UPDATE users SET deleted_at = ? WHERE id = ? AND deleted_at IS NULL`, now, 1); err != nil {
  139. t.Fatalf("软删: %v", err)
  140. }
  141. // 已软删 → 同作用域再 UPDATE 必须 0 行
  142. res, err = db.Exec(`UPDATE users SET name = ? WHERE id = ? AND deleted_at IS NULL`, "alice3", 1)
  143. if err != nil {
  144. t.Fatalf("已软删后再 UPDATE: %v", err)
  145. }
  146. if n, _ := res.RowsAffected(); n != 0 {
  147. t.Fatalf("已软删行 RowsAffected=%d, 想要 0", n)
  148. }
  149. var cnt int
  150. if err := db.QueryRow(`SELECT COUNT(*) FROM users WHERE deleted_at IS NULL`).Scan(&cnt); err != nil || cnt != 0 {
  151. t.Fatalf("软删后 IS NULL 行数=%d err=%v, 想要 0", cnt, err)
  152. }
  153. }
  154. // TestLimitOffsetPlaceholders 修复 2: LIMIT/OFFSET 接受 ? 占位符。
  155. func TestLimitOffsetPlaceholders(t *testing.T) {
  156. db := openTestDB(t)
  157. if _, err := db.Exec(`CREATE TABLE t (id BIGINT PRIMARY KEY)`); err != nil {
  158. t.Fatal(err)
  159. }
  160. for i := 1; i <= 10; i++ {
  161. if _, err := db.Exec(`INSERT INTO t (id) VALUES (?)`, i); err != nil {
  162. t.Fatal(err)
  163. }
  164. }
  165. // LIMIT ? OFFSET ?
  166. var got []int64
  167. rows, err := db.Query(`SELECT id FROM t ORDER BY id LIMIT ? OFFSET ?`, 3, 2)
  168. if err != nil {
  169. t.Fatalf("LIMIT ? OFFSET ?: %v", err)
  170. }
  171. for rows.Next() {
  172. var id int64
  173. if err := rows.Scan(&id); err != nil {
  174. t.Fatal(err)
  175. }
  176. got = append(got, id)
  177. }
  178. rows.Close()
  179. if len(got) != 3 || got[0] != 3 || got[1] != 4 || got[2] != 5 {
  180. t.Fatalf("LIMIT ? OFFSET ? 得 %v, 想要 [3 4 5]", got)
  181. }
  182. // MySQL 写法 LIMIT offset,count 全占位
  183. got = got[:0]
  184. rows, err = db.Query(`SELECT id FROM t ORDER BY id LIMIT ?, ?`, 2, 3)
  185. if err != nil {
  186. t.Fatalf("LIMIT ?,?: %v", err)
  187. }
  188. for rows.Next() {
  189. var id int64
  190. if err := rows.Scan(&id); err != nil {
  191. t.Fatal(err)
  192. }
  193. got = append(got, id)
  194. }
  195. rows.Close()
  196. if len(got) != 3 || got[0] != 3 || got[1] != 4 || got[2] != 5 {
  197. t.Fatalf("LIMIT ?,? 得 %v, 想要 [3 4 5]", got)
  198. }
  199. // 字面量路径兼容 (回归)
  200. var one int64
  201. if err := db.QueryRow(`SELECT id FROM t ORDER BY id LIMIT 1`).Scan(&one); err != nil || one != 1 {
  202. t.Fatalf("字面量 LIMIT: one=%d err=%v", one, err)
  203. }
  204. // 非整数参数必须报错
  205. if _, err := db.Query(`SELECT id FROM t LIMIT ?`, 1.5); err == nil {
  206. t.Fatalf("LIMIT 1.5 应报错")
  207. }
  208. if _, err := db.Query(`SELECT id FROM t LIMIT ?`, "abc"); err == nil {
  209. t.Fatalf("LIMIT 'abc' 应报错")
  210. }
  211. // Prepare 路径 (stmt.NumInput 必须把占位符计入)
  212. stmt, err := db.Prepare(`SELECT id FROM t ORDER BY id LIMIT ? OFFSET ?`)
  213. if err != nil {
  214. t.Fatalf("prepare: %v", err)
  215. }
  216. defer stmt.Close()
  217. got = got[:0]
  218. srows, err := stmt.Query(2, 4)
  219. if err != nil {
  220. t.Fatalf("stmt query: %v", err)
  221. }
  222. for srows.Next() {
  223. var id int64
  224. if err := srows.Scan(&id); err != nil {
  225. t.Fatal(err)
  226. }
  227. got = append(got, id)
  228. }
  229. srows.Close()
  230. if len(got) != 2 || got[0] != 5 || got[1] != 6 {
  231. t.Fatalf("prepare LIMIT ? OFFSET ? 得 %v, 想要 [5 6]", got)
  232. }
  233. }
  234. // TestSelectNoFrom 修复 3: SELECT 无 FROM 的字面量值清单 (探活语句)。
  235. func TestSelectNoFrom(t *testing.T) {
  236. db := openTestDB(t)
  237. // SELECT 1 —— 最常见的探活语句
  238. var n int64
  239. if err := db.QueryRow(`SELECT 1`).Scan(&n); err != nil || n != 1 {
  240. t.Fatalf("SELECT 1: n=%d err=%v", n, err)
  241. }
  242. // 值清单: 数字 / 字符串 / NULL 混排
  243. rows, err := db.Query(`SELECT 'a', 1, NULL`)
  244. if err != nil {
  245. t.Fatalf("SELECT 值清单: %v", err)
  246. }
  247. defer rows.Close()
  248. cols, err := rows.Columns()
  249. if err != nil {
  250. t.Fatal(err)
  251. }
  252. if len(cols) != 3 {
  253. t.Fatalf("列数=%d, 想要 3", len(cols))
  254. }
  255. if !rows.Next() {
  256. t.Fatal("值清单应恰有一行")
  257. }
  258. var s []byte
  259. var i int64
  260. var nu sql.NullInt64
  261. if err := rows.Scan(&s, &i, &nu); err != nil {
  262. t.Fatalf("scan: %v", err)
  263. }
  264. if string(s) != "a" || i != 1 || nu.Valid {
  265. t.Fatalf("值=%q %d %v, 想要 a 1 NULL", s, i, nu)
  266. }
  267. if rows.Next() {
  268. t.Fatal("值清单应只有一行")
  269. }
  270. // 浮点字面量
  271. var f float64
  272. if err := db.QueryRow(`SELECT 1.5`).Scan(&f); err != nil || f != 1.5 {
  273. t.Fatalf("SELECT 1.5: f=%v err=%v", f, err)
  274. }
  275. // 表达式/函数不支持 → 明确错误而非静默做错
  276. if _, err := db.Query(`SELECT 1 + 1`); err == nil {
  277. t.Fatalf("SELECT 1 + 1 应报错 (不支持表达式)")
  278. }
  279. if _, err := db.Query(`SELECT COUNT(*)`); err == nil {
  280. t.Fatalf("SELECT COUNT(*) 无 FROM 应报错")
  281. }
  282. }
  283. // TestOnConflictFriendlyError 修复 4: UPSERT 语法给出明确报错文案。
  284. func TestOnConflictFriendlyError(t *testing.T) {
  285. db := openTestDB(t)
  286. if _, err := db.Exec(`CREATE TABLE t (id BIGINT PRIMARY KEY, v INT)`); err != nil {
  287. t.Fatal(err)
  288. }
  289. cases := []string{
  290. `INSERT INTO t (id, v) VALUES (1, 2) ON CONFLICT (id) DO UPDATE SET v = 3`,
  291. `INSERT INTO t (id, v) VALUES (1, 2) ON DUPLICATE KEY UPDATE v = 3`,
  292. }
  293. for _, q := range cases {
  294. _, err := db.Exec(q)
  295. if err == nil {
  296. t.Fatalf("%q 应报错", q)
  297. }
  298. if !strings.Contains(err.Error(), "不支持 UPSERT") ||
  299. !strings.Contains(err.Error(), "ON CONFLICT/ON DUPLICATE KEY UPDATE") {
  300. t.Fatalf("报错文案不对: %v", err)
  301. }
  302. }
  303. }