| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607 |
- package executor
- import (
- "strings"
- "testing"
- "github.com/danfragoso/pizzasql-next/pkg/storage"
- )
- // transactional DDL: CREATE TABLE and its rows are visible to the transaction
- // that issued them, invisible to other sessions, and discarded by ROLLBACK.
- func TestTransactionalDDLCreateTableRollback(t *testing.T) {
- _, schema, table := newTestDB(t)
- writer := newExec(schema, table)
- observer := newExec(schema, table)
- execMust(t, writer, "BEGIN")
- execMust(t, writer, "CREATE TABLE staged (id INTEGER PRIMARY KEY, v TEXT)")
- execMust(t, writer, "INSERT INTO staged VALUES (1, 'one')")
- // Read-your-writes inside the transaction.
- if res := execMust(t, writer, "SELECT v FROM staged WHERE id = 1"); res.RowCount != 1 || res.Rows[0][0] != "one" {
- t.Fatalf("staged table read = %v", res.Rows)
- }
- // Another session must not see the staged table.
- if _, err := execSQL(observer, "SELECT * FROM staged"); err == nil {
- t.Fatal("observer saw a table created in another session's uncommitted transaction")
- }
- execMust(t, writer, "ROLLBACK")
- if _, err := execSQL(writer, "SELECT * FROM staged"); err == nil {
- t.Fatal("staged table survived ROLLBACK")
- }
- if schema.TableExists("staged") {
- t.Fatal("durable catalog contains a rolled-back table")
- }
- if _, err := schema.GetSchema("staged"); err == nil {
- t.Fatal("durable catalog still resolves the rolled-back table")
- }
- // DDL and its rows are one unit: the staged INSERT left no durable rows.
- if rows, err := table.Select("staged", nil); err == nil && len(rows) > 0 {
- t.Fatalf("rolled-back table left %d durable rows", len(rows))
- }
- }
- // transactional DDL: DROP TABLE is undone by ROLLBACK, rows included.
- func TestTransactionalDDLDropTableRollback(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE ledger (id INTEGER PRIMARY KEY, v TEXT)")
- execMust(t, e, "INSERT INTO ledger VALUES (1, 'a'), (2, 'b')")
- execMust(t, e, "BEGIN")
- execMust(t, e, "DROP TABLE ledger")
- if _, err := execSQL(e, "SELECT * FROM ledger"); err == nil {
- t.Fatal("dropped table is still visible to the transaction")
- }
- execMust(t, e, "ROLLBACK")
- res := execMust(t, e, "SELECT id, v FROM ledger ORDER BY id")
- if res.RowCount != 2 {
- t.Fatalf("rows after rollback = %v", res.Rows)
- }
- // The catalog and the rows must both be intact after the rollback.
- if !schema.TableExists("ledger") {
- t.Fatal("durable catalog lost the table")
- }
- row, err := table.GetByPK("ledger", "1")
- if err != nil || row["v"] != "a" {
- t.Fatalf("durable row after rollback: %v %v", row, err)
- }
- }
- // transactional DDL: ALTER TABLE variants are undone by ROLLBACK.
- func TestTransactionalDDLAlterRollback(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE people (id INTEGER PRIMARY KEY, name TEXT)")
- execMust(t, e, "INSERT INTO people VALUES (1, 'ada')")
- execMust(t, e, "BEGIN")
- execMust(t, e, "ALTER TABLE people ADD COLUMN age INTEGER DEFAULT 30")
- // Read-your-writes: the new column exists for this session and carries the
- // backfilled default.
- res := execMust(t, e, "SELECT age FROM people WHERE id = 1")
- if res.RowCount != 1 || res.Rows[0][0] != int64(30) {
- t.Fatalf("staged ALTER ADD COLUMN read = %v", res.Rows)
- }
- execMust(t, e, "ALTER TABLE people RENAME COLUMN name TO full_name")
- if res := execMust(t, e, "SELECT full_name FROM people"); res.Rows[0][0] != "ada" {
- t.Fatalf("staged RENAME COLUMN read = %v", res.Rows)
- }
- execMust(t, e, "ALTER TABLE people RENAME TO humans")
- if res := execMust(t, e, "SELECT full_name FROM humans"); res.Rows[0][0] != "ada" {
- t.Fatalf("staged RENAME TABLE read = %v", res.Rows)
- }
- execMust(t, e, "ROLLBACK")
- res = execMust(t, e, "SELECT id, name FROM people")
- if res.RowCount != 1 || res.Rows[0][1] != "ada" {
- t.Fatalf("rows after rollback = %v", res.Rows)
- }
- if _, err := execSQL(e, "SELECT age FROM people"); err == nil {
- t.Fatal("rolled-back ADD COLUMN is still visible")
- }
- if _, err := execSQL(e, "SELECT * FROM humans"); err == nil {
- t.Fatal("rolled-back RENAME TABLE is still visible")
- }
- if _, err := execSQL(e, "SELECT * FROM people"); err != nil {
- t.Fatalf("original table name lost by ROLLBACK: %v", err)
- }
- }
- // transactional DDL: CREATE INDEX is transaction-scoped.
- func TestTransactionalDDLCreateIndexRollback(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE items (id INTEGER PRIMARY KEY, sku TEXT)")
- execMust(t, e, "INSERT INTO items VALUES (1, 'a'), (2, 'b')")
- execMust(t, e, "BEGIN")
- execMust(t, e, "CREATE UNIQUE INDEX idx_items_sku ON items (sku)")
- if !e.session.IndexExists("idx_items_sku") {
- t.Fatal("staged index missing from the transaction's catalog")
- }
- if schema.IndexExists("idx_items_sku") {
- t.Fatal("staged index leaked into the committed catalog")
- }
- execMust(t, e, "ROLLBACK")
- if schema.IndexExists("idx_items_sku") {
- t.Fatal("rolled-back index survived in the durable catalog")
- }
- // The index name is reusable, which proves the catalog write was discarded.
- execMust(t, e, "CREATE INDEX idx_items_sku ON items (sku)")
- }
- // transactional DDL: another session keeps reading the committed catalog until
- // the DDL commit publishes it.
- func TestTransactionalDDLTwoSessionVisibility(t *testing.T) {
- _, schema, table := newTestDB(t)
- writer := newExec(schema, table)
- observer := newExec(schema, table)
- execMust(t, writer, "CREATE TABLE base (id INTEGER PRIMARY KEY, v TEXT)")
- execMust(t, observer, "SELECT * FROM base")
- execMust(t, writer, "INSERT INTO base VALUES (1, 'committed')")
- execMust(t, writer, "BEGIN")
- execMust(t, writer, "CREATE TABLE extra (id INTEGER PRIMARY KEY)")
- execMust(t, writer, "ALTER TABLE base ADD COLUMN note TEXT DEFAULT 'new'")
- execMust(t, writer, "CREATE INDEX idx_base_note ON base (note)")
- // The observer sees neither the new table nor the new column, and it still
- // reads the committed rows.
- if _, err := execSQL(observer, "SELECT * FROM extra"); err == nil {
- t.Fatal("observer saw staged CREATE TABLE before COMMIT")
- }
- if _, err := execSQL(observer, "SELECT note FROM base"); err == nil {
- t.Fatal("observer saw a staged column before COMMIT")
- }
- if res := execMust(t, observer, "SELECT v FROM base"); res.RowCount != 1 || res.Rows[0][0] != "committed" {
- t.Fatalf("observer read = %v", res.Rows)
- }
- execMust(t, writer, "COMMIT")
- if _, err := execSQL(observer, "SELECT * FROM extra"); err != nil {
- t.Fatalf("observer did not see committed CREATE TABLE: %v", err)
- }
- // The observer's executor resynchronizes its analyzer catalog from the
- // bumped catalog version.
- res := execMust(t, observer, "SELECT note FROM base")
- if res.RowCount != 1 || res.Rows[0][0] != "new" {
- t.Fatalf("observer row after COMMIT = %v", res.Rows)
- }
- if !schema.IndexExists("idx_base_note") {
- t.Fatal("committed index missing from the durable catalog")
- }
- }
- // transactional DDL: COMMIT publishes every staged statement atomically and the
- // result reloads from durable storage.
- func TestTransactionalDDLCommitPublishesAndReloads(t *testing.T) {
- pool, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE orders (id INTEGER PRIMARY KEY, total INTEGER)")
- execMust(t, e, "INSERT INTO orders VALUES (1, 10)")
- execMust(t, e, "BEGIN")
- execMust(t, e, "ALTER TABLE orders ADD COLUMN region TEXT DEFAULT 'eu'")
- execMust(t, e, "CREATE TABLE shipments (id INTEGER PRIMARY KEY, order_id INTEGER)")
- execMust(t, e, "INSERT INTO shipments VALUES (100, 1)")
- execMust(t, e, "CREATE INDEX idx_orders_region ON orders (region)")
- execMust(t, e, "COMMIT")
- // Reload the catalog from durable state with fresh managers over the same KV
- // server: the commit must have written schema, index, and rows together.
- reloadedSchemas := storage.NewSchemaManager(pool, "testdb")
- reloadedTables := storage.NewTableManager(pool, reloadedSchemas, "testdb")
- orders, err := reloadedSchemas.GetSchema("orders")
- if err != nil {
- t.Fatal(err)
- }
- if _, ok := orders.GetColumn("region"); !ok {
- t.Fatal("committed ADD COLUMN missing after reload")
- }
- if !reloadedSchemas.IndexExists("idx_orders_region") {
- t.Fatal("committed index missing after reload")
- }
- if !reloadedSchemas.TableExists("shipments") {
- t.Fatal("committed CREATE TABLE missing after reload")
- }
- orderRow, err := reloadedTables.GetByPK("orders", "1")
- if err != nil {
- t.Fatal(err)
- }
- if orderRow["region"] != "eu" {
- t.Fatalf("backfilled default after reload = %v", orderRow)
- }
- shipment, err := reloadedTables.GetByPK("shipments", "100")
- if err != nil || shipment["order_id"] != int64(1) {
- t.Fatalf("committed insert after reload: %v %v", shipment, err)
- }
- }
- // transactional DDL: DML and DDL commit or roll back as one unit.
- func TestTransactionalDDLWithDML(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE events (id INTEGER PRIMARY KEY, kind TEXT)")
- execMust(t, e, "BEGIN")
- execMust(t, e, "CREATE TABLE archive (id INTEGER PRIMARY KEY, kind TEXT)")
- execMust(t, e, "INSERT INTO archive VALUES (1, 'old')")
- execMust(t, e, "INSERT INTO events VALUES (1, 'new')")
- execMust(t, e, "ALTER TABLE events ADD COLUMN archived INTEGER DEFAULT 0")
- execMust(t, e, "ROLLBACK")
- if _, err := execSQL(e, "SELECT * FROM archive"); err == nil {
- t.Fatal("rollback kept a table created with DML in the same transaction")
- }
- if res := execMust(t, e, "SELECT COUNT(*) FROM events"); res.Rows[0][0] != int64(0) {
- t.Fatalf("rollback kept a DML row: %v", res.Rows)
- }
- if _, err := execSQL(e, "SELECT archived FROM events"); err == nil {
- t.Fatal("rollback kept a staged column")
- }
- // The same sequence commits as one unit.
- execMust(t, e, "BEGIN")
- execMust(t, e, "CREATE TABLE archive (id INTEGER PRIMARY KEY, kind TEXT)")
- execMust(t, e, "INSERT INTO archive VALUES (1, 'old')")
- execMust(t, e, "INSERT INTO events VALUES (1, 'new')")
- execMust(t, e, "ALTER TABLE events ADD COLUMN archived INTEGER DEFAULT 0")
- execMust(t, e, "COMMIT")
- if res := execMust(t, e, "SELECT COUNT(*) FROM archive"); res.Rows[0][0] != int64(1) {
- t.Fatalf("committed CREATE TABLE rows = %v", res.Rows)
- }
- if res := execMust(t, e, "SELECT archived FROM events WHERE id = 1"); res.Rows[0][0] != int64(0) {
- t.Fatalf("committed ADD COLUMN default = %v", res.Rows)
- }
- }
- // Transactional DDL: a rejected statement leaves the transaction's prior schema
- // intact, including DDL staged before it.
- func TestTransactionalDDLFailureLeavesPriorSchema(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE parent (id INTEGER PRIMARY KEY)")
- execMust(t, e, "CREATE TABLE child (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id))")
- execMust(t, e, "INSERT INTO parent VALUES (1)")
- execMust(t, e, "INSERT INTO child VALUES (10, 1)")
- execMust(t, e, "BEGIN")
- execMust(t, e, "CREATE TABLE scratch (id INTEGER PRIMARY KEY)")
- // Duplicate primary key inside the staged table fails the statement.
- if _, err := execSQL(e, "INSERT INTO scratch VALUES (1), (1)"); err == nil {
- t.Fatal("duplicate primary key accepted")
- }
- // Dropping a referenced parent is rejected.
- if _, err := execSQL(e, "DROP TABLE parent"); err == nil {
- t.Fatal("dropped a table another table references")
- }
- // Adding a NOT NULL column without a default to a populated table fails.
- if _, err := execSQL(e, "ALTER TABLE child ADD COLUMN required TEXT NOT NULL"); err == nil {
- t.Fatal("NOT NULL column without default accepted on a populated table")
- }
- // Earlier staged DDL is still visible and staged DML is intact.
- if res := execMust(t, e, "SELECT COUNT(*) FROM scratch"); res.Rows[0][0] != int64(0) {
- t.Fatalf("scratch rows = %v", res.Rows)
- }
- if res := execMust(t, e, "SELECT COUNT(*) FROM child"); res.Rows[0][0] != int64(1) {
- t.Fatalf("child rows after failed statements = %v", res.Rows)
- }
- execMust(t, e, "COMMIT")
- if !schema.TableExists("scratch") {
- t.Fatal("committed staged table missing after a failed statement")
- }
- if schema.TableExists("scratch") && schema.TableExists("nonexistent") {
- t.Fatal("unreachable")
- }
- }
- // Transactional DDL: a multi-statement migration rolls back as one unit.
- func TestTransactionalDDLMultiStatementMigrationRollback(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER)")
- execMust(t, e, "BEGIN")
- execMust(t, e, "CREATE TABLE audit (id INTEGER PRIMARY KEY, account_id INTEGER)")
- execMust(t, e, "ALTER TABLE accounts ADD COLUMN currency TEXT DEFAULT 'EUR'")
- execMust(t, e, "CREATE UNIQUE INDEX idx_accounts_currency ON accounts (currency)")
- execMust(t, e, "INSERT INTO accounts VALUES (1, 100, 'EUR')")
- execMust(t, e, "INSERT INTO audit VALUES (1, 1)")
- execMust(t, e, "ROLLBACK")
- if schema.TableExists("audit") || schema.IndexExists("idx_accounts_currency") {
- t.Fatal("migration objects survived ROLLBACK")
- }
- accounts, err := schema.GetSchema("accounts")
- if err != nil {
- t.Fatal(err)
- }
- if _, ok := accounts.GetColumn("currency"); ok {
- t.Fatal("migration column survived ROLLBACK")
- }
- }
- // Transactional DDL: a UNIQUE index staged by the transaction is enforced by
- // the commit's validation, so duplicate values never become durable.
- func TestTransactionalDDLStagedUniqueIndexValidation(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER)")
- execMust(t, e, "INSERT INTO accounts VALUES (1, 100)")
- execMust(t, e, "BEGIN")
- execMust(t, e, "ALTER TABLE accounts ADD COLUMN currency TEXT DEFAULT 'EUR'")
- execMust(t, e, "CREATE UNIQUE INDEX idx_accounts_currency ON accounts (currency)")
- execMust(t, e, "INSERT INTO accounts (id, balance) VALUES (2, 200)")
- // Both rows now carry the backfilled default 'EUR', so the staged UNIQUE
- // index must reject the transaction at commit.
- if _, err := execSQL(e, "COMMIT"); err == nil || !strings.Contains(err.Error(), "UNIQUE constraint failed") {
- t.Fatalf("commit with duplicate staged UNIQUE values = %v", err)
- }
- if schema.IndexExists("idx_accounts_currency") {
- t.Fatal("failed commit published the staged index")
- }
- if res := execMust(t, e, "SELECT COUNT(*) FROM accounts"); res.Rows[0][0] != int64(1) {
- t.Fatalf("failed commit published staged rows: %v", res.Rows)
- }
- }
- // Transactional DDL: validations run against the staged catalog.
- func TestTransactionalDDLValidationsUseStagedSchema(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "BEGIN")
- execMust(t, e, "CREATE TABLE parents (a INTEGER, b INTEGER, PRIMARY KEY (a, b))")
- execMust(t, e, "CREATE TABLE kids (id INTEGER PRIMARY KEY, pa INTEGER, pb INTEGER, FOREIGN KEY (pa, pb) REFERENCES parents(a, b))")
- execMust(t, e, "INSERT INTO parents VALUES (1, 2)")
- execMust(t, e, "INSERT INTO kids VALUES (10, 1, 2)")
- // Composite primary key: the same pair collides, a different pair does not.
- if _, err := execSQL(e, "INSERT INTO parents VALUES (1, 2)"); err == nil {
- t.Fatal("duplicate composite primary key accepted")
- }
- execMust(t, e, "INSERT INTO parents VALUES (3, 4)")
- // CHECK added by a staged ALTER TABLE is enforced by staged writes.
- execMust(t, e, "ALTER TABLE kids ADD COLUMN age INTEGER CHECK (age >= 0)")
- execMust(t, e, "INSERT INTO kids VALUES (12, 3, 4, 5)")
- if _, err := execSQL(e, "INSERT INTO kids VALUES (13, 3, 4, -1)"); err == nil {
- t.Fatal("staged CHECK constraint not enforced")
- }
- // NOT NULL added by a staged ALTER TABLE is enforced by staged writes.
- execMust(t, e, "ALTER TABLE kids ADD COLUMN tag TEXT NOT NULL DEFAULT 't'")
- if _, err := execSQL(e, "INSERT INTO kids (id, pa, pb, age, tag) VALUES (14, 3, 4, 1, NULL)"); err == nil {
- t.Fatal("staged NOT NULL constraint not enforced")
- }
- execMust(t, e, "COMMIT")
- // The staged constraints are durable and still enforced.
- if _, err := execSQL(e, "INSERT INTO kids VALUES (15, 3, 4, -1, 'x')"); err == nil {
- t.Fatal("committed CHECK constraint not enforced")
- }
- // A composite foreign key declared by staged DDL is validated at commit
- // against the transaction's final state.
- execMust(t, e, "BEGIN")
- execMust(t, e, "INSERT INTO kids (id, pa, pb) VALUES (11, 1, 9)")
- if _, err := execSQL(e, "COMMIT"); err == nil || !strings.Contains(err.Error(), "FOREIGN KEY constraint failed") {
- t.Fatalf("commit with a dangling staged composite foreign key = %v", err)
- }
- if res := execMust(t, e, "SELECT COUNT(*) FROM kids WHERE id = 11"); res.Rows[0][0] != int64(0) {
- t.Fatalf("failed commit published the dangling row: %v", res.Rows)
- }
- if _, err := execSQL(e, "INSERT INTO kids (id, pa, pb) VALUES (16, 8, 9)"); err == nil {
- t.Fatal("committed FOREIGN KEY not enforced")
- }
- }
- // Transactional DDL: cascades and composite keys keep working across a commit.
- func TestTransactionalDDLCascadesAcrossCommit(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "BEGIN")
- execMust(t, e, "CREATE TABLE parents (id INTEGER PRIMARY KEY)")
- execMust(t, e, "CREATE TABLE kids (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id) ON DELETE CASCADE)")
- execMust(t, e, "INSERT INTO parents VALUES (1)")
- execMust(t, e, "INSERT INTO kids VALUES (10, 1)")
- execMust(t, e, "COMMIT")
- if res := execMust(t, e, "SELECT COUNT(*) FROM kids"); res.Rows[0][0] != int64(1) {
- t.Fatalf("kids = %v", res.Rows)
- }
- execMust(t, e, "DELETE FROM parents WHERE id = 1")
- if res := execMust(t, e, "SELECT COUNT(*) FROM kids"); res.Rows[0][0] != int64(0) {
- t.Fatalf("cascade did not delete the child row: %v", res.Rows)
- }
- }
- // Transactional DDL: a concurrent committed DDL that conflicts with the staged
- // change fails the commit with a serialization error instead of corrupting the
- // catalog.
- func TestTransactionalDDLConcurrentConflict(t *testing.T) {
- _, schema, table := newTestDB(t)
- first := newExec(schema, table)
- second := newExec(schema, table)
- execMust(t, first, "BEGIN")
- execMust(t, first, "CREATE TABLE races (id INTEGER PRIMARY KEY)")
- // A second session creates the same table and commits first.
- execMust(t, second, "CREATE TABLE races (id INTEGER PRIMARY KEY, v TEXT)")
- if _, err := execSQL(first, "COMMIT"); err == nil {
- t.Fatal("conflicting DDL commit succeeded")
- } else if !strings.Contains(err.Error(), "serialization") {
- t.Fatalf("conflicting DDL commit error = %v, want a serialization error", err)
- }
- // The committed definition wins and stays intact.
- stored, err := schema.GetSchema("races")
- if err != nil {
- t.Fatal(err)
- }
- if _, ok := stored.GetColumn("v"); !ok {
- t.Fatalf("concurrent committed schema was overwritten: %+v", stored.Columns)
- }
- }
- // Transactional DDL: a savepoint rollback also reverts staged DDL.
- func TestTransactionalDDLSavepointRollback(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
- execMust(t, e, "BEGIN")
- execMust(t, e, "CREATE TABLE before_sp (id INTEGER PRIMARY KEY)")
- execMust(t, e, "SAVEPOINT sp1")
- execMust(t, e, "CREATE TABLE after_sp (id INTEGER PRIMARY KEY)")
- execMust(t, e, "ALTER TABLE t ADD COLUMN v TEXT DEFAULT 'x'")
- execMust(t, e, "ROLLBACK TO SAVEPOINT sp1")
- if _, err := execSQL(e, "SELECT * FROM after_sp"); err == nil {
- t.Fatal("DDL after the savepoint survived ROLLBACK TO SAVEPOINT")
- }
- if _, err := execSQL(e, "SELECT v FROM t"); err == nil {
- t.Fatal("ALTER after the savepoint survived ROLLBACK TO SAVEPOINT")
- }
- if _, err := execSQL(e, "SELECT * FROM before_sp"); err != nil {
- t.Fatalf("DDL before the savepoint was lost: %v", err)
- }
- execMust(t, e, "COMMIT")
- if !schema.TableExists("before_sp") || schema.TableExists("after_sp") {
- t.Fatal("committed savepoint result is wrong")
- }
- }
- // transactional DDL: DROP INDEX is undone by ROLLBACK and stays invisible to
- // other sessions until COMMIT.
- func TestTransactionalDDLDropIndexRollback(t *testing.T) {
- _, schema, table := newTestDB(t)
- writer := newExec(schema, table)
- observer := newExec(schema, table)
- execMust(t, writer, "CREATE TABLE docs (id INTEGER PRIMARY KEY, slug TEXT)")
- execMust(t, writer, "CREATE UNIQUE INDEX idx_docs_slug ON docs (slug)")
- execMust(t, writer, "INSERT INTO docs VALUES (1, 'a')")
- execMust(t, writer, "BEGIN")
- execMust(t, writer, "DROP INDEX idx_docs_slug")
- if writer.session.IndexExists("idx_docs_slug") {
- t.Fatal("staged DROP INDEX is still visible to the transaction")
- }
- if !observer.session.IndexExists("idx_docs_slug") {
- t.Fatal("staged DROP INDEX became visible to another session before COMMIT")
- }
- execMust(t, writer, "ROLLBACK")
- if !schema.IndexExists("idx_docs_slug") {
- t.Fatal("rollback did not restore the dropped index")
- }
- // The duplicate still fails, proving the unique index survived intact.
- if _, err := execSQL(writer, "INSERT INTO docs VALUES (2, 'a')"); err == nil {
- t.Fatal("unique index lost its enforcement after a rolled-back DROP INDEX")
- }
- execMust(t, writer, "BEGIN")
- execMust(t, writer, "DROP INDEX idx_docs_slug")
- execMust(t, writer, "COMMIT")
- if schema.IndexExists("idx_docs_slug") {
- t.Fatal("committed DROP INDEX did not reach the catalog")
- }
- execMust(t, observer, "INSERT INTO docs VALUES (3, 'a')")
- }
- // transactional DDL: a renamed table stays writable inside the transaction and
- // the staged rows move with it at commit.
- func TestTransactionalDDLRenameTableReadYourWrites(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE old_name (id INTEGER PRIMARY KEY, v TEXT)")
- execMust(t, e, "INSERT INTO old_name VALUES (1, 'kept')")
- execMust(t, e, "BEGIN")
- execMust(t, e, "ALTER TABLE old_name RENAME TO new_name")
- execMust(t, e, "INSERT INTO new_name VALUES (2, 'staged')")
- if res := execMust(t, e, "SELECT v FROM new_name ORDER BY id"); res.RowCount != 2 {
- t.Fatalf("rows after staged rename = %v", res.Rows)
- }
- execMust(t, e, "COMMIT")
- if schema.TableExists("old_name") {
- t.Fatal("renamed table is still addressable under its old name")
- }
- res := execMust(t, e, "SELECT id, v FROM new_name ORDER BY id")
- if res.RowCount != 2 || res.Rows[0][1] != "kept" || res.Rows[1][1] != "staged" {
- t.Fatalf("rows after committed rename = %v", res.Rows)
- }
- // The durable rows moved rather than being copied.
- if rows, err := table.Select("old_name", nil); err == nil && len(rows) > 0 {
- t.Fatalf("old data prefix still holds %d rows", len(rows))
- }
- }
- // transactional DDL: another session keeps reading a dropped table's rows until
- // the drop commits.
- func TestTransactionalDDLDropTableTwoSessions(t *testing.T) {
- _, schema, table := newTestDB(t)
- writer := newExec(schema, table)
- observer := newExec(schema, table)
- execMust(t, writer, "CREATE TABLE doomed (id INTEGER PRIMARY KEY, v TEXT)")
- execMust(t, writer, "INSERT INTO doomed VALUES (1, 'still here')")
- execMust(t, writer, "BEGIN")
- execMust(t, writer, "DROP TABLE doomed")
- if res := execMust(t, observer, "SELECT v FROM doomed"); res.RowCount != 1 || res.Rows[0][0] != "still here" {
- t.Fatalf("observer rows while the drop is uncommitted = %v", res.Rows)
- }
- execMust(t, writer, "COMMIT")
- if _, err := execSQL(observer, "SELECT * FROM doomed"); err == nil {
- t.Fatal("observer still reads a dropped table after COMMIT")
- }
- if rows, err := table.Select("doomed", nil); err == nil && len(rows) > 0 {
- t.Fatalf("dropped table left %d durable rows", len(rows))
- }
- }
- // transactional DDL: a write that commits after the DDL statement but before the
- // DDL commit makes the commit fail with a serialization error rather than
- // silently discarding the concurrent write.
- func TestTransactionalDDLConcurrentWriteConflicts(t *testing.T) {
- _, schema, table := newTestDB(t)
- writer := newExec(schema, table)
- other := newExec(schema, table)
- execMust(t, writer, "CREATE TABLE shared (id INTEGER PRIMARY KEY, v TEXT)")
- execMust(t, writer, "INSERT INTO shared VALUES (1, 'a')")
- execMust(t, writer, "BEGIN")
- execMust(t, writer, "ALTER TABLE shared ADD COLUMN note TEXT DEFAULT 'x'")
- // Another session writes the table between the DDL statement and the commit.
- execMust(t, other, "INSERT INTO shared VALUES (2, 'b')")
- if _, err := execSQL(writer, "COMMIT"); err == nil {
- t.Fatal("commit over a concurrent write succeeded")
- } else if !strings.Contains(err.Error(), "serialization") {
- t.Fatalf("commit error = %v, want a serialization error", err)
- }
- // The concurrent row is intact and the column was never published.
- if res := execMust(t, other, "SELECT COUNT(*) FROM shared"); res.Rows[0][0] != int64(2) {
- t.Fatalf("concurrent rows after failed commit = %v", res.Rows)
- }
- if _, err := execSQL(other, "SELECT note FROM shared"); err == nil {
- t.Fatal("failed commit published the staged column")
- }
- }
|