| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- package pgserver
- // Compatibility suite for the pChat-style workload: DDL, upsert idempotency,
- // constraints, pagination, parameters, and error codes. It runs only when
- // PIZZASQL_COMPAT=1 so the regular unit suite stays fast; `make compat` enables
- // it and writes a JSON report.
- //
- // The suite distinguishes:
- // - pass: the build behaves as documented
- // - missing: a documented capability is absent from this build (for example a
- // rejection introduced by a newer release)
- // - fail: behavior contradicts the documented contract
- //
- // PIZZASQL_COMPAT_STRICT=1 turns "missing" into a failure, which is what a
- // release gate should use. The compatibility document is pkg/capabilities.
- import (
- "encoding/json"
- "os"
- "strconv"
- "testing"
- )
- func rowValue(rows [][]string, index int) string {
- if len(rows) <= index || len(rows[index]) == 0 {
- return ""
- }
- return rows[index][0]
- }
- type compatResult struct {
- Name string `json:"name"`
- Status string `json:"status"` // pass, missing, fail
- Detail string `json:"detail,omitempty"`
- }
- type compatReport struct {
- Engine string `json:"engine"`
- Results []compatResult `json:"results"`
- }
- func TestCompatibilitySuite(t *testing.T) {
- if os.Getenv("PIZZASQL_COMPAT") != "1" {
- t.Skip("set PIZZASQL_COMPAT=1 (or run make compat) to run the compatibility suite")
- }
- strict := os.Getenv("PIZZASQL_COMPAT_STRICT") == "1"
- c, client := newDataConnection(t)
- report := compatReport{Engine: "pizzasql"}
- record := func(name, status, detail string) {
- report.Results = append(report.Results, compatResult{Name: name, Status: status, Detail: detail})
- }
- // exec runs a statement and returns the SQLSTATE code ("" on success) and
- // decoded rows.
- exec := func(sql string) (string, [][]string) {
- msgs := runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(sql), 0)})
- if len(msgs) == 0 {
- return "", nil
- }
- if msgs[0].Type == MsgErrorResponse {
- return errorCode(msgs[0]), nil
- }
- var rows [][]string
- for _, msg := range msgs[1:] {
- if msg.Type != MsgDataRow {
- continue
- }
- rows = append(rows, dataRowValues(t, msg))
- }
- return "", rows
- }
- expectOK := func(name, sql string) {
- if code, _ := exec(sql); code != "" {
- record(name, "fail", "unexpected SQLSTATE "+code)
- return
- }
- record(name, "pass", "")
- }
- expectCode := func(name, sql, want string) {
- code, _ := exec(sql)
- switch code {
- case want:
- record(name, "pass", "")
- case "":
- record(name, "missing", "statement succeeded; this build does not enforce "+want)
- default:
- record(name, "missing", "got SQLSTATE "+code+", want "+want)
- }
- }
- expectRows := func(name, sql string, want ...string) {
- code, rows := exec(sql)
- if code != "" {
- record(name, "fail", "unexpected SQLSTATE "+code)
- return
- }
- if len(rows) != 1 || len(rows[0]) != len(want) {
- record(name, "fail", "unexpected rows")
- return
- }
- for i := range want {
- if rows[0][i] != want[i] {
- record(name, "fail", "row "+rows[0][i]+" != "+want[i])
- return
- }
- }
- record(name, "pass", "")
- }
- expectOK("ddl.create_table", `CREATE TABLE accounts (
- id INTEGER PRIMARY KEY,
- email TEXT NOT NULL UNIQUE,
- plan TEXT NOT NULL DEFAULT 'free',
- seats INTEGER NOT NULL DEFAULT 1
- )`)
- expectOK("dml.insert", `INSERT INTO accounts (id, email, plan, seats) VALUES (1, 'a@x', 'pro', 3)`)
- expectRows("dml.select_constant_default", `SELECT plan, seats FROM accounts WHERE id = 1`, "pro", "3")
- expectOK("upsert.first", `INSERT INTO accounts (id, email, plan) VALUES (2, 'b@x', 'free') ON CONFLICT(id) DO UPDATE SET plan = excluded.plan`)
- expectOK("upsert.idempotent", `INSERT INTO accounts (id, email, plan) VALUES (2, 'b@x', 'pro') ON CONFLICT(id) DO UPDATE SET plan = excluded.plan`)
- expectRows("upsert.result", `SELECT plan FROM accounts WHERE id = 2`, "pro")
- expectCode("constraints.unique_23505", `INSERT INTO accounts (id, email) VALUES (3, 'a@x')`, "23505")
- expectCode("ddl.composite_pk_0A000", `CREATE TABLE bad_pk (a INTEGER, b INTEGER, PRIMARY KEY (a, b))`, "0A000")
- expectCode("ddl.nonconstant_default_0A000", `CREATE TABLE bad_default (id INTEGER PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)`, "0A000")
- expectCode("locking.for_update_0A000", `SELECT * FROM accounts FOR UPDATE`, "0A000")
- expectCode("arrays.any_0A000", `SELECT * FROM accounts WHERE id = ANY('{1,2}')`, "0A000")
- // Documented current limitation: CHECK and FOREIGN KEY are parsed but not
- // enforced. A release that starts rejecting them will report "missing"
- // here and the capability document must be updated.
- expectOK("limitations.check_not_enforced", `CREATE TABLE checked (n INTEGER CHECK (n > 0))`)
- expectOK("limitations.check_insert_violation", `INSERT INTO checked (n) VALUES (-1)`)
- expectOK("limitations.foreign_key_not_enforced", `CREATE TABLE child (id INTEGER PRIMARY KEY, parent_id INTEGER, FOREIGN KEY (parent_id) REFERENCES accounts(id))`)
- expectOK("limitations.orphan_insert", `INSERT INTO child (id, parent_id) VALUES (1, 999)`)
- expectCode("errors.syntax_42601", `SELEC 1`, "42601")
- // Explicit transactions buffer writes and validate unique indexes at
- // COMMIT, so the duplicate insert itself succeeds and the commit fails.
- expectOK("tx.begin", `BEGIN`)
- expectOK("tx.buffer_duplicate", `INSERT INTO accounts (id, email) VALUES (3, 'a@x')`)
- expectCode("tx.commit_unique_23505", `COMMIT`, "23505")
- // A failed COMMIT has already rolled back; the session must be usable
- // again instead of being stuck in the failed-transaction state.
- expectRows("tx.after_failed_commit_idle", `SELECT 1`, "1")
- expectOK("tx.begin_after_failed_commit", `BEGIN`)
- expectOK("tx.rollback", `ROLLBACK`)
- expectOK("pagination.seed", `INSERT INTO accounts (id, email) VALUES (10, 'p10@x'), (11, 'p11@x'), (12, 'p12@x')`)
- code, rows := exec(`SELECT _rowid_ FROM accounts WHERE _rowid_ > 1 ORDER BY _rowid_ LIMIT 2`)
- first, firstErr := strconv.ParseInt(rowValue(rows, 0), 10, 64)
- second, secondErr := strconv.ParseInt(rowValue(rows, 1), 10, 64)
- switch {
- case code != "":
- record("pagination.rowid", "fail", "unexpected SQLSTATE "+code)
- case len(rows) == 2 && firstErr == nil && secondErr == nil && first < second:
- record("pagination.rowid", "pass", "")
- default:
- record("pagination.rowid", "fail", "cursor did not advance")
- }
- failures, missing := 0, 0
- for _, result := range report.Results {
- switch result.Status {
- case "fail":
- failures++
- t.Errorf("%s: %s", result.Name, result.Detail)
- case "missing":
- missing++
- t.Logf("%s: %s", result.Name, result.Detail)
- }
- }
- if path := os.Getenv("PIZZASQL_COMPAT_REPORT"); path != "" {
- body, err := json.MarshalIndent(report, "", " ")
- if err != nil {
- t.Fatalf("marshal report: %v", err)
- }
- if err := os.WriteFile(path, body, 0o644); err != nil {
- t.Fatalf("write report: %v", err)
- }
- }
- if failures > 0 || (strict && missing > 0) {
- t.Fatalf("compatibility failures=%d missing=%d (strict=%v)", failures, missing, strict)
- }
- }
|