check_constraint_test.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. package executor
  2. import (
  3. "errors"
  4. "strings"
  5. "testing"
  6. "time"
  7. "github.com/danfragoso/pizzasql-next/pkg/lexer"
  8. "github.com/danfragoso/pizzasql-next/pkg/parser"
  9. "github.com/danfragoso/pizzasql-next/pkg/pgerror"
  10. "github.com/danfragoso/pizzasql-next/pkg/storage"
  11. )
  12. // requireCheckViolation asserts that err is a CHECK violation for constraint.
  13. func requireCheckViolation(t *testing.T, err error, constraint string) {
  14. t.Helper()
  15. var violation *storage.CheckViolationError
  16. if !errors.As(err, &violation) {
  17. t.Fatalf("error = %v, want CheckViolationError", err)
  18. }
  19. if constraint != "" && violation.Constraint != constraint {
  20. t.Fatalf("constraint = %q, want %q", violation.Constraint, constraint)
  21. }
  22. if !strings.HasPrefix(err.Error(), "CHECK constraint failed: ") {
  23. t.Fatalf("message = %q, want the CHECK constraint failure prefix", err.Error())
  24. }
  25. }
  26. // Inline, table-level, named, and unnamed CHECK declarations all persist and
  27. // enforce the same predicate.
  28. func TestCheckConstraintDeclarationForms(t *testing.T) {
  29. tests := []struct {
  30. name string
  31. ddl string
  32. constraint string // expected label on violation
  33. }{
  34. {
  35. name: "inline unnamed",
  36. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > 0))",
  37. },
  38. {
  39. name: "table level unnamed",
  40. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER, CHECK (age > 0))",
  41. },
  42. {
  43. name: "column level named",
  44. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CONSTRAINT positive_age CHECK (age > 0))",
  45. constraint: "positive_age",
  46. },
  47. {
  48. name: "table level named",
  49. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER, CONSTRAINT positive_age CHECK (age > 0))",
  50. constraint: "positive_age",
  51. },
  52. }
  53. for _, tt := range tests {
  54. t.Run(tt.name, func(t *testing.T) {
  55. _, schema, table := newTestDB(t)
  56. e := newExec(schema, table)
  57. execMust(t, e, tt.ddl)
  58. stored, err := schema.GetSchema("t")
  59. if err != nil {
  60. t.Fatalf("get schema: %v", err)
  61. }
  62. if len(stored.Checks) != 1 {
  63. t.Fatalf("checks = %+v, want exactly one persisted CHECK", stored.Checks)
  64. }
  65. if tt.constraint != "" && stored.Checks[0].Name != tt.constraint {
  66. t.Fatalf("check name = %q, want %q", stored.Checks[0].Name, tt.constraint)
  67. }
  68. if _, err := execSQL(e, "INSERT INTO t (id, age) VALUES (1, -1)"); err == nil {
  69. t.Fatal("insert of a violating row succeeded")
  70. } else {
  71. requireCheckViolation(t, err, tt.constraint)
  72. }
  73. execMust(t, e, "INSERT INTO t (id, age) VALUES (2, 1)")
  74. execMust(t, e, "INSERT INTO t (id, age) VALUES (3, NULL)")
  75. })
  76. }
  77. }
  78. // A CHECK predicate that is NULL or unknown for a row passes, matching SQL
  79. // three-valued logic, on INSERT and on UPDATE.
  80. func TestCheckConstraintNullAndUnknownPass(t *testing.T) {
  81. _, schema, table := newTestDB(t)
  82. e := newExec(schema, table)
  83. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER, CHECK (age > 0 AND age < 200))")
  84. execMust(t, e, "INSERT INTO t (id, age) VALUES (1, NULL)")
  85. execMust(t, e, "INSERT INTO t (id) VALUES (2)")
  86. execMust(t, e, "INSERT INTO t (id, age) VALUES (3, 30)")
  87. execMust(t, e, "UPDATE t SET age = NULL WHERE id = 3")
  88. res := execMust(t, e, "SELECT id, age FROM t ORDER BY id")
  89. if res.RowCount != 3 {
  90. t.Fatalf("rows = %d, want 3 nullable rows", res.RowCount)
  91. }
  92. for _, row := range res.Rows {
  93. if row[1] != nil {
  94. t.Fatalf("age = %v, want NULL", row[1])
  95. }
  96. }
  97. }
  98. // Multi-row INSERT is atomic: a violating row rejects the whole statement and
  99. // leaves no rows behind.
  100. func TestCheckConstraintMultiRowInsertIsAtomic(t *testing.T) {
  101. _, schema, table := newTestDB(t)
  102. e := newExec(schema, table)
  103. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > 0))")
  104. _, err := execSQL(e, "INSERT INTO t (id, age) VALUES (1, 10), (2, -5)")
  105. requireCheckViolation(t, err, "")
  106. if res := execMust(t, e, "SELECT id FROM t"); res.RowCount != 0 {
  107. t.Fatalf("rows after rejected multi-row insert = %d, want 0", res.RowCount)
  108. }
  109. execMust(t, e, "INSERT INTO t (id, age) VALUES (1, 10), (2, 20)")
  110. if res := execMust(t, e, "SELECT id FROM t ORDER BY id"); res.RowCount != 2 {
  111. t.Fatalf("rows = %d, want 2", res.RowCount)
  112. }
  113. }
  114. // UPDATE re-evaluates every CHECK predicate against the post-assignment row and
  115. // rolls the row back when a predicate fails.
  116. func TestCheckConstraintUpdateViolation(t *testing.T) {
  117. _, schema, table := newTestDB(t)
  118. e := newExec(schema, table)
  119. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > 0))")
  120. execMust(t, e, "INSERT INTO t (id, age) VALUES (1, 10)")
  121. _, err := execSQL(e, "UPDATE t SET age = -1 WHERE id = 1")
  122. requireCheckViolation(t, err, "")
  123. res := execMust(t, e, "SELECT age FROM t WHERE id = 1")
  124. if res.RowCount != 1 || res.Rows[0][0] != int64(10) {
  125. t.Fatalf("age after rejected update = %v, want 10", res.Rows)
  126. }
  127. execMust(t, e, "UPDATE t SET age = age + 5 WHERE id = 1")
  128. res = execMust(t, e, "SELECT age FROM t WHERE id = 1")
  129. if res.Rows[0][0] != int64(15) {
  130. t.Fatalf("age after valid update = %v, want 15", res.Rows[0][0])
  131. }
  132. }
  133. // ON CONFLICT DO UPDATE validates the merged row against the CHECK predicates
  134. // before it is staged.
  135. func TestCheckConstraintOnConflictDoUpdateViolation(t *testing.T) {
  136. _, schema, table := newTestDB(t)
  137. e := newExec(schema, table)
  138. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, qty INTEGER CHECK (qty > 0))")
  139. execMust(t, e, "INSERT INTO t (id, qty) VALUES (1, 5)")
  140. _, err := execSQL(e, "INSERT INTO t (id, qty) VALUES (1, -2) ON CONFLICT (id) DO UPDATE SET qty = excluded.qty")
  141. requireCheckViolation(t, err, "")
  142. res := execMust(t, e, "SELECT qty FROM t WHERE id = 1")
  143. if res.Rows[0][0] != int64(5) {
  144. t.Fatalf("qty after rejected upsert = %v, want 5", res.Rows[0][0])
  145. }
  146. execMust(t, e, "INSERT INTO t (id, qty) VALUES (1, 7) ON CONFLICT (id) DO UPDATE SET qty = excluded.qty")
  147. res = execMust(t, e, "SELECT qty FROM t WHERE id = 1")
  148. if res.Rows[0][0] != int64(7) {
  149. t.Fatalf("qty after valid upsert = %v, want 7", res.Rows[0][0])
  150. }
  151. }
  152. // Every CHECK on a table is enforced, and the violation names the constraint
  153. // that rejected the row.
  154. func TestCheckConstraintMultipleChecks(t *testing.T) {
  155. _, schema, table := newTestDB(t)
  156. e := newExec(schema, table)
  157. execMust(t, e, `CREATE TABLE t (
  158. id INTEGER PRIMARY KEY,
  159. age INTEGER CHECK (age > 0),
  160. score INTEGER,
  161. CONSTRAINT score_range CHECK (score >= 0 AND score <= 100)
  162. )`)
  163. stored, err := schema.GetSchema("t")
  164. if err != nil {
  165. t.Fatalf("get schema: %v", err)
  166. }
  167. if len(stored.Checks) != 2 {
  168. t.Fatalf("checks = %+v, want 2", stored.Checks)
  169. }
  170. _, err = execSQL(e, "INSERT INTO t (id, age, score) VALUES (1, -1, 50)")
  171. requireCheckViolation(t, err, "")
  172. _, err = execSQL(e, "INSERT INTO t (id, age, score) VALUES (2, 10, 101)")
  173. requireCheckViolation(t, err, "score_range")
  174. execMust(t, e, "INSERT INTO t (id, age, score) VALUES (3, 10, 100)")
  175. if res := execMust(t, e, "SELECT id FROM t"); res.RowCount != 1 {
  176. t.Fatalf("rows = %d, want 1", res.RowCount)
  177. }
  178. }
  179. // ADD COLUMN ... CHECK validates the rows that already exist, using the value
  180. // the constant-default backfill will write (or NULL when there is no default).
  181. func TestAlterTableAddColumnCheckValidatesExistingRows(t *testing.T) {
  182. _, schema, table := newTestDB(t)
  183. e := newExec(schema, table)
  184. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  185. execMust(t, e, "INSERT INTO t (id) VALUES (1), (2)")
  186. // A default satisfying the predicate is backfilled and the DDL succeeds.
  187. execMust(t, e, "ALTER TABLE t ADD COLUMN qty INTEGER DEFAULT 5 CHECK (qty >= 0)")
  188. stored, err := schema.GetSchema("t")
  189. if err != nil {
  190. t.Fatalf("get schema: %v", err)
  191. }
  192. if _, ok := stored.GetColumn("qty"); !ok {
  193. t.Fatal("ADD COLUMN ... CHECK published no column")
  194. }
  195. if len(stored.Checks) != 1 {
  196. t.Fatalf("checks = %+v, want 1", stored.Checks)
  197. }
  198. res := execMust(t, e, "SELECT qty FROM t ORDER BY id")
  199. if len(res.Rows) != 2 || res.Rows[0][0] != int64(5) || res.Rows[1][0] != int64(5) {
  200. t.Fatalf("backfilled qty = %v, want [5 5]", res.Rows)
  201. }
  202. // A violation is enforced for later writes as well.
  203. if _, err := execSQL(e, "INSERT INTO t (id, qty) VALUES (3, -1)"); err == nil {
  204. t.Fatal("insert violating the ADD COLUMN check succeeded")
  205. } else {
  206. requireCheckViolation(t, err, "")
  207. }
  208. // A default that violates the predicate rejects the DDL and publishes
  209. // nothing: no column, no constraint, and no rewritten rows.
  210. _, err = execSQL(e, "ALTER TABLE t ADD COLUMN bad INTEGER DEFAULT -1 CHECK (bad >= 0)")
  211. requireCheckViolation(t, err, "")
  212. stored, err = schema.GetSchema("t")
  213. if err != nil {
  214. t.Fatalf("get schema after rejected DDL: %v", err)
  215. }
  216. if _, ok := stored.GetColumn("bad"); ok {
  217. t.Fatal("rejected ADD COLUMN ... CHECK left the column in the schema")
  218. }
  219. if len(stored.Checks) != 1 {
  220. t.Fatalf("checks after rejected DDL = %+v, want the original one", stored.Checks)
  221. }
  222. // Without a default the existing rows read NULL, which passes.
  223. execMust(t, e, "ALTER TABLE t ADD COLUMN note TEXT CHECK (LENGTH(note) > 0)")
  224. if res := execMust(t, e, "SELECT id, note FROM t ORDER BY id"); res.RowCount != 2 || res.Rows[0][1] != nil {
  225. t.Fatalf("rows after nullable ADD COLUMN = %v, want NULL notes", res.Rows)
  226. }
  227. }
  228. // A clock-reading default must reach an ADD COLUMN ... CHECK predicate as the
  229. // value the backfill writes, not as NULL (which CHECK always passes). Validation
  230. // resolves the default through the same clock-aware evaluator as the rewrite, so
  231. // a per-row clock default is checked per row.
  232. func TestAlterTableAddColumnCheckSeesClockDefault(t *testing.T) {
  233. _, schema, table := newTestDB(t)
  234. e := newExec(schema, table)
  235. e.clock = (&steppingClock{now: time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)}).Now
  236. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  237. execMust(t, e, "INSERT INTO t (id) VALUES (1), (2)")
  238. execMust(t, e, "ALTER TABLE t ADD COLUMN ts TEXT DEFAULT CURRENT_TIMESTAMP CHECK (ts IS NOT NULL)")
  239. res := execMust(t, e, "SELECT id, ts FROM t ORDER BY id")
  240. if res.RowCount != 2 {
  241. t.Fatalf("rows = %d, want 2", res.RowCount)
  242. }
  243. if res.Rows[0][1] == nil || res.Rows[1][1] == nil {
  244. t.Fatalf("backfilled ts = %v, want evaluated timestamps", res.Rows)
  245. }
  246. if res.Rows[0][1] == res.Rows[1][1] {
  247. t.Fatalf("both rows got ts %v; the backfill evaluates per row", res.Rows[0][1])
  248. }
  249. stored, err := schema.GetSchema("t")
  250. if err != nil {
  251. t.Fatalf("get schema: %v", err)
  252. }
  253. if _, ok := stored.GetColumn("ts"); !ok {
  254. t.Fatal("ADD COLUMN ... CHECK published no column")
  255. }
  256. if len(stored.Checks) != 1 {
  257. t.Fatalf("checks = %+v, want 1", stored.Checks)
  258. }
  259. // A clock default that violates the predicate rejects the DDL. NULL passes
  260. // CHECK, so this also proves the predicate saw the evaluated value.
  261. _, err = execSQL(e, "ALTER TABLE t ADD COLUMN bad_ts TEXT DEFAULT CURRENT_TIMESTAMP CHECK (bad_ts < '2026-01-02 03:04:05')")
  262. requireCheckViolation(t, err, "")
  263. stored, err = schema.GetSchema("t")
  264. if err != nil {
  265. t.Fatalf("get schema after rejected DDL: %v", err)
  266. }
  267. if _, ok := stored.GetColumn("bad_ts"); ok {
  268. t.Fatal("rejected ADD COLUMN ... CHECK left the clock-default column in the schema")
  269. }
  270. if len(stored.Checks) != 1 {
  271. t.Fatalf("checks after rejected DDL = %+v, want the original one", stored.Checks)
  272. }
  273. }
  274. // A constant default that violates an ADD COLUMN ... CHECK still fails with
  275. // 23514 and publishes nothing.
  276. func TestAlterTableAddColumnCheckRejectsViolatingConstantDefault(t *testing.T) {
  277. _, schema, table := newTestDB(t)
  278. e := newExec(schema, table)
  279. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  280. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  281. _, err := execSQL(e, "ALTER TABLE t ADD COLUMN n INTEGER DEFAULT -1 CHECK (n >= 0)")
  282. requireCheckViolation(t, err, "")
  283. stored, err := schema.GetSchema("t")
  284. if err != nil {
  285. t.Fatalf("get schema: %v", err)
  286. }
  287. if _, ok := stored.GetColumn("n"); ok {
  288. t.Fatal("rejected ADD COLUMN ... CHECK left the column in the schema")
  289. }
  290. if len(stored.Checks) != 0 {
  291. t.Fatalf("checks = %+v, want none", stored.Checks)
  292. }
  293. }
  294. // A transaction that stages a row before a concurrent ADD COLUMN ... CHECK is
  295. // validated at commit, so the backfilled default cannot bypass the predicate.
  296. func TestAlterTableAddColumnCheckRejectsRowStagedBeforeDDL(t *testing.T) {
  297. _, schema, table := newTestDB(t)
  298. writer := newExec(schema, table)
  299. ddl := newExec(schema, table)
  300. execMust(t, writer, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  301. execMust(t, writer, "BEGIN")
  302. execMust(t, writer, "INSERT INTO t (id) VALUES (1)")
  303. // The table is durably empty, so the ALTER accepts the constraint.
  304. execMust(t, ddl, "ALTER TABLE t ADD COLUMN qty INTEGER DEFAULT -1 CHECK (qty >= 0)")
  305. _, err := execSQL(writer, "COMMIT")
  306. requireCheckViolation(t, err, "")
  307. if got := execMust(t, ddl, "SELECT id FROM t").RowCount; got != 0 {
  308. t.Fatalf("rows after rejected commit = %d, want 0", got)
  309. }
  310. }
  311. // The persisted predicate is durable: a fresh SchemaManager loaded from the same
  312. // store keeps enforcing it.
  313. func TestCheckConstraintDurableReload(t *testing.T) {
  314. pool, schema, table := newTestDB(t)
  315. e := newExec(schema, table)
  316. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER, CONSTRAINT positive_age CHECK (age > 0))")
  317. execMust(t, e, "INSERT INTO t (id, age) VALUES (1, 10)")
  318. reloaded := storage.NewSchemaManager(pool, "testdb")
  319. reloadedTable := storage.NewTableManager(pool, reloaded, "testdb")
  320. reopened := newExec(reloaded, reloadedTable)
  321. stored, err := reloaded.GetSchema("t")
  322. if err != nil {
  323. t.Fatalf("reload schema: %v", err)
  324. }
  325. if len(stored.Checks) != 1 || stored.Checks[0].Name != "positive_age" || !strings.Contains(stored.Checks[0].Expression, "age") {
  326. t.Fatalf("reloaded checks = %+v, want the persisted positive_age predicate", stored.Checks)
  327. }
  328. _, err = execSQL(reopened, "INSERT INTO t (id, age) VALUES (2, -3)")
  329. requireCheckViolation(t, err, "positive_age")
  330. execMust(t, reopened, "INSERT INTO t (id, age) VALUES (3, 30)")
  331. execMust(t, reopened, "UPDATE t SET age = 40 WHERE id = 3")
  332. if _, err := execSQL(reopened, "UPDATE t SET age = -40 WHERE id = 3"); err == nil {
  333. t.Fatal("update violating the reloaded check succeeded")
  334. }
  335. }
  336. // The emulated SQLite catalog exposes the constraint in the table SQL.
  337. func TestCheckConstraintInSQLiteCatalog(t *testing.T) {
  338. _, schema, table := newTestDB(t)
  339. e := newExec(schema, table)
  340. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER, CONSTRAINT positive_age CHECK (age > 0), CHECK (id > 0))")
  341. res := execMust(t, e, "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 't'")
  342. if res.RowCount != 1 {
  343. t.Fatalf("sqlite_master rows = %d, want 1", res.RowCount)
  344. }
  345. sql, _ := res.Rows[0][0].(string)
  346. for _, want := range []string{"CONSTRAINT `positive_age` CHECK", "(id > 0)"} {
  347. if !strings.Contains(sql, want) {
  348. t.Fatalf("catalog SQL %q does not contain %q", sql, want)
  349. }
  350. }
  351. res = execMust(t, e, "SELECT sql FROM sqlite_schema WHERE name = 't'")
  352. if res.RowCount != 1 || !strings.Contains(res.Rows[0][0].(string), "positive_age") {
  353. t.Fatalf("sqlite_schema SQL = %v, want the named CHECK", res.Rows)
  354. }
  355. }
  356. // Non-deterministic and non-row predicates are rejected with feature-not-
  357. // supported instead of being accepted as constraints the engine cannot enforce.
  358. func TestCheckConstraintRejectsUnsupportedExpressions(t *testing.T) {
  359. tests := []struct {
  360. name string
  361. setup []string
  362. ddl string
  363. }{
  364. {
  365. name: "subquery",
  366. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > (SELECT 1)))",
  367. },
  368. {
  369. name: "aggregate",
  370. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (COUNT(age) > 0))",
  371. },
  372. {
  373. name: "window function",
  374. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (ROW_NUMBER() OVER (ORDER BY age) > 0))",
  375. },
  376. {
  377. name: "non-deterministic function",
  378. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (RANDOM() > 0))",
  379. },
  380. {
  381. name: "qualified column",
  382. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (t.age > 0))",
  383. },
  384. {
  385. name: "qualified column in parentheses",
  386. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK ((t.age) > 0))",
  387. },
  388. {
  389. name: "qualified column in cast",
  390. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (CAST(t.age AS INTEGER) > 0))",
  391. },
  392. {
  393. name: "unknown column",
  394. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (missing > 0))",
  395. },
  396. {
  397. name: "alter add column subquery",
  398. setup: []string{"CREATE TABLE t (id INTEGER PRIMARY KEY)"},
  399. ddl: "ALTER TABLE t ADD COLUMN age INTEGER CHECK (age > (SELECT 1))",
  400. },
  401. }
  402. for _, tt := range tests {
  403. t.Run(tt.name, func(t *testing.T) {
  404. _, schema, table := newTestDB(t)
  405. e := newExec(schema, table)
  406. for _, sql := range tt.setup {
  407. execMust(t, e, sql)
  408. }
  409. if _, err := execSQL(e, tt.ddl); !errors.Is(err, pgerror.ErrFeatureNotSupported) {
  410. t.Fatalf("%s: error = %v, want ErrFeatureNotSupported", tt.ddl, err)
  411. }
  412. // A rejected declaration publishes nothing: no table when the DDL
  413. // was CREATE TABLE, and no added column or check for ALTER TABLE.
  414. stored, err := schema.GetSchema("t")
  415. if err != nil {
  416. if len(tt.setup) == 0 {
  417. return
  418. }
  419. t.Fatalf("get schema: %v", err)
  420. }
  421. if len(stored.Checks) != 0 {
  422. t.Fatalf("checks after rejected DDL = %+v, want none", stored.Checks)
  423. }
  424. if _, ok := stored.GetColumn("age"); ok {
  425. t.Fatalf("column age after rejected DDL = ok, want absent")
  426. }
  427. })
  428. }
  429. }
  430. // A CHECK predicate is stored as SQL text, so DROP/RENAME COLUMN refuse to leave
  431. // it referencing a column that no longer exists.
  432. func TestCheckConstraintGuardsColumnChange(t *testing.T) {
  433. _, schema, table := newTestDB(t)
  434. e := newExec(schema, table)
  435. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > 0), note TEXT)")
  436. for _, sql := range []string{
  437. "ALTER TABLE t RENAME COLUMN age TO years",
  438. "ALTER TABLE t DROP COLUMN age",
  439. } {
  440. if _, err := execSQL(e, sql); !errors.Is(err, pgerror.ErrFeatureNotSupported) {
  441. t.Fatalf("%s: error = %v, want ErrFeatureNotSupported", sql, err)
  442. }
  443. }
  444. // Columns the predicate does not reference can still be renamed or dropped.
  445. execMust(t, e, "ALTER TABLE t RENAME COLUMN note TO memo")
  446. execMust(t, e, "INSERT INTO t (id, age, memo) VALUES (1, 5, 'ok')")
  447. execMust(t, e, "ALTER TABLE t DROP COLUMN memo")
  448. }
  449. // The accepted expression surface survives the round trip through the durable
  450. // schema text: each predicate is re-parsed and still enforced after a reload.
  451. func TestCheckConstraintExpressionRoundTrip(t *testing.T) {
  452. tests := []struct {
  453. name string
  454. check string
  455. columns string
  456. valid string // VALUES list for a row that satisfies the predicate
  457. violated string // VALUES list for a row that violates it
  458. }{
  459. {"comparison and boolean", "age > 0 AND age < 200", "id, age", "(1, 10)", "(1, 500)"},
  460. {"LIKE", "name LIKE 'a%'", "id, name", "(1, 'abc')", "(1, 'zzz')"},
  461. {"CAST", "CAST(score AS INTEGER) >= 0", "id, score", "(1, 5)", "(1, -5)"},
  462. {"BETWEEN", "LENGTH(code) BETWEEN 1 AND 8", "id, code", "(1, 'abcd')", "(1, 'toolongcode')"},
  463. {"function", "LOWER(status) = 'ready'", "id, status", "(1, 'READY')", "(1, 'waiting')"},
  464. {"is null", "note IS NOT NULL", "id, note", "(1, 'x')", "(1, NULL)"},
  465. }
  466. for _, tt := range tests {
  467. t.Run(tt.name, func(t *testing.T) {
  468. pool, schema, table := newTestDB(t)
  469. e := newExec(schema, table)
  470. execMust(t, e, `CREATE TABLE t (
  471. id INTEGER PRIMARY KEY,
  472. age INTEGER,
  473. name TEXT,
  474. score INTEGER,
  475. code TEXT,
  476. status TEXT,
  477. note TEXT,
  478. CHECK (`+tt.check+`)
  479. )`)
  480. execMust(t, e, "INSERT INTO t ("+tt.columns+") VALUES "+tt.valid)
  481. reloaded := storage.NewSchemaManager(pool, "testdb")
  482. reopened := newExec(reloaded, storage.NewTableManager(pool, reloaded, "testdb"))
  483. if _, err := execSQL(reopened, "INSERT INTO t ("+tt.columns+") VALUES "+tt.violated); err == nil {
  484. t.Fatalf("reloaded table accepted a row violating %s", tt.check)
  485. } else {
  486. requireCheckViolation(t, err, "")
  487. }
  488. })
  489. }
  490. }
  491. // The persisted predicate text must round-trip. A quoted keyword column is the
  492. // motivating case: FormatExpr quotes it, so CREATE TABLE, later writes, and a
  493. // durable reload all keep binding to the column instead of the keyword.
  494. func TestCheckConstraintQuotedKeywordColumn(t *testing.T) {
  495. pool, schema, table := newTestDB(t)
  496. e := newExec(schema, table)
  497. execMust(t, e, `CREATE TABLE t (id INTEGER PRIMARY KEY, "order" INTEGER CHECK ("order" > 0))`)
  498. stored, err := schema.GetSchema("t")
  499. if err != nil {
  500. t.Fatalf("get schema: %v", err)
  501. }
  502. if len(stored.Checks) != 1 {
  503. t.Fatalf("checks = %+v, want 1", stored.Checks)
  504. }
  505. if !strings.Contains(stored.Checks[0].Expression, `"order"`) {
  506. t.Fatalf("stored expression = %q, want the keyword column quoted", stored.Checks[0].Expression)
  507. }
  508. _, err = execSQL(e, `INSERT INTO t (id, "order") VALUES (1, -1)`)
  509. requireCheckViolation(t, err, "")
  510. execMust(t, e, `INSERT INTO t (id, "order") VALUES (1, 3)`)
  511. // The durable text stays evaluable after reload.
  512. reloaded := storage.NewSchemaManager(pool, "testdb")
  513. reopened := newExec(reloaded, storage.NewTableManager(pool, reloaded, "testdb"))
  514. _, err = execSQL(reopened, `INSERT INTO t (id, "order") VALUES (2, -2)`)
  515. requireCheckViolation(t, err, "")
  516. execMust(t, reopened, `INSERT INTO t (id, "order") VALUES (2, 4)`)
  517. }
  518. // A predicate whose formatted text cannot be re-parsed is rejected at DDL time
  519. // instead of being published as a constraint that fails every later write.
  520. func TestCheckConstraintRejectsNonRoundTrippingExpression(t *testing.T) {
  521. schema := &storage.Schema{Name: "t", Columns: []storage.Column{{Name: "id", Type: "INTEGER"}}}
  522. // FormatExpr renders a number literal verbatim; this one is not valid SQL.
  523. _, err := checkConstraintFor("", &parser.LiteralExpr{Type: lexer.TokenNumber, Value: "1 +"}, schema)
  524. if !errors.Is(err, pgerror.ErrFeatureNotSupported) {
  525. t.Fatalf("error = %v, want ErrFeatureNotSupported", err)
  526. }
  527. if !strings.Contains(err.Error(), "cannot be stored") {
  528. t.Fatalf("error = %q, want the round-trip explanation", err.Error())
  529. }
  530. }