| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592 |
- package executor
- import (
- "errors"
- "strings"
- "sync"
- "testing"
- "github.com/danfragoso/pizzasql-next/pkg/storage"
- )
- // ADD COLUMN with a constant DEFAULT materializes the value into rows that
- // already exist, so the column reads the default everywhere.
- func TestAlterTableAddColumnBackfillsPopulatedTable(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
- execMust(t, e, "INSERT INTO t (id) VALUES (1), (2)")
- execMust(t, e, "ALTER TABLE t ADD COLUMN note TEXT DEFAULT 'later'")
- res := execMust(t, e, "SELECT id, note FROM t ORDER BY id")
- if res.RowCount != 2 {
- t.Fatalf("row count = %d, want 2", res.RowCount)
- }
- for _, row := range res.Rows {
- if row[1] != "later" {
- t.Fatalf("existing row %v note = %v, want later", row[0], row[1])
- }
- }
- execMust(t, e, "INSERT INTO t (id) VALUES (3)")
- res = execMust(t, e, "SELECT note FROM t WHERE id = 3")
- if got := res.Rows[0][0]; got != "later" {
- t.Fatalf("new row note = %v, want later", got)
- }
- }
- // A DEFAULT declared on an empty table has no rows to rewrite; later INSERTs
- // that omit the column take it, and an explicit NULL still wins.
- func TestAlterTableAddColumnDefaultOnEmptyTable(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
- execMust(t, e, "ALTER TABLE t ADD COLUMN note TEXT DEFAULT 'later'")
- execMust(t, e, "INSERT INTO t (id) VALUES (1)")
- execMust(t, e, "INSERT INTO t (id, note) VALUES (2, NULL)")
- res := execMust(t, e, "SELECT id, note FROM t ORDER BY id")
- if len(res.Rows) != 2 || res.Rows[0][1] != "later" || res.Rows[1][1] != nil {
- t.Fatalf("rows = %v, want [[1 later] [2 NULL]]", res.Rows)
- }
- }
- // ADD COLUMN without a DEFAULT leaves existing rows NULL.
- func TestAlterTableAddColumnWithoutDefaultLeavesExistingRowsNull(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
- execMust(t, e, "INSERT INTO t (id) VALUES (1), (2)")
- execMust(t, e, "ALTER TABLE t ADD COLUMN note TEXT")
- res := execMust(t, e, "SELECT id, note FROM t ORDER BY id")
- if res.RowCount != 2 {
- t.Fatalf("row count = %d, want 2", res.RowCount)
- }
- for _, row := range res.Rows {
- if row[1] != nil {
- t.Fatalf("existing row %v note = %v, want NULL", row[0], row[1])
- }
- }
- }
- // A NOT NULL column with a constant DEFAULT backfills existing rows, so the
- // constraint holds for rows that predate the statement.
- func TestAlterTableAddColumnNotNullWithDefaultBackfills(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
- execMust(t, e, "INSERT INTO t (id) VALUES (1), (2)")
- execMust(t, e, "ALTER TABLE t ADD COLUMN status TEXT NOT NULL DEFAULT 'pending'")
- fresh := newExec(schema, table)
- res := execMust(t, fresh, "SELECT status FROM t ORDER BY id")
- if res.RowCount != 2 {
- t.Fatalf("row count = %d, want 2", res.RowCount)
- }
- for _, row := range res.Rows {
- if row[0] != "pending" {
- t.Fatalf("existing status = %v, want pending", row[0])
- }
- }
- execMust(t, e, "INSERT INTO t (id) VALUES (3)")
- res = execMust(t, e, "SELECT status FROM t WHERE id = 3")
- if got := res.Rows[0][0]; got != "pending" {
- t.Fatalf("new row status = %v, want pending", got)
- }
- }
- // A NOT NULL column without a DEFAULT has nothing to write into existing rows
- // and must not report the table as migrated.
- func TestAlterTableAddColumnNotNullWithoutDefaultRejectsPopulatedTable(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
- execMust(t, e, "INSERT INTO t (id) VALUES (1)")
- _, err := execSQL(e, "ALTER TABLE t ADD COLUMN status TEXT NOT NULL")
- var violation *storage.NotNullViolationError
- if !errors.As(err, &violation) {
- t.Fatalf("error = %v, want NotNullViolationError", err)
- }
- s, err := schema.GetSchema("t")
- if err != nil {
- t.Fatalf("get schema: %v", err)
- }
- if _, exists := s.GetColumn("status"); exists {
- t.Fatal("rejected ADD COLUMN left the column in the schema")
- }
- if rows := execMust(t, e, "SELECT id FROM t").Rows; len(rows) != 1 {
- t.Fatalf("rows = %v, want the existing row untouched", rows)
- }
- }
- // The same statement is legal on an empty table; the constraint is then
- // enforced on the INSERTs that follow.
- func TestAlterTableAddColumnNotNullWithoutDefaultOnEmptyTable(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
- execMust(t, e, "ALTER TABLE t ADD COLUMN status TEXT NOT NULL")
- _, err := execSQL(e, "INSERT INTO t (id) VALUES (1)")
- var violation *storage.NotNullViolationError
- if !errors.As(err, &violation) {
- t.Fatalf("insert without status error = %v, want NotNullViolationError", err)
- }
- execMust(t, e, "INSERT INTO t (id, status) VALUES (1, 'active')")
- res := execMust(t, e, "SELECT status FROM t WHERE id = 1")
- if got := res.Rows[0][0]; got != "active" {
- t.Fatalf("status = %v, want active", got)
- }
- }
- // Backfilling one constant into several rows would duplicate the value, so the
- // UNIQUE constraint must reject the ALTER instead of creating a silently
- // non-unique column.
- func TestAlterTableAddColumnUniqueWithDefaultRejectsPopulatedTable(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
- execMust(t, e, "INSERT INTO t (id) VALUES (1), (2)")
- _, err := execSQL(e, "ALTER TABLE t ADD COLUMN code TEXT DEFAULT 'same' UNIQUE")
- var violation *storage.UniqueViolationError
- if !errors.As(err, &violation) {
- t.Fatalf("error = %v, want UniqueViolationError", err)
- }
- s, err := schema.GetSchema("t")
- if err != nil {
- t.Fatalf("get schema: %v", err)
- }
- if _, exists := s.GetColumn("code"); exists {
- t.Fatal("rejected ADD COLUMN left the column in the schema")
- }
- indexes, err := schema.ListTableIndexes("t")
- if err != nil {
- t.Fatalf("list indexes: %v", err)
- }
- if len(indexes) != 0 {
- t.Fatalf("indexes = %v, want none", indexes)
- }
- }
- // A single-row table can take a UNIQUE column with a default, and the
- // constraint is enforced for every later INSERT that omits the column. The
- // column and the index that validates it must be visible together.
- func TestAlterTableAddColumnUniqueWithDefaultEnforcesLaterInserts(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
- execMust(t, e, "INSERT INTO t (id) VALUES (1)")
- execMust(t, e, "ALTER TABLE t ADD COLUMN code TEXT DEFAULT 'same' UNIQUE")
- s, err := schema.GetSchema("t")
- if err != nil {
- t.Fatalf("get schema: %v", err)
- }
- if _, exists := s.GetColumn("code"); !exists {
- t.Fatal("ADD COLUMN ... UNIQUE published no column")
- }
- indexes, err := schema.ListTableIndexes("t")
- if err != nil {
- t.Fatalf("list indexes: %v", err)
- }
- if len(indexes) != 1 {
- t.Fatalf("indexes = %v, want the one unique index for column code", indexes)
- }
- if !indexes[0].Unique || len(indexes[0].Columns) != 1 || indexes[0].Columns[0].Name != "code" {
- t.Fatalf("index = %+v, want a unique index over code", indexes[0])
- }
- _, err = execSQL(e, "INSERT INTO t (id) VALUES (2)")
- var violation *storage.UniqueViolationError
- if !errors.As(err, &violation) {
- t.Fatalf("insert with duplicated default error = %v, want UniqueViolationError", err)
- }
- execMust(t, e, "INSERT INTO t (id, code) VALUES (2, 'other')")
- res := execMust(t, e, "SELECT code FROM t ORDER BY id")
- if len(res.Rows) != 2 || res.Rows[0][0] != "same" || res.Rows[1][0] != "other" {
- t.Fatalf("codes = %v, want [same other]", res.Rows)
- }
- }
- // A transaction can stage a row image before a concurrent ADD COLUMN publishes
- // a required column. The commit must re-validate that row against the schema now
- // in effect instead of persisting a durable NOT NULL violation.
- func TestAlterTableAddColumnRejectsRowStagedBeforeDDL(t *testing.T) {
- _, schema, table := newTestDB(t)
- writer := newExec(schema, table)
- ddl := newExec(schema, table)
- execMust(t, writer, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
- execMust(t, writer, "BEGIN")
- execMust(t, writer, "INSERT INTO t (id) VALUES (1)")
- // The table is durably empty, so the ALTER accepts the required column.
- execMust(t, ddl, "ALTER TABLE t ADD COLUMN status TEXT NOT NULL")
- _, err := execSQL(writer, "COMMIT")
- var violation *storage.NotNullViolationError
- if !errors.As(err, &violation) {
- t.Fatalf("commit error = %v, want NotNullViolationError", err)
- }
- if got := execMust(t, ddl, "SELECT id FROM t").RowCount; got != 0 {
- t.Fatalf("rows after rejected commit = %d, want 0", got)
- }
- }
- // The same race is harmless when the new column has a default: the commit
- // materializes it into the staged row, exactly as the backfill did for rows that
- // were already durable.
- func TestAlterTableAddColumnDefaultsRowStagedBeforeDDL(t *testing.T) {
- _, schema, table := newTestDB(t)
- writer := newExec(schema, table)
- ddl := newExec(schema, table)
- execMust(t, writer, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
- execMust(t, writer, "BEGIN")
- execMust(t, writer, "INSERT INTO t (id) VALUES (1)")
- execMust(t, ddl, "ALTER TABLE t ADD COLUMN status TEXT NOT NULL DEFAULT 'pending'")
- execMust(t, writer, "COMMIT")
- res := execMust(t, ddl, "SELECT id, status FROM t")
- if len(res.Rows) != 1 || res.Rows[0][1] != "pending" {
- t.Fatalf("rows = %v, want [[1 pending]]", res.Rows)
- }
- }
- // IF NOT EXISTS stays idempotent on a populated table: the second statement is
- // a no-op even when it declares a different default.
- func TestAlterTableAddColumnIfNotExistsKeepsBackfilledValues(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
- execMust(t, e, "INSERT INTO t (id) VALUES (1)")
- execMust(t, e, "ALTER TABLE t ADD COLUMN IF NOT EXISTS note TEXT DEFAULT 'first'")
- execMust(t, e, "ALTER TABLE t ADD COLUMN IF NOT EXISTS note TEXT DEFAULT 'second'")
- res := execMust(t, e, "SELECT note FROM t WHERE id = 1")
- if got := res.Rows[0][0]; got != "first" {
- t.Fatalf("note = %v, want the first default to stand", got)
- }
- s, err := schema.GetSchema("t")
- if err != nil {
- t.Fatalf("get schema: %v", err)
- }
- count := 0
- for _, col := range s.Columns {
- if col.Name == "note" {
- count++
- }
- }
- if count != 1 {
- t.Fatalf("column note appears %d times, want 1", count)
- }
- }
- func TestAlterTableAddColumnIfNotExistsIsIdempotent(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
- execMust(t, e, "ALTER TABLE t ADD COLUMN IF NOT EXISTS c TEXT")
- execMust(t, e, "ALTER TABLE t ADD COLUMN IF NOT EXISTS c TEXT")
- s, err := schema.GetSchema("t")
- if err != nil {
- t.Fatalf("get schema: %v", err)
- }
- count := 0
- for _, col := range s.Columns {
- if col.Name == "c" {
- count++
- }
- }
- if count != 1 {
- t.Fatalf("column c appears %d times, want 1", count)
- }
- }
- // Concurrent IF NOT EXISTS ADD COLUMN on one engine instance must not duplicate
- // metadata; the schema manager serializes the check-and-append.
- func TestAlterTableAddColumnIfNotExistsConcurrent(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
- var wg sync.WaitGroup
- for i := 0; i < 8; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- session := newExec(schema, table)
- if _, err := execSQL(session, "ALTER TABLE t ADD COLUMN IF NOT EXISTS c TEXT"); err != nil {
- t.Errorf("add column: %v", err)
- }
- }()
- }
- wg.Wait()
- s, err := schema.GetSchema("t")
- if err != nil {
- t.Fatalf("get schema: %v", err)
- }
- count := 0
- for _, col := range s.Columns {
- if col.Name == "c" {
- count++
- }
- }
- if count != 1 {
- t.Fatalf("column c appears %d times, want 1", count)
- }
- }
- // DROP COLUMN rewrites every remaining row so the removed value does not stay
- // in durable storage.
- func TestAlterTableDropColumnRewritesPopulatedRows(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, keep TEXT, dropme TEXT)")
- execMust(t, e, "INSERT INTO t (id, keep, dropme) VALUES (1, 'a', 'secret'), (2, 'b', 'secret')")
- execMust(t, e, "ALTER TABLE t DROP COLUMN dropme")
- res := execMust(t, e, "SELECT * FROM t ORDER BY id")
- if len(res.Columns) != 2 || res.Columns[0] != "id" || res.Columns[1] != "keep" {
- t.Fatalf("columns after drop = %v, want [id keep]", res.Columns)
- }
- if res.RowCount != 2 || res.Rows[0][1] != "a" || res.Rows[1][1] != "b" {
- t.Fatalf("rows after drop = %v", res.Rows)
- }
- if _, err := execSQL(e, "SELECT dropme FROM t"); err == nil {
- t.Fatal("expected dropped column to be unknown")
- }
- }
- // RENAME COLUMN rewrites the durable index definitions along with the schema
- // and rows, so single- and multi-column indexes name the new column while
- // keeping their original index names.
- func TestAlterTableRenameColumnUpdatesIndexMetadata(t *testing.T) {
- pool, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, a TEXT, b TEXT)")
- execMust(t, e, "INSERT INTO t (id, a, b) VALUES (1, 'x', 'y')")
- execMust(t, e, "CREATE INDEX idx_a ON t(a)")
- execMust(t, e, "CREATE INDEX idx_ab ON t(a, b)")
- execMust(t, e, "ALTER TABLE t RENAME COLUMN a TO aa")
- single, err := schema.GetIndex("idx_a")
- if err != nil {
- t.Fatalf("get idx_a: %v", err)
- }
- if len(single.Columns) != 1 || single.Columns[0].Name != "aa" {
- t.Fatalf("idx_a columns = %#v, want [aa]", single.Columns)
- }
- composite, err := schema.GetIndex("idx_ab")
- if err != nil {
- t.Fatalf("get idx_ab: %v", err)
- }
- if len(composite.Columns) != 2 || composite.Columns[0].Name != "aa" || composite.Columns[1].Name != "b" {
- t.Fatalf("idx_ab columns = %#v, want [aa b]", composite.Columns)
- }
- // index_info reports the renamed column and resolves its column id.
- res := execMust(t, e, "PRAGMA index_info(idx_a)")
- if len(res.Rows) != 1 || res.Rows[0][1] != int64(1) || res.Rows[0][2] != "aa" {
- t.Fatalf("index_info(idx_a) = %v, want [[0 1 aa]]", res.Rows)
- }
- // sqlite_master renders the recreated index over the new column name.
- res = execMust(t, e, "SELECT sql FROM sqlite_master WHERE type='index' AND name='idx_ab'")
- if len(res.Rows) != 1 || !strings.Contains(res.Rows[0][0].(string), "(`aa`, `b`)") {
- t.Fatalf("recreated idx_ab SQL = %v, want a definition over aa, b", res.Rows)
- }
- // The definition is durable, not just cached on this SchemaManager.
- reloaded := storage.NewSchemaManager(pool, "testdb")
- composite, err = reloaded.GetIndex("idx_ab")
- if err != nil {
- t.Fatalf("reload idx_ab: %v", err)
- }
- if len(composite.Columns) != 2 || composite.Columns[0].Name != "aa" || composite.Columns[1].Name != "b" {
- t.Fatalf("reloaded idx_ab columns = %#v, want [aa b]", composite.Columns)
- }
- // Index-backed equality lookup still returns the row after the rename.
- res = execMust(t, e, "SELECT id, aa FROM t WHERE aa = 'x'")
- if res.RowCount != 1 || res.Rows[0][0] != int64(1) || res.Rows[0][1] != "x" {
- t.Fatalf("indexed lookup after rename = %v, want [[1 x]]", res.Rows)
- }
- }
- // A UNIQUE index keeps enforcing uniqueness after its column is renamed.
- func TestAlterTableRenameColumnKeepsUniqueIndexEnforcement(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT, tag TEXT)")
- execMust(t, e, "CREATE UNIQUE INDEX uq_email ON t(email)")
- execMust(t, e, "INSERT INTO t (id, email, tag) VALUES (1, 'a', 'first')")
- if _, err := execSQL(e, "INSERT INTO t (id, email, tag) VALUES (2, 'a', 'dup')"); err == nil || !strings.Contains(err.Error(), "UNIQUE constraint failed") {
- t.Fatalf("pre-rename duplicate error = %v, want UNIQUE constraint failure", err)
- }
- execMust(t, e, "ALTER TABLE t RENAME COLUMN email TO mail")
- if _, err := execSQL(e, "INSERT INTO t (id, mail, tag) VALUES (3, 'a', 'dup')"); err == nil || !strings.Contains(err.Error(), "UNIQUE constraint failed") {
- t.Fatalf("post-rename duplicate error = %v, want UNIQUE constraint failure", err)
- }
- execMust(t, e, "INSERT INTO t (id, mail, tag) VALUES (4, 'b', 'ok')")
- res := execMust(t, e, "SELECT id, mail, tag FROM t ORDER BY id")
- if res.RowCount != 2 || res.Rows[0][1] != "a" || res.Rows[1][1] != "b" {
- t.Fatalf("rows after rename = %v, want two distinct emails", res.Rows)
- }
- res = execMust(t, e, "PRAGMA index_list(t)")
- if len(res.Rows) != 1 || res.Rows[0][1] != "uq_email" || res.Rows[0][2] != int64(1) {
- t.Fatalf("index_list(t) = %v, want the unique uq_email index", res.Rows)
- }
- }
- // ON CONFLICT resolves against the renamed column and rejects the old name.
- func TestAlterTableRenameColumnResolvesOnConflictTarget(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT, tag TEXT)")
- execMust(t, e, "CREATE UNIQUE INDEX uq_email ON t(email)")
- execMust(t, e, "INSERT INTO t (id, email, tag) VALUES (1, 'a', 'old')")
- execMust(t, e, "ALTER TABLE t RENAME COLUMN email TO mail")
- execMust(t, e, "INSERT INTO t (id, mail, tag) VALUES (2, 'a', 'new') ON CONFLICT (mail) DO UPDATE SET tag = excluded.tag")
- res := execMust(t, e, "SELECT id, mail, tag FROM t")
- if res.RowCount != 1 || res.Rows[0][0] != int64(1) || res.Rows[0][2] != "new" {
- t.Fatalf("upsert on renamed column = %v, want the existing row updated", res.Rows)
- }
- 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") {
- t.Fatalf("upsert on old column error = %v, want column not found: email", err)
- }
- }
- // Renaming a column back restores the original index metadata, and renaming to
- // an existing column name still fails without touching that metadata.
- func TestAlterTableRenameColumnCycleRestoresIndexMetadata(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, a TEXT)")
- execMust(t, e, "CREATE UNIQUE INDEX uq_a ON t(a)")
- execMust(t, e, "INSERT INTO t (id, a) VALUES (1, 'x')")
- execMust(t, e, "ALTER TABLE t RENAME COLUMN a TO b")
- execMust(t, e, "ALTER TABLE t RENAME COLUMN b TO a")
- idx, err := schema.GetIndex("uq_a")
- if err != nil {
- t.Fatalf("get uq_a: %v", err)
- }
- if len(idx.Columns) != 1 || idx.Columns[0].Name != "a" {
- t.Fatalf("uq_a columns after cycle = %#v, want [a]", idx.Columns)
- }
- if _, err := execSQL(e, "INSERT INTO t (id, a) VALUES (2, 'x')"); err == nil || !strings.Contains(err.Error(), "UNIQUE constraint failed") {
- t.Fatalf("duplicate error after rename cycle = %v, want UNIQUE constraint failure", err)
- }
- if _, err := execSQL(e, "ALTER TABLE t RENAME COLUMN a TO id"); err == nil || !strings.Contains(err.Error(), "column already exists: id") {
- t.Fatalf("rename to existing column error = %v, want column already exists: id", err)
- }
- idx, err = schema.GetIndex("uq_a")
- if err != nil {
- t.Fatalf("get uq_a after rejected rename: %v", err)
- }
- if len(idx.Columns) != 1 || idx.Columns[0].Name != "a" {
- t.Fatalf("uq_a columns after rejected rename = %#v, want [a]", idx.Columns)
- }
- }
- // A column referenced by an expression index cannot be renamed: the engine
- // stores expression text verbatim, so the rename is rejected instead of leaving
- // a UNIQUE expression index silently unbound from the column it enforces.
- func TestAlterTableRenameColumnRejectsExpressionIndexDependency(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT, tag TEXT)")
- execMust(t, e, "CREATE UNIQUE INDEX uq_lower_email ON t(lower(email))")
- execMust(t, e, "INSERT INTO t (id, email, tag) VALUES (1, 'a@x', 'keep')")
- _, err := execSQL(e, "ALTER TABLE t RENAME COLUMN email TO mail")
- if err == nil {
- t.Fatal("rename of a column used by an expression index must be rejected")
- }
- for _, want := range []string{"uq_lower_email", "lower(email)"} {
- if !strings.Contains(err.Error(), want) {
- t.Fatalf("rename error = %v, want it to name %q", err, want)
- }
- }
- // Nothing was renamed, and the UNIQUE expression index still enforces.
- sch, err := schema.GetSchema("t")
- if err != nil {
- t.Fatalf("get schema: %v", err)
- }
- if _, ok := sch.GetColumn("email"); !ok {
- t.Fatal("rejected rename removed the email column")
- }
- if _, ok := sch.GetColumn("mail"); ok {
- t.Fatal("rejected rename added the mail column")
- }
- if _, err := execSQL(e, "INSERT INTO t (id, email, tag) VALUES (2, 'a@x', 'dup')"); err == nil || !strings.Contains(err.Error(), "UNIQUE constraint failed") {
- t.Fatalf("duplicate on expression index after rejected rename = %v, want UNIQUE constraint failure", err)
- }
- // A column the expression does not reference can still be renamed.
- execMust(t, e, "ALTER TABLE t RENAME COLUMN tag TO label")
- idx, err := schema.GetIndex("uq_lower_email")
- if err != nil {
- t.Fatalf("get uq_lower_email: %v", err)
- }
- if len(idx.Columns) != 1 || idx.Columns[0].Expression != "lower(email)" {
- t.Fatalf("uq_lower_email columns = %#v, want the untouched lower(email) expression", idx.Columns)
- }
- if res := execMust(t, e, "SELECT label FROM t WHERE id = 1"); res.RowCount != 1 || res.Rows[0][0] != "keep" {
- t.Fatalf("renamed label = %v, want keep", res.Rows)
- }
- }
- // Conflict targets that name a hidden rowid alias are not table columns but are
- // still valid targets, including after a rename.
- func TestAlterTableRenameColumnKeepsRowIDConflictTarget(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, a TEXT)")
- execMust(t, e, "INSERT INTO t (id, a) VALUES (1, 'x')")
- execMust(t, e, "ALTER TABLE t RENAME COLUMN a TO b")
- execMust(t, e, "INSERT INTO t (id, b) VALUES (2, 'y') ON CONFLICT (rowid) DO NOTHING")
- execMust(t, e, "INSERT INTO t (id, b) VALUES (3, 'z') ON CONFLICT (oid) DO NOTHING")
- execMust(t, e, "INSERT INTO t (id, b) VALUES (4, 'w') ON CONFLICT (_rowid_) DO NOTHING")
- if res := execMust(t, e, "SELECT count(*) FROM t"); res.Rows[0][0] != int64(4) {
- t.Fatalf("row count = %v, want 4", res.Rows[0][0])
- }
- }
|