|
|
@@ -1,36 +1,76 @@
|
|
|
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
|
|
|
- wantPrimaryKey string
|
|
|
+ name string
|
|
|
+ sql string
|
|
|
+ wantErr string
|
|
|
+ wantErrSQLState bool
|
|
|
+ wantPrimaryKey string
|
|
|
+ wantKeyColumns []string
|
|
|
}{
|
|
|
{
|
|
|
- name: "composite table-level primary key is rejected",
|
|
|
- sql: "CREATE TABLE t (a TEXT, b TEXT, PRIMARY KEY (a, b))",
|
|
|
- wantErr: "composite primary key",
|
|
|
+ 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: "duplicate column composite primary key is rejected",
|
|
|
- sql: "CREATE TABLE t (a TEXT, PRIMARY KEY (a, a))",
|
|
|
- wantErr: "composite primary key",
|
|
|
+ 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,
|
|
|
},
|
|
|
}
|
|
|
|
|
|
@@ -47,6 +87,9 @@ func TestCreateTablePrimaryKeyConstraints(t *testing.T) {
|
|
|
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")
|
|
|
}
|
|
|
@@ -66,7 +109,595 @@ func TestCreateTablePrimaryKeyConstraints(t *testing.T) {
|
|
|
if got.PrimaryKey != tt.wantPrimaryKey {
|
|
|
t.Fatalf("PrimaryKey = %q, want %q", got.PrimaryKey, tt.wantPrimaryKey)
|
|
|
}
|
|
|
- execMust(t, e, "INSERT INTO t (a) VALUES ('x')")
|
|
|
+ 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])
|
|
|
+ }
|
|
|
+}
|