ddl_defaults_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. package executor
  2. import (
  3. "errors"
  4. "testing"
  5. "time"
  6. "github.com/danfragoso/pizzasql-next/pkg/pgerror"
  7. "github.com/danfragoso/pizzasql-next/pkg/storage"
  8. )
  9. // testClock lets a test move the clock forward deterministically between
  10. // statements, so per-statement default evaluation is observable without
  11. // depending on wall-clock resolution.
  12. type testClock struct {
  13. now time.Time
  14. }
  15. func (c *testClock) Now() time.Time { return c.now }
  16. func (c *testClock) Advance(d time.Duration) { c.now = c.now.Add(d) }
  17. func newTestClock() *testClock {
  18. return &testClock{now: time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)}
  19. }
  20. // steppingClock advances one second per call, so a test can see whether a
  21. // value was evaluated once per statement or once per row.
  22. type steppingClock struct {
  23. now time.Time
  24. }
  25. func (c *steppingClock) Now() time.Time {
  26. current := c.now
  27. c.now = c.now.Add(time.Second)
  28. return current
  29. }
  30. func TestCreateTableWithCurrentTimestampDefaultAppliesOnInsert(t *testing.T) {
  31. _, schema, table := newTestDB(t)
  32. e := newExec(schema, table)
  33. clock := newTestClock()
  34. e.clock = clock.Now
  35. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
  36. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  37. res := execMust(t, e, "SELECT created_at FROM t WHERE id = 1")
  38. if res.RowCount != 1 {
  39. t.Fatalf("row count = %d, want 1", res.RowCount)
  40. }
  41. want := "2026-01-02 03:04:05"
  42. if got := res.Rows[0][0]; got != want {
  43. t.Fatalf("created_at = %v, want %q", got, want)
  44. }
  45. }
  46. func TestCurrentDateAndTimeDefaults(t *testing.T) {
  47. _, schema, table := newTestDB(t)
  48. e := newExec(schema, table)
  49. e.clock = newTestClock().Now
  50. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, d DATE DEFAULT CURRENT_DATE, tm TEXT DEFAULT CURRENT_TIME)")
  51. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  52. res := execMust(t, e, "SELECT d, tm FROM t WHERE id = 1")
  53. if got, want := res.Rows[0][0], "2026-01-02"; got != want {
  54. t.Fatalf("date = %v, want %q", got, want)
  55. }
  56. if got, want := res.Rows[0][1], "03:04:05"; got != want {
  57. t.Fatalf("time = %v, want %q", got, want)
  58. }
  59. }
  60. func TestWhitelistedDatetimeNowDefaults(t *testing.T) {
  61. _, schema, table := newTestDB(t)
  62. e := newExec(schema, table)
  63. e.clock = newTestClock().Now
  64. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, ts TIMESTAMP DEFAULT datetime('now'), d DATE DEFAULT date('now'), tm TEXT DEFAULT time('now'))")
  65. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  66. res := execMust(t, e, "SELECT ts, d, tm FROM t WHERE id = 1")
  67. if got, want := res.Rows[0][0], "2026-01-02 03:04:05"; got != want {
  68. t.Fatalf("ts = %v, want %q", got, want)
  69. }
  70. if got, want := res.Rows[0][1], "2026-01-02"; got != want {
  71. t.Fatalf("d = %v, want %q", got, want)
  72. }
  73. if got, want := res.Rows[0][2], "03:04:05"; got != want {
  74. t.Fatalf("tm = %v, want %q", got, want)
  75. }
  76. }
  77. func TestExplicitNullWinsOverNonConstantDefault(t *testing.T) {
  78. _, schema, table := newTestDB(t)
  79. e := newExec(schema, table)
  80. e.clock = newTestClock().Now
  81. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
  82. execMust(t, e, "INSERT INTO t (id, created_at) VALUES (1, NULL)")
  83. res := execMust(t, e, "SELECT created_at FROM t WHERE id = 1")
  84. if got := res.Rows[0][0]; got != nil {
  85. t.Fatalf("created_at = %v, want NULL (an explicit NULL wins)", got)
  86. }
  87. }
  88. func TestUpdateOmittingColumnDoesNotReevaluateDefault(t *testing.T) {
  89. _, schema, table := newTestDB(t)
  90. e := newExec(schema, table)
  91. clock := newTestClock()
  92. e.clock = clock.Now
  93. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, label TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
  94. execMust(t, e, "INSERT INTO t (id, label) VALUES (1, 'first')")
  95. clock.Advance(time.Hour)
  96. execMust(t, e, "UPDATE t SET label = 'second' WHERE id = 1")
  97. res := execMust(t, e, "SELECT label, created_at FROM t WHERE id = 1")
  98. if got := res.Rows[0][0]; got != "second" {
  99. t.Fatalf("label = %v, want second", got)
  100. }
  101. if got, want := res.Rows[0][1], "2026-01-02 03:04:05"; got != want {
  102. t.Fatalf("created_at = %v, want the INSERT-time %q (UPDATE must not re-evaluate)", got, want)
  103. }
  104. }
  105. func TestInsertDefaultValuesUsesNonConstantDefault(t *testing.T) {
  106. _, schema, table := newTestDB(t)
  107. e := newExec(schema, table)
  108. e.clock = newTestClock().Now
  109. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, label TEXT DEFAULT 'new', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
  110. execMust(t, e, "INSERT INTO t DEFAULT VALUES")
  111. res := execMust(t, e, "SELECT label, created_at FROM t")
  112. if res.RowCount != 1 {
  113. t.Fatalf("row count = %d, want 1", res.RowCount)
  114. }
  115. if got := res.Rows[0][0]; got != "new" {
  116. t.Fatalf("label = %v, want new", got)
  117. }
  118. if got, want := res.Rows[0][1], "2026-01-02 03:04:05"; got != want {
  119. t.Fatalf("created_at = %v, want %q", got, want)
  120. }
  121. }
  122. func TestNonConstantDefaultEvaluatedOncePerStatement(t *testing.T) {
  123. _, schema, table := newTestDB(t)
  124. e := newExec(schema, table)
  125. clock := newTestClock()
  126. e.clock = clock.Now
  127. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
  128. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  129. clock.Advance(time.Minute)
  130. execMust(t, e, "INSERT INTO t (id) VALUES (2)")
  131. res := execMust(t, e, "SELECT id, created_at FROM t ORDER BY id")
  132. first := res.Rows[0][1]
  133. second := res.Rows[1][1]
  134. if first == second {
  135. t.Fatalf("two INSERT statements produced the same timestamp %v; each statement must evaluate its own default", first)
  136. }
  137. if compare(first, second) >= 0 {
  138. t.Fatalf("timestamps %v then %v are not ordered", first, second)
  139. }
  140. }
  141. func TestNonConstantDefaultAppliesInsideTransaction(t *testing.T) {
  142. _, schema, table := newTestDB(t)
  143. e := newExec(schema, table)
  144. e.clock = newTestClock().Now
  145. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
  146. execMust(t, e, "BEGIN")
  147. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  148. execMust(t, e, "COMMIT")
  149. // Read through a fresh executor so the value is the committed row, not the
  150. // transaction's staged overlay.
  151. e2 := newExec(schema, table)
  152. res := execMust(t, e2, "SELECT created_at FROM t WHERE id = 1")
  153. if got, want := res.Rows[0][0], "2026-01-02 03:04:05"; got != want {
  154. t.Fatalf("created_at = %v, want %q", got, want)
  155. }
  156. }
  157. func TestMultiRowInsertSharesStatementDefault(t *testing.T) {
  158. _, schema, table := newTestDB(t)
  159. e := newExec(schema, table)
  160. e.clock = newTestClock().Now
  161. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
  162. execMust(t, e, "INSERT INTO t (id) VALUES (1), (2)")
  163. res := execMust(t, e, "SELECT created_at FROM t")
  164. if res.Rows[0][0] != res.Rows[1][0] {
  165. t.Fatalf("multi-row INSERT timestamps = %v and %v, want one evaluation per statement", res.Rows[0][0], res.Rows[1][0])
  166. }
  167. }
  168. func TestAlterTableAddColumnBackfillsNonConstantDefaultOncePerStatement(t *testing.T) {
  169. _, schema, table := newTestDB(t)
  170. e := newExec(schema, table)
  171. // A stepping clock makes a per-row evaluation visible: every backfilled row
  172. // must share the one instant sampled for the ALTER statement.
  173. base := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
  174. calls := 0
  175. e.clock = func() time.Time {
  176. at := base.Add(time.Duration(calls) * time.Second)
  177. calls++
  178. return at
  179. }
  180. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  181. execMust(t, e, "INSERT INTO t (id) VALUES (1), (2)")
  182. execMust(t, e, "ALTER TABLE t ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
  183. if calls != 1 {
  184. t.Fatalf("clock sampled %d times, want once per ALTER statement", calls)
  185. }
  186. res := execMust(t, e, "SELECT id, created_at FROM t ORDER BY id")
  187. first, second := res.Rows[0][1], res.Rows[1][1]
  188. if first != second {
  189. t.Fatalf("timestamps %v and %v differ; the backfill must use one statement instant", first, second)
  190. }
  191. if got, want := first, "2026-01-02 03:04:05"; got != want {
  192. t.Fatalf("created_at = %v, want the statement instant %q", got, want)
  193. }
  194. col, ok := e.schemaColumn(t, "t", "created_at")
  195. if !ok {
  196. t.Fatal("created_at column missing from schema")
  197. }
  198. if col.DefaultExpr == "" {
  199. t.Fatal("ADD COLUMN did not persist the default expression")
  200. }
  201. if col.Default != nil {
  202. t.Fatalf("ADD COLUMN stored an evaluated default %v instead of an expression", col.Default)
  203. }
  204. }
  205. func TestMultipleClockDefaultsInOneRowShareInstant(t *testing.T) {
  206. _, schema, table := newTestDB(t)
  207. e := newExec(schema, table)
  208. // A stepping clock makes per-column sampling visible: the two columns must
  209. // still share the single instant sampled for the statement.
  210. e.clock = (&steppingClock{now: time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)}).Now
  211. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
  212. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  213. res := execMust(t, e, "SELECT created_at, updated_at FROM t WHERE id = 1")
  214. if got := res.Rows[0][0]; got != "2026-01-02 03:04:05" {
  215. t.Fatalf("created_at = %v, want the statement instant", got)
  216. }
  217. if res.Rows[0][0] != res.Rows[0][1] {
  218. t.Fatalf("created_at = %v, updated_at = %v; both clock defaults must share one instant", res.Rows[0][0], res.Rows[0][1])
  219. }
  220. }
  221. func TestParenthesizedClockDefaults(t *testing.T) {
  222. _, schema, table := newTestDB(t)
  223. e := newExec(schema, table)
  224. e.clock = newTestClock().Now
  225. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, a TIMESTAMP DEFAULT (CURRENT_TIMESTAMP), b TIMESTAMP DEFAULT (datetime('now')))")
  226. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  227. execMust(t, e, "ALTER TABLE t ADD COLUMN c TIMESTAMP DEFAULT (CURRENT_TIMESTAMP)")
  228. res := execMust(t, e, "SELECT a, b, c FROM t WHERE id = 1")
  229. want := "2026-01-02 03:04:05"
  230. for i, name := range []string{"a", "b", "c"} {
  231. if got := res.Rows[0][i]; got != want {
  232. t.Fatalf("%s = %v, want %q", name, got, want)
  233. }
  234. }
  235. execMust(t, e, "INSERT INTO t (id) VALUES (2)")
  236. res = execMust(t, e, "SELECT a, b, c FROM t WHERE id = 2")
  237. if got := res.Rows[0][2]; got != want {
  238. t.Fatalf("new row c = %v, want %q", got, want)
  239. }
  240. }
  241. func TestAlterTableAddColumnUniqueClockDefaultRejectsDuplicates(t *testing.T) {
  242. _, schema, table := newTestDB(t)
  243. e := newExec(schema, table)
  244. e.clock = newTestClock().Now // one fixed instant: both backfilled rows collide
  245. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  246. execMust(t, e, "INSERT INTO t (id) VALUES (1), (2)")
  247. _, err := execSQL(e, "ALTER TABLE t ADD COLUMN tag TEXT DEFAULT CURRENT_TIMESTAMP UNIQUE")
  248. var violation *storage.UniqueViolationError
  249. if !errors.As(err, &violation) {
  250. t.Fatalf("ALTER error = %v, want a UNIQUE violation", err)
  251. }
  252. // The duplicate must abort before the column and index become visible.
  253. stored, gerr := storage.NewSchemaManager(schema.GetPool(), schema.GetDatabaseName()).GetSchema("t")
  254. if gerr != nil {
  255. t.Fatalf("reload schema: %v", gerr)
  256. }
  257. if _, ok := stored.GetColumn("tag"); ok {
  258. t.Fatal("aborted ADD COLUMN published the tag column")
  259. }
  260. if e.schema.IndexExists("uniq_t_tag") {
  261. t.Fatal("aborted ADD COLUMN published the UNIQUE index")
  262. }
  263. }
  264. func TestAlterTableAddColumnUniqueClockDefaultUsesStatementInstant(t *testing.T) {
  265. _, schema, table := newTestDB(t)
  266. e := newExec(schema, table)
  267. base := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
  268. calls := 0
  269. e.clock = func() time.Time {
  270. at := base.Add(time.Duration(calls) * time.Second)
  271. calls++
  272. return at
  273. }
  274. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  275. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  276. execMust(t, e, "ALTER TABLE t ADD COLUMN tag TEXT DEFAULT CURRENT_TIMESTAMP UNIQUE")
  277. if calls != 1 {
  278. t.Fatalf("clock sampled %d times, want once per ALTER statement", calls)
  279. }
  280. res := execMust(t, e, "SELECT id, tag FROM t ORDER BY id")
  281. if got, want := res.Rows[0][1], "2026-01-02 03:04:05"; got != want {
  282. t.Fatalf("tag = %v, want the statement instant %q", got, want)
  283. }
  284. // A later INSERT samples its own statement instant, so it does not collide.
  285. execMust(t, e, "INSERT INTO t (id) VALUES (2)")
  286. next := execMust(t, e, "SELECT tag FROM t WHERE id = 2")
  287. if next.Rows[0][0] == res.Rows[0][1] {
  288. t.Fatalf("later INSERT tag = %v, want a new statement instant", next.Rows[0][0])
  289. }
  290. // The published index still rejects an explicit duplicate.
  291. if _, err := execSQL(e, "INSERT INTO t (id, tag) VALUES (3, '"+res.Rows[0][1].(string)+"')"); err == nil {
  292. t.Fatal("expected the published UNIQUE index to reject a duplicate")
  293. }
  294. }
  295. // schemaColumn reads a column from the durable schema through a fresh manager,
  296. // which also proves the expression survives a reload.
  297. func (e *Executor) schemaColumn(t *testing.T, table, column string) (*storage.Column, bool) {
  298. t.Helper()
  299. reloaded := storage.NewSchemaManager(e.schema.GetPool(), e.schema.GetDatabaseName())
  300. schema, err := reloaded.GetSchema(table)
  301. if err != nil {
  302. t.Fatalf("reload schema: %v", err)
  303. }
  304. return schema.GetColumn(column)
  305. }
  306. func TestNonConstantDefaultSurvivesReload(t *testing.T) {
  307. pool, schema, table := newTestDB(t)
  308. e := newExec(schema, table)
  309. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
  310. reloaded := storage.NewSchemaManager(pool, "testdb")
  311. reloadedTable := storage.NewTableManager(pool, reloaded, "testdb")
  312. e2 := newExec(reloaded, reloadedTable)
  313. clock := newTestClock()
  314. e2.clock = clock.Now
  315. col, ok := e2.schemaColumn(t, "t", "created_at")
  316. if !ok {
  317. t.Fatal("created_at column missing after reload")
  318. }
  319. if col.DefaultExpr != "CURRENT_TIMESTAMP" {
  320. t.Fatalf("reloaded DefaultExpr = %q, want CURRENT_TIMESTAMP", col.DefaultExpr)
  321. }
  322. if col.Default != nil {
  323. t.Fatalf("reloaded Default = %v, want nil", col.Default)
  324. }
  325. execMust(t, e2, "INSERT INTO t (id) VALUES (1)")
  326. res := execMust(t, e2, "SELECT created_at FROM t WHERE id = 1")
  327. if got, want := res.Rows[0][0], "2026-01-02 03:04:05"; got != want {
  328. t.Fatalf("created_at after reload = %v, want %q", got, want)
  329. }
  330. }
  331. func TestUnsupportedNonConstantDefaultsAreRejected(t *testing.T) {
  332. _, schema, table := newTestDB(t)
  333. e := newExec(schema, table)
  334. for _, sql := range []string{
  335. "CREATE TABLE t1 (id INTEGER PRIMARY KEY, token TEXT DEFAULT lower('X'))",
  336. "CREATE TABLE t2 (id INTEGER PRIMARY KEY, token TEXT DEFAULT upper('x'))",
  337. "CREATE TABLE t3 (id INTEGER PRIMARY KEY, n INTEGER DEFAULT (2 + 3))",
  338. } {
  339. if _, err := execSQL(e, sql); !errors.Is(err, pgerror.ErrFeatureNotSupported) {
  340. t.Fatalf("%s: error = %v, want ErrFeatureNotSupported", sql, err)
  341. }
  342. }
  343. }
  344. func TestAlterTableRejectsUnsupportedNonConstantDefault(t *testing.T) {
  345. _, schema, table := newTestDB(t)
  346. e := newExec(schema, table)
  347. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  348. if _, err := execSQL(e, "ALTER TABLE t ADD COLUMN token TEXT DEFAULT lower('X')"); !errors.Is(err, pgerror.ErrFeatureNotSupported) {
  349. t.Fatalf("ALTER error = %v, want ErrFeatureNotSupported", err)
  350. }
  351. }
  352. func TestConstantDefaultsStillApplyOnInsert(t *testing.T) {
  353. _, schema, table := newTestDB(t)
  354. e := newExec(schema, table)
  355. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, label TEXT DEFAULT 'new', score INTEGER DEFAULT -1, flag BOOLEAN DEFAULT false, note TEXT DEFAULT NULL)")
  356. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  357. res := execMust(t, e, "SELECT label, score, flag, note FROM t WHERE id = 1")
  358. if res.RowCount != 1 {
  359. t.Fatalf("row count = %d, want 1", res.RowCount)
  360. }
  361. if got := res.Rows[0][0]; got != "new" {
  362. t.Fatalf("label = %v, want new", got)
  363. }
  364. if got := res.Rows[0][1]; got != int64(-1) {
  365. t.Fatalf("score = %v (%T), want int64(-1)", got, got)
  366. }
  367. if got := res.Rows[0][2]; got != false {
  368. t.Fatalf("flag = %v, want false", got)
  369. }
  370. if got := res.Rows[0][3]; got != nil {
  371. t.Fatalf("note = %v, want NULL", got)
  372. }
  373. }
  374. func TestAlterTableAddColumnBackfillsExistingRows(t *testing.T) {
  375. _, schema, table := newTestDB(t)
  376. e := newExec(schema, table)
  377. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  378. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  379. execMust(t, e, "ALTER TABLE t ADD COLUMN status TEXT DEFAULT 'pending'")
  380. res := execMust(t, e, "SELECT id, status FROM t WHERE id = 1")
  381. if res.RowCount != 1 {
  382. t.Fatalf("row count = %d, want 1", res.RowCount)
  383. }
  384. if got := res.Rows[0][1]; got != "pending" {
  385. t.Fatalf("existing row status = %v, want pending (ADD COLUMN backfills)", got)
  386. }
  387. execMust(t, e, "INSERT INTO t (id) VALUES (2)")
  388. res = execMust(t, e, "SELECT status FROM t WHERE id = 2")
  389. if got := res.Rows[0][0]; got != "pending" {
  390. t.Fatalf("new row status = %v, want pending", got)
  391. }
  392. }