2
0

check_constraint_test.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  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 at the statement instant, and the rewrite writes that
  231. // same instant, so every backfilled row carries the value the predicate checked.
  232. func TestAlterTableAddColumnCheckSeesClockDefault(t *testing.T) {
  233. _, schema, table := newTestDB(t)
  234. e := newExec(schema, table)
  235. base := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
  236. calls := 0
  237. e.clock = func() time.Time {
  238. at := base.Add(time.Duration(calls) * time.Second)
  239. calls++
  240. return at
  241. }
  242. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  243. execMust(t, e, "INSERT INTO t (id) VALUES (1), (2)")
  244. execMust(t, e, "ALTER TABLE t ADD COLUMN ts TEXT DEFAULT CURRENT_TIMESTAMP CHECK (ts IS NOT NULL)")
  245. if calls != 1 {
  246. t.Fatalf("clock sampled %d times, want once per ALTER statement", calls)
  247. }
  248. res := execMust(t, e, "SELECT id, ts FROM t ORDER BY id")
  249. if res.RowCount != 2 {
  250. t.Fatalf("rows = %d, want 2", res.RowCount)
  251. }
  252. if got, want := res.Rows[0][1], "2026-01-02 03:04:05"; got != want {
  253. t.Fatalf("row 1 ts = %v, want the statement instant %q", got, want)
  254. }
  255. if res.Rows[0][1] != res.Rows[1][1] {
  256. t.Fatalf("timestamps %v and %v differ; the backfill must use one statement instant", res.Rows[0][1], res.Rows[1][1])
  257. }
  258. stored, err := schema.GetSchema("t")
  259. if err != nil {
  260. t.Fatalf("get schema: %v", err)
  261. }
  262. if _, ok := stored.GetColumn("ts"); !ok {
  263. t.Fatal("ADD COLUMN ... CHECK published no column")
  264. }
  265. if len(stored.Checks) != 1 {
  266. t.Fatalf("checks = %+v, want 1", stored.Checks)
  267. }
  268. // A clock default that violates the predicate rejects the DDL. NULL passes
  269. // CHECK, so this also proves the predicate saw the evaluated value.
  270. e.clock = newTestClock().Now
  271. _, err = execSQL(e, "ALTER TABLE t ADD COLUMN bad_ts TEXT DEFAULT CURRENT_TIMESTAMP CHECK (bad_ts < '2026-01-02 03:04:05')")
  272. requireCheckViolation(t, err, "")
  273. stored, err = schema.GetSchema("t")
  274. if err != nil {
  275. t.Fatalf("get schema after rejected DDL: %v", err)
  276. }
  277. if _, ok := stored.GetColumn("bad_ts"); ok {
  278. t.Fatal("rejected ADD COLUMN ... CHECK left the clock-default column in the schema")
  279. }
  280. if len(stored.Checks) != 1 {
  281. t.Fatalf("checks after rejected DDL = %+v, want the original one", stored.Checks)
  282. }
  283. }
  284. // One instant is sampled per ALTER statement and shared by the CHECK validation
  285. // and the rewrite. A stepping clock reproduces the old race: a second sample in
  286. // the rewrite could cross a literal boundary and publish a row that violates the
  287. // predicate the statement just validated.
  288. func TestAlterTableAddColumnCheckUsesOneInstantForValidationAndRewrite(t *testing.T) {
  289. _, schema, table := newTestDB(t)
  290. e := newExec(schema, table)
  291. base := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
  292. boundary := "2026-01-02 03:04:06"
  293. calls := 0
  294. e.clock = func() time.Time {
  295. at := base.Add(time.Duration(calls) * time.Second)
  296. calls++
  297. return at
  298. }
  299. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  300. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  301. execMust(t, e, "ALTER TABLE t ADD COLUMN ts TEXT DEFAULT CURRENT_TIMESTAMP CHECK (ts < '"+boundary+"')")
  302. if calls != 1 {
  303. t.Fatalf("clock sampled %d times, want one instant per ALTER statement", calls)
  304. }
  305. res := execMust(t, e, "SELECT ts FROM t WHERE id = 1")
  306. if got := res.Rows[0][0]; got == nil || got == boundary || compare(got, boundary) >= 0 {
  307. t.Fatalf("backfilled ts = %v, want the pre-boundary statement instant %q", got, base.Format("2006-01-02 15:04:05"))
  308. }
  309. if len(execMust(t, e, "SELECT ts FROM t").Rows) != 1 {
  310. t.Fatal("ADD COLUMN ... CHECK published an unexpected row set")
  311. }
  312. }
  313. // A constant default that violates an ADD COLUMN ... CHECK still fails with
  314. // 23514 and publishes nothing.
  315. func TestAlterTableAddColumnCheckRejectsViolatingConstantDefault(t *testing.T) {
  316. _, schema, table := newTestDB(t)
  317. e := newExec(schema, table)
  318. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  319. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  320. _, err := execSQL(e, "ALTER TABLE t ADD COLUMN n INTEGER DEFAULT -1 CHECK (n >= 0)")
  321. requireCheckViolation(t, err, "")
  322. stored, err := schema.GetSchema("t")
  323. if err != nil {
  324. t.Fatalf("get schema: %v", err)
  325. }
  326. if _, ok := stored.GetColumn("n"); ok {
  327. t.Fatal("rejected ADD COLUMN ... CHECK left the column in the schema")
  328. }
  329. if len(stored.Checks) != 0 {
  330. t.Fatalf("checks = %+v, want none", stored.Checks)
  331. }
  332. }
  333. // A transaction that stages a row before a concurrent ADD COLUMN ... CHECK is
  334. // validated at commit, so the backfilled default cannot bypass the predicate.
  335. func TestAlterTableAddColumnCheckRejectsRowStagedBeforeDDL(t *testing.T) {
  336. _, schema, table := newTestDB(t)
  337. writer := newExec(schema, table)
  338. ddl := newExec(schema, table)
  339. execMust(t, writer, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  340. execMust(t, writer, "BEGIN")
  341. execMust(t, writer, "INSERT INTO t (id) VALUES (1)")
  342. // The table is durably empty, so the ALTER accepts the constraint.
  343. execMust(t, ddl, "ALTER TABLE t ADD COLUMN qty INTEGER DEFAULT -1 CHECK (qty >= 0)")
  344. _, err := execSQL(writer, "COMMIT")
  345. requireCheckViolation(t, err, "")
  346. if got := execMust(t, ddl, "SELECT id FROM t").RowCount; got != 0 {
  347. t.Fatalf("rows after rejected commit = %d, want 0", got)
  348. }
  349. }
  350. // The persisted predicate is durable: a fresh SchemaManager loaded from the same
  351. // store keeps enforcing it.
  352. func TestCheckConstraintDurableReload(t *testing.T) {
  353. pool, schema, table := newTestDB(t)
  354. e := newExec(schema, table)
  355. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER, CONSTRAINT positive_age CHECK (age > 0))")
  356. execMust(t, e, "INSERT INTO t (id, age) VALUES (1, 10)")
  357. reloaded := storage.NewSchemaManager(pool, "testdb")
  358. reloadedTable := storage.NewTableManager(pool, reloaded, "testdb")
  359. reopened := newExec(reloaded, reloadedTable)
  360. stored, err := reloaded.GetSchema("t")
  361. if err != nil {
  362. t.Fatalf("reload schema: %v", err)
  363. }
  364. if len(stored.Checks) != 1 || stored.Checks[0].Name != "positive_age" || !strings.Contains(stored.Checks[0].Expression, "age") {
  365. t.Fatalf("reloaded checks = %+v, want the persisted positive_age predicate", stored.Checks)
  366. }
  367. _, err = execSQL(reopened, "INSERT INTO t (id, age) VALUES (2, -3)")
  368. requireCheckViolation(t, err, "positive_age")
  369. execMust(t, reopened, "INSERT INTO t (id, age) VALUES (3, 30)")
  370. execMust(t, reopened, "UPDATE t SET age = 40 WHERE id = 3")
  371. if _, err := execSQL(reopened, "UPDATE t SET age = -40 WHERE id = 3"); err == nil {
  372. t.Fatal("update violating the reloaded check succeeded")
  373. }
  374. }
  375. // The emulated SQLite catalog exposes the constraint in the table SQL.
  376. func TestCheckConstraintInSQLiteCatalog(t *testing.T) {
  377. _, schema, table := newTestDB(t)
  378. e := newExec(schema, table)
  379. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER, CONSTRAINT positive_age CHECK (age > 0), CHECK (id > 0))")
  380. res := execMust(t, e, "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 't'")
  381. if res.RowCount != 1 {
  382. t.Fatalf("sqlite_master rows = %d, want 1", res.RowCount)
  383. }
  384. sql, _ := res.Rows[0][0].(string)
  385. for _, want := range []string{"CONSTRAINT `positive_age` CHECK", "(id > 0)"} {
  386. if !strings.Contains(sql, want) {
  387. t.Fatalf("catalog SQL %q does not contain %q", sql, want)
  388. }
  389. }
  390. res = execMust(t, e, "SELECT sql FROM sqlite_schema WHERE name = 't'")
  391. if res.RowCount != 1 || !strings.Contains(res.Rows[0][0].(string), "positive_age") {
  392. t.Fatalf("sqlite_schema SQL = %v, want the named CHECK", res.Rows)
  393. }
  394. }
  395. // Non-deterministic and non-row predicates are rejected with feature-not-
  396. // supported instead of being accepted as constraints the engine cannot enforce.
  397. func TestCheckConstraintRejectsUnsupportedExpressions(t *testing.T) {
  398. tests := []struct {
  399. name string
  400. setup []string
  401. ddl string
  402. }{
  403. {
  404. name: "subquery",
  405. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > (SELECT 1)))",
  406. },
  407. {
  408. name: "aggregate",
  409. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (COUNT(age) > 0))",
  410. },
  411. {
  412. name: "window function",
  413. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (ROW_NUMBER() OVER (ORDER BY age) > 0))",
  414. },
  415. {
  416. name: "non-deterministic function",
  417. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (RANDOM() > 0))",
  418. },
  419. {
  420. name: "qualified column",
  421. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (t.age > 0))",
  422. },
  423. {
  424. name: "qualified column in parentheses",
  425. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK ((t.age) > 0))",
  426. },
  427. {
  428. name: "qualified column in cast",
  429. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (CAST(t.age AS INTEGER) > 0))",
  430. },
  431. {
  432. name: "unknown column",
  433. ddl: "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (missing > 0))",
  434. },
  435. {
  436. name: "alter add column subquery",
  437. setup: []string{"CREATE TABLE t (id INTEGER PRIMARY KEY)"},
  438. ddl: "ALTER TABLE t ADD COLUMN age INTEGER CHECK (age > (SELECT 1))",
  439. },
  440. }
  441. for _, tt := range tests {
  442. t.Run(tt.name, func(t *testing.T) {
  443. _, schema, table := newTestDB(t)
  444. e := newExec(schema, table)
  445. for _, sql := range tt.setup {
  446. execMust(t, e, sql)
  447. }
  448. if _, err := execSQL(e, tt.ddl); !errors.Is(err, pgerror.ErrFeatureNotSupported) {
  449. t.Fatalf("%s: error = %v, want ErrFeatureNotSupported", tt.ddl, err)
  450. }
  451. // A rejected declaration publishes nothing: no table when the DDL
  452. // was CREATE TABLE, and no added column or check for ALTER TABLE.
  453. stored, err := schema.GetSchema("t")
  454. if err != nil {
  455. if len(tt.setup) == 0 {
  456. return
  457. }
  458. t.Fatalf("get schema: %v", err)
  459. }
  460. if len(stored.Checks) != 0 {
  461. t.Fatalf("checks after rejected DDL = %+v, want none", stored.Checks)
  462. }
  463. if _, ok := stored.GetColumn("age"); ok {
  464. t.Fatalf("column age after rejected DDL = ok, want absent")
  465. }
  466. })
  467. }
  468. }
  469. // A CHECK predicate is stored as SQL text, so DROP/RENAME COLUMN refuse to leave
  470. // it referencing a column that no longer exists.
  471. func TestCheckConstraintGuardsColumnChange(t *testing.T) {
  472. _, schema, table := newTestDB(t)
  473. e := newExec(schema, table)
  474. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > 0), note TEXT)")
  475. for _, sql := range []string{
  476. "ALTER TABLE t RENAME COLUMN age TO years",
  477. "ALTER TABLE t DROP COLUMN age",
  478. } {
  479. if _, err := execSQL(e, sql); !errors.Is(err, pgerror.ErrFeatureNotSupported) {
  480. t.Fatalf("%s: error = %v, want ErrFeatureNotSupported", sql, err)
  481. }
  482. }
  483. // Columns the predicate does not reference can still be renamed or dropped.
  484. execMust(t, e, "ALTER TABLE t RENAME COLUMN note TO memo")
  485. execMust(t, e, "INSERT INTO t (id, age, memo) VALUES (1, 5, 'ok')")
  486. execMust(t, e, "ALTER TABLE t DROP COLUMN memo")
  487. }
  488. // The accepted expression surface survives the round trip through the durable
  489. // schema text: each predicate is re-parsed and still enforced after a reload.
  490. func TestCheckConstraintExpressionRoundTrip(t *testing.T) {
  491. tests := []struct {
  492. name string
  493. check string
  494. columns string
  495. valid string // VALUES list for a row that satisfies the predicate
  496. violated string // VALUES list for a row that violates it
  497. }{
  498. {"comparison and boolean", "age > 0 AND age < 200", "id, age", "(1, 10)", "(1, 500)"},
  499. {"LIKE", "name LIKE 'a%'", "id, name", "(1, 'abc')", "(1, 'zzz')"},
  500. {"CAST", "CAST(score AS INTEGER) >= 0", "id, score", "(1, 5)", "(1, -5)"},
  501. {"BETWEEN", "LENGTH(code) BETWEEN 1 AND 8", "id, code", "(1, 'abcd')", "(1, 'toolongcode')"},
  502. {"function", "LOWER(status) = 'ready'", "id, status", "(1, 'READY')", "(1, 'waiting')"},
  503. {"is null", "note IS NOT NULL", "id, note", "(1, 'x')", "(1, NULL)"},
  504. }
  505. for _, tt := range tests {
  506. t.Run(tt.name, func(t *testing.T) {
  507. pool, schema, table := newTestDB(t)
  508. e := newExec(schema, table)
  509. execMust(t, e, `CREATE TABLE t (
  510. id INTEGER PRIMARY KEY,
  511. age INTEGER,
  512. name TEXT,
  513. score INTEGER,
  514. code TEXT,
  515. status TEXT,
  516. note TEXT,
  517. CHECK (`+tt.check+`)
  518. )`)
  519. execMust(t, e, "INSERT INTO t ("+tt.columns+") VALUES "+tt.valid)
  520. reloaded := storage.NewSchemaManager(pool, "testdb")
  521. reopened := newExec(reloaded, storage.NewTableManager(pool, reloaded, "testdb"))
  522. if _, err := execSQL(reopened, "INSERT INTO t ("+tt.columns+") VALUES "+tt.violated); err == nil {
  523. t.Fatalf("reloaded table accepted a row violating %s", tt.check)
  524. } else {
  525. requireCheckViolation(t, err, "")
  526. }
  527. })
  528. }
  529. }
  530. // The persisted predicate text must round-trip. A quoted keyword column is the
  531. // motivating case: FormatExpr quotes it, so CREATE TABLE, later writes, and a
  532. // durable reload all keep binding to the column instead of the keyword.
  533. func TestCheckConstraintQuotedKeywordColumn(t *testing.T) {
  534. pool, schema, table := newTestDB(t)
  535. e := newExec(schema, table)
  536. execMust(t, e, `CREATE TABLE t (id INTEGER PRIMARY KEY, "order" INTEGER CHECK ("order" > 0))`)
  537. stored, err := schema.GetSchema("t")
  538. if err != nil {
  539. t.Fatalf("get schema: %v", err)
  540. }
  541. if len(stored.Checks) != 1 {
  542. t.Fatalf("checks = %+v, want 1", stored.Checks)
  543. }
  544. if !strings.Contains(stored.Checks[0].Expression, `"order"`) {
  545. t.Fatalf("stored expression = %q, want the keyword column quoted", stored.Checks[0].Expression)
  546. }
  547. _, err = execSQL(e, `INSERT INTO t (id, "order") VALUES (1, -1)`)
  548. requireCheckViolation(t, err, "")
  549. execMust(t, e, `INSERT INTO t (id, "order") VALUES (1, 3)`)
  550. // The durable text stays evaluable after reload.
  551. reloaded := storage.NewSchemaManager(pool, "testdb")
  552. reopened := newExec(reloaded, storage.NewTableManager(pool, reloaded, "testdb"))
  553. _, err = execSQL(reopened, `INSERT INTO t (id, "order") VALUES (2, -2)`)
  554. requireCheckViolation(t, err, "")
  555. execMust(t, reopened, `INSERT INTO t (id, "order") VALUES (2, 4)`)
  556. }
  557. // A predicate whose formatted text cannot be re-parsed is rejected at DDL time
  558. // instead of being published as a constraint that fails every later write.
  559. func TestCheckConstraintRejectsNonRoundTrippingExpression(t *testing.T) {
  560. schema := &storage.Schema{Name: "t", Columns: []storage.Column{{Name: "id", Type: "INTEGER"}}}
  561. // FormatExpr renders a number literal verbatim; this one is not valid SQL.
  562. _, err := checkConstraintFor("", &parser.LiteralExpr{Type: lexer.TokenNumber, Value: "1 +"}, schema)
  563. if !errors.Is(err, pgerror.ErrFeatureNotSupported) {
  564. t.Fatalf("error = %v, want ErrFeatureNotSupported", err)
  565. }
  566. if !strings.Contains(err.Error(), "cannot be stored") {
  567. t.Fatalf("error = %q, want the round-trip explanation", err.Error())
  568. }
  569. }