| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703 |
- package executor
- import (
- "errors"
- "strings"
- "testing"
- "github.com/danfragoso/pizzasql-next/pkg/pgerror"
- "github.com/danfragoso/pizzasql-next/pkg/storage"
- )
- func TestCreateTablePrimaryKeyConstraints(t *testing.T) {
- tests := []struct {
- name string
- sql string
- wantErr string
- wantErrSQLState bool
- wantPrimaryKey string
- wantKeyColumns []string
- }{
- {
- name: "composite table-level primary key is accepted",
- sql: "CREATE TABLE t (a TEXT, b TEXT, PRIMARY KEY (a, b))",
- wantPrimaryKey: "",
- wantKeyColumns: []string{"a", "b"},
- },
- {
- name: "three-column table-level primary key is accepted in declaration order",
- sql: "CREATE TABLE t (a TEXT, b TEXT, c TEXT, PRIMARY KEY (c, a, b))",
- wantPrimaryKey: "",
- wantKeyColumns: []string{"c", "a", "b"},
- },
- {
- name: "single-column table-level primary key is accepted",
- sql: "CREATE TABLE t (a TEXT, PRIMARY KEY (a))",
- wantPrimaryKey: "a",
- wantKeyColumns: []string{"a"},
- },
- {
- name: "inline primary key is accepted",
- sql: "CREATE TABLE t (a TEXT PRIMARY KEY)",
- wantPrimaryKey: "a",
- wantKeyColumns: []string{"a"},
- },
- {
- name: "two inline primary keys are rejected",
- sql: "CREATE TABLE t (a TEXT PRIMARY KEY, b TEXT PRIMARY KEY)",
- wantErr: "more than one inline PRIMARY KEY",
- wantErrSQLState: true,
- },
- {
- name: "inline and table-level primary keys are rejected together",
- sql: "CREATE TABLE t (a TEXT PRIMARY KEY, b TEXT, PRIMARY KEY (b))",
- wantErr: "mixes an inline PRIMARY KEY with a table-level PRIMARY KEY",
- wantErrSQLState: true,
- },
- {
- name: "duplicate column in a composite primary key is rejected",
- sql: "CREATE TABLE t (a TEXT, PRIMARY KEY (a, a))",
- wantErr: "appears more than once in the PRIMARY KEY",
- wantErrSQLState: true,
- },
- {
- name: "unknown primary key column is rejected",
- sql: "CREATE TABLE t (a TEXT, PRIMARY KEY (a, missing))",
- wantErr: `PRIMARY KEY column "missing" does not exist`,
- wantErrSQLState: true,
- },
- {
- name: "two table-level primary keys are rejected",
- sql: "CREATE TABLE t (a TEXT, b TEXT, PRIMARY KEY (a), PRIMARY KEY (b))",
- wantErr: "declares more than one PRIMARY KEY",
- wantErrSQLState: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- _, err := execSQL(e, tt.sql)
- if tt.wantErr != "" {
- if err == nil {
- t.Fatalf("expected %q to fail", tt.sql)
- }
- if !strings.Contains(err.Error(), tt.wantErr) {
- t.Fatalf("error = %q, want it to contain %q", err, tt.wantErr)
- }
- if tt.wantErrSQLState && !errors.Is(err, pgerror.ErrFeatureNotSupported) {
- t.Fatalf("error = %v, want SQLSTATE 0A000 (feature not supported)", err)
- }
- if schema.TableExists("t") {
- t.Fatal("rejected CREATE TABLE left a schema behind")
- }
- if _, err := execSQL(e, "SELECT * FROM t"); err == nil {
- t.Fatal("rejected CREATE TABLE left a usable table behind")
- }
- return
- }
- if err != nil {
- t.Fatalf("exec %q: %v", tt.sql, err)
- }
- got, err := schema.GetSchema("t")
- if err != nil {
- t.Fatalf("get schema: %v", err)
- }
- if got.PrimaryKey != tt.wantPrimaryKey {
- t.Fatalf("PrimaryKey = %q, want %q", got.PrimaryKey, tt.wantPrimaryKey)
- }
- if strings.Join(got.PrimaryKeyList(), ",") != strings.Join(tt.wantKeyColumns, ",") {
- t.Fatalf("PrimaryKeyList = %v, want %v", got.PrimaryKeyList(), tt.wantKeyColumns)
- }
- for _, col := range got.Columns {
- if got.IsPrimaryKeyColumn(col.Name) && col.Nullable {
- t.Fatalf("primary key column %s is nullable", col.Name)
- }
- }
- })
- }
- }
- func TestCompositePrimaryKeyDML(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE pairs (a TEXT, b INTEGER, v TEXT, PRIMARY KEY (a, b))")
- execMust(t, e, "INSERT INTO pairs (a, b, v) VALUES ('x', 1, 'one')")
- execMust(t, e, "INSERT INTO pairs (a, b, v) VALUES ('x', 2, 'two')")
- execMust(t, e, "INSERT INTO pairs (a, b, v) VALUES ('y', 1, 'other')")
- // A full-key predicate resolves exactly one row.
- res := execMust(t, e, "SELECT v FROM pairs WHERE a = 'x' AND b = 1")
- if res.RowCount != 1 || res.Rows[0][0] != "one" {
- t.Fatalf("full-key select = %#v, want one row 'one'", res.Rows)
- }
- // A single key column is only a prefix: it must not be treated as a point
- // lookup that returns whichever row was stored first.
- res = execMust(t, e, "SELECT b FROM pairs WHERE a = 'x' ORDER BY b")
- if res.RowCount != 2 {
- t.Fatalf("prefix select returned %d rows, want 2", res.RowCount)
- }
- // Duplicate full key.
- if _, err := execSQL(e, "INSERT INTO pairs (a, b, v) VALUES ('x', 1, 'dup')"); err == nil {
- t.Fatal("expected a duplicate composite key to fail")
- } else if !isConflictError(err) {
- t.Fatalf("duplicate error = %v, want a primary-key conflict", err)
- }
- // Missing component: NOT NULL violation, never a rowid auto-assign.
- _, err := execSQL(e, "INSERT INTO pairs (a, v) VALUES ('z', 'no b')")
- var notNull *storage.NotNullViolationError
- if !errors.As(err, ¬Null) || notNull.Column != "b" {
- t.Fatalf("missing component error = %v, want a NOT NULL violation on b", err)
- }
- _, err = execSQL(e, "INSERT INTO pairs (a, b, v) VALUES ('z', NULL, 'null b')")
- if !errors.As(err, ¬Null) || notNull.Column != "b" {
- t.Fatalf("NULL component error = %v, want a NOT NULL violation on b", err)
- }
- if res := execMust(t, e, "SELECT count(*) FROM pairs"); res.Rows[0][0] != int64(3) {
- t.Fatalf("rejected inserts stored rows: count = %v", res.Rows[0][0])
- }
- // Update by full key.
- res = execMust(t, e, "UPDATE pairs SET v = 'ONE' WHERE a = 'x' AND b = 1")
- if res.RowsAffected != 1 {
- t.Fatalf("update affected %d rows, want 1", res.RowsAffected)
- }
- if res = execMust(t, e, "SELECT v FROM pairs WHERE a = 'x' AND b = 1"); res.RowCount != 1 || res.Rows[0][0] != "ONE" {
- t.Fatalf("row after update = %#v, want 'ONE'", res.Rows)
- }
- // Moving one key component moves the durable row.
- execMust(t, e, "UPDATE pairs SET b = 3 WHERE a = 'x' AND b = 1")
- if res = execMust(t, e, "SELECT v FROM pairs WHERE a = 'x' AND b = 3"); res.RowCount != 1 || res.Rows[0][0] != "ONE" {
- t.Fatalf("row under the new key = %#v, want 'ONE'", res.Rows)
- }
- if res = execMust(t, e, "SELECT v FROM pairs WHERE a = 'x' AND b = 1"); res.RowCount != 0 {
- t.Fatalf("old key still resolves %d rows", res.RowCount)
- }
- // Moving onto an existing key is a duplicate.
- if _, err := execSQL(e, "UPDATE pairs SET b = 2 WHERE a = 'x' AND b = 3"); err == nil {
- t.Fatal("expected a key move onto an existing row to fail")
- } else if !isConflictError(err) {
- t.Fatalf("key move error = %v, want a primary-key conflict", err)
- }
- // Clearing a key component is a NOT NULL violation, not a key move.
- _, err = execSQL(e, "UPDATE pairs SET b = NULL WHERE a = 'x' AND b = 2")
- if !errors.As(err, ¬Null) || notNull.Column != "b" {
- t.Fatalf("NULL key update error = %v, want a NOT NULL violation on b", err)
- }
- // Delete by full key removes exactly one row.
- res = execMust(t, e, "DELETE FROM pairs WHERE a = 'x' AND b = 2")
- if res.RowsAffected != 1 {
- t.Fatalf("delete affected %d rows, want 1", res.RowsAffected)
- }
- if res = execMust(t, e, "SELECT count(*) FROM pairs"); res.Rows[0][0] != int64(2) {
- t.Fatalf("row count after delete = %v, want 2", res.Rows[0][0])
- }
- }
- func TestCompositePrimaryKeyOnConflict(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE counters (a TEXT, b TEXT, n INTEGER, PRIMARY KEY (a, b))")
- execMust(t, e, "INSERT INTO counters (a, b, n) VALUES ('x', 'y', 1)")
- execMust(t, e, "INSERT INTO counters (a, b, n) VALUES ('x', 'y', 2) ON CONFLICT (a, b) DO UPDATE SET n = excluded.n")
- if res := execMust(t, e, "SELECT n FROM counters WHERE a = 'x' AND b = 'y'"); res.RowCount != 1 || res.Rows[0][0] != int64(2) {
- t.Fatalf("upsert result = %#v, want one row with n = 2", res.Rows)
- }
- execMust(t, e, "INSERT INTO counters (a, b, n) VALUES ('x', 'y', 9) ON CONFLICT (a, b) DO NOTHING")
- if res := execMust(t, e, "SELECT n FROM counters WHERE a = 'x' AND b = 'y'"); res.RowCount != 1 || res.Rows[0][0] != int64(2) {
- t.Fatalf("DO NOTHING result = %#v, want the existing row with n = 2", res.Rows)
- }
- // A targetless conflict clause uses the full primary-key column list.
- execMust(t, e, "INSERT INTO counters (a, b, n) VALUES ('x', 'y', 9) ON CONFLICT DO NOTHING")
- if res := execMust(t, e, "SELECT count(*) FROM counters"); res.Rows[0][0] != int64(1) {
- t.Fatalf("targetless DO NOTHING inserted a row: count = %v", res.Rows[0][0])
- }
- // REPLACE resolves the conflict on the full key.
- execMust(t, e, "INSERT OR REPLACE INTO counters (a, b, n) VALUES ('x', 'y', 5)")
- if res := execMust(t, e, "SELECT n FROM counters WHERE a = 'x' AND b = 'y'"); res.RowCount != 1 || res.Rows[0][0] != int64(5) {
- t.Fatalf("REPLACE result = %#v, want one row with n = 5", res.Rows)
- }
- // A different key is not a conflict.
- execMust(t, e, "INSERT INTO counters (a, b, n) VALUES ('x', 'z', 7) ON CONFLICT (a, b) DO NOTHING")
- if res := execMust(t, e, "SELECT n FROM counters WHERE a = 'x' AND b = 'z'"); res.RowCount != 1 || res.Rows[0][0] != int64(7) {
- t.Fatalf("new key result = %#v, want one row with n = 7", res.Rows)
- }
- // ON CONFLICT UPDATE on a full key with the columns in the other order is a
- // distinct target and must not match the declared key.
- execMust(t, e, "INSERT INTO counters (a, b, n) VALUES ('q', 'r', 1) ON CONFLICT (b, a) DO UPDATE SET n = excluded.n")
- if res := execMust(t, e, "SELECT count(*) FROM counters WHERE a = 'q' AND b = 'r'"); res.Rows[0][0] != int64(1) {
- t.Fatalf("reordered conflict target rows = %v, want 1", res.Rows[0][0])
- }
- }
- func TestCompositePrimaryKeyCatalog(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE pairs (a TEXT, b INTEGER, v TEXT, PRIMARY KEY (a, b))")
- res := execMust(t, e, "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'pairs'")
- if res.RowCount != 1 {
- t.Fatalf("sqlite_master rows = %d, want 1", res.RowCount)
- }
- createSQL, _ := res.Rows[0][0].(string)
- if !strings.Contains(createSQL, "PRIMARY KEY (`a`, `b`)") {
- t.Fatalf("sqlite_master SQL = %q, want a table-level PRIMARY KEY (a, b)", createSQL)
- }
- if strings.Count(createSQL, "PRIMARY KEY") != 1 {
- t.Fatalf("sqlite_master SQL = %q, want exactly one PRIMARY KEY clause", createSQL)
- }
- res = execMust(t, e, "PRAGMA table_info(pairs)")
- if res.RowCount != 3 {
- t.Fatalf("table_info rows = %d, want 3", res.RowCount)
- }
- wantPK := []int64{1, 2, 0}
- for i, row := range res.Rows {
- if row[1] != []string{"a", "b", "v"}[i] {
- t.Fatalf("table_info row %d column = %v", i, row[1])
- }
- if row[5] != wantPK[i] {
- t.Fatalf("table_info pk ordinal for %v = %v, want %d", row[1], row[5], wantPK[i])
- }
- }
- // Both key columns are implicitly NOT NULL.
- if res.Rows[0][3] != int64(1) || res.Rows[1][3] != int64(1) {
- t.Fatalf("table_info notnull = %v/%v, want 1/1", res.Rows[0][3], res.Rows[1][3])
- }
- res = execMust(t, e, "PRAGMA table_xinfo(pairs)")
- if res.RowCount != 3 || res.Rows[1][5] != int64(2) {
- t.Fatalf("table_xinfo = %#v, want pk ordinal 2 on the second key column", res.Rows)
- }
- }
- func TestCompositePrimaryKeyDurableReload(t *testing.T) {
- pool, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE pairs (a TEXT, b TEXT, v TEXT, PRIMARY KEY (a, b))")
- execMust(t, e, "INSERT INTO pairs (a, b, v) VALUES ('x', 'y', 'one')")
- execMust(t, e, "INSERT INTO pairs (a, b, v) VALUES ('x', 'z', 'two')")
- // Simulate a process restart: new managers share only the durable KV state.
- reloadedSchema := storage.NewSchemaManager(pool, "testdb")
- reloadedTable := storage.NewTableManager(pool, reloadedSchema, "testdb")
- reloaded := newExec(reloadedSchema, reloadedTable)
- got, err := reloadedSchema.GetSchema("pairs")
- if err != nil {
- t.Fatalf("get reloaded schema: %v", err)
- }
- if got.PrimaryKey != "" || strings.Join(got.PrimaryKeyList(), ",") != "a,b" {
- t.Fatalf("reloaded key = %q/%v, want composite a,b", got.PrimaryKey, got.PrimaryKeyList())
- }
- res := execMust(t, reloaded, "SELECT v FROM pairs WHERE a = 'x' AND b = 'y'")
- if res.RowCount != 1 || res.Rows[0][0] != "one" {
- t.Fatalf("reloaded full-key read = %#v, want one row 'one'", res.Rows)
- }
- if res = execMust(t, reloaded, "SELECT count(*) FROM pairs"); res.Rows[0][0] != int64(2) {
- t.Fatalf("reloaded count = %v, want 2", res.Rows[0][0])
- }
- execMust(t, reloaded, "UPDATE pairs SET v = 'updated' WHERE a = 'x' AND b = 'z'")
- if res = execMust(t, reloaded, "SELECT v FROM pairs WHERE a = 'x' AND b = 'z'"); res.RowCount != 1 || res.Rows[0][0] != "updated" {
- t.Fatalf("reloaded update = %#v, want one row 'updated'", res.Rows)
- }
- }
- func TestCompositePrimaryKeyRejectsDuplicateInsert(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE pairs (a TEXT, b TEXT, PRIMARY KEY (a, b))")
- execMust(t, e, "INSERT INTO pairs (a, b) VALUES ('x', 'y')")
- if _, err := execSQL(e, "INSERT INTO pairs (a, b) VALUES ('x', 'y')"); err == nil {
- t.Fatal("expected a duplicate composite key to fail")
- } else if !isConflictError(err) {
- t.Fatalf("duplicate error = %v, want a primary-key conflict", err)
- }
- execMust(t, e, "INSERT INTO pairs (a, b) VALUES ('x', 'z')")
- execMust(t, e, "INSERT INTO pairs (a, b) VALUES ('y', 'y')")
- if res := execMust(t, e, "SELECT count(*) FROM pairs"); res.Rows[0][0] != int64(3) {
- t.Fatalf("row count = %v, want 3", res.Rows[0][0])
- }
- }
- func TestIntegerPrimaryKeyRemainsRowIDAlias(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")
- execMust(t, e, "INSERT INTO t (v) VALUES ('auto')")
- res := execMust(t, e, "SELECT id, rowid FROM t")
- if res.RowCount != 1 || res.Rows[0][0] != int64(1) || res.Rows[0][1] != int64(1) {
- t.Fatalf("INTEGER PRIMARY KEY row = %#v, want id and rowid 1", res.Rows)
- }
- // A composite key is never a rowid alias: the key columns keep the supplied
- // values and the hidden rowid is engine-assigned.
- execMust(t, e, "CREATE TABLE pairs (a INTEGER, b TEXT, PRIMARY KEY (a, b))")
- execMust(t, e, "INSERT INTO pairs (a, b) VALUES (7, 'x')")
- res = execMust(t, e, "SELECT a, b, rowid FROM pairs")
- if res.RowCount != 1 || res.Rows[0][0] != int64(7) || res.Rows[0][2] != int64(1) {
- t.Fatalf("composite key row = %#v, want a=7 b='x' rowid=1", res.Rows)
- }
- }
- func TestCompositePrimaryKeyInsertSelect(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE src (a TEXT, b TEXT)")
- execMust(t, e, "INSERT INTO src (a, b) VALUES ('x', 'y'), ('x', 'z'), ('q', 'r')")
- execMust(t, e, "CREATE TABLE pairs (a TEXT, b TEXT, v TEXT, PRIMARY KEY (a, b))")
- execMust(t, e, "INSERT INTO pairs (a, b, v) SELECT a, b, 'copied' FROM src")
- if res := execMust(t, e, "SELECT count(*) FROM pairs"); res.Rows[0][0] != int64(3) {
- t.Fatalf("bulk insert stored %v rows, want 3", res.Rows[0][0])
- }
- if res := execMust(t, e, "SELECT v FROM pairs WHERE a = 'x' AND b = 'z'"); res.RowCount != 1 || res.Rows[0][0] != "copied" {
- t.Fatalf("bulk row = %#v, want one row 'copied'", res.Rows)
- }
- // A bulk insert that repeats a key fails atomically.
- if _, err := execSQL(e, "INSERT INTO pairs (a, b, v) SELECT a, b, 'again' FROM src"); err == nil {
- t.Fatal("expected a duplicate-key bulk insert to fail")
- } else if !isConflictError(err) {
- t.Fatalf("bulk duplicate error = %v, want a key conflict", err)
- }
- if res := execMust(t, e, "SELECT count(*) FROM pairs"); res.Rows[0][0] != int64(3) {
- t.Fatalf("failed bulk insert changed the row count: %v", res.Rows[0][0])
- }
- }
- func TestCompositePrimaryKeyAlterTable(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE pairs (a TEXT, b TEXT, v TEXT, PRIMARY KEY (a, b))")
- execMust(t, e, "INSERT INTO pairs (a, b, v) VALUES ('x', 'y', 'one')")
- // Renaming a key column keeps the key list and the durable row keys.
- execMust(t, e, "ALTER TABLE pairs RENAME COLUMN a TO k")
- got, err := schema.GetSchema("pairs")
- if err != nil {
- t.Fatalf("get schema: %v", err)
- }
- if strings.Join(got.PrimaryKeyList(), ",") != "k,b" {
- t.Fatalf("key list after rename = %v, want [k b]", got.PrimaryKeyList())
- }
- if res := execMust(t, e, "SELECT v FROM pairs WHERE k = 'x' AND b = 'y'"); res.RowCount != 1 || res.Rows[0][0] != "one" {
- t.Fatalf("read after rename = %#v, want one row 'one'", res.Rows)
- }
- // A key column cannot be dropped, so the key arity is immutable.
- if _, err := execSQL(e, "ALTER TABLE pairs DROP COLUMN b"); err == nil {
- t.Fatal("expected DROP COLUMN of a key column to fail")
- } else if !strings.Contains(err.Error(), "cannot drop primary key column") {
- t.Fatalf("DROP COLUMN error = %v, want a primary-key rejection", err)
- }
- }
- // An upsert updates the conflicting row in place, so it cannot move that row to
- // a different key. Accepting the assignment would leave the durable row under
- // the old key while its body carries the new key values, and a later full-key
- // update would create a second row.
- func TestUpsertRejectsPrimaryKeyAssignment(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE pairs (a TEXT, b TEXT, v TEXT, PRIMARY KEY (a, b))")
- execMust(t, e, "INSERT INTO pairs (a, b, v) VALUES ('x', 'y', 'one')")
- for _, sql := range []string{
- "INSERT INTO pairs (a, b, v) VALUES ('x', 'y', 'two') ON CONFLICT (a, b) DO UPDATE SET v = excluded.v, a = 'moved'",
- "INSERT INTO pairs (a, b, v) VALUES ('new', 'key', 'v') ON CONFLICT (a, b) DO UPDATE SET b = excluded.b",
- } {
- _, err := execSQL(e, sql)
- if err == nil {
- t.Fatalf("%q succeeded, want a primary-key rejection", sql)
- }
- if !errors.Is(err, pgerror.ErrFeatureNotSupported) {
- t.Fatalf("%q error = %v, want SQLSTATE 0A000", sql, err)
- }
- if !strings.Contains(err.Error(), "cannot assign primary key column") {
- t.Fatalf("%q error = %v, want a primary-key assignment message", sql, err)
- }
- }
- // No ghost row: the original row kept both its key and its value.
- if res := execMust(t, e, "SELECT a, b, v FROM pairs"); res.RowCount != 1 || res.Rows[0][0] != "x" || res.Rows[0][1] != "y" || res.Rows[0][2] != "one" {
- t.Fatalf("rows after rejected upserts = %#v, want the original ('x','y','one')", res.Rows)
- }
- if res := execMust(t, e, "SELECT v FROM pairs WHERE a = 'x' AND b = 'y'"); res.RowCount != 1 || res.Rows[0][0] != "one" {
- t.Fatalf("full-key read after rejected upserts = %#v", res.Rows)
- }
- execMust(t, e, "UPDATE pairs SET v = 'updated' WHERE a = 'x' AND b = 'y'")
- if res := execMust(t, e, "SELECT count(*) FROM pairs"); res.Rows[0][0] != int64(1) {
- t.Fatalf("row count after full-key update = %v, want 1 (no split row)", res.Rows[0][0])
- }
- // A single-column key gets the same rejection.
- execMust(t, e, "CREATE TABLE single (id INTEGER PRIMARY KEY, v TEXT)")
- execMust(t, e, "INSERT INTO single (v) VALUES ('one')")
- if _, err := execSQL(e, "INSERT INTO single (id, v) VALUES (1, 'two') ON CONFLICT (id) DO UPDATE SET id = excluded.id"); err == nil {
- t.Fatal("single-column upsert key assignment succeeded, want a rejection")
- } else if !errors.Is(err, pgerror.ErrFeatureNotSupported) {
- t.Fatalf("single-column upsert error = %v, want SQLSTATE 0A000", err)
- }
- }
- // A key column that takes its value from a constant DEFAULT must be resolved
- // before conflict matching, otherwise the candidate key is encoded from a
- // missing component and the upsert misses its own row.
- func TestUpsertWithConstantDefaultKeyColumns(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE konst (a TEXT DEFAULT 'd', b TEXT DEFAULT 'e', v TEXT, PRIMARY KEY (a, b))")
- execMust(t, e, "INSERT INTO konst (b, v) VALUES ('y', 'two')")
- if res := execMust(t, e, "SELECT a, b, v FROM konst"); res.RowCount != 1 || res.Rows[0][0] != "d" || res.Rows[0][1] != "y" {
- t.Fatalf("defaulted key row = %#v, want ('d','y')", res.Rows)
- }
- // DO UPDATE hits the row whose key came from the DEFAULT.
- execMust(t, e, "INSERT INTO konst (b, v) VALUES ('y', 'updated') ON CONFLICT (a, b) DO UPDATE SET v = excluded.v")
- if res := execMust(t, e, "SELECT v FROM konst WHERE a = 'd' AND b = 'y'"); res.RowCount != 1 || res.Rows[0][0] != "updated" {
- t.Fatalf("DO UPDATE result = %#v, want one row 'updated'", res.Rows)
- }
- // DO NOTHING suppresses the duplicate.
- execMust(t, e, "INSERT INTO konst (b, v) VALUES ('y', 'ignored') ON CONFLICT (a, b) DO NOTHING")
- if res := execMust(t, e, "SELECT v FROM konst WHERE a = 'd' AND b = 'y'"); res.RowCount != 1 || res.Rows[0][0] != "updated" {
- t.Fatalf("DO NOTHING result = %#v, want the existing 'updated' row", res.Rows)
- }
- // Targetless ON CONFLICT uses the defaulted key too.
- execMust(t, e, "INSERT INTO konst (b, v) VALUES ('y', 'ignored') ON CONFLICT DO NOTHING")
- if res := execMust(t, e, "SELECT count(*) FROM konst"); res.Rows[0][0] != int64(1) {
- t.Fatalf("targetless DO NOTHING inserted a row: count = %v", res.Rows[0][0])
- }
- // INSERT OR REPLACE replaces the defaulted-key row instead of duplicating it.
- execMust(t, e, "INSERT OR REPLACE INTO konst (b, v) VALUES ('y', 'replaced')")
- if res := execMust(t, e, "SELECT count(*) FROM konst"); res.Rows[0][0] != int64(1) {
- t.Fatalf("OR REPLACE left %v rows, want 1", res.Rows[0][0])
- }
- if res := execMust(t, e, "SELECT v FROM konst WHERE a = 'd' AND b = 'y'"); res.RowCount != 1 || res.Rows[0][0] != "replaced" {
- t.Fatalf("OR REPLACE result = %#v, want one row 'replaced'", res.Rows)
- }
- // A single-column TEXT key with a DEFAULT follows the same path and must not
- // fall back to an engine-assigned rowid key.
- execMust(t, e, "CREATE TABLE single (id TEXT DEFAULT 'k', v TEXT, PRIMARY KEY (id))")
- execMust(t, e, "INSERT INTO single (v) VALUES ('a')")
- if res := execMust(t, e, "SELECT id FROM single"); res.RowCount != 1 || res.Rows[0][0] != "k" {
- t.Fatalf("single defaulted key = %#v, want one row with id 'k'", res.Rows)
- }
- execMust(t, e, "INSERT INTO single (v) VALUES ('b') ON CONFLICT (id) DO UPDATE SET v = excluded.v")
- if res := execMust(t, e, "SELECT v FROM single"); res.RowCount != 1 || res.Rows[0][0] != "b" {
- t.Fatalf("single DO UPDATE = %#v, want one row 'b'", res.Rows)
- }
- execMust(t, e, "INSERT INTO single (v) VALUES ('c') ON CONFLICT DO NOTHING")
- if res := execMust(t, e, "SELECT count(*) FROM single"); res.Rows[0][0] != int64(1) {
- t.Fatalf("single targetless DO NOTHING count = %v, want 1", res.Rows[0][0])
- }
- execMust(t, e, "INSERT OR REPLACE INTO single (v) VALUES ('d')")
- if res := execMust(t, e, "SELECT v FROM single"); res.RowCount != 1 || res.Rows[0][0] != "d" {
- t.Fatalf("single OR REPLACE = %#v, want one row 'd'", res.Rows)
- }
- }
- func TestAlterTableAddColumnPrimaryKeyRejected(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (a TEXT)")
- execMust(t, e, "INSERT INTO t (a) VALUES ('x')")
- _, err := execSQL(e, "ALTER TABLE t ADD COLUMN id INTEGER PRIMARY KEY")
- if err == nil {
- t.Fatal("ALTER TABLE ADD COLUMN PRIMARY KEY succeeded, want a rejection")
- }
- if !errors.Is(err, pgerror.ErrFeatureNotSupported) {
- t.Fatalf("error = %v, want SQLSTATE 0A000", err)
- }
- if !strings.Contains(err.Error(), "cannot declare PRIMARY KEY") {
- t.Fatalf("error = %v, want an ADD COLUMN PRIMARY KEY message", err)
- }
- got, err := schema.GetSchema("t")
- if err != nil {
- t.Fatalf("get schema: %v", err)
- }
- if _, exists := got.GetColumn("id"); exists {
- t.Fatal("rejected ADD COLUMN published the column")
- }
- }
- func TestGeneratedColumnInPrimaryKeyRejected(t *testing.T) {
- for _, sql := range []string{
- "CREATE TABLE g (a TEXT, b TEXT GENERATED ALWAYS AS (a) STORED, PRIMARY KEY (b))",
- "CREATE TABLE g (a TEXT, b TEXT GENERATED ALWAYS AS (a) STORED PRIMARY KEY)",
- } {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- _, err := execSQL(e, sql)
- if err == nil {
- t.Fatalf("%q succeeded, want a generated-column rejection", sql)
- }
- if !errors.Is(err, pgerror.ErrFeatureNotSupported) {
- t.Fatalf("%q error = %v, want SQLSTATE 0A000", sql, err)
- }
- if !strings.Contains(err.Error(), "cannot be part of a PRIMARY KEY") {
- t.Fatalf("%q error = %v, want a PRIMARY KEY message", sql, err)
- }
- if schema.TableExists("g") {
- t.Fatalf("%q left a schema behind", sql)
- }
- }
- }
- // RETURNING must observe key components that the storage layer resolved from a
- // catalog DEFAULT; the candidate row does not carry them until then.
- func TestCompositePrimaryKeyReturningWithDefaultedKey(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE pairs (a TEXT DEFAULT 'd', b TEXT DEFAULT 'e', v TEXT, PRIMARY KEY (a, b))")
- res := execMust(t, e, "INSERT INTO pairs (v) VALUES ('one') RETURNING a, b, v")
- if res.RowCount != 1 || res.Rows[0][0] != "d" || res.Rows[0][1] != "e" || res.Rows[0][2] != "one" {
- t.Fatalf("RETURNING = %#v, want ('d','e','one')", res.Rows)
- }
- // A partially defaulted key also returns the stored values.
- res = execMust(t, e, "INSERT INTO pairs (b, v) VALUES ('y', 'two') RETURNING *")
- if res.RowCount != 1 {
- t.Fatalf("RETURNING * rows = %d, want 1", res.RowCount)
- }
- values := map[string]interface{}{}
- for i, name := range res.Columns {
- values[name] = res.Rows[0][i]
- }
- if values["a"] != "d" || values["b"] != "y" || values["v"] != "two" {
- t.Fatalf("RETURNING * row = %#v, want a='d' b='y' v='two'", values)
- }
- }
- // ON CONFLICT targets address the key column set, not its declaration order:
- // (b, a) and (a, b) name the same PRIMARY KEY (a, b). SQLite requires the
- // constraint's order; this engine already accepts arbitrary target lists and
- // documents the set match here.
- func TestOnConflictReorderedPrimaryKeyTarget(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE pairs (a TEXT, b TEXT, v TEXT, PRIMARY KEY (a, b))")
- execMust(t, e, "INSERT INTO pairs (a, b, v) VALUES ('x', 'y', 'one')")
- execMust(t, e, "INSERT INTO pairs (a, b, v) VALUES ('x', 'y', 'two') ON CONFLICT (b, a) DO UPDATE SET v = excluded.v")
- res := execMust(t, e, "SELECT count(*) FROM pairs")
- if res.Rows[0][0] != int64(1) {
- t.Fatalf("reordered target left %v rows, want 1", res.Rows[0][0])
- }
- res = execMust(t, e, "SELECT v FROM pairs WHERE a = 'x' AND b = 'y'")
- if res.RowCount != 1 || res.Rows[0][0] != "two" {
- t.Fatalf("reordered target result = %#v, want the updated row 'two'", res.Rows)
- }
- execMust(t, e, "INSERT INTO pairs (a, b, v) VALUES ('x', 'y', 'three') ON CONFLICT (b, a) DO NOTHING")
- res = execMust(t, e, "SELECT v FROM pairs WHERE a = 'x' AND b = 'y'")
- if res.RowCount != 1 || res.Rows[0][0] != "two" {
- t.Fatalf("reordered DO NOTHING result = %#v, want the existing 'two' row", res.Rows)
- }
- }
- // A composite primary key must coexist with scan-validated UNIQUE indexes:
- // key moves keep index lookups correct, ON CONFLICT (unique_col) upserts the
- // row, and INSERT OR REPLACE resolves both the unique index and the primary key.
- func TestCompositePrimaryKeyWithUniqueIndex(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE members (org TEXT, user_id INTEGER, email TEXT UNIQUE, v TEXT, PRIMARY KEY (org, user_id))")
- execMust(t, e, "INSERT INTO members (org, user_id, email, v) VALUES ('o', 1, 'a@x', 'one')")
- // ON CONFLICT (unique_col) updates the row under its composite key.
- execMust(t, e, "INSERT INTO members (org, user_id, email, v) VALUES ('o', 1, 'a@x', 'two') ON CONFLICT (email) DO UPDATE SET v = excluded.v")
- if res := execMust(t, e, "SELECT v FROM members WHERE org = 'o' AND user_id = 1"); res.RowCount != 1 || res.Rows[0][0] != "two" {
- t.Fatalf("unique-target upsert = %#v, want one row 'two'", res.Rows)
- }
- // A unique conflict on a different key is suppressed by DO NOTHING.
- execMust(t, e, "INSERT INTO members (org, user_id, email, v) VALUES ('o', 2, 'a@x', 'duplicate email') ON CONFLICT (email) DO NOTHING")
- if res := execMust(t, e, "SELECT count(*) FROM members"); res.Rows[0][0] != int64(1) {
- t.Fatalf("unique DO NOTHING count = %v, want 1", res.Rows[0][0])
- }
- // Moving a key component keeps the unique index entry attached to the row.
- execMust(t, e, "UPDATE members SET user_id = 3 WHERE org = 'o' AND user_id = 1")
- if res := execMust(t, e, "SELECT v FROM members WHERE org = 'o' AND user_id = 3"); res.RowCount != 1 || res.Rows[0][0] != "two" {
- t.Fatalf("row after key move = %#v, want one row 'two'", res.Rows)
- }
- if res := execMust(t, e, "SELECT v FROM members WHERE email = 'a@x'"); res.RowCount != 1 || res.Rows[0][0] != "two" {
- t.Fatalf("unique index after key move = %#v, want one row 'two'", res.Rows)
- }
- // OR REPLACE resolves the unique index by removing the old row and keeping
- // exactly the new key.
- execMust(t, e, "INSERT OR REPLACE INTO members (org, user_id, email, v) VALUES ('o', 9, 'a@x', 'replaced')")
- if res := execMust(t, e, "SELECT count(*) FROM members"); res.Rows[0][0] != int64(1) {
- t.Fatalf("unique OR REPLACE count = %v, want 1", res.Rows[0][0])
- }
- if res := execMust(t, e, "SELECT v FROM members WHERE org = 'o' AND user_id = 9"); res.RowCount != 1 || res.Rows[0][0] != "replaced" {
- t.Fatalf("unique OR REPLACE row = %#v, want one row 'replaced'", res.Rows)
- }
- if res := execMust(t, e, "SELECT v FROM members WHERE org = 'o' AND user_id = 3"); res.RowCount != 0 {
- t.Fatalf("old composite key still resolves %d rows", res.RowCount)
- }
- // OR REPLACE resolving the primary key works with a unique index present.
- execMust(t, e, "INSERT OR REPLACE INTO members (org, user_id, email, v) VALUES ('o', 9, 'b@x', 'key replaced')")
- if res := execMust(t, e, "SELECT email, v FROM members WHERE org = 'o' AND user_id = 9"); res.RowCount != 1 || res.Rows[0][0] != "b@x" || res.Rows[0][1] != "key replaced" {
- t.Fatalf("PK OR REPLACE row = %#v, want ('b@x','key replaced')", res.Rows)
- }
- if res := execMust(t, e, "SELECT v FROM members WHERE email = 'a@x'"); res.RowCount != 0 {
- t.Fatalf("stale unique index entry still resolves %d rows", res.RowCount)
- }
- }
- // A composite table has no INTEGER PRIMARY KEY alias, so last_insert_rowid()
- // reports the engine-assigned hidden rowid and advances per inserted row.
- func TestLastInsertRowIDOnCompositeTable(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE pairs (a TEXT, b TEXT, PRIMARY KEY (a, b))")
- res := execMust(t, e, "INSERT INTO pairs (a, b) VALUES ('x', 'y')")
- if res.LastInsertID != 1 {
- t.Fatalf("first LastInsertID = %d, want 1", res.LastInsertID)
- }
- if res := execMust(t, e, "SELECT last_insert_rowid()"); res.RowCount != 1 || res.Rows[0][0] != int64(1) {
- t.Fatalf("last_insert_rowid() = %#v, want 1", res.Rows)
- }
- res = execMust(t, e, "INSERT INTO pairs (a, b) VALUES ('x', 'z')")
- if res.LastInsertID != 2 {
- t.Fatalf("second LastInsertID = %d, want 2", res.LastInsertID)
- }
- if res := execMust(t, e, "SELECT last_insert_rowid()"); res.RowCount != 1 || res.Rows[0][0] != int64(2) {
- t.Fatalf("last_insert_rowid() after second insert = %#v, want 2", res.Rows)
- }
- // A suppressed conflict does not advance last_insert_rowid().
- execMust(t, e, "INSERT INTO pairs (a, b) VALUES ('x', 'z') ON CONFLICT (a, b) DO NOTHING")
- if res := execMust(t, e, "SELECT last_insert_rowid()"); res.Rows[0][0] != int64(2) {
- t.Fatalf("last_insert_rowid() after upsert = %v, want 2", res.Rows[0][0])
- }
- }
|