| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430 |
- package executor
- import (
- "fmt"
- "strings"
- "testing"
- "github.com/danfragoso/pizzasql-next/pkg/storage"
- )
- func TestInsertReturningGeneratedIDAndProjections(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)")
- res := execMust(t, e, "INSERT INTO users (name) VALUES ('alice') RETURNING id, name, id + 1 AS next_id")
- if len(res.Columns) != 3 {
- t.Fatalf("expected 3 columns, got %v", res.Columns)
- }
- if res.Columns[0] != "id" || res.Columns[1] != "name" || res.Columns[2] != "next_id" {
- t.Fatalf("unexpected columns %v", res.Columns)
- }
- if res.RowCount != 1 {
- t.Fatalf("expected 1 row, got %d", res.RowCount)
- }
- if res.Rows[0][0] != int64(1) || res.Rows[0][1] != "alice" || res.Rows[0][2] != int64(2) {
- t.Fatalf("unexpected row %v", res.Rows[0])
- }
- if res.LastInsertID != 1 {
- t.Fatalf("expected LastInsertID 1, got %d", res.LastInsertID)
- }
- }
- func TestInsertReturningStar(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")
- res := execMust(t, e, "INSERT INTO t (v) VALUES ('x') RETURNING *")
- if res.RowCount != 1 || len(res.Columns) != 2 {
- t.Fatalf("unexpected result %v %v", res.Columns, res.Rows)
- }
- if res.Rows[0][1] != "x" {
- t.Fatalf("unexpected row %v", res.Rows[0])
- }
- }
- func TestUpdateReturning(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 VALUES (1, 'a'), (2, 'b')")
- res := execMust(t, e, "UPDATE t SET v = upper(v) RETURNING id, v")
- if res.RowCount != 2 {
- t.Fatalf("expected 2 rows, got %d", res.RowCount)
- }
- got := map[interface{}]interface{}{}
- for _, row := range res.Rows {
- got[row[0]] = row[1]
- }
- if got[int64(1)] != "A" || got[int64(2)] != "B" {
- t.Fatalf("unexpected returning rows %v", res.Rows)
- }
- }
- func TestDeleteReturning(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 VALUES (1, 'a'), (2, 'b')")
- res := execMust(t, e, "DELETE FROM t WHERE id = 1 RETURNING id, v")
- if res.RowCount != 1 || res.Rows[0][0] != int64(1) || res.Rows[0][1] != "a" {
- t.Fatalf("unexpected returning rows %v", res.Rows)
- }
- if res.RowsAffected != 1 {
- t.Fatalf("expected RowsAffected 1, got %d", res.RowsAffected)
- }
- }
- func TestSQLiteVersionDistinctFromPizzasqlVersion(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- sqlite := execMust(t, e, "SELECT sqlite_version()")
- psql := execMust(t, e, "SELECT pizzasql_version()")
- if sqlite.Rows[0][0] != SQLiteCompatVersion {
- t.Fatalf("sqlite_version = %v, want %s", sqlite.Rows[0][0], SQLiteCompatVersion)
- }
- if SQLiteCompatVersion < "3.35.0" {
- t.Fatalf("SQLite compatibility floor must be >= 3.35.0, got %s", SQLiteCompatVersion)
- }
- if sqlite.Rows[0][0] == psql.Rows[0][0] {
- t.Fatalf("sqlite_version and pizzasql_version must differ")
- }
- }
- func TestGeneratedStoredColumnRecompute(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (a INTEGER, b INTEGER, total INTEGER GENERATED ALWAYS AS (a + b) STORED)")
- execMust(t, e, "INSERT INTO t (a, b) VALUES (2, 3)")
- res := execMust(t, e, "SELECT total FROM t")
- if res.Rows[0][0] != int64(5) {
- t.Fatalf("generated total = %v, want 5", res.Rows[0][0])
- }
- execMust(t, e, "UPDATE t SET a = 10")
- res = execMust(t, e, "SELECT total FROM t")
- if res.Rows[0][0] != int64(13) {
- t.Fatalf("generated total after update = %v, want 13", res.Rows[0][0])
- }
- }
- func TestGeneratedColumnReferencesAutoIncrementID(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, stored INTEGER GENERATED ALWAYS AS (id + 1) STORED)")
- res := execMust(t, e, "INSERT INTO t (id) VALUES (NULL) RETURNING id, stored")
- if res.Rows[0][0] != int64(1) || res.Rows[0][1] != int64(2) {
- t.Fatalf("generated id reference = %v, want [1 2]", res.Rows[0])
- }
- res = execMust(t, e, "SELECT stored FROM t WHERE id = 1")
- if res.Rows[0][0] != int64(2) {
- t.Fatalf("persisted generated value = %v, want 2", res.Rows[0][0])
- }
- }
- func TestGeneratedColumnRejectsUserWrites(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a + 1) STORED)")
- if _, err := execSQL(e, "INSERT INTO t (a, b) VALUES (1, 99)"); err == nil {
- t.Fatal("expected explicit insert into generated column to fail")
- }
- if _, err := execSQL(e, "UPDATE t SET b = 5"); err == nil {
- t.Fatal("expected update of generated column to fail")
- }
- }
- func TestInsertOrReplacePrimaryKey(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 VALUES (1, 'a')")
- execMust(t, e, "INSERT OR REPLACE INTO t VALUES (1, 'b')")
- res := execMust(t, e, "SELECT v FROM t")
- if res.RowCount != 1 || res.Rows[0][0] != "b" {
- t.Fatalf("INSERT OR REPLACE result = %v", res.Rows)
- }
- }
- func TestTableUniqueOnConflictReplace(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, `CREATE TABLE t (
- site_id INTEGER,
- path TEXT,
- total INTEGER,
- CONSTRAINT "t#site#path" UNIQUE(site_id, path) ON CONFLICT REPLACE
- )`)
- execMust(t, e, "INSERT INTO t (site_id, path, total) VALUES (1, '/a', 10)")
- execMust(t, e, "INSERT INTO t (site_id, path, total) VALUES (1, '/a', 42)")
- res := execMust(t, e, "SELECT total FROM t WHERE site_id = 1 AND path = '/a'")
- if res.RowCount != 1 {
- t.Fatalf("expected 1 row after replace, got %d", res.RowCount)
- }
- if res.Rows[0][0] != int64(42) {
- t.Fatalf("expected replaced total 42, got %v", res.Rows[0][0])
- }
- }
- func TestInsertIgnoreUniqueIndex(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE)")
- execMust(t, e, "INSERT INTO users VALUES (1, 'same@example.com')")
- res := execMust(t, e, "INSERT OR IGNORE INTO users VALUES (2, 'same@example.com')")
- if res.RowsAffected != 0 {
- t.Fatalf("INSERT OR IGNORE affected %d rows, want 0", res.RowsAffected)
- }
- res = execMust(t, e, "INSERT INTO users VALUES (3, 'same@example.com') ON CONFLICT DO NOTHING")
- if res.RowsAffected != 0 {
- t.Fatalf("targetless DO NOTHING affected %d rows, want 0", res.RowsAffected)
- }
- res = execMust(t, e, "SELECT id FROM users")
- if res.RowCount != 1 || res.Rows[0][0] != int64(1) {
- t.Fatalf("users = %v, want only id 1", res.Rows)
- }
- execMust(t, e, "INSERT INTO users VALUES (4, NULL)")
- execMust(t, e, "INSERT OR IGNORE INTO users VALUES (5, NULL)")
- execMust(t, e, "INSERT INTO users VALUES (6, NULL) ON CONFLICT DO NOTHING")
- res = execMust(t, e, "SELECT count(*) FROM users WHERE email IS NULL")
- if res.Rows[0][0] != int64(3) {
- t.Fatalf("NULL unique values = %v, want 3 rows", res.Rows[0][0])
- }
- }
- func TestJSON1Functions(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- res := execMust(t, e, `SELECT json_extract('{"a": 1, "b": [10, 20]}', '$.b[1]')`)
- if res.Rows[0][0] != int64(20) {
- t.Fatalf("json_extract = %v, want 20", res.Rows[0][0])
- }
- res = execMust(t, e, `SELECT json_set('{"a": 1}', '$.a', 2)`)
- if res.Rows[0][0] != `{"a":2}` {
- t.Fatalf("json_set = %v", res.Rows[0][0])
- }
- // The JSON subtype must survive nested calls, matching GoatCounter's
- // json_insert(json_extract(...), '$[#]', json(...)) pattern.
- res = execMust(t, e, `SELECT json_insert(json_extract('{"w":[]}', '$.w'), '$[#]', json('{"n":"languages"}'))`)
- if res.Rows[0][0] != `[{"n":"languages"}]` {
- t.Fatalf("json_insert with json() = %v", res.Rows[0][0])
- }
- res = execMust(t, e, `SELECT json_replace('{"collect": 1}', '$.collect', json_extract('{"collect": 1}', '$.collect') | 64)`)
- if res.Rows[0][0] != `{"collect":65}` {
- t.Fatalf("json_replace with bitwise = %v", res.Rows[0][0])
- }
- res = execMust(t, e, `SELECT json_group_array(x) FROM (SELECT 1 AS x UNION ALL SELECT 2 UNION ALL SELECT 3) AS t`)
- if res.Rows[0][0] != `[1,2,3]` {
- t.Fatalf("json_group_array = %v", res.Rows[0][0])
- }
- }
- func TestBitwiseOperators(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- cases := map[string]int64{
- "SELECT 6 & 3": 2,
- "SELECT 6 | 1": 7,
- "SELECT 1 << 4": 16,
- "SELECT 32 >> 2": 8,
- "SELECT ~0": -1,
- "SELECT 1 + 2 | 4": 7, // (1+2)|4
- "SELECT 2 | 1 * 8": 10,
- }
- for sql, want := range cases {
- res := execMust(t, e, sql)
- if res.Rows[0][0] != want {
- t.Errorf("%s = %v, want %d", sql, res.Rows[0][0], want)
- }
- }
- }
- func TestPercentDiff(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- res := execMust(t, e, "SELECT percent_diff(10, 15)")
- if res.Rows[0][0] != float64(50) {
- t.Fatalf("percent_diff(10,15) = %v, want 50", res.Rows[0][0])
- }
- res = execMust(t, e, "SELECT percent_diff(0, 5)")
- if f, ok := res.Rows[0][0].(float64); !ok || f <= 0 {
- t.Fatalf("percent_diff(0,5) should be +Inf, got %v", res.Rows[0][0])
- }
- res = execMust(t, e, "SELECT percent_diff(NULL, 5)")
- if res.Rows[0][0] != nil {
- t.Fatalf("percent_diff(NULL,5) should be NULL, got %v", res.Rows[0][0])
- }
- }
- func TestBlobLiteralStorageAndFunctions(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE blobs (id INTEGER PRIMARY KEY, data BLOB)")
- execMust(t, e, "INSERT INTO blobs (id, data) VALUES (1, X'00FF10')")
- res := execMust(t, e, "SELECT data FROM blobs WHERE id = 1")
- b, ok := res.Rows[0][0].([]byte)
- if !ok || len(b) != 3 || b[0] != 0x00 || b[1] != 0xFF || b[2] != 0x10 {
- t.Fatalf("blob round-trip = %#v", res.Rows[0][0])
- }
- res = execMust(t, e, "SELECT hex(data), typeof(data) FROM blobs WHERE id = 1")
- if res.Rows[0][0] != "00FF10" {
- t.Fatalf("hex = %v", res.Rows[0][0])
- }
- if res.Rows[0][1] != "blob" {
- t.Fatalf("typeof = %v", res.Rows[0][1])
- }
- res = execMust(t, e, "SELECT unhex('00FF')")
- if b, ok := res.Rows[0][0].([]byte); !ok || len(b) != 2 || b[1] != 0xFF {
- t.Fatalf("unhex = %#v", res.Rows[0][0])
- }
- res = execMust(t, e, "SELECT CAST('abc' AS BLOB)")
- if b, ok := res.Rows[0][0].([]byte); !ok || string(b) != "abc" {
- t.Fatalf("cast to blob = %#v", res.Rows[0][0])
- }
- }
- func TestExpressionUniqueIndexLower(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)")
- execMust(t, e, "CREATE UNIQUE INDEX users_email_lower ON users (lower(email))")
- execMust(t, e, "INSERT INTO users (id, email) VALUES (1, 'Alice@Example.com')")
- if _, err := execSQL(e, "INSERT INTO users (id, email) VALUES (2, 'alice@example.com')"); err == nil {
- t.Fatal("expected expression unique index to reject a case-insensitive duplicate")
- }
- // A NULL or distinct value is still allowed.
- execMust(t, e, "INSERT INTO users (id, email) VALUES (3, 'bob@example.com')")
- }
- func TestExpressionUniqueIndexReplace(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT, tag TEXT)")
- execMust(t, e, "CREATE UNIQUE INDEX users_email_lower ON users (lower(email))")
- execMust(t, e, "INSERT INTO users (id, email, tag) VALUES (1, 'Alice@Example.com', 'old')")
- // INSERT OR REPLACE must replace the conflicting row even though the
- // conflict is on a case-insensitive expression index.
- execMust(t, e, "INSERT OR REPLACE INTO users (id, email, tag) VALUES (2, 'alice@example.com', 'new')")
- res := execMust(t, e, "SELECT id, tag FROM users")
- if res.RowCount != 1 {
- t.Fatalf("expected 1 row after expression replace, got %d: %v", res.RowCount, res.Rows)
- }
- if res.Rows[0][0] != int64(2) || res.Rows[0][1] != "new" {
- t.Fatalf("unexpected replaced row %v", res.Rows[0])
- }
- }
- func TestInsertReturningMetadataTypes(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (id BIGINT PRIMARY KEY, name TEXT)")
- res := execMust(t, e, "INSERT INTO t (id, name) VALUES (7, 'x') RETURNING id, name")
- if len(res.ColumnTypes) != 2 || res.ColumnTypes[0] != "BIGINT" || res.ColumnTypes[1] != "TEXT" {
- t.Fatalf("unexpected returning column types %v", res.ColumnTypes)
- }
- }
- func TestGeneratedColumnReturning(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a * 2) STORED)")
- res := execMust(t, e, "INSERT INTO t (a) VALUES (21) RETURNING a, b")
- if res.Rows[0][1] != int64(42) {
- t.Fatalf("expected generated b=42 in RETURNING, got %v", res.Rows[0][1])
- }
- }
- func TestGeneratedExpressionIndexText(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE users (email TEXT)")
- execMust(t, e, "CREATE UNIQUE INDEX users_email_lower ON users (lower(email))")
- idx, err := schema.GetIndex("users_email_lower")
- if err != nil {
- t.Fatalf("GetIndex: %v", err)
- }
- if len(idx.Columns) != 1 || idx.Columns[0].Expression == "" {
- t.Fatalf("expected persisted expression, got %#v", idx.Columns)
- }
- if !strings.Contains(strings.ToLower(idx.Columns[0].Expression), "lower(") {
- t.Fatalf("unexpected expression text %q", idx.Columns[0].Expression)
- }
- }
- func TestJoinUsingMultipleColumns(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE counts (site_id INTEGER, path_id INTEGER, total INTEGER)")
- execMust(t, e, "CREATE TABLE paths (site_id INTEGER, path_id INTEGER, path TEXT)")
- execMust(t, e, "INSERT INTO counts VALUES (1, 1, 4), (1, 2, 8), (2, 1, 16)")
- execMust(t, e, "INSERT INTO paths VALUES (1, 1, '/one'), (1, 2, '/two'), (2, 2, '/other')")
- res := execMust(t, e, "SELECT paths.path, counts.total FROM counts JOIN paths USING (site_id, path_id) ORDER BY counts.total")
- if res.RowCount != 2 || res.Rows[0][0] != "/one" || res.Rows[1][0] != "/two" {
- t.Fatalf("JOIN USING rows = %v", res.Rows)
- }
- }
- func TestJoinUsingInCommaFromList(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE base (id INTEGER)")
- execMust(t, e, "CREATE TABLE left_rows (id INTEGER)")
- execMust(t, e, "CREATE TABLE right_rows (id INTEGER)")
- execMust(t, e, "INSERT INTO base VALUES (1)")
- execMust(t, e, "INSERT INTO left_rows VALUES (1)")
- execMust(t, e, "INSERT INTO right_rows VALUES (1), (2)")
- res := execMust(t, e, "SELECT left_rows.id FROM base, left_rows JOIN right_rows USING (id)")
- if res.RowCount != 1 {
- t.Fatalf("mixed JOIN USING returned %d rows, want 1: %v", res.RowCount, res.Rows)
- }
- }
- func TestSQLiteDynamicTypingAssignments(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE settings (value VARCHAR)")
- execMust(t, e, "INSERT INTO settings VALUES (2)")
- execMust(t, e, "UPDATE settings SET value = X'0102'")
- res := execMust(t, e, "SELECT typeof(value), hex(value) FROM settings")
- if res.Rows[0][0] != "blob" || res.Rows[0][1] != "0102" {
- t.Fatalf("dynamic value = %v", res.Rows[0])
- }
- }
- func TestRenameTableAboveSingleBatchLimit(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE old_rows (id INTEGER PRIMARY KEY, value TEXT)")
- // A rename writes two KV operations per row; 32,768 rows exceed PizzaKV's
- // 65,535-operation batch limit.
- rows := make([]storage.Row, 32768)
- for i := range rows {
- rows[i] = storage.Row{"id": int64(i + 1), "value": fmt.Sprintf("v%d", i+1)}
- }
- if _, err := table.InsertBulk("old_rows", rows); err != nil {
- t.Fatalf("insert rows: %v", err)
- }
- execMust(t, e, "ALTER TABLE old_rows RENAME TO new_rows")
- res := execMust(t, e, "SELECT count(*) FROM new_rows")
- if res.Rows[0][0] != int64(len(rows)) {
- t.Fatalf("renamed table has %v rows, want %d", res.Rows[0][0], len(rows))
- }
- }
|