compat_suite_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. package pgserver
  2. // Compatibility suite for the pChat-style workload: DDL, upsert idempotency,
  3. // constraints, pagination, parameters, and error codes. It runs only when
  4. // PIZZASQL_COMPAT=1 so the regular unit suite stays fast; `make compat` enables
  5. // it and writes a JSON report.
  6. //
  7. // The suite distinguishes:
  8. // - pass: the build behaves as documented
  9. // - missing: a documented capability is absent from this build (for example a
  10. // rejection introduced by a newer release)
  11. // - fail: behavior contradicts the documented contract
  12. //
  13. // PIZZASQL_COMPAT_STRICT=1 turns "missing" into a failure, which is what a
  14. // release gate should use. The compatibility document is pkg/capabilities.
  15. import (
  16. "encoding/json"
  17. "os"
  18. "strconv"
  19. "testing"
  20. )
  21. func rowValue(rows [][]string, index int) string {
  22. if len(rows) <= index || len(rows[index]) == 0 {
  23. return ""
  24. }
  25. return rows[index][0]
  26. }
  27. type compatResult struct {
  28. Name string `json:"name"`
  29. Status string `json:"status"` // pass, missing, fail
  30. Detail string `json:"detail,omitempty"`
  31. }
  32. type compatReport struct {
  33. Engine string `json:"engine"`
  34. Results []compatResult `json:"results"`
  35. }
  36. func TestCompatibilitySuite(t *testing.T) {
  37. if os.Getenv("PIZZASQL_COMPAT") != "1" {
  38. t.Skip("set PIZZASQL_COMPAT=1 (or run make compat) to run the compatibility suite")
  39. }
  40. strict := os.Getenv("PIZZASQL_COMPAT_STRICT") == "1"
  41. c, client := newDataConnection(t)
  42. report := compatReport{Engine: "pizzasql"}
  43. record := func(name, status, detail string) {
  44. report.Results = append(report.Results, compatResult{Name: name, Status: status, Detail: detail})
  45. }
  46. // exec runs a statement and returns the SQLSTATE code ("" on success) and
  47. // decoded rows.
  48. exec := func(sql string) (string, [][]string) {
  49. msgs := runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(sql), 0)})
  50. if len(msgs) == 0 {
  51. return "", nil
  52. }
  53. if msgs[0].Type == MsgErrorResponse {
  54. return errorCode(msgs[0]), nil
  55. }
  56. var rows [][]string
  57. for _, msg := range msgs[1:] {
  58. if msg.Type != MsgDataRow {
  59. continue
  60. }
  61. rows = append(rows, dataRowValues(t, msg))
  62. }
  63. return "", rows
  64. }
  65. expectOK := func(name, sql string) {
  66. if code, _ := exec(sql); code != "" {
  67. record(name, "fail", "unexpected SQLSTATE "+code)
  68. return
  69. }
  70. record(name, "pass", "")
  71. }
  72. expectCode := func(name, sql, want string) {
  73. code, _ := exec(sql)
  74. switch code {
  75. case want:
  76. record(name, "pass", "")
  77. case "":
  78. record(name, "missing", "statement succeeded; this build does not enforce "+want)
  79. default:
  80. record(name, "missing", "got SQLSTATE "+code+", want "+want)
  81. }
  82. }
  83. expectRows := func(name, sql string, want ...string) {
  84. code, rows := exec(sql)
  85. if code != "" {
  86. record(name, "fail", "unexpected SQLSTATE "+code)
  87. return
  88. }
  89. if len(rows) != 1 || len(rows[0]) != len(want) {
  90. record(name, "fail", "unexpected rows")
  91. return
  92. }
  93. for i := range want {
  94. if rows[0][i] != want[i] {
  95. record(name, "fail", "row "+rows[0][i]+" != "+want[i])
  96. return
  97. }
  98. }
  99. record(name, "pass", "")
  100. }
  101. expectOK("ddl.create_table", `CREATE TABLE accounts (
  102. id INTEGER PRIMARY KEY,
  103. email TEXT NOT NULL UNIQUE,
  104. plan TEXT NOT NULL DEFAULT 'free',
  105. seats INTEGER NOT NULL DEFAULT 1
  106. )`)
  107. expectOK("dml.insert", `INSERT INTO accounts (id, email, plan, seats) VALUES (1, 'a@x', 'pro', 3)`)
  108. expectRows("dml.select_constant_default", `SELECT plan, seats FROM accounts WHERE id = 1`, "pro", "3")
  109. expectOK("upsert.first", `INSERT INTO accounts (id, email, plan) VALUES (2, 'b@x', 'free') ON CONFLICT(id) DO UPDATE SET plan = excluded.plan`)
  110. expectOK("upsert.idempotent", `INSERT INTO accounts (id, email, plan) VALUES (2, 'b@x', 'pro') ON CONFLICT(id) DO UPDATE SET plan = excluded.plan`)
  111. expectRows("upsert.result", `SELECT plan FROM accounts WHERE id = 2`, "pro")
  112. expectCode("constraints.unique_23505", `INSERT INTO accounts (id, email) VALUES (3, 'a@x')`, "23505")
  113. expectOK("ddl.composite_pk", `CREATE TABLE composite_pk (a INTEGER, b TEXT, PRIMARY KEY (a, b))`)
  114. expectOK("dml.composite_pk_insert", `INSERT INTO composite_pk (a, b) VALUES (1, 'x')`)
  115. expectRows("dml.composite_pk_select", `SELECT count(*) FROM composite_pk WHERE a = 1 AND b = 'x'`, "1")
  116. expectCode("constraints.composite_pk_23505", `INSERT INTO composite_pk (a, b) VALUES (1, 'x')`, "23505")
  117. expectCode("constraints.composite_pk_23502", `INSERT INTO composite_pk (a) VALUES (2)`, "23502")
  118. expectOK("ddl.timestamp_default", `CREATE TABLE stamped (id INTEGER PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)`)
  119. expectOK("ddl.timestamp_default_insert", `INSERT INTO stamped (id) VALUES (1)`)
  120. expectRows("ddl.timestamp_default_applied", `SELECT count(*) FROM stamped WHERE created_at IS NOT NULL`, "1")
  121. expectOK("ddl.parenthesized_defaults", `CREATE TABLE paren_defaults (id INTEGER PRIMARY KEY, a TIMESTAMP DEFAULT (CURRENT_TIMESTAMP), b TIMESTAMP DEFAULT (datetime('now')))`)
  122. expectOK("ddl.parenthesized_defaults_insert", `INSERT INTO paren_defaults (id) VALUES (1)`)
  123. expectRows("ddl.parenthesized_defaults_applied", `SELECT count(*) FROM paren_defaults WHERE a IS NOT NULL AND b IS NOT NULL`, "1")
  124. expectCode("ddl.nonconstant_default_0A000", `CREATE TABLE bad_default (id INTEGER PRIMARY KEY, token TEXT DEFAULT lower('x'))`, "0A000")
  125. expectCode("locking.for_update_0A000", `SELECT * FROM accounts FOR UPDATE`, "0A000")
  126. // Array types, array literals, ANY/ALL, and array parameters.
  127. expectOK("arrays.ddl", `CREATE TABLE array_values (id INTEGER PRIMARY KEY, tags TEXT[], nums INTEGER[])`)
  128. expectOK("arrays.insert", `INSERT INTO array_values VALUES (1, '{a,b}', '{1,2,NULL}')`)
  129. expectRows("arrays.any", `SELECT count(*) FROM array_values WHERE 'a' = ANY(tags)`, "1")
  130. expectRows("arrays.any_null_element", `SELECT count(*) FROM array_values WHERE 3 = ANY(nums)`, "0")
  131. expectRows("arrays.all", `SELECT count(*) FROM array_values WHERE 9 <> ALL('{1,2}')`, "1")
  132. expectRows("arrays.all_null_element", `SELECT count(*) FROM array_values WHERE 9 <> ALL(nums)`, "0")
  133. expectRows("arrays.subscript", `SELECT tags[1] FROM array_values`, "a")
  134. expectRows("arrays.literal_compare", `SELECT count(*) FROM array_values WHERE tags = '{a, b}'`, "1")
  135. expectRows("arrays.numeric_exact", `SELECT '{12345678901234567890,1.10}'::NUMERIC[]`, "{12345678901234567890,1.10}")
  136. expectCode("arrays.index_type_42804", `SELECT nums[1.5] FROM array_values`, "42804")
  137. expectRows("arrays.length", `SELECT array_length(tags, 1) FROM array_values`, "2")
  138. expectRows("arrays.any_parameter", `SELECT count(*) FROM array_values WHERE id = ANY('{1}')`, "1")
  139. expectCode("arrays.multidim_0A000", `CREATE TABLE array_bad (v INTEGER[][])`, "0A000")
  140. expectRows("arrays.contains", `SELECT count(*) FROM array_values WHERE nums @> ARRAY[1]`, "1")
  141. expectRows("arrays.contains_missing", `SELECT count(*) FROM array_values WHERE nums @> ARRAY[9]`, "0")
  142. expectRows("arrays.contained_by", `SELECT count(*) FROM array_values WHERE ARRAY[1] <@ nums`, "1")
  143. expectRows("arrays.contains_null_element", `SELECT count(*) FROM array_values WHERE nums @> ARRAY[NULL]::INTEGER[]`, "1")
  144. expectRows("arrays.contains_empty", `SELECT count(*) FROM array_values WHERE nums @> ARRAY[]::INTEGER[]`, "1")
  145. expectRows("arrays.contains_null_operand", `SELECT count(*) FROM array_values WHERE nums @> NULL`, "0")
  146. expectRows("srf.unnest_alias_list", `SELECT count(*) FROM unnest(ARRAY[1,2]) AS t(n) WHERE n > 1`, "1")
  147. expectRows("srf.jsonb_each_alias_list", `SELECT count(*) FROM jsonb_each('{"a":1}') AS e(k, v) WHERE k = 'a'`, "1")
  148. expectCode("srf.alias_list_arity_42P10", `SELECT * FROM jsonb_each('{"a":1}') AS e(k)`, "42P10")
  149. // CHECK and FOREIGN KEY are accepted and enforced.
  150. expectOK("ddl.check_accepted", `CREATE TABLE checked (n INTEGER CHECK (n > 0))`)
  151. expectCode("constraints.check_23514", `INSERT INTO checked (n) VALUES (-1)`, "23514")
  152. expectOK("ddl.foreign_key_accepted", `CREATE TABLE child (id INTEGER PRIMARY KEY, parent_id INTEGER, FOREIGN KEY (parent_id) REFERENCES accounts(id))`)
  153. expectOK("dml.foreign_key_valid", `INSERT INTO child (id, parent_id) VALUES (1, 1)`)
  154. expectCode("constraints.foreign_key_23503", `INSERT INTO child (id, parent_id) VALUES (2, 99)`, "23503")
  155. expectCode("constraints.foreign_key_parent_delete_23503", `DELETE FROM accounts WHERE id = 1`, "23503")
  156. expectCode("errors.syntax_42601", `SELEC 1`, "42601")
  157. // Explicit transactions buffer writes and validate unique indexes at
  158. // COMMIT, so the duplicate insert itself succeeds and the commit fails.
  159. expectOK("tx.begin", `BEGIN`)
  160. expectOK("tx.buffer_duplicate", `INSERT INTO accounts (id, email) VALUES (3, 'a@x')`)
  161. expectCode("tx.commit_unique_23505", `COMMIT`, "23505")
  162. // A failed COMMIT has already rolled back; the session must be usable
  163. // again instead of being stuck in the failed-transaction state.
  164. expectRows("tx.after_failed_commit_idle", `SELECT 1`, "1")
  165. expectOK("tx.begin_after_failed_commit", `BEGIN`)
  166. expectOK("tx.rollback", `ROLLBACK`)
  167. expectOK("pagination.seed", `INSERT INTO accounts (id, email) VALUES (10, 'p10@x'), (11, 'p11@x'), (12, 'p12@x')`)
  168. code, rows := exec(`SELECT _rowid_ FROM accounts WHERE _rowid_ > 1 ORDER BY _rowid_ LIMIT 2`)
  169. first, firstErr := strconv.ParseInt(rowValue(rows, 0), 10, 64)
  170. second, secondErr := strconv.ParseInt(rowValue(rows, 1), 10, 64)
  171. switch {
  172. case code != "":
  173. record("pagination.rowid", "fail", "unexpected SQLSTATE "+code)
  174. case len(rows) == 2 && firstErr == nil && secondErr == nil && first < second:
  175. record("pagination.rowid", "pass", "")
  176. default:
  177. record("pagination.rowid", "fail", "cursor did not advance")
  178. }
  179. failures, missing := 0, 0
  180. for _, result := range report.Results {
  181. switch result.Status {
  182. case "fail":
  183. failures++
  184. t.Errorf("%s: %s", result.Name, result.Detail)
  185. case "missing":
  186. missing++
  187. t.Logf("%s: %s", result.Name, result.Detail)
  188. }
  189. }
  190. if path := os.Getenv("PIZZASQL_COMPAT_REPORT"); path != "" {
  191. body, err := json.MarshalIndent(report, "", " ")
  192. if err != nil {
  193. t.Fatalf("marshal report: %v", err)
  194. }
  195. if err := os.WriteFile(path, body, 0o644); err != nil {
  196. t.Fatalf("write report: %v", err)
  197. }
  198. }
  199. if failures > 0 || (strict && missing > 0) {
  200. t.Fatalf("compatibility failures=%d missing=%d (strict=%v)", failures, missing, strict)
  201. }
  202. }