transactional_ddl_test.go 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853
  1. package executor
  2. import (
  3. "strings"
  4. "testing"
  5. "github.com/danfragoso/pizzasql-next/pkg/storage"
  6. )
  7. // transactional DDL: CREATE TABLE and its rows are visible to the transaction
  8. // that issued them, invisible to other sessions, and discarded by ROLLBACK.
  9. func TestTransactionalDDLCreateTableRollback(t *testing.T) {
  10. _, schema, table := newTestDB(t)
  11. writer := newExec(schema, table)
  12. observer := newExec(schema, table)
  13. execMust(t, writer, "BEGIN")
  14. execMust(t, writer, "CREATE TABLE staged (id INTEGER PRIMARY KEY, v TEXT)")
  15. execMust(t, writer, "INSERT INTO staged VALUES (1, 'one')")
  16. // Read-your-writes inside the transaction.
  17. if res := execMust(t, writer, "SELECT v FROM staged WHERE id = 1"); res.RowCount != 1 || res.Rows[0][0] != "one" {
  18. t.Fatalf("staged table read = %v", res.Rows)
  19. }
  20. // Another session must not see the staged table.
  21. if _, err := execSQL(observer, "SELECT * FROM staged"); err == nil {
  22. t.Fatal("observer saw a table created in another session's uncommitted transaction")
  23. }
  24. execMust(t, writer, "ROLLBACK")
  25. if _, err := execSQL(writer, "SELECT * FROM staged"); err == nil {
  26. t.Fatal("staged table survived ROLLBACK")
  27. }
  28. if schema.TableExists("staged") {
  29. t.Fatal("durable catalog contains a rolled-back table")
  30. }
  31. if _, err := schema.GetSchema("staged"); err == nil {
  32. t.Fatal("durable catalog still resolves the rolled-back table")
  33. }
  34. // DDL and its rows are one unit: the staged INSERT left no durable rows.
  35. if rows, err := table.Select("staged", nil); err == nil && len(rows) > 0 {
  36. t.Fatalf("rolled-back table left %d durable rows", len(rows))
  37. }
  38. }
  39. // transactional DDL: DROP TABLE is undone by ROLLBACK, rows included.
  40. func TestTransactionalDDLDropTableRollback(t *testing.T) {
  41. _, schema, table := newTestDB(t)
  42. e := newExec(schema, table)
  43. execMust(t, e, "CREATE TABLE ledger (id INTEGER PRIMARY KEY, v TEXT)")
  44. execMust(t, e, "INSERT INTO ledger VALUES (1, 'a'), (2, 'b')")
  45. execMust(t, e, "BEGIN")
  46. execMust(t, e, "DROP TABLE ledger")
  47. if _, err := execSQL(e, "SELECT * FROM ledger"); err == nil {
  48. t.Fatal("dropped table is still visible to the transaction")
  49. }
  50. execMust(t, e, "ROLLBACK")
  51. res := execMust(t, e, "SELECT id, v FROM ledger ORDER BY id")
  52. if res.RowCount != 2 {
  53. t.Fatalf("rows after rollback = %v", res.Rows)
  54. }
  55. // The catalog and the rows must both be intact after the rollback.
  56. if !schema.TableExists("ledger") {
  57. t.Fatal("durable catalog lost the table")
  58. }
  59. row, err := table.GetByPK("ledger", "1")
  60. if err != nil || row["v"] != "a" {
  61. t.Fatalf("durable row after rollback: %v %v", row, err)
  62. }
  63. }
  64. // transactional DDL: ALTER TABLE variants are undone by ROLLBACK.
  65. func TestTransactionalDDLAlterRollback(t *testing.T) {
  66. _, schema, table := newTestDB(t)
  67. e := newExec(schema, table)
  68. execMust(t, e, "CREATE TABLE people (id INTEGER PRIMARY KEY, name TEXT)")
  69. execMust(t, e, "INSERT INTO people VALUES (1, 'ada')")
  70. execMust(t, e, "BEGIN")
  71. execMust(t, e, "ALTER TABLE people ADD COLUMN age INTEGER DEFAULT 30")
  72. // Read-your-writes: the new column exists for this session and carries the
  73. // backfilled default.
  74. res := execMust(t, e, "SELECT age FROM people WHERE id = 1")
  75. if res.RowCount != 1 || res.Rows[0][0] != int64(30) {
  76. t.Fatalf("staged ALTER ADD COLUMN read = %v", res.Rows)
  77. }
  78. execMust(t, e, "ALTER TABLE people RENAME COLUMN name TO full_name")
  79. if res := execMust(t, e, "SELECT full_name FROM people"); res.Rows[0][0] != "ada" {
  80. t.Fatalf("staged RENAME COLUMN read = %v", res.Rows)
  81. }
  82. execMust(t, e, "ALTER TABLE people RENAME TO humans")
  83. if res := execMust(t, e, "SELECT full_name FROM humans"); res.Rows[0][0] != "ada" {
  84. t.Fatalf("staged RENAME TABLE read = %v", res.Rows)
  85. }
  86. execMust(t, e, "ROLLBACK")
  87. res = execMust(t, e, "SELECT id, name FROM people")
  88. if res.RowCount != 1 || res.Rows[0][1] != "ada" {
  89. t.Fatalf("rows after rollback = %v", res.Rows)
  90. }
  91. if _, err := execSQL(e, "SELECT age FROM people"); err == nil {
  92. t.Fatal("rolled-back ADD COLUMN is still visible")
  93. }
  94. if _, err := execSQL(e, "SELECT * FROM humans"); err == nil {
  95. t.Fatal("rolled-back RENAME TABLE is still visible")
  96. }
  97. if _, err := execSQL(e, "SELECT * FROM people"); err != nil {
  98. t.Fatalf("original table name lost by ROLLBACK: %v", err)
  99. }
  100. }
  101. // transactional DDL: CREATE INDEX is transaction-scoped.
  102. func TestTransactionalDDLCreateIndexRollback(t *testing.T) {
  103. _, schema, table := newTestDB(t)
  104. e := newExec(schema, table)
  105. execMust(t, e, "CREATE TABLE items (id INTEGER PRIMARY KEY, sku TEXT)")
  106. execMust(t, e, "INSERT INTO items VALUES (1, 'a'), (2, 'b')")
  107. execMust(t, e, "BEGIN")
  108. execMust(t, e, "CREATE UNIQUE INDEX idx_items_sku ON items (sku)")
  109. if !e.session.IndexExists("idx_items_sku") {
  110. t.Fatal("staged index missing from the transaction's catalog")
  111. }
  112. if schema.IndexExists("idx_items_sku") {
  113. t.Fatal("staged index leaked into the committed catalog")
  114. }
  115. execMust(t, e, "ROLLBACK")
  116. if schema.IndexExists("idx_items_sku") {
  117. t.Fatal("rolled-back index survived in the durable catalog")
  118. }
  119. // The index name is reusable, which proves the catalog write was discarded.
  120. execMust(t, e, "CREATE INDEX idx_items_sku ON items (sku)")
  121. }
  122. // transactional DDL: another session keeps reading the committed catalog until
  123. // the DDL commit publishes it.
  124. func TestTransactionalDDLTwoSessionVisibility(t *testing.T) {
  125. _, schema, table := newTestDB(t)
  126. writer := newExec(schema, table)
  127. observer := newExec(schema, table)
  128. execMust(t, writer, "CREATE TABLE base (id INTEGER PRIMARY KEY, v TEXT)")
  129. execMust(t, observer, "SELECT * FROM base")
  130. execMust(t, writer, "INSERT INTO base VALUES (1, 'committed')")
  131. execMust(t, writer, "BEGIN")
  132. execMust(t, writer, "CREATE TABLE extra (id INTEGER PRIMARY KEY)")
  133. execMust(t, writer, "ALTER TABLE base ADD COLUMN note TEXT DEFAULT 'new'")
  134. execMust(t, writer, "CREATE INDEX idx_base_note ON base (note)")
  135. // The observer sees neither the new table nor the new column, and it still
  136. // reads the committed rows.
  137. if _, err := execSQL(observer, "SELECT * FROM extra"); err == nil {
  138. t.Fatal("observer saw staged CREATE TABLE before COMMIT")
  139. }
  140. if _, err := execSQL(observer, "SELECT note FROM base"); err == nil {
  141. t.Fatal("observer saw a staged column before COMMIT")
  142. }
  143. if res := execMust(t, observer, "SELECT v FROM base"); res.RowCount != 1 || res.Rows[0][0] != "committed" {
  144. t.Fatalf("observer read = %v", res.Rows)
  145. }
  146. execMust(t, writer, "COMMIT")
  147. if _, err := execSQL(observer, "SELECT * FROM extra"); err != nil {
  148. t.Fatalf("observer did not see committed CREATE TABLE: %v", err)
  149. }
  150. // The observer's executor resynchronizes its analyzer catalog from the
  151. // bumped catalog version.
  152. res := execMust(t, observer, "SELECT note FROM base")
  153. if res.RowCount != 1 || res.Rows[0][0] != "new" {
  154. t.Fatalf("observer row after COMMIT = %v", res.Rows)
  155. }
  156. if !schema.IndexExists("idx_base_note") {
  157. t.Fatal("committed index missing from the durable catalog")
  158. }
  159. }
  160. // transactional DDL: COMMIT publishes every staged statement atomically and the
  161. // result reloads from durable storage.
  162. func TestTransactionalDDLCommitPublishesAndReloads(t *testing.T) {
  163. pool, schema, table := newTestDB(t)
  164. e := newExec(schema, table)
  165. execMust(t, e, "CREATE TABLE orders (id INTEGER PRIMARY KEY, total INTEGER)")
  166. execMust(t, e, "INSERT INTO orders VALUES (1, 10)")
  167. execMust(t, e, "BEGIN")
  168. execMust(t, e, "ALTER TABLE orders ADD COLUMN region TEXT DEFAULT 'eu'")
  169. execMust(t, e, "CREATE TABLE shipments (id INTEGER PRIMARY KEY, order_id INTEGER)")
  170. execMust(t, e, "INSERT INTO shipments VALUES (100, 1)")
  171. execMust(t, e, "CREATE INDEX idx_orders_region ON orders (region)")
  172. execMust(t, e, "COMMIT")
  173. // Reload the catalog from durable state with fresh managers over the same KV
  174. // server: the commit must have written schema, index, and rows together.
  175. reloadedSchemas := storage.NewSchemaManager(pool, "testdb")
  176. reloadedTables := storage.NewTableManager(pool, reloadedSchemas, "testdb")
  177. orders, err := reloadedSchemas.GetSchema("orders")
  178. if err != nil {
  179. t.Fatal(err)
  180. }
  181. if _, ok := orders.GetColumn("region"); !ok {
  182. t.Fatal("committed ADD COLUMN missing after reload")
  183. }
  184. if !reloadedSchemas.IndexExists("idx_orders_region") {
  185. t.Fatal("committed index missing after reload")
  186. }
  187. if !reloadedSchemas.TableExists("shipments") {
  188. t.Fatal("committed CREATE TABLE missing after reload")
  189. }
  190. orderRow, err := reloadedTables.GetByPK("orders", "1")
  191. if err != nil {
  192. t.Fatal(err)
  193. }
  194. if orderRow["region"] != "eu" {
  195. t.Fatalf("backfilled default after reload = %v", orderRow)
  196. }
  197. shipment, err := reloadedTables.GetByPK("shipments", "100")
  198. if err != nil || shipment["order_id"] != int64(1) {
  199. t.Fatalf("committed insert after reload: %v %v", shipment, err)
  200. }
  201. }
  202. // transactional DDL: DML and DDL commit or roll back as one unit.
  203. func TestTransactionalDDLWithDML(t *testing.T) {
  204. _, schema, table := newTestDB(t)
  205. e := newExec(schema, table)
  206. execMust(t, e, "CREATE TABLE events (id INTEGER PRIMARY KEY, kind TEXT)")
  207. execMust(t, e, "BEGIN")
  208. execMust(t, e, "CREATE TABLE archive (id INTEGER PRIMARY KEY, kind TEXT)")
  209. execMust(t, e, "INSERT INTO archive VALUES (1, 'old')")
  210. execMust(t, e, "INSERT INTO events VALUES (1, 'new')")
  211. execMust(t, e, "ALTER TABLE events ADD COLUMN archived INTEGER DEFAULT 0")
  212. execMust(t, e, "ROLLBACK")
  213. if _, err := execSQL(e, "SELECT * FROM archive"); err == nil {
  214. t.Fatal("rollback kept a table created with DML in the same transaction")
  215. }
  216. if res := execMust(t, e, "SELECT COUNT(*) FROM events"); res.Rows[0][0] != int64(0) {
  217. t.Fatalf("rollback kept a DML row: %v", res.Rows)
  218. }
  219. if _, err := execSQL(e, "SELECT archived FROM events"); err == nil {
  220. t.Fatal("rollback kept a staged column")
  221. }
  222. // The same sequence commits as one unit.
  223. execMust(t, e, "BEGIN")
  224. execMust(t, e, "CREATE TABLE archive (id INTEGER PRIMARY KEY, kind TEXT)")
  225. execMust(t, e, "INSERT INTO archive VALUES (1, 'old')")
  226. execMust(t, e, "INSERT INTO events VALUES (1, 'new')")
  227. execMust(t, e, "ALTER TABLE events ADD COLUMN archived INTEGER DEFAULT 0")
  228. execMust(t, e, "COMMIT")
  229. if res := execMust(t, e, "SELECT COUNT(*) FROM archive"); res.Rows[0][0] != int64(1) {
  230. t.Fatalf("committed CREATE TABLE rows = %v", res.Rows)
  231. }
  232. if res := execMust(t, e, "SELECT archived FROM events WHERE id = 1"); res.Rows[0][0] != int64(0) {
  233. t.Fatalf("committed ADD COLUMN default = %v", res.Rows)
  234. }
  235. }
  236. // Transactional DDL: a rejected statement leaves the transaction's prior schema
  237. // intact, including DDL staged before it.
  238. func TestTransactionalDDLFailureLeavesPriorSchema(t *testing.T) {
  239. _, schema, table := newTestDB(t)
  240. e := newExec(schema, table)
  241. execMust(t, e, "CREATE TABLE parent (id INTEGER PRIMARY KEY)")
  242. execMust(t, e, "CREATE TABLE child (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id))")
  243. execMust(t, e, "INSERT INTO parent VALUES (1)")
  244. execMust(t, e, "INSERT INTO child VALUES (10, 1)")
  245. execMust(t, e, "BEGIN")
  246. execMust(t, e, "CREATE TABLE scratch (id INTEGER PRIMARY KEY)")
  247. // Duplicate primary key inside the staged table fails the statement.
  248. if _, err := execSQL(e, "INSERT INTO scratch VALUES (1), (1)"); err == nil {
  249. t.Fatal("duplicate primary key accepted")
  250. }
  251. // Dropping a referenced parent is rejected.
  252. if _, err := execSQL(e, "DROP TABLE parent"); err == nil {
  253. t.Fatal("dropped a table another table references")
  254. }
  255. // Adding a NOT NULL column without a default to a populated table fails.
  256. if _, err := execSQL(e, "ALTER TABLE child ADD COLUMN required TEXT NOT NULL"); err == nil {
  257. t.Fatal("NOT NULL column without default accepted on a populated table")
  258. }
  259. // Earlier staged DDL is still visible and staged DML is intact.
  260. if res := execMust(t, e, "SELECT COUNT(*) FROM scratch"); res.Rows[0][0] != int64(0) {
  261. t.Fatalf("scratch rows = %v", res.Rows)
  262. }
  263. if res := execMust(t, e, "SELECT COUNT(*) FROM child"); res.Rows[0][0] != int64(1) {
  264. t.Fatalf("child rows after failed statements = %v", res.Rows)
  265. }
  266. execMust(t, e, "COMMIT")
  267. if !schema.TableExists("scratch") {
  268. t.Fatal("committed staged table missing after a failed statement")
  269. }
  270. if schema.TableExists("scratch") && schema.TableExists("nonexistent") {
  271. t.Fatal("unreachable")
  272. }
  273. }
  274. // Transactional DDL: a multi-statement migration rolls back as one unit.
  275. func TestTransactionalDDLMultiStatementMigrationRollback(t *testing.T) {
  276. _, schema, table := newTestDB(t)
  277. e := newExec(schema, table)
  278. execMust(t, e, "CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER)")
  279. execMust(t, e, "BEGIN")
  280. execMust(t, e, "CREATE TABLE audit (id INTEGER PRIMARY KEY, account_id INTEGER)")
  281. execMust(t, e, "ALTER TABLE accounts ADD COLUMN currency TEXT DEFAULT 'EUR'")
  282. execMust(t, e, "CREATE UNIQUE INDEX idx_accounts_currency ON accounts (currency)")
  283. execMust(t, e, "INSERT INTO accounts VALUES (1, 100, 'EUR')")
  284. execMust(t, e, "INSERT INTO audit VALUES (1, 1)")
  285. execMust(t, e, "ROLLBACK")
  286. if schema.TableExists("audit") || schema.IndexExists("idx_accounts_currency") {
  287. t.Fatal("migration objects survived ROLLBACK")
  288. }
  289. accounts, err := schema.GetSchema("accounts")
  290. if err != nil {
  291. t.Fatal(err)
  292. }
  293. if _, ok := accounts.GetColumn("currency"); ok {
  294. t.Fatal("migration column survived ROLLBACK")
  295. }
  296. }
  297. // Transactional DDL: a UNIQUE index staged by the transaction is enforced by
  298. // the commit's validation, so duplicate values never become durable.
  299. func TestTransactionalDDLStagedUniqueIndexValidation(t *testing.T) {
  300. _, schema, table := newTestDB(t)
  301. e := newExec(schema, table)
  302. execMust(t, e, "CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER)")
  303. execMust(t, e, "INSERT INTO accounts VALUES (1, 100)")
  304. execMust(t, e, "BEGIN")
  305. execMust(t, e, "ALTER TABLE accounts ADD COLUMN currency TEXT DEFAULT 'EUR'")
  306. execMust(t, e, "CREATE UNIQUE INDEX idx_accounts_currency ON accounts (currency)")
  307. execMust(t, e, "INSERT INTO accounts (id, balance) VALUES (2, 200)")
  308. // Both rows now carry the backfilled default 'EUR', so the staged UNIQUE
  309. // index must reject the transaction at commit.
  310. if _, err := execSQL(e, "COMMIT"); err == nil || !strings.Contains(err.Error(), "UNIQUE constraint failed") {
  311. t.Fatalf("commit with duplicate staged UNIQUE values = %v", err)
  312. }
  313. if schema.IndexExists("idx_accounts_currency") {
  314. t.Fatal("failed commit published the staged index")
  315. }
  316. if res := execMust(t, e, "SELECT COUNT(*) FROM accounts"); res.Rows[0][0] != int64(1) {
  317. t.Fatalf("failed commit published staged rows: %v", res.Rows)
  318. }
  319. }
  320. // Transactional DDL: validations run against the staged catalog.
  321. func TestTransactionalDDLValidationsUseStagedSchema(t *testing.T) {
  322. _, schema, table := newTestDB(t)
  323. e := newExec(schema, table)
  324. execMust(t, e, "BEGIN")
  325. execMust(t, e, "CREATE TABLE parents (a INTEGER, b INTEGER, PRIMARY KEY (a, b))")
  326. execMust(t, e, "CREATE TABLE kids (id INTEGER PRIMARY KEY, pa INTEGER, pb INTEGER, FOREIGN KEY (pa, pb) REFERENCES parents(a, b))")
  327. execMust(t, e, "INSERT INTO parents VALUES (1, 2)")
  328. execMust(t, e, "INSERT INTO kids VALUES (10, 1, 2)")
  329. // Composite primary key: the same pair collides, a different pair does not.
  330. if _, err := execSQL(e, "INSERT INTO parents VALUES (1, 2)"); err == nil {
  331. t.Fatal("duplicate composite primary key accepted")
  332. }
  333. execMust(t, e, "INSERT INTO parents VALUES (3, 4)")
  334. // CHECK added by a staged ALTER TABLE is enforced by staged writes.
  335. execMust(t, e, "ALTER TABLE kids ADD COLUMN age INTEGER CHECK (age >= 0)")
  336. execMust(t, e, "INSERT INTO kids VALUES (12, 3, 4, 5)")
  337. if _, err := execSQL(e, "INSERT INTO kids VALUES (13, 3, 4, -1)"); err == nil {
  338. t.Fatal("staged CHECK constraint not enforced")
  339. }
  340. // NOT NULL added by a staged ALTER TABLE is enforced by staged writes.
  341. execMust(t, e, "ALTER TABLE kids ADD COLUMN tag TEXT NOT NULL DEFAULT 't'")
  342. if _, err := execSQL(e, "INSERT INTO kids (id, pa, pb, age, tag) VALUES (14, 3, 4, 1, NULL)"); err == nil {
  343. t.Fatal("staged NOT NULL constraint not enforced")
  344. }
  345. execMust(t, e, "COMMIT")
  346. // The staged constraints are durable and still enforced.
  347. if _, err := execSQL(e, "INSERT INTO kids VALUES (15, 3, 4, -1, 'x')"); err == nil {
  348. t.Fatal("committed CHECK constraint not enforced")
  349. }
  350. // A composite foreign key declared by staged DDL is validated at commit
  351. // against the transaction's final state.
  352. execMust(t, e, "BEGIN")
  353. execMust(t, e, "INSERT INTO kids (id, pa, pb) VALUES (11, 1, 9)")
  354. if _, err := execSQL(e, "COMMIT"); err == nil || !strings.Contains(err.Error(), "FOREIGN KEY constraint failed") {
  355. t.Fatalf("commit with a dangling staged composite foreign key = %v", err)
  356. }
  357. if res := execMust(t, e, "SELECT COUNT(*) FROM kids WHERE id = 11"); res.Rows[0][0] != int64(0) {
  358. t.Fatalf("failed commit published the dangling row: %v", res.Rows)
  359. }
  360. if _, err := execSQL(e, "INSERT INTO kids (id, pa, pb) VALUES (16, 8, 9)"); err == nil {
  361. t.Fatal("committed FOREIGN KEY not enforced")
  362. }
  363. }
  364. // Transactional DDL: cascades and composite keys keep working across a commit.
  365. func TestTransactionalDDLCascadesAcrossCommit(t *testing.T) {
  366. _, schema, table := newTestDB(t)
  367. e := newExec(schema, table)
  368. execMust(t, e, "BEGIN")
  369. execMust(t, e, "CREATE TABLE parents (id INTEGER PRIMARY KEY)")
  370. execMust(t, e, "CREATE TABLE kids (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id) ON DELETE CASCADE)")
  371. execMust(t, e, "INSERT INTO parents VALUES (1)")
  372. execMust(t, e, "INSERT INTO kids VALUES (10, 1)")
  373. execMust(t, e, "COMMIT")
  374. if res := execMust(t, e, "SELECT COUNT(*) FROM kids"); res.Rows[0][0] != int64(1) {
  375. t.Fatalf("kids = %v", res.Rows)
  376. }
  377. execMust(t, e, "DELETE FROM parents WHERE id = 1")
  378. if res := execMust(t, e, "SELECT COUNT(*) FROM kids"); res.Rows[0][0] != int64(0) {
  379. t.Fatalf("cascade did not delete the child row: %v", res.Rows)
  380. }
  381. }
  382. // Transactional DDL: a concurrent committed DDL that conflicts with the staged
  383. // change fails the commit with a serialization error instead of corrupting the
  384. // catalog.
  385. func TestTransactionalDDLConcurrentConflict(t *testing.T) {
  386. _, schema, table := newTestDB(t)
  387. first := newExec(schema, table)
  388. second := newExec(schema, table)
  389. execMust(t, first, "BEGIN")
  390. execMust(t, first, "CREATE TABLE races (id INTEGER PRIMARY KEY)")
  391. // A second session creates the same table and commits first.
  392. execMust(t, second, "CREATE TABLE races (id INTEGER PRIMARY KEY, v TEXT)")
  393. if _, err := execSQL(first, "COMMIT"); err == nil {
  394. t.Fatal("conflicting DDL commit succeeded")
  395. } else if !strings.Contains(err.Error(), "serialization") {
  396. t.Fatalf("conflicting DDL commit error = %v, want a serialization error", err)
  397. }
  398. // The committed definition wins and stays intact.
  399. stored, err := schema.GetSchema("races")
  400. if err != nil {
  401. t.Fatal(err)
  402. }
  403. if _, ok := stored.GetColumn("v"); !ok {
  404. t.Fatalf("concurrent committed schema was overwritten: %+v", stored.Columns)
  405. }
  406. }
  407. // Transactional DDL: a savepoint rollback also reverts staged DDL.
  408. func TestTransactionalDDLSavepointRollback(t *testing.T) {
  409. _, schema, table := newTestDB(t)
  410. e := newExec(schema, table)
  411. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  412. execMust(t, e, "BEGIN")
  413. execMust(t, e, "CREATE TABLE before_sp (id INTEGER PRIMARY KEY)")
  414. execMust(t, e, "SAVEPOINT sp1")
  415. execMust(t, e, "CREATE TABLE after_sp (id INTEGER PRIMARY KEY)")
  416. execMust(t, e, "ALTER TABLE t ADD COLUMN v TEXT DEFAULT 'x'")
  417. execMust(t, e, "ROLLBACK TO SAVEPOINT sp1")
  418. if _, err := execSQL(e, "SELECT * FROM after_sp"); err == nil {
  419. t.Fatal("DDL after the savepoint survived ROLLBACK TO SAVEPOINT")
  420. }
  421. if _, err := execSQL(e, "SELECT v FROM t"); err == nil {
  422. t.Fatal("ALTER after the savepoint survived ROLLBACK TO SAVEPOINT")
  423. }
  424. if _, err := execSQL(e, "SELECT * FROM before_sp"); err != nil {
  425. t.Fatalf("DDL before the savepoint was lost: %v", err)
  426. }
  427. execMust(t, e, "COMMIT")
  428. if !schema.TableExists("before_sp") || schema.TableExists("after_sp") {
  429. t.Fatal("committed savepoint result is wrong")
  430. }
  431. }
  432. // transactional DDL: DROP INDEX is undone by ROLLBACK and stays invisible to
  433. // other sessions until COMMIT.
  434. func TestTransactionalDDLDropIndexRollback(t *testing.T) {
  435. _, schema, table := newTestDB(t)
  436. writer := newExec(schema, table)
  437. observer := newExec(schema, table)
  438. execMust(t, writer, "CREATE TABLE docs (id INTEGER PRIMARY KEY, slug TEXT)")
  439. execMust(t, writer, "CREATE UNIQUE INDEX idx_docs_slug ON docs (slug)")
  440. execMust(t, writer, "INSERT INTO docs VALUES (1, 'a')")
  441. execMust(t, writer, "BEGIN")
  442. execMust(t, writer, "DROP INDEX idx_docs_slug")
  443. if writer.session.IndexExists("idx_docs_slug") {
  444. t.Fatal("staged DROP INDEX is still visible to the transaction")
  445. }
  446. if !observer.session.IndexExists("idx_docs_slug") {
  447. t.Fatal("staged DROP INDEX became visible to another session before COMMIT")
  448. }
  449. execMust(t, writer, "ROLLBACK")
  450. if !schema.IndexExists("idx_docs_slug") {
  451. t.Fatal("rollback did not restore the dropped index")
  452. }
  453. // The duplicate still fails, proving the unique index survived intact.
  454. if _, err := execSQL(writer, "INSERT INTO docs VALUES (2, 'a')"); err == nil {
  455. t.Fatal("unique index lost its enforcement after a rolled-back DROP INDEX")
  456. }
  457. execMust(t, writer, "BEGIN")
  458. execMust(t, writer, "DROP INDEX idx_docs_slug")
  459. execMust(t, writer, "COMMIT")
  460. if schema.IndexExists("idx_docs_slug") {
  461. t.Fatal("committed DROP INDEX did not reach the catalog")
  462. }
  463. execMust(t, observer, "INSERT INTO docs VALUES (3, 'a')")
  464. }
  465. // transactional DDL: a renamed table stays writable inside the transaction and
  466. // the staged rows move with it at commit.
  467. func TestTransactionalDDLRenameTableReadYourWrites(t *testing.T) {
  468. _, schema, table := newTestDB(t)
  469. e := newExec(schema, table)
  470. execMust(t, e, "CREATE TABLE old_name (id INTEGER PRIMARY KEY, v TEXT)")
  471. execMust(t, e, "INSERT INTO old_name VALUES (1, 'kept')")
  472. execMust(t, e, "BEGIN")
  473. execMust(t, e, "ALTER TABLE old_name RENAME TO new_name")
  474. execMust(t, e, "INSERT INTO new_name VALUES (2, 'staged')")
  475. if res := execMust(t, e, "SELECT v FROM new_name ORDER BY id"); res.RowCount != 2 {
  476. t.Fatalf("rows after staged rename = %v", res.Rows)
  477. }
  478. execMust(t, e, "COMMIT")
  479. if schema.TableExists("old_name") {
  480. t.Fatal("renamed table is still addressable under its old name")
  481. }
  482. res := execMust(t, e, "SELECT id, v FROM new_name ORDER BY id")
  483. if res.RowCount != 2 || res.Rows[0][1] != "kept" || res.Rows[1][1] != "staged" {
  484. t.Fatalf("rows after committed rename = %v", res.Rows)
  485. }
  486. // The durable rows moved rather than being copied.
  487. if rows, err := table.Select("old_name", nil); err == nil && len(rows) > 0 {
  488. t.Fatalf("old data prefix still holds %d rows", len(rows))
  489. }
  490. }
  491. // transactional DDL: another session keeps reading a dropped table's rows until
  492. // the drop commits.
  493. func TestTransactionalDDLDropTableTwoSessions(t *testing.T) {
  494. _, schema, table := newTestDB(t)
  495. writer := newExec(schema, table)
  496. observer := newExec(schema, table)
  497. execMust(t, writer, "CREATE TABLE doomed (id INTEGER PRIMARY KEY, v TEXT)")
  498. execMust(t, writer, "INSERT INTO doomed VALUES (1, 'still here')")
  499. execMust(t, writer, "BEGIN")
  500. execMust(t, writer, "DROP TABLE doomed")
  501. if res := execMust(t, observer, "SELECT v FROM doomed"); res.RowCount != 1 || res.Rows[0][0] != "still here" {
  502. t.Fatalf("observer rows while the drop is uncommitted = %v", res.Rows)
  503. }
  504. execMust(t, writer, "COMMIT")
  505. if _, err := execSQL(observer, "SELECT * FROM doomed"); err == nil {
  506. t.Fatal("observer still reads a dropped table after COMMIT")
  507. }
  508. if rows, err := table.Select("doomed", nil); err == nil && len(rows) > 0 {
  509. t.Fatalf("dropped table left %d durable rows", len(rows))
  510. }
  511. }
  512. // transactional DDL: a write that commits after the DDL statement but before the
  513. // DDL commit makes the commit fail with a serialization error rather than
  514. // silently discarding the concurrent write.
  515. func TestTransactionalDDLConcurrentWriteConflicts(t *testing.T) {
  516. _, schema, table := newTestDB(t)
  517. writer := newExec(schema, table)
  518. other := newExec(schema, table)
  519. execMust(t, writer, "CREATE TABLE shared (id INTEGER PRIMARY KEY, v TEXT)")
  520. execMust(t, writer, "INSERT INTO shared VALUES (1, 'a')")
  521. execMust(t, writer, "BEGIN")
  522. execMust(t, writer, "ALTER TABLE shared ADD COLUMN note TEXT DEFAULT 'x'")
  523. // Another session writes the table between the DDL statement and the commit.
  524. execMust(t, other, "INSERT INTO shared VALUES (2, 'b')")
  525. if _, err := execSQL(writer, "COMMIT"); err == nil {
  526. t.Fatal("commit over a concurrent write succeeded")
  527. } else if !strings.Contains(err.Error(), "serialization") {
  528. t.Fatalf("commit error = %v, want a serialization error", err)
  529. }
  530. // The concurrent row is intact and the column was never published.
  531. if res := execMust(t, other, "SELECT COUNT(*) FROM shared"); res.Rows[0][0] != int64(2) {
  532. t.Fatalf("concurrent rows after failed commit = %v", res.Rows)
  533. }
  534. if _, err := execSQL(other, "SELECT note FROM shared"); err == nil {
  535. t.Fatal("failed commit published the staged column")
  536. }
  537. }
  538. // transactional DDL: a rolled-back DROP INDEX and a committed re-creation both
  539. // leave the derived index usable, not stuck on a stale disabled marker.
  540. func TestTransactionalDDLIndexCachesSurviveRollback(t *testing.T) {
  541. _, schema, table := newTestDB(t)
  542. e := newExec(schema, table)
  543. execMust(t, e, "CREATE TABLE docs (id INTEGER PRIMARY KEY, slug TEXT)")
  544. execMust(t, e, "CREATE INDEX idx_docs_slug ON docs (slug)")
  545. execMust(t, e, "INSERT INTO docs VALUES (1, 'a'), (2, 'b')")
  546. execMust(t, e, "BEGIN")
  547. execMust(t, e, "DROP INDEX idx_docs_slug")
  548. execMust(t, e, "ROLLBACK")
  549. if _, err := e.session.SelectByIndex("docs", "idx_docs_slug", "b"); err != nil {
  550. t.Fatalf("index unusable after rolling back DROP INDEX: %v", err)
  551. }
  552. // The rollback must not leave the shared derived caches describing the
  553. // staged drop: the index still resolves, still has its entries, and is not
  554. // stuck on the disabled marker ClearIndex sets.
  555. index, err := e.session.GetIndex("idx_docs_slug")
  556. if err != nil {
  557. t.Fatalf("index missing after rolling back DROP INDEX: %v", err)
  558. }
  559. if rows, err := e.session.SelectByIndex("docs", index.Name, "a"); err != nil || len(rows) != 1 {
  560. t.Fatalf("index entries after rolling back DROP INDEX: rows=%v err=%v", rows, err)
  561. }
  562. // A rolled-back CREATE INDEX must not leave derived entries or a cleared
  563. // disabled marker behind either.
  564. execMust(t, e, "BEGIN")
  565. execMust(t, e, "CREATE INDEX idx_docs_slug_rollback ON docs (slug)")
  566. if !e.session.IndexExists("idx_docs_slug_rollback") {
  567. t.Fatal("staged index missing from the transaction catalog")
  568. }
  569. execMust(t, e, "ROLLBACK")
  570. if _, err := e.session.GetIndex("idx_docs_slug_rollback"); err == nil {
  571. t.Fatal("rolled-back index definition survived")
  572. }
  573. if _, err := e.session.SelectByIndex("docs", "idx_docs_slug_rollback", "b"); err == nil {
  574. t.Fatal("rolled-back index left usable derived entries")
  575. }
  576. // A committed drop followed by a transactional re-creation must rebuild the
  577. // index rather than inherit the disabled marker.
  578. execMust(t, e, "DROP INDEX idx_docs_slug")
  579. execMust(t, e, "BEGIN")
  580. execMust(t, e, "CREATE INDEX idx_docs_slug ON docs (slug)")
  581. execMust(t, e, "COMMIT")
  582. if _, err := e.session.SelectByIndex("docs", "idx_docs_slug", "b"); err != nil {
  583. t.Fatalf("index unusable after committing a re-created index: %v", err)
  584. }
  585. }
  586. // Transactional DDL: a RENAME followed by an UPDATE must still delete the
  587. // committed row under the old prefix. The updated image lives in the session
  588. // overlay under the new prefix, so the commit has to move-or-delete every
  589. // durable old key, not only the rows whose image it rewrites.
  590. func TestTransactionalDDLRenameThenUpdateKeepsNoOrphanRows(t *testing.T) {
  591. _, schema, table := newTestDB(t)
  592. e := newExec(schema, table)
  593. execMust(t, e, "CREATE TABLE a (id INTEGER PRIMARY KEY, v TEXT)")
  594. execMust(t, e, "INSERT INTO a VALUES (1, 'one'), (2, 'two')")
  595. execMust(t, e, "BEGIN")
  596. execMust(t, e, "ALTER TABLE a RENAME TO b")
  597. execMust(t, e, "UPDATE b SET v = 'changed' WHERE id = 1")
  598. execMust(t, e, "COMMIT")
  599. if schema.TableExists("a") {
  600. t.Fatal("renamed table is still addressable under its old name")
  601. }
  602. if res := execMust(t, e, "SELECT id, v FROM b ORDER BY id"); res.RowCount != 2 || res.Rows[0][1] != "changed" || res.Rows[1][1] != "two" {
  603. t.Fatalf("rows after committed rename+update = %v", res.Rows)
  604. }
  605. // The decisive check: a table re-created with the old name must start empty.
  606. // Any durable row left under _data:a: would resurface here as a ghost.
  607. execMust(t, e, "CREATE TABLE a (id INTEGER PRIMARY KEY, v TEXT)")
  608. if res := execMust(t, e, "SELECT COUNT(*) FROM a"); res.Rows[0][0] != int64(0) {
  609. t.Fatalf("orphaned rows resurfaced under the re-created old name: %v", res.Rows)
  610. }
  611. if rows, err := table.Select("b", nil); err != nil || len(rows) != 2 {
  612. t.Fatalf("durable rows after rename+update = %v err=%v", rows, err)
  613. }
  614. }
  615. // Transactional DDL: the same guarantee for a staged DELETE after a RENAME.
  616. func TestTransactionalDDLRenameThenDeleteKeepsNoOrphanRows(t *testing.T) {
  617. _, schema, table := newTestDB(t)
  618. e := newExec(schema, table)
  619. execMust(t, e, "CREATE TABLE a (id INTEGER PRIMARY KEY, v TEXT)")
  620. execMust(t, e, "INSERT INTO a VALUES (1, 'one'), (2, 'two')")
  621. execMust(t, e, "BEGIN")
  622. execMust(t, e, "ALTER TABLE a RENAME TO b")
  623. execMust(t, e, "DELETE FROM b WHERE id = 2")
  624. execMust(t, e, "COMMIT")
  625. if res := execMust(t, e, "SELECT id FROM b"); res.RowCount != 1 || res.Rows[0][0] != int64(1) {
  626. t.Fatalf("rows after committed rename+delete = %v", res.Rows)
  627. }
  628. execMust(t, e, "CREATE TABLE a (id INTEGER PRIMARY KEY, v TEXT)")
  629. if res := execMust(t, e, "SELECT COUNT(*) FROM a"); res.Rows[0][0] != int64(0) {
  630. t.Fatalf("deleted row or its sibling resurfaced under the re-created old name: %v", res.Rows)
  631. }
  632. if rows, err := table.Select("b", nil); err != nil || len(rows) != 1 {
  633. t.Fatalf("durable rows after rename+delete = %v err=%v", rows, err)
  634. }
  635. }
  636. // Transactional DDL: renaming a table away and back inside one transaction must
  637. // not publish a put+delete for the same key; all rows and the schema survive.
  638. func TestTransactionalDDLRenameAwayAndBack(t *testing.T) {
  639. _, schema, table := newTestDB(t)
  640. e := newExec(schema, table)
  641. execMust(t, e, "CREATE TABLE a (id INTEGER PRIMARY KEY, v TEXT)")
  642. execMust(t, e, "INSERT INTO a VALUES (1, 'one'), (2, 'two')")
  643. execMust(t, e, "BEGIN")
  644. execMust(t, e, "ALTER TABLE a RENAME TO b")
  645. execMust(t, e, "ALTER TABLE b RENAME TO a")
  646. execMust(t, e, "INSERT INTO a VALUES (3, 'three')")
  647. execMust(t, e, "COMMIT")
  648. if schema.TableExists("b") {
  649. t.Fatal("intermediate rename name survived the commit")
  650. }
  651. if res := execMust(t, e, "SELECT id, v FROM a ORDER BY id"); res.RowCount != 3 {
  652. t.Fatalf("rows after rename away and back = %v", res.Rows)
  653. }
  654. if rows, err := table.Select("a", nil); err != nil || len(rows) != 3 {
  655. t.Fatalf("durable rows after rename away and back = %v err=%v", rows, err)
  656. }
  657. }
  658. // Transactional DDL: ROLLBACK TO SAVEPOINT can be repeated. The savepoint is
  659. // retained, so DDL and rows staged after the first rollback must not leak into
  660. // the state a second rollback installs.
  661. func TestTransactionalDDLRepeatedRollbackToSavepoint(t *testing.T) {
  662. _, schema, table := newTestDB(t)
  663. e := newExec(schema, table)
  664. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  665. execMust(t, e, "BEGIN")
  666. execMust(t, e, "SAVEPOINT s")
  667. execMust(t, e, "CREATE TABLE x (id INTEGER PRIMARY KEY)")
  668. execMust(t, e, "INSERT INTO t VALUES (1)")
  669. execMust(t, e, "ROLLBACK TO SAVEPOINT s")
  670. execMust(t, e, "CREATE TABLE y (id INTEGER PRIMARY KEY)")
  671. execMust(t, e, "INSERT INTO t VALUES (2)")
  672. execMust(t, e, "ROLLBACK TO SAVEPOINT s")
  673. execMust(t, e, "COMMIT")
  674. if schema.TableExists("x") || schema.TableExists("y") {
  675. t.Fatalf("DDL staged between rollbacks survived: x=%v y=%v", schema.TableExists("x"), schema.TableExists("y"))
  676. }
  677. if res := execMust(t, e, "SELECT COUNT(*) FROM t"); res.Rows[0][0] != int64(0) {
  678. t.Fatalf("rows staged between rollbacks survived: %v", res.Rows)
  679. }
  680. }
  681. // Transactional DDL: committing DDL on one table must not reset another table's
  682. // ROWID allocator. A concurrent transaction that staged a TEXT-keyed insert
  683. // holds a ROWID that is not durable yet; if the allocator is wiped, a later
  684. // insert derives that same value again and both rows become durable with the
  685. // same _rowid_, mis-associating every rowid-keyed index cache entry.
  686. func TestTransactionalDDLCommitKeepsUnrelatedRowIDAllocators(t *testing.T) {
  687. _, schema, table := newTestDB(t)
  688. stager := newExec(schema, table)
  689. ddl := newExec(schema, table)
  690. other := newExec(schema, table)
  691. execMust(t, stager, "CREATE TABLE events (name TEXT PRIMARY KEY, v TEXT)")
  692. execMust(t, stager, "BEGIN")
  693. execMust(t, stager, "INSERT INTO events VALUES ('staged', 'a')")
  694. // An unrelated transactional DDL commit must not touch the events allocator.
  695. execMust(t, ddl, "BEGIN")
  696. execMust(t, ddl, "CREATE TABLE side (id INTEGER PRIMARY KEY)")
  697. execMust(t, ddl, "COMMIT")
  698. // Another session allocates its own ROWID while the first insert is staged.
  699. execMust(t, other, "INSERT INTO events VALUES ('later', 'b')")
  700. execMust(t, stager, "COMMIT")
  701. rows, err := table.Select("events", nil)
  702. if err != nil {
  703. t.Fatal(err)
  704. }
  705. if len(rows) != 2 {
  706. t.Fatalf("events rows = %d, want 2", len(rows))
  707. }
  708. seen := make(map[int64]bool, len(rows))
  709. for _, row := range rows {
  710. rowid, ok := row["_rowid_"].(int64)
  711. if !ok {
  712. t.Fatalf("row without a rowid: %v", row)
  713. }
  714. if seen[rowid] {
  715. t.Fatalf("duplicate durable _rowid_ %d after an unrelated DDL commit: %v", rowid, rows)
  716. }
  717. seen[rowid] = true
  718. }
  719. }
  720. // Transactional DDL: a RENAME whose row move cannot fit in the commit's single
  721. // storage transaction fails at COMMIT with an explicit limit error instead of
  722. // buffering the whole table and failing inside the storage protocol, and the
  723. // failed commit publishes nothing.
  724. func TestTransactionalDDLRenameExceedingBatchCeilingFails(t *testing.T) {
  725. _, schema, table := newTestDB(t)
  726. e := newExec(schema, table)
  727. execMust(t, e, "CREATE TABLE big (id INTEGER PRIMARY KEY, v TEXT)")
  728. // A rename publishes a put and a delete per row, so just over half of the
  729. // store's 65,535-operation ceiling is enough to exceed it.
  730. const rows = 33000
  731. const chunk = 5000
  732. for start := 0; start < rows; start += chunk {
  733. batch := make([]storage.Row, 0, chunk)
  734. for i := start; i < start+chunk && i < rows; i++ {
  735. batch = append(batch, storage.Row{"id": int64(i + 1), "v": "x"})
  736. }
  737. if _, err := table.InsertBulk("big", batch); err != nil {
  738. t.Fatalf("seed rows %d-%d: %v", start, start+len(batch), err)
  739. }
  740. }
  741. execMust(t, e, "BEGIN")
  742. execMust(t, e, "ALTER TABLE big RENAME TO big_renamed")
  743. _, err := execSQL(e, "COMMIT")
  744. if err == nil {
  745. t.Fatal("commit of an oversized rename succeeded")
  746. }
  747. if !strings.Contains(err.Error(), "storage batch limit") {
  748. t.Fatalf("commit error = %v, want the storage batch limit", err)
  749. }
  750. // Nothing was published: the old name, its schema, and every row survive.
  751. if !schema.TableExists("big") || schema.TableExists("big_renamed") {
  752. t.Fatalf("failed commit published the rename: big=%v big_renamed=%v", schema.TableExists("big"), schema.TableExists("big_renamed"))
  753. }
  754. if count, err := table.CountFast("big"); err != nil || count != rows {
  755. t.Fatalf("rows after failed commit = %d err=%v, want %d", count, err, rows)
  756. }
  757. }