alter_table_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. package executor
  2. import (
  3. "strings"
  4. "sync"
  5. "testing"
  6. "github.com/danfragoso/pizzasql-next/pkg/storage"
  7. )
  8. // ADD COLUMN only appends schema metadata. Existing rows are not rewritten, so
  9. // a declared DEFAULT applies to later INSERTs only.
  10. func TestAlterTableAddColumnDoesNotBackfillPopulatedTable(t *testing.T) {
  11. _, schema, table := newTestDB(t)
  12. e := newExec(schema, table)
  13. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  14. execMust(t, e, "INSERT INTO t (id) VALUES (1), (2)")
  15. execMust(t, e, "ALTER TABLE t ADD COLUMN note TEXT DEFAULT 'later'")
  16. res := execMust(t, e, "SELECT id, note FROM t ORDER BY id")
  17. if res.RowCount != 2 {
  18. t.Fatalf("row count = %d, want 2", res.RowCount)
  19. }
  20. for _, row := range res.Rows {
  21. if row[1] != nil {
  22. t.Fatalf("existing row %v note = %v, want NULL (no backfill)", row[0], row[1])
  23. }
  24. }
  25. execMust(t, e, "INSERT INTO t (id) VALUES (3)")
  26. res = execMust(t, e, "SELECT note FROM t WHERE id = 3")
  27. if got := res.Rows[0][0]; got != "later" {
  28. t.Fatalf("new row note = %v, want later", got)
  29. }
  30. }
  31. func TestAlterTableAddColumnIfNotExistsIsIdempotent(t *testing.T) {
  32. _, schema, table := newTestDB(t)
  33. e := newExec(schema, table)
  34. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  35. execMust(t, e, "ALTER TABLE t ADD COLUMN IF NOT EXISTS c TEXT")
  36. execMust(t, e, "ALTER TABLE t ADD COLUMN IF NOT EXISTS c TEXT")
  37. s, err := schema.GetSchema("t")
  38. if err != nil {
  39. t.Fatalf("get schema: %v", err)
  40. }
  41. count := 0
  42. for _, col := range s.Columns {
  43. if col.Name == "c" {
  44. count++
  45. }
  46. }
  47. if count != 1 {
  48. t.Fatalf("column c appears %d times, want 1", count)
  49. }
  50. }
  51. // Concurrent IF NOT EXISTS ADD COLUMN on one engine instance must not duplicate
  52. // metadata; the schema manager serializes the check-and-append.
  53. func TestAlterTableAddColumnIfNotExistsConcurrent(t *testing.T) {
  54. _, schema, table := newTestDB(t)
  55. e := newExec(schema, table)
  56. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  57. var wg sync.WaitGroup
  58. for i := 0; i < 8; i++ {
  59. wg.Add(1)
  60. go func() {
  61. defer wg.Done()
  62. session := newExec(schema, table)
  63. if _, err := execSQL(session, "ALTER TABLE t ADD COLUMN IF NOT EXISTS c TEXT"); err != nil {
  64. t.Errorf("add column: %v", err)
  65. }
  66. }()
  67. }
  68. wg.Wait()
  69. s, err := schema.GetSchema("t")
  70. if err != nil {
  71. t.Fatalf("get schema: %v", err)
  72. }
  73. count := 0
  74. for _, col := range s.Columns {
  75. if col.Name == "c" {
  76. count++
  77. }
  78. }
  79. if count != 1 {
  80. t.Fatalf("column c appears %d times, want 1", count)
  81. }
  82. }
  83. // DROP COLUMN rewrites every remaining row so the removed value does not stay
  84. // in durable storage.
  85. func TestAlterTableDropColumnRewritesPopulatedRows(t *testing.T) {
  86. _, schema, table := newTestDB(t)
  87. e := newExec(schema, table)
  88. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, keep TEXT, dropme TEXT)")
  89. execMust(t, e, "INSERT INTO t (id, keep, dropme) VALUES (1, 'a', 'secret'), (2, 'b', 'secret')")
  90. execMust(t, e, "ALTER TABLE t DROP COLUMN dropme")
  91. res := execMust(t, e, "SELECT * FROM t ORDER BY id")
  92. if len(res.Columns) != 2 || res.Columns[0] != "id" || res.Columns[1] != "keep" {
  93. t.Fatalf("columns after drop = %v, want [id keep]", res.Columns)
  94. }
  95. if res.RowCount != 2 || res.Rows[0][1] != "a" || res.Rows[1][1] != "b" {
  96. t.Fatalf("rows after drop = %v", res.Rows)
  97. }
  98. if _, err := execSQL(e, "SELECT dropme FROM t"); err == nil {
  99. t.Fatal("expected dropped column to be unknown")
  100. }
  101. }
  102. // RENAME COLUMN rewrites the durable index definitions along with the schema
  103. // and rows, so single- and multi-column indexes name the new column while
  104. // keeping their original index names.
  105. func TestAlterTableRenameColumnUpdatesIndexMetadata(t *testing.T) {
  106. pool, schema, table := newTestDB(t)
  107. e := newExec(schema, table)
  108. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, a TEXT, b TEXT)")
  109. execMust(t, e, "INSERT INTO t (id, a, b) VALUES (1, 'x', 'y')")
  110. execMust(t, e, "CREATE INDEX idx_a ON t(a)")
  111. execMust(t, e, "CREATE INDEX idx_ab ON t(a, b)")
  112. execMust(t, e, "ALTER TABLE t RENAME COLUMN a TO aa")
  113. single, err := schema.GetIndex("idx_a")
  114. if err != nil {
  115. t.Fatalf("get idx_a: %v", err)
  116. }
  117. if len(single.Columns) != 1 || single.Columns[0].Name != "aa" {
  118. t.Fatalf("idx_a columns = %#v, want [aa]", single.Columns)
  119. }
  120. composite, err := schema.GetIndex("idx_ab")
  121. if err != nil {
  122. t.Fatalf("get idx_ab: %v", err)
  123. }
  124. if len(composite.Columns) != 2 || composite.Columns[0].Name != "aa" || composite.Columns[1].Name != "b" {
  125. t.Fatalf("idx_ab columns = %#v, want [aa b]", composite.Columns)
  126. }
  127. // index_info reports the renamed column and resolves its column id.
  128. res := execMust(t, e, "PRAGMA index_info(idx_a)")
  129. if len(res.Rows) != 1 || res.Rows[0][1] != int64(1) || res.Rows[0][2] != "aa" {
  130. t.Fatalf("index_info(idx_a) = %v, want [[0 1 aa]]", res.Rows)
  131. }
  132. // sqlite_master renders the recreated index over the new column name.
  133. res = execMust(t, e, "SELECT sql FROM sqlite_master WHERE type='index' AND name='idx_ab'")
  134. if len(res.Rows) != 1 || !strings.Contains(res.Rows[0][0].(string), "(`aa`, `b`)") {
  135. t.Fatalf("recreated idx_ab SQL = %v, want a definition over aa, b", res.Rows)
  136. }
  137. // The definition is durable, not just cached on this SchemaManager.
  138. reloaded := storage.NewSchemaManager(pool, "testdb")
  139. composite, err = reloaded.GetIndex("idx_ab")
  140. if err != nil {
  141. t.Fatalf("reload idx_ab: %v", err)
  142. }
  143. if len(composite.Columns) != 2 || composite.Columns[0].Name != "aa" || composite.Columns[1].Name != "b" {
  144. t.Fatalf("reloaded idx_ab columns = %#v, want [aa b]", composite.Columns)
  145. }
  146. // Index-backed equality lookup still returns the row after the rename.
  147. res = execMust(t, e, "SELECT id, aa FROM t WHERE aa = 'x'")
  148. if res.RowCount != 1 || res.Rows[0][0] != int64(1) || res.Rows[0][1] != "x" {
  149. t.Fatalf("indexed lookup after rename = %v, want [[1 x]]", res.Rows)
  150. }
  151. }
  152. // A UNIQUE index keeps enforcing uniqueness after its column is renamed.
  153. func TestAlterTableRenameColumnKeepsUniqueIndexEnforcement(t *testing.T) {
  154. _, schema, table := newTestDB(t)
  155. e := newExec(schema, table)
  156. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT, tag TEXT)")
  157. execMust(t, e, "CREATE UNIQUE INDEX uq_email ON t(email)")
  158. execMust(t, e, "INSERT INTO t (id, email, tag) VALUES (1, 'a', 'first')")
  159. if _, err := execSQL(e, "INSERT INTO t (id, email, tag) VALUES (2, 'a', 'dup')"); err == nil || !strings.Contains(err.Error(), "UNIQUE constraint failed") {
  160. t.Fatalf("pre-rename duplicate error = %v, want UNIQUE constraint failure", err)
  161. }
  162. execMust(t, e, "ALTER TABLE t RENAME COLUMN email TO mail")
  163. if _, err := execSQL(e, "INSERT INTO t (id, mail, tag) VALUES (3, 'a', 'dup')"); err == nil || !strings.Contains(err.Error(), "UNIQUE constraint failed") {
  164. t.Fatalf("post-rename duplicate error = %v, want UNIQUE constraint failure", err)
  165. }
  166. execMust(t, e, "INSERT INTO t (id, mail, tag) VALUES (4, 'b', 'ok')")
  167. res := execMust(t, e, "SELECT id, mail, tag FROM t ORDER BY id")
  168. if res.RowCount != 2 || res.Rows[0][1] != "a" || res.Rows[1][1] != "b" {
  169. t.Fatalf("rows after rename = %v, want two distinct emails", res.Rows)
  170. }
  171. res = execMust(t, e, "PRAGMA index_list(t)")
  172. if len(res.Rows) != 1 || res.Rows[0][1] != "uq_email" || res.Rows[0][2] != int64(1) {
  173. t.Fatalf("index_list(t) = %v, want the unique uq_email index", res.Rows)
  174. }
  175. }
  176. // ON CONFLICT resolves against the renamed column and rejects the old name.
  177. func TestAlterTableRenameColumnResolvesOnConflictTarget(t *testing.T) {
  178. _, schema, table := newTestDB(t)
  179. e := newExec(schema, table)
  180. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT, tag TEXT)")
  181. execMust(t, e, "CREATE UNIQUE INDEX uq_email ON t(email)")
  182. execMust(t, e, "INSERT INTO t (id, email, tag) VALUES (1, 'a', 'old')")
  183. execMust(t, e, "ALTER TABLE t RENAME COLUMN email TO mail")
  184. execMust(t, e, "INSERT INTO t (id, mail, tag) VALUES (2, 'a', 'new') ON CONFLICT (mail) DO UPDATE SET tag = excluded.tag")
  185. res := execMust(t, e, "SELECT id, mail, tag FROM t")
  186. if res.RowCount != 1 || res.Rows[0][0] != int64(1) || res.Rows[0][2] != "new" {
  187. t.Fatalf("upsert on renamed column = %v, want the existing row updated", res.Rows)
  188. }
  189. if _, err := execSQL(e, "INSERT INTO t (id, mail, tag) VALUES (3, 'a', 'x') ON CONFLICT (email) DO UPDATE SET tag = excluded.tag"); err == nil || !strings.Contains(err.Error(), "column not found: email") {
  190. t.Fatalf("upsert on old column error = %v, want column not found: email", err)
  191. }
  192. }
  193. // Renaming a column back restores the original index metadata, and renaming to
  194. // an existing column name still fails without touching that metadata.
  195. func TestAlterTableRenameColumnCycleRestoresIndexMetadata(t *testing.T) {
  196. _, schema, table := newTestDB(t)
  197. e := newExec(schema, table)
  198. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, a TEXT)")
  199. execMust(t, e, "CREATE UNIQUE INDEX uq_a ON t(a)")
  200. execMust(t, e, "INSERT INTO t (id, a) VALUES (1, 'x')")
  201. execMust(t, e, "ALTER TABLE t RENAME COLUMN a TO b")
  202. execMust(t, e, "ALTER TABLE t RENAME COLUMN b TO a")
  203. idx, err := schema.GetIndex("uq_a")
  204. if err != nil {
  205. t.Fatalf("get uq_a: %v", err)
  206. }
  207. if len(idx.Columns) != 1 || idx.Columns[0].Name != "a" {
  208. t.Fatalf("uq_a columns after cycle = %#v, want [a]", idx.Columns)
  209. }
  210. if _, err := execSQL(e, "INSERT INTO t (id, a) VALUES (2, 'x')"); err == nil || !strings.Contains(err.Error(), "UNIQUE constraint failed") {
  211. t.Fatalf("duplicate error after rename cycle = %v, want UNIQUE constraint failure", err)
  212. }
  213. if _, err := execSQL(e, "ALTER TABLE t RENAME COLUMN a TO id"); err == nil || !strings.Contains(err.Error(), "column already exists: id") {
  214. t.Fatalf("rename to existing column error = %v, want column already exists: id", err)
  215. }
  216. idx, err = schema.GetIndex("uq_a")
  217. if err != nil {
  218. t.Fatalf("get uq_a after rejected rename: %v", err)
  219. }
  220. if len(idx.Columns) != 1 || idx.Columns[0].Name != "a" {
  221. t.Fatalf("uq_a columns after rejected rename = %#v, want [a]", idx.Columns)
  222. }
  223. }
  224. // A column referenced by an expression index cannot be renamed: the engine
  225. // stores expression text verbatim, so the rename is rejected instead of leaving
  226. // a UNIQUE expression index silently unbound from the column it enforces.
  227. func TestAlterTableRenameColumnRejectsExpressionIndexDependency(t *testing.T) {
  228. _, schema, table := newTestDB(t)
  229. e := newExec(schema, table)
  230. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT, tag TEXT)")
  231. execMust(t, e, "CREATE UNIQUE INDEX uq_lower_email ON t(lower(email))")
  232. execMust(t, e, "INSERT INTO t (id, email, tag) VALUES (1, 'a@x', 'keep')")
  233. _, err := execSQL(e, "ALTER TABLE t RENAME COLUMN email TO mail")
  234. if err == nil {
  235. t.Fatal("rename of a column used by an expression index must be rejected")
  236. }
  237. for _, want := range []string{"uq_lower_email", "lower(email)"} {
  238. if !strings.Contains(err.Error(), want) {
  239. t.Fatalf("rename error = %v, want it to name %q", err, want)
  240. }
  241. }
  242. // Nothing was renamed, and the UNIQUE expression index still enforces.
  243. sch, err := schema.GetSchema("t")
  244. if err != nil {
  245. t.Fatalf("get schema: %v", err)
  246. }
  247. if _, ok := sch.GetColumn("email"); !ok {
  248. t.Fatal("rejected rename removed the email column")
  249. }
  250. if _, ok := sch.GetColumn("mail"); ok {
  251. t.Fatal("rejected rename added the mail column")
  252. }
  253. if _, err := execSQL(e, "INSERT INTO t (id, email, tag) VALUES (2, 'a@x', 'dup')"); err == nil || !strings.Contains(err.Error(), "UNIQUE constraint failed") {
  254. t.Fatalf("duplicate on expression index after rejected rename = %v, want UNIQUE constraint failure", err)
  255. }
  256. // A column the expression does not reference can still be renamed.
  257. execMust(t, e, "ALTER TABLE t RENAME COLUMN tag TO label")
  258. idx, err := schema.GetIndex("uq_lower_email")
  259. if err != nil {
  260. t.Fatalf("get uq_lower_email: %v", err)
  261. }
  262. if len(idx.Columns) != 1 || idx.Columns[0].Expression != "lower(email)" {
  263. t.Fatalf("uq_lower_email columns = %#v, want the untouched lower(email) expression", idx.Columns)
  264. }
  265. if res := execMust(t, e, "SELECT label FROM t WHERE id = 1"); res.RowCount != 1 || res.Rows[0][0] != "keep" {
  266. t.Fatalf("renamed label = %v, want keep", res.Rows)
  267. }
  268. }
  269. // Conflict targets that name a hidden rowid alias are not table columns but are
  270. // still valid targets, including after a rename.
  271. func TestAlterTableRenameColumnKeepsRowIDConflictTarget(t *testing.T) {
  272. _, schema, table := newTestDB(t)
  273. e := newExec(schema, table)
  274. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, a TEXT)")
  275. execMust(t, e, "INSERT INTO t (id, a) VALUES (1, 'x')")
  276. execMust(t, e, "ALTER TABLE t RENAME COLUMN a TO b")
  277. execMust(t, e, "INSERT INTO t (id, b) VALUES (2, 'y') ON CONFLICT (rowid) DO NOTHING")
  278. execMust(t, e, "INSERT INTO t (id, b) VALUES (3, 'z') ON CONFLICT (oid) DO NOTHING")
  279. execMust(t, e, "INSERT INTO t (id, b) VALUES (4, 'w') ON CONFLICT (_rowid_) DO NOTHING")
  280. if res := execMust(t, e, "SELECT count(*) FROM t"); res.Rows[0][0] != int64(4) {
  281. t.Fatalf("row count = %v, want 4", res.Rows[0][0])
  282. }
  283. }