check_constraint_test.go 17 KB

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