ddl_defaults_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. package executor
  2. import (
  3. "errors"
  4. "fmt"
  5. "testing"
  6. "time"
  7. "github.com/danfragoso/pizzasql-next/pkg/pgerror"
  8. "github.com/danfragoso/pizzasql-next/pkg/storage"
  9. )
  10. // testClock lets a test move the clock forward deterministically between
  11. // statements, so per-statement default evaluation is observable without
  12. // depending on wall-clock resolution.
  13. type testClock struct {
  14. now time.Time
  15. }
  16. func (c *testClock) Now() time.Time { return c.now }
  17. func (c *testClock) Advance(d time.Duration) { c.now = c.now.Add(d) }
  18. func newTestClock() *testClock {
  19. return &testClock{now: time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)}
  20. }
  21. func TestCreateTableWithCurrentTimestampDefaultAppliesOnInsert(t *testing.T) {
  22. _, schema, table := newTestDB(t)
  23. e := newExec(schema, table)
  24. clock := newTestClock()
  25. e.clock = clock.Now
  26. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
  27. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  28. res := execMust(t, e, "SELECT created_at FROM t WHERE id = 1")
  29. if res.RowCount != 1 {
  30. t.Fatalf("row count = %d, want 1", res.RowCount)
  31. }
  32. want := "2026-01-02 03:04:05"
  33. if got := res.Rows[0][0]; got != want {
  34. t.Fatalf("created_at = %v, want %q", got, want)
  35. }
  36. }
  37. func TestCurrentDateAndTimeDefaults(t *testing.T) {
  38. _, schema, table := newTestDB(t)
  39. e := newExec(schema, table)
  40. e.clock = newTestClock().Now
  41. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, d DATE DEFAULT CURRENT_DATE, tm TEXT DEFAULT CURRENT_TIME)")
  42. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  43. res := execMust(t, e, "SELECT d, tm FROM t WHERE id = 1")
  44. if got, want := res.Rows[0][0], "2026-01-02"; got != want {
  45. t.Fatalf("date = %v, want %q", got, want)
  46. }
  47. if got, want := res.Rows[0][1], "03:04:05"; got != want {
  48. t.Fatalf("time = %v, want %q", got, want)
  49. }
  50. }
  51. func TestWhitelistedDatetimeNowDefaults(t *testing.T) {
  52. _, schema, table := newTestDB(t)
  53. e := newExec(schema, table)
  54. e.clock = newTestClock().Now
  55. 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'))")
  56. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  57. res := execMust(t, e, "SELECT ts, d, tm FROM t WHERE id = 1")
  58. if got, want := res.Rows[0][0], "2026-01-02 03:04:05"; got != want {
  59. t.Fatalf("ts = %v, want %q", got, want)
  60. }
  61. if got, want := res.Rows[0][1], "2026-01-02"; got != want {
  62. t.Fatalf("d = %v, want %q", got, want)
  63. }
  64. if got, want := res.Rows[0][2], "03:04:05"; got != want {
  65. t.Fatalf("tm = %v, want %q", got, want)
  66. }
  67. }
  68. func TestExplicitNullWinsOverNonConstantDefault(t *testing.T) {
  69. _, schema, table := newTestDB(t)
  70. e := newExec(schema, table)
  71. e.clock = newTestClock().Now
  72. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
  73. execMust(t, e, "INSERT INTO t (id, created_at) VALUES (1, NULL)")
  74. res := execMust(t, e, "SELECT created_at FROM t WHERE id = 1")
  75. if got := res.Rows[0][0]; got != nil {
  76. t.Fatalf("created_at = %v, want NULL (an explicit NULL wins)", got)
  77. }
  78. }
  79. func TestUpdateOmittingColumnDoesNotReevaluateDefault(t *testing.T) {
  80. _, schema, table := newTestDB(t)
  81. e := newExec(schema, table)
  82. clock := newTestClock()
  83. e.clock = clock.Now
  84. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, label TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
  85. execMust(t, e, "INSERT INTO t (id, label) VALUES (1, 'first')")
  86. clock.Advance(time.Hour)
  87. execMust(t, e, "UPDATE t SET label = 'second' WHERE id = 1")
  88. res := execMust(t, e, "SELECT label, created_at FROM t WHERE id = 1")
  89. if got := res.Rows[0][0]; got != "second" {
  90. t.Fatalf("label = %v, want second", got)
  91. }
  92. if got, want := res.Rows[0][1], "2026-01-02 03:04:05"; got != want {
  93. t.Fatalf("created_at = %v, want the INSERT-time %q (UPDATE must not re-evaluate)", got, want)
  94. }
  95. }
  96. func TestInsertDefaultValuesUsesNonConstantDefault(t *testing.T) {
  97. _, schema, table := newTestDB(t)
  98. e := newExec(schema, table)
  99. e.clock = newTestClock().Now
  100. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, label TEXT DEFAULT 'new', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
  101. execMust(t, e, "INSERT INTO t DEFAULT VALUES")
  102. res := execMust(t, e, "SELECT label, created_at FROM t")
  103. if res.RowCount != 1 {
  104. t.Fatalf("row count = %d, want 1", res.RowCount)
  105. }
  106. if got := res.Rows[0][0]; got != "new" {
  107. t.Fatalf("label = %v, want new", got)
  108. }
  109. if got, want := res.Rows[0][1], "2026-01-02 03:04:05"; got != want {
  110. t.Fatalf("created_at = %v, want %q", got, want)
  111. }
  112. }
  113. func TestNonConstantDefaultEvaluatedOncePerStatement(t *testing.T) {
  114. _, schema, table := newTestDB(t)
  115. e := newExec(schema, table)
  116. clock := newTestClock()
  117. e.clock = clock.Now
  118. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
  119. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  120. clock.Advance(time.Minute)
  121. execMust(t, e, "INSERT INTO t (id) VALUES (2)")
  122. res := execMust(t, e, "SELECT id, created_at FROM t ORDER BY id")
  123. first := res.Rows[0][1]
  124. second := res.Rows[1][1]
  125. if first == second {
  126. t.Fatalf("two INSERT statements produced the same timestamp %v; each statement must evaluate its own default", first)
  127. }
  128. if compare(first, second) >= 0 {
  129. t.Fatalf("timestamps %v then %v are not ordered", first, second)
  130. }
  131. }
  132. func TestNonConstantDefaultAppliesInsideTransaction(t *testing.T) {
  133. _, schema, table := newTestDB(t)
  134. e := newExec(schema, table)
  135. e.clock = newTestClock().Now
  136. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
  137. execMust(t, e, "BEGIN")
  138. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  139. execMust(t, e, "COMMIT")
  140. // Read through a fresh executor so the value is the committed row, not the
  141. // transaction's staged overlay.
  142. e2 := newExec(schema, table)
  143. res := execMust(t, e2, "SELECT created_at FROM t WHERE id = 1")
  144. if got, want := res.Rows[0][0], "2026-01-02 03:04:05"; got != want {
  145. t.Fatalf("created_at = %v, want %q", got, want)
  146. }
  147. }
  148. func TestMultiRowInsertSharesStatementDefault(t *testing.T) {
  149. _, schema, table := newTestDB(t)
  150. e := newExec(schema, table)
  151. e.clock = newTestClock().Now
  152. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
  153. execMust(t, e, "INSERT INTO t (id) VALUES (1), (2)")
  154. res := execMust(t, e, "SELECT created_at FROM t")
  155. if res.Rows[0][0] != res.Rows[1][0] {
  156. t.Fatalf("multi-row INSERT timestamps = %v and %v, want one evaluation per statement", res.Rows[0][0], res.Rows[1][0])
  157. }
  158. }
  159. func TestAlterTableAddColumnBackfillsNonConstantDefaultPerRow(t *testing.T) {
  160. _, schema, table := newTestDB(t)
  161. e := newExec(schema, table)
  162. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  163. execMust(t, e, "INSERT INTO t (id) VALUES (1), (2)")
  164. // The storage-level evaluator is the per-row injection point for the
  165. // rewrite; a counting evaluator proves each rewritten row is stamped again.
  166. calls := 0
  167. schema.SetDefaultEvaluator(func(expression string) (interface{}, error) {
  168. calls++
  169. return fmt.Sprintf("stamp-%d", calls), nil
  170. })
  171. execMust(t, e, "ALTER TABLE t ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
  172. if calls != 2 {
  173. t.Fatalf("evaluator calls = %d, want one per rewritten row (2)", calls)
  174. }
  175. res := execMust(t, e, "SELECT id, created_at FROM t ORDER BY id")
  176. if got, want := res.Rows[0][1], "stamp-1"; got != want {
  177. t.Fatalf("row 1 created_at = %v, want %q", got, want)
  178. }
  179. if got, want := res.Rows[1][1], "stamp-2"; got != want {
  180. t.Fatalf("row 2 created_at = %v, want %q", got, want)
  181. }
  182. col, ok := e.schemaColumn(t, "t", "created_at")
  183. if !ok {
  184. t.Fatal("created_at column missing from schema")
  185. }
  186. if col.DefaultExpr == "" {
  187. t.Fatal("ADD COLUMN did not persist the default expression")
  188. }
  189. if col.Default != nil {
  190. t.Fatalf("ADD COLUMN stored an evaluated default %v instead of an expression", col.Default)
  191. }
  192. }
  193. // schemaColumn reads a column from the durable schema through a fresh manager,
  194. // which also proves the expression survives a reload.
  195. func (e *Executor) schemaColumn(t *testing.T, table, column string) (*storage.Column, bool) {
  196. t.Helper()
  197. reloaded := storage.NewSchemaManager(e.schema.GetPool(), e.schema.GetDatabaseName())
  198. schema, err := reloaded.GetSchema(table)
  199. if err != nil {
  200. t.Fatalf("reload schema: %v", err)
  201. }
  202. return schema.GetColumn(column)
  203. }
  204. func TestNonConstantDefaultSurvivesReload(t *testing.T) {
  205. pool, schema, table := newTestDB(t)
  206. e := newExec(schema, table)
  207. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
  208. reloaded := storage.NewSchemaManager(pool, "testdb")
  209. reloadedTable := storage.NewTableManager(pool, reloaded, "testdb")
  210. e2 := newExec(reloaded, reloadedTable)
  211. clock := newTestClock()
  212. e2.clock = clock.Now
  213. col, ok := e2.schemaColumn(t, "t", "created_at")
  214. if !ok {
  215. t.Fatal("created_at column missing after reload")
  216. }
  217. if col.DefaultExpr != "CURRENT_TIMESTAMP" {
  218. t.Fatalf("reloaded DefaultExpr = %q, want CURRENT_TIMESTAMP", col.DefaultExpr)
  219. }
  220. if col.Default != nil {
  221. t.Fatalf("reloaded Default = %v, want nil", col.Default)
  222. }
  223. execMust(t, e2, "INSERT INTO t (id) VALUES (1)")
  224. res := execMust(t, e2, "SELECT created_at FROM t WHERE id = 1")
  225. if got, want := res.Rows[0][0], "2026-01-02 03:04:05"; got != want {
  226. t.Fatalf("created_at after reload = %v, want %q", got, want)
  227. }
  228. }
  229. func TestUnsupportedNonConstantDefaultsAreRejected(t *testing.T) {
  230. _, schema, table := newTestDB(t)
  231. e := newExec(schema, table)
  232. for _, sql := range []string{
  233. "CREATE TABLE t1 (id INTEGER PRIMARY KEY, token TEXT DEFAULT lower('X'))",
  234. "CREATE TABLE t2 (id INTEGER PRIMARY KEY, token TEXT DEFAULT upper('x'))",
  235. "CREATE TABLE t3 (id INTEGER PRIMARY KEY, n INTEGER DEFAULT (2 + 3))",
  236. } {
  237. if _, err := execSQL(e, sql); !errors.Is(err, pgerror.ErrFeatureNotSupported) {
  238. t.Fatalf("%s: error = %v, want ErrFeatureNotSupported", sql, err)
  239. }
  240. }
  241. }
  242. func TestAlterTableRejectsUnsupportedNonConstantDefault(t *testing.T) {
  243. _, schema, table := newTestDB(t)
  244. e := newExec(schema, table)
  245. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  246. if _, err := execSQL(e, "ALTER TABLE t ADD COLUMN token TEXT DEFAULT lower('X')"); !errors.Is(err, pgerror.ErrFeatureNotSupported) {
  247. t.Fatalf("ALTER error = %v, want ErrFeatureNotSupported", err)
  248. }
  249. }
  250. func TestConstantDefaultsStillApplyOnInsert(t *testing.T) {
  251. _, schema, table := newTestDB(t)
  252. e := newExec(schema, table)
  253. 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)")
  254. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  255. res := execMust(t, e, "SELECT label, score, flag, note FROM t WHERE id = 1")
  256. if res.RowCount != 1 {
  257. t.Fatalf("row count = %d, want 1", res.RowCount)
  258. }
  259. if got := res.Rows[0][0]; got != "new" {
  260. t.Fatalf("label = %v, want new", got)
  261. }
  262. if got := res.Rows[0][1]; got != int64(-1) {
  263. t.Fatalf("score = %v (%T), want int64(-1)", got, got)
  264. }
  265. if got := res.Rows[0][2]; got != false {
  266. t.Fatalf("flag = %v, want false", got)
  267. }
  268. if got := res.Rows[0][3]; got != nil {
  269. t.Fatalf("note = %v, want NULL", got)
  270. }
  271. }
  272. func TestAlterTableAddColumnBackfillsExistingRows(t *testing.T) {
  273. _, schema, table := newTestDB(t)
  274. e := newExec(schema, table)
  275. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  276. execMust(t, e, "INSERT INTO t (id) VALUES (1)")
  277. execMust(t, e, "ALTER TABLE t ADD COLUMN status TEXT DEFAULT 'pending'")
  278. res := execMust(t, e, "SELECT id, status FROM t WHERE id = 1")
  279. if res.RowCount != 1 {
  280. t.Fatalf("row count = %d, want 1", res.RowCount)
  281. }
  282. if got := res.Rows[0][1]; got != "pending" {
  283. t.Fatalf("existing row status = %v, want pending (ADD COLUMN backfills)", got)
  284. }
  285. execMust(t, e, "INSERT INTO t (id) VALUES (2)")
  286. res = execMust(t, e, "SELECT status FROM t WHERE id = 2")
  287. if got := res.Rows[0][0]; got != "pending" {
  288. t.Fatalf("new row status = %v, want pending", got)
  289. }
  290. }