concat_like_test.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. package vrtxsql
  2. // SQLite 方言 '||' 字符串拼接测试: 拼接链作 LIKE/NOT LIKE 的左操作数
  3. // (生产形态: WHERE ','||service_name||',' LIKE '%,check,%' AND deleted_at IS NULL,
  4. // 语义 = 逗号分隔列表的元素精确包含 x —— 'check'/'a,check,b' 命中, 'check2'/'precheck' 不命中)。
  5. import (
  6. "database/sql"
  7. "strings"
  8. "testing"
  9. )
  10. func openConcatTestDB(t *testing.T) *sql.DB {
  11. t.Helper()
  12. db := openTestDB(t)
  13. if _, err := db.Exec(`CREATE TABLE services (
  14. id BIGINT PRIMARY KEY,
  15. service_name VARCHAR(64) NOT NULL,
  16. deleted_at VARCHAR(32) NULL
  17. )`); err != nil {
  18. t.Fatalf("create table: %v", err)
  19. }
  20. rows := []struct {
  21. id int64
  22. name string
  23. deleted sql.NullString
  24. }{
  25. {1, "check", sql.NullString{}}, // 单元素精确命中
  26. {2, "a,check,b", sql.NullString{}}, // 列表中间元素命中
  27. {3, "check2", sql.NullString{}}, // 前缀相似, 不命中
  28. {4, "precheck", sql.NullString{}}, // 后缀相似, 不命中
  29. {5, "other", sql.NullString{String: "2024-01-01", Valid: true}}, // 已删除
  30. }
  31. for _, r := range rows {
  32. if _, err := db.Exec(`INSERT INTO services (id, service_name, deleted_at) VALUES (?, ?, ?)`,
  33. r.id, r.name, r.deleted); err != nil {
  34. t.Fatalf("insert %d: %v", r.id, err)
  35. }
  36. }
  37. return db
  38. }
  39. func queryServiceIDs(t *testing.T, db *sql.DB, query string, args ...interface{}) []int64 {
  40. t.Helper()
  41. rows, err := db.Query(query, args...)
  42. if err != nil {
  43. t.Fatalf("query %q: %v", query, err)
  44. }
  45. defer rows.Close()
  46. var ids []int64
  47. for rows.Next() {
  48. var id int64
  49. var name string
  50. var deletedAt sql.NullString
  51. if err := rows.Scan(&id, &name, &deletedAt); err != nil {
  52. t.Fatalf("scan: %v", err)
  53. }
  54. ids = append(ids, id)
  55. }
  56. if err := rows.Err(); err != nil {
  57. t.Fatalf("rows: %v", err)
  58. }
  59. return ids
  60. }
  61. func TestConcatLikeMatchesExactListElement(t *testing.T) {
  62. db := openConcatTestDB(t)
  63. // ','||service_name||',' LIKE '%,check,%': 恰好命中 1 ('check') 和 2 ('a,check,b')。
  64. got := queryServiceIDs(t, db,
  65. `SELECT id, service_name, deleted_at FROM services WHERE ','||service_name||',' LIKE '%,check,%' ORDER BY id`)
  66. if len(got) != 2 || got[0] != 1 || got[1] != 2 {
  67. t.Fatalf("got %v, 想要 [1 2]", got)
  68. }
  69. }
  70. func TestConcatNotLikeReverses(t *testing.T) {
  71. db := openConcatTestDB(t)
  72. got := queryServiceIDs(t, db,
  73. `SELECT id, service_name, deleted_at FROM services WHERE ','||service_name||',' NOT LIKE '%,check,%' ORDER BY id`)
  74. // 3 (check2) 与 4 (precheck) 反向命中; 5 (other) 是已删除行但本条件不含 deleted_at。
  75. if len(got) != 3 || got[0] != 3 || got[1] != 4 || got[2] != 5 {
  76. t.Fatalf("got %v, 想要 [3 4 5]", got)
  77. }
  78. }
  79. func TestConcatLikeWithIsNullCombination(t *testing.T) {
  80. db := openConcatTestDB(t)
  81. // 生产触发 SQL 同款形态: 拼接 LIKE + AND deleted_at IS NULL + LIMIT。
  82. var id int64
  83. var name string
  84. err := db.QueryRow(
  85. `SELECT id, service_name FROM services WHERE ','||service_name||',' like '%,check,%' AND deleted_at IS NULL ORDER BY id LIMIT 1`).
  86. Scan(&id, &name)
  87. if err != nil {
  88. t.Fatalf("query: %v", err)
  89. }
  90. if id != 1 || name != "check" {
  91. t.Fatalf("got id=%d name=%q, 想要 id=1 name=check", id, name)
  92. }
  93. // NOT LIKE + IS NULL: 未删除且不含 check 元素 → 3 / 4。
  94. got := queryServiceIDs(t, db,
  95. `SELECT id, service_name, deleted_at FROM services WHERE ','||service_name||',' NOT LIKE '%,check,%' AND deleted_at IS NULL ORDER BY id`)
  96. if len(got) != 2 || got[0] != 3 || got[1] != 4 {
  97. t.Fatalf("got %v, 想要 [3 4]", got)
  98. }
  99. }
  100. func TestConcatLikePatternPlaceholder(t *testing.T) {
  101. db := openConcatTestDB(t)
  102. // 模式走占位符 (GORM Where("... LIKE ?", pat) 形态)。
  103. got := queryServiceIDs(t, db,
  104. `SELECT id, service_name, deleted_at FROM services WHERE ','||service_name||',' LIKE ? ORDER BY id`, "%,check,%")
  105. if len(got) != 2 || got[0] != 1 || got[1] != 2 {
  106. t.Fatalf("got %v, 想要 [1 2]", got)
  107. }
  108. }
  109. func TestConcatRejectedForNonLikeOperators(t *testing.T) {
  110. db := openConcatTestDB(t)
  111. queries := []string{
  112. `SELECT id FROM services WHERE ','||service_name||',' = ',check,'`,
  113. `SELECT id FROM services WHERE ','||service_name||',' IN (',check,')`,
  114. `SELECT id FROM services WHERE ','||service_name||',' IS NULL`,
  115. `SELECT id FROM services WHERE service_name||',' LIKE '%,check,%' AND ','||service_name = ',check'`,
  116. }
  117. for _, q := range queries {
  118. if _, err := db.Query(q); err == nil {
  119. t.Fatalf("%q 应报错 (|| 拼接仅支持 LIKE 匹配)", q)
  120. } else if !strings.Contains(err.Error(), "|| 拼接仅支持 LIKE 匹配") {
  121. t.Fatalf("%q 报错 %q, 期望包含 '|| 拼接仅支持 LIKE 匹配'", q, err)
  122. }
  123. }
  124. // 单个 '|' 仍是 tokenizer 报错。
  125. if _, err := db.Query(`SELECT id FROM services WHERE service_name | 'x' LIKE 'y'`); err == nil {
  126. t.Fatalf("单个 '|' 应报无法识别的字符")
  127. } else if !strings.Contains(err.Error(), "无法识别的字符") {
  128. t.Fatalf("单个 '|' 报错 %q, 期望 '无法识别的字符'", err)
  129. }
  130. }