check_constraint_test.go 19 KB

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