diff --git a/testing/all.test b/testing/all.test index 602174abf..52098ce45 100755 --- a/testing/all.test +++ b/testing/all.test @@ -48,3 +48,4 @@ source $testdir/upsert.test source $testdir/window.test source $testdir/partial_idx.test source $testdir/foreign_keys.test +source $testdir/returning.test diff --git a/testing/returning.test b/testing/returning.test new file mode 100755 index 000000000..23a9382c3 --- /dev/null +++ b/testing/returning.test @@ -0,0 +1,1012 @@ +#!/usr/bin/env tclsh +set testdir [file dirname $argv0] +source $testdir/tester.tcl +source $testdir/sqlite3/tester.tcl + +# ============================================================================ +# INSERT RETURNING tests +# ============================================================================ + +# Basic column references +do_execsql_test_on_specific_db {:memory:} insert-returning-single-column { + CREATE TABLE t (id INTEGER, name TEXT, value REAL); + INSERT INTO t VALUES (1, 'test', 10.5) RETURNING id; +} {1} + +do_execsql_test_on_specific_db {:memory:} insert-returning-multiple-columns { + CREATE TABLE t (id INTEGER, name TEXT, value REAL); + INSERT INTO t VALUES (1, 'test', 10.5) RETURNING id, name; +} {1|test} + +do_execsql_test_on_specific_db {:memory:} insert-returning-all-columns { + CREATE TABLE t (id INTEGER, name TEXT, value REAL); + INSERT INTO t VALUES (1, 'test', 10.5) RETURNING *; +} {1|test|10.5} + +# Table-qualified column references +do_execsql_test_on_specific_db {:memory:} insert-returning-table-qualified { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING t.id, t.name; +} {1|test} + +# Arbitrary expressions not referencing columns +do_execsql_test_on_specific_db {:memory:} insert-returning-literal { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING 42; +} {42} + +do_execsql_test_on_specific_db {:memory:} insert-returning-constant-expression { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING 2 + 3 * 4; +} {14} + +do_execsql_test_on_specific_db {:memory:} insert-returning-string-literal { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING 'hello world'; +} {"hello world"} + +# Expressions referencing result columns +do_execsql_test_on_specific_db {:memory:} insert-returning-column-arithmetic { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 10) RETURNING 2 * value; +} {20} + +do_execsql_test_on_specific_db {:memory:} insert-returning-complex-expression { + CREATE TABLE t (id INTEGER, x INTEGER, y INTEGER); + INSERT INTO t VALUES (1, 5, 3) RETURNING x + y * 2; +} {11} + +do_execsql_test_on_specific_db {:memory:} insert-returning-multiple-column-expression { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER, c INTEGER); + INSERT INTO t VALUES (1, 2, 3, 4) RETURNING a * b + c; +} {10} + +# Function calls +do_execsql_test_on_specific_db {:memory:} insert-returning-function-call { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'hello') RETURNING upper(name); +} {HELLO} + +do_execsql_test_on_specific_db {:memory:} insert-returning-function-multiple-columns { + CREATE TABLE t (id INTEGER, first TEXT, last TEXT); + INSERT INTO t VALUES (1, 'john', 'doe') RETURNING upper(first) || ' ' || upper(last); +} {"JOHN DOE"} + +do_execsql_test_on_specific_db {:memory:} insert-returning-function-with-expression { + CREATE TABLE t (id INTEGER, x INTEGER, y INTEGER); + INSERT INTO t VALUES (1, 5, 3) RETURNING abs(x - y); +} {2} + +# Mixed expressions +do_execsql_test_on_specific_db {:memory:} insert-returning-mixed-expressions { + CREATE TABLE t (id INTEGER, name TEXT, value INTEGER); + INSERT INTO t VALUES (1, 'test', 10) RETURNING id, upper(name), value * 3, 42; +} {1|TEST|30|42} + +# Multiple rows +do_execsql_test_on_specific_db {:memory:} insert-returning-multiple-rows { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'first'), (2, 'second'), (3, 'third') RETURNING id, name; +} {1|first +2|second +3|third} + +do_execsql_test_on_specific_db {:memory:} insert-returning-multiple-rows-expressions { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 10), (2, 20), (3, 30) RETURNING id, value * 2; +} {1|20 +2|40 +3|60} + +# NULL handling +do_execsql_test_on_specific_db {:memory:} insert-returning-null-values { + CREATE TABLE t (id INTEGER, name TEXT, value INTEGER); + INSERT INTO t VALUES (1, NULL, NULL) RETURNING id, name, value; +} {1||} + +do_execsql_test_on_specific_db {:memory:} insert-returning-null-expression { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, NULL) RETURNING coalesce(name, 'default'); +} {default} + +# Rowid +do_execsql_test_on_specific_db {:memory:} insert-returning-rowid { + CREATE TABLE t (name TEXT); + INSERT INTO t VALUES ('test') RETURNING rowid, name; +} {1|test} + +do_execsql_test_on_specific_db {:memory:} insert-returning-rowid-expression { + CREATE TABLE t (name TEXT); + INSERT INTO t VALUES ('test') RETURNING rowid * 2; +} {2} + +# Auto-increment +do_execsql_test_on_specific_db {:memory:} insert-returning-autoincrement { + CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT); + INSERT INTO t (name) VALUES ('test') RETURNING id, name; +} {1|test} + +# Complex nested expressions +do_execsql_test_on_specific_db {:memory:} insert-returning-nested-expressions { + CREATE TABLE t (id INTEGER, x INTEGER, y INTEGER, z INTEGER); + INSERT INTO t VALUES (1, 2, 3, 4) RETURNING (x + y) * (z - 1); +} {15} + +do_execsql_test_on_specific_db {:memory:} insert-returning-case-expression { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5) RETURNING CASE WHEN value > 10 THEN 'high' WHEN value > 0 THEN 'low' ELSE 'zero' END; +} {low} + +# String operations +do_execsql_test_on_specific_db {:memory:} insert-returning-string-concat { + CREATE TABLE t (id INTEGER, first TEXT, last TEXT); + INSERT INTO t VALUES (1, 'John', 'Doe') RETURNING first || ' ' || last; +} {"John Doe"} + +do_execsql_test_on_specific_db {:memory:} insert-returning-substring { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'hello world') RETURNING substr(name, 1, 5); +} {hello} + +# ============================================================================ +# INSERT ON CONFLICT RETURNING tests +# ============================================================================ + +# RETURNING on INSERT path (no conflict) +do_execsql_test_on_specific_db {:memory:} upsert-returning-insert-path { + CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO t VALUES (1, 'new') + ON CONFLICT DO UPDATE SET name = excluded.name + RETURNING id, name; +} {1|new} + +# RETURNING on UPDATE path (conflict occurs) +do_execsql_test_on_specific_db {:memory:} upsert-returning-update-path { + CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO t VALUES (1, 'old'); + INSERT INTO t VALUES (1, 'new') + ON CONFLICT DO UPDATE SET name = excluded.name + RETURNING id, name; +} {1|new} + +# RETURNING on DO NOTHING (should return empty) +do_execsql_test_on_specific_db {:memory:} upsert-returning-do-nothing { + CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO t VALUES (1, 'old'); + INSERT INTO t VALUES (1, 'ignored') + ON CONFLICT DO NOTHING + RETURNING id, name; +} {} + +# RETURNING with table-qualified columns +do_execsql_test_on_specific_db {:memory:} upsert-returning-table-qualified { + CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO t VALUES (1, 'old'); + INSERT INTO t VALUES (1, 'new') + ON CONFLICT DO UPDATE SET name = excluded.name + RETURNING t.id, t.name; +} {1|new} + +# RETURNING with expressions on INSERT path +do_execsql_test_on_specific_db {:memory:} upsert-returning-expression-insert { + CREATE TABLE t (id INTEGER PRIMARY KEY, value INTEGER); + INSERT INTO t VALUES (1, 10) + ON CONFLICT DO UPDATE SET value = excluded.value + RETURNING id, value * 2; +} {1|20} + +# RETURNING with expressions on UPDATE path +do_execsql_test_on_specific_db {:memory:} upsert-returning-expression-update { + CREATE TABLE t (id INTEGER PRIMARY KEY, value INTEGER); + INSERT INTO t VALUES (1, 5); + INSERT INTO t VALUES (1, 10) + ON CONFLICT DO UPDATE SET value = excluded.value + RETURNING id, value * 3; +} {1|30} + +# RETURNING with complex expressions +do_execsql_test_on_specific_db {:memory:} upsert-returning-complex-expression { + CREATE TABLE t (id INTEGER PRIMARY KEY, x INTEGER, y INTEGER); + INSERT INTO t VALUES (1, 2, 3); + INSERT INTO t VALUES (1, 5, 7) + ON CONFLICT DO UPDATE SET x = excluded.x, y = excluded.y + RETURNING id, x + y * 2; +} {1|19} + +# RETURNING with function calls +do_execsql_test_on_specific_db {:memory:} upsert-returning-function { + CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO t VALUES (1, 'hello'); + INSERT INTO t VALUES (1, 'world') + ON CONFLICT DO UPDATE SET name = excluded.name + RETURNING id, upper(name); +} {1|WORLD} + +# RETURNING with multiple rows (mixed insert/update) +do_execsql_test_on_specific_db {:memory:} upsert-returning-multiple-rows { + CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO t VALUES (1, 'old1'), (2, 'old2'); + INSERT INTO t VALUES (1, 'new1'), (2, 'new2'), (3, 'new3') + ON CONFLICT DO UPDATE SET name = excluded.name + RETURNING id, name; +} {1|new1 +2|new2 +3|new3} + +# RETURNING with WHERE clause in DO UPDATE +do_execsql_test_on_specific_db {:memory:} upsert-returning-where-update { + CREATE TABLE t (id INTEGER PRIMARY KEY, value INTEGER); + INSERT INTO t VALUES (1, 5); + INSERT INTO t VALUES (1, 10) + ON CONFLICT DO UPDATE SET value = excluded.value WHERE excluded.value > t.value + RETURNING id, value; +} {1|10} + +do_execsql_test_on_specific_db {:memory:} upsert-returning-where-no-update { + CREATE TABLE t (id INTEGER PRIMARY KEY, value INTEGER); + INSERT INTO t VALUES (1, 10); + INSERT INTO t VALUES (1, 5) + ON CONFLICT DO UPDATE SET value = excluded.value WHERE excluded.value > t.value + RETURNING id, value; +} {} + +# RETURNING with arbitrary expressions not referencing columns +do_execsql_test_on_specific_db {:memory:} upsert-returning-literal { + CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO t VALUES (1, 'test') + ON CONFLICT DO UPDATE SET name = excluded.name + RETURNING 42; +} {42} + +# RETURNING with mixed insert/update in single statement (first one doesn't conflict, second one does) +do_execsql_test_on_specific_db {:memory:} upsert-returning-mixed-operations { + CREATE TABLE t (id INTEGER PRIMARY KEY, value INTEGER); + INSERT INTO t VALUES (1, 5); + INSERT INTO t VALUES (1, 10), (2, 20) + ON CONFLICT DO UPDATE SET value = excluded.value + t.value + RETURNING id, value; +} {1|15 +2|20} + +# ============================================================================ +# INSERT ... SELECT ... RETURNING tests +# ============================================================================ + +# Basic INSERT ... SELECT with RETURNING +do_execsql_test_on_specific_db {:memory:} insert-select-returning-basic { + CREATE TABLE u (a INTEGER, b INTEGER PRIMARY KEY, c TEXT); + CREATE TABLE t (a TEXT, b INTEGER, c INTEGER PRIMARY KEY); + INSERT INTO u VALUES (1, 2, 'test'), (2, 4, 'data'), (3, 6, 'more'); + INSERT INTO t SELECT c, b, b * 2 FROM u RETURNING *; +} {test|2|4 +data|4|8 +more|6|12} + +# INSERT ... SELECT with WHERE clause and RETURNING +do_execsql_test_on_specific_db {:memory:} insert-select-returning-where { + CREATE TABLE u (a INTEGER, b INTEGER PRIMARY KEY, c TEXT); + CREATE TABLE t (a TEXT, b INTEGER, c INTEGER PRIMARY KEY); + INSERT INTO u SELECT value, value * 2, 'kekkers' FROM generate_series(1, 10); + INSERT INTO t SELECT concat(c, '-lollers') as lul, b, b * 2 FROM u WHERE a > 5 RETURNING *; +} {kekkers-lollers|12|24 +kekkers-lollers|14|28 +kekkers-lollers|16|32 +kekkers-lollers|18|36 +kekkers-lollers|20|40} + +# INSERT ... SELECT with RETURNING specific columns +do_execsql_test_on_specific_db {:memory:} insert-select-returning-columns { + CREATE TABLE u (a INTEGER, b INTEGER, c TEXT); + CREATE TABLE t (a TEXT, b INTEGER, c INTEGER); + INSERT INTO u VALUES (1, 10, 'x'), (2, 20, 'y'), (3, 30, 'z'); + INSERT INTO t SELECT c, b, a FROM u RETURNING a, c; +} {x|1 +y|2 +z|3} + +# INSERT ... SELECT with RETURNING expressions +do_execsql_test_on_specific_db {:memory:} insert-select-returning-expressions { + CREATE TABLE u (a INTEGER, b INTEGER); + CREATE TABLE t (x INTEGER, y INTEGER); + INSERT INTO u VALUES (5, 10), (15, 20); + INSERT INTO t SELECT a, b FROM u RETURNING x + y, x * y; +} {15|50 +35|300} + +# INSERT ... SELECT with RETURNING and function calls +do_execsql_test_on_specific_db {:memory:} insert-select-returning-functions { + CREATE TABLE u (name TEXT, value INTEGER); + CREATE TABLE t (name TEXT, value INTEGER); + INSERT INTO u VALUES ('hello', 100), ('world', 200); + INSERT INTO t SELECT name, value FROM u RETURNING upper(name), value / 10; +} {HELLO|10 +WORLD|20} + +# INSERT ... SELECT with RETURNING and aggregates in source +do_execsql_test_on_specific_db {:memory:} insert-select-returning-aggregate-source { + CREATE TABLE u (category TEXT, amount INTEGER); + CREATE TABLE t (category TEXT, total INTEGER); + INSERT INTO u VALUES ('A', 10), ('A', 20), ('B', 30), ('B', 40); + INSERT INTO t SELECT category, SUM(amount) FROM u GROUP BY category RETURNING *; +} {A|30 +B|70} + +# INSERT ... SELECT with RETURNING literal values +do_execsql_test_on_specific_db {:memory:} insert-select-returning-literals { + CREATE TABLE u (id INTEGER); + CREATE TABLE t (id INTEGER, constant INTEGER); + INSERT INTO u VALUES (1), (2), (3); + INSERT INTO t SELECT id, 42 FROM u RETURNING constant; +} {42 +42 +42} + +# INSERT ... SELECT with RETURNING and ORDER BY in source +do_execsql_test_on_specific_db {:memory:} insert-select-returning-ordered { + CREATE TABLE u (value INTEGER); + CREATE TABLE t (value INTEGER); + INSERT INTO u VALUES (30), (10), (20); + INSERT INTO t SELECT value FROM u ORDER BY value RETURNING value; +} {10 +20 +30} + +# ============================================================================ +# UPDATE RETURNING tests +# ============================================================================ + +# Basic column references +do_execsql_test_on_specific_db {:memory:} update-returning-single-column { + CREATE TABLE t (id INTEGER, name TEXT, value REAL); + INSERT INTO t VALUES (1, 'test', 10.5); + UPDATE t SET value = 20.5 WHERE id = 1 RETURNING id; +} {1} + +do_execsql_test_on_specific_db {:memory:} update-returning-multiple-columns { + CREATE TABLE t (id INTEGER, name TEXT, value REAL); + INSERT INTO t VALUES (1, 'test', 10.5); + UPDATE t SET value = 20.5 WHERE id = 1 RETURNING id, name, value; +} {1|test|20.5} + +do_execsql_test_on_specific_db {:memory:} update-returning-all-columns { + CREATE TABLE t (id INTEGER, name TEXT, value REAL); + INSERT INTO t VALUES (1, 'test', 10.5); + UPDATE t SET value = 20.5 WHERE id = 1 RETURNING *; +} {1|test|20.5} + +# Table-qualified column references +do_execsql_test_on_specific_db {:memory:} update-returning-table-qualified { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'old'); + UPDATE t SET name = 'new' WHERE id = 1 RETURNING t.id, t.name; +} {1|new} + +# Arbitrary expressions not referencing columns +do_execsql_test_on_specific_db {:memory:} update-returning-literal { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 10); + UPDATE t SET value = 20 WHERE id = 1 RETURNING 42; +} {42} + +do_execsql_test_on_specific_db {:memory:} update-returning-constant-expression { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1); + UPDATE t SET id = 2 RETURNING 2 + 3 * 4; +} {14} + +# Expressions referencing updated columns +do_execsql_test_on_specific_db {:memory:} update-returning-column-arithmetic { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 10); + UPDATE t SET value = 20 WHERE id = 1 RETURNING 2 * value; +} {40} + +do_execsql_test_on_specific_db {:memory:} update-returning-complex-expression { + CREATE TABLE t (id INTEGER, x INTEGER, y INTEGER); + INSERT INTO t VALUES (1, 5, 3); + UPDATE t SET x = 8 WHERE id = 1 RETURNING x + y * 2; +} {14} + +do_execsql_test_on_specific_db {:memory:} update-returning-multiple-column-expression { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER, c INTEGER); + INSERT INTO t VALUES (1, 2, 3, 4); + UPDATE t SET a = 5, b = 6 WHERE id = 1 RETURNING a * b + c; +} {34} + +# Function calls +do_execsql_test_on_specific_db {:memory:} update-returning-function-call { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'hello'); + UPDATE t SET name = 'world' WHERE id = 1 RETURNING upper(name); +} {WORLD} + +do_execsql_test_on_specific_db {:memory:} update-returning-function-multiple-columns { + CREATE TABLE t (id INTEGER, first TEXT, last TEXT); + INSERT INTO t VALUES (1, 'john', 'doe'); + UPDATE t SET first = 'jane', last = 'smith' WHERE id = 1 RETURNING upper(first) || ' ' || upper(last); +} {"JANE SMITH"} + +# Mixed expressions +do_execsql_test_on_specific_db {:memory:} update-returning-mixed-expressions { + CREATE TABLE t (id INTEGER, name TEXT, value INTEGER); + INSERT INTO t VALUES (1, 'test', 10); + UPDATE t SET name = 'updated', value = 30 WHERE id = 1 RETURNING id, upper(name), value * 2, 42; +} {1|UPDATED|60|42} + +# Multiple rows +do_execsql_test_on_specific_db {:memory:} update-returning-multiple-rows { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'first'), (2, 'second'), (3, 'third'); + UPDATE t SET name = 'updated' RETURNING id, name; +} {1|updated +2|updated +3|updated} + +do_execsql_test_on_specific_db {:memory:} update-returning-multiple-rows-expressions { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 10), (2, 20), (3, 30); + UPDATE t SET value = value * 2 RETURNING id, value; +} {1|20 +2|40 +3|60} + +# WHERE clause filtering +do_execsql_test_on_specific_db {:memory:} update-returning-with-where { + CREATE TABLE t (id INTEGER, name TEXT, active INTEGER); + INSERT INTO t VALUES (1, 'first', 1), (2, 'second', 0), (3, 'third', 1); + UPDATE t SET name = 'updated' WHERE active = 1 RETURNING id, name; +} {1|updated +3|updated} + +# Old vs new values +do_execsql_test_on_specific_db {:memory:} update-returning-old-vs-new-values { + CREATE TABLE t (id INTEGER, counter INTEGER); + INSERT INTO t VALUES (1, 5); + UPDATE t SET counter = counter + 10 WHERE id = 1 RETURNING id, counter; +} {1|15} + +# NULL handling +do_execsql_test_on_specific_db {:memory:} update-returning-null-values { + CREATE TABLE t (id INTEGER, name TEXT, value INTEGER); + INSERT INTO t VALUES (1, 'test', 10); + UPDATE t SET name = NULL, value = NULL WHERE id = 1 RETURNING id, name, value; +} {1||} + +do_execsql_test_on_specific_db {:memory:} update-returning-null-expression { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test'); + UPDATE t SET name = NULL WHERE id = 1 RETURNING coalesce(name, 'default'); +} {default} + +# Rowid +do_execsql_test_on_specific_db {:memory:} update-returning-rowid { + CREATE TABLE t (name TEXT); + INSERT INTO t VALUES ('test'); + UPDATE t SET name = 'updated' RETURNING rowid, name; +} {1|updated} + +do_execsql_test_on_specific_db {:memory:} update-returning-rowid-expression { + CREATE TABLE t (name TEXT); + INSERT INTO t VALUES ('test'); + UPDATE t SET name = 'updated' RETURNING rowid * 2; +} {2} + +# Complex nested expressions +do_execsql_test_on_specific_db {:memory:} update-returning-nested-expressions { + CREATE TABLE t (id INTEGER, x INTEGER, y INTEGER, z INTEGER); + INSERT INTO t VALUES (1, 2, 3, 4); + UPDATE t SET x = 5, y = 6 WHERE id = 1 RETURNING (x + y) * (z - 1); +} {33} + +do_execsql_test_on_specific_db {:memory:} update-returning-case-expression { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5); + UPDATE t SET value = 15 WHERE id = 1 RETURNING CASE WHEN value > 10 THEN 'high' WHEN value > 0 THEN 'low' ELSE 'zero' END; +} {high} + +# String operations +do_execsql_test_on_specific_db {:memory:} update-returning-string-concat { + CREATE TABLE t (id INTEGER, first TEXT, last TEXT); + INSERT INTO t VALUES (1, 'John', 'Doe'); + UPDATE t SET first = 'Jane', last = 'Smith' WHERE id = 1 RETURNING first || ' ' || last; +} {"Jane Smith"} + +do_execsql_test_on_specific_db {:memory:} update-returning-substring { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'hello world'); + UPDATE t SET name = 'goodbye world' WHERE id = 1 RETURNING substr(name, 1, 7); +} {goodbye} + +# Row value updates +do_execsql_test_on_specific_db {:memory:} update-returning-row-values { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test'); + UPDATE t SET (id, name) = (2, 'mordor') RETURNING id, name; +} {2|mordor} + +# ============================================================================ +# Edge cases and complex scenarios +# ============================================================================ + +# RETURNING expressions that don't reference any columns +do_execsql_test_on_specific_db {:memory:} insert-returning-no-column-reference { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING 1 + 1, 'constant', NULL; +} {2|constant|} + +do_execsql_test_on_specific_db {:memory:} update-returning-no-column-reference { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1); + UPDATE t SET id = 2 RETURNING 1 + 1, 'constant', NULL; +} {2|constant|} + +# RETURNING with aggregate-like expressions (should work per-row) +do_execsql_test_on_specific_db {:memory:} insert-returning-sum-expression { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER); + INSERT INTO t VALUES (1, 2, 3) RETURNING a + b; +} {5} + +do_execsql_test_on_specific_db {:memory:} update-returning-sum-expression { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER); + INSERT INTO t VALUES (1, 2, 3); + UPDATE t SET a = 5, b = 7 WHERE id = 1 RETURNING a + b; +} {12} + +# ============================================================================ +# NASTY EDGE CASES - Things that should work but might break +# ============================================================================ + +# Column name same as table name - ambiguity resolution +do_execsql_test_on_specific_db {:memory:} insert-returning-column-same-as-table-name { + CREATE TABLE t (t INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING t.t, t.name; +} {1|test} + +do_execsql_test_on_specific_db {:memory:} insert-returning-column-same-as-table-name-unqualified { + CREATE TABLE t (t INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING t, name; +} {1|test} + +# RETURNING with rowid when INTEGER PRIMARY KEY exists - can reference both +do_execsql_test_on_specific_db {:memory:} insert-returning-rowid-and-pk-alias { + CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING rowid, id, name; +} {1|1|test} + +do_execsql_test_on_specific_db {:memory:} update-returning-rowid-and-pk-alias { + CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO t VALUES (1, 'old'); + UPDATE t SET name = 'new' WHERE id = 1 RETURNING rowid, id, name; +} {1|1|new} + +# RETURNING with expressions referencing updated columns in UPDATE +do_execsql_test_on_specific_db {:memory:} update-returning-reference-updated-column { + CREATE TABLE t (id INTEGER, x INTEGER, y INTEGER); + INSERT INTO t VALUES (1, 5, 10); + UPDATE t SET x = 20 WHERE id = 1 RETURNING x, x * 2, x + y; +} {20|40|30} + +do_execsql_test_on_specific_db {:memory:} update-returning-reference-multiple-updated-columns { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER, c INTEGER); + INSERT INTO t VALUES (1, 1, 2, 3); + UPDATE t SET a = 10, b = 20 WHERE id = 1 RETURNING a, b, a + b, a * b + c; +} {10|20|30|203} + +# RETURNING with NULLIF edge cases +do_execsql_test_on_specific_db {:memory:} insert-returning-nullif { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5) RETURNING NULLIF(value, 5); +} {} + +do_execsql_test_on_specific_db {:memory:} insert-returning-nullif-no-match { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5) RETURNING NULLIF(value, 10); +} {5} + +do_execsql_test_on_specific_db {:memory:} update-returning-nullif { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5); + UPDATE t SET value = 10 WHERE id = 1 RETURNING NULLIF(value, 5); +} {10} + +# RETURNING with COALESCE multiple arguments +do_execsql_test_on_specific_db {:memory:} insert-returning-coalesce-multiple { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER, c INTEGER); + INSERT INTO t VALUES (1, NULL, NULL, 42) RETURNING COALESCE(a, b, c); +} {42} + +do_execsql_test_on_specific_db {:memory:} insert-returning-coalesce-all-null { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER); + INSERT INTO t VALUES (1, NULL, NULL) RETURNING COALESCE(a, b, 'default'); +} {default} + +# RETURNING with arithmetic on NULL +do_execsql_test_on_specific_db {:memory:} insert-returning-null-arithmetic { + CREATE TABLE t (id INTEGER, x INTEGER, y INTEGER); + INSERT INTO t VALUES (1, NULL, 5) RETURNING x + y, x * y, x - y; +} {||} + +do_execsql_test_on_specific_db {:memory:} update-returning-null-arithmetic { + CREATE TABLE t (id INTEGER, x INTEGER, y INTEGER); + INSERT INTO t VALUES (1, 10, 5); + UPDATE t SET x = NULL WHERE id = 1 RETURNING x + y, x * y; +} {|} + +# RETURNING with string functions on NULL +do_execsql_test_on_specific_db {:memory:} insert-returning-string-func-null { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, NULL) RETURNING upper(name), length(name), substr(name, 1, 5); +} {||} + +do_execsql_test_on_specific_db {:memory:} update-returning-string-func-null { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test'); + UPDATE t SET name = NULL WHERE id = 1 RETURNING upper(name), length(name); +} {|} + +# RETURNING with CASE expressions having NULL branches +do_execsql_test_on_specific_db {:memory:} insert-returning-case-with-null { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5) RETURNING CASE WHEN value > 10 THEN 'high' WHEN value > 0 THEN NULL ELSE 'zero' END; +} {} + +do_execsql_test_on_specific_db {:memory:} insert-returning-case-nested { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER); + INSERT INTO t VALUES (1, 5, 10) RETURNING CASE WHEN a > b THEN a WHEN a < b THEN b ELSE NULL END; +} {10} + +# RETURNING with type conversions +do_execsql_test_on_specific_db {:memory:} insert-returning-type-conversion { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 42) RETURNING CAST(value AS TEXT), CAST(value AS REAL); +} {42|42.0} + +do_execsql_test_on_specific_db {:memory:} update-returning-type-conversion { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 42); + UPDATE t SET value = 100 WHERE id = 1 RETURNING CAST(value AS TEXT); +} {100} + +# RETURNING when updating rowid directly (if supported) +do_execsql_test_on_specific_db {:memory:} update-rowid-returning { + CREATE TABLE t (name TEXT); + INSERT INTO t VALUES ('test'); + UPDATE t SET rowid = 999 WHERE rowid = 1 RETURNING rowid, name; +} {999|test} + +# RETURNING with expressions referencing columns not in result set +do_execsql_test_on_specific_db {:memory:} insert-returning-reference-hidden-column { + CREATE TABLE t (id INTEGER, secret INTEGER, public INTEGER); + INSERT INTO t VALUES (1, 100, 50) RETURNING public, secret * 2; +} {50|200} + +do_execsql_test_on_specific_db {:memory:} update-returning-reference-hidden-column { + CREATE TABLE t (id INTEGER, secret INTEGER, public INTEGER); + INSERT INTO t VALUES (1, 100, 50); + UPDATE t SET public = 75 WHERE id = 1 RETURNING public, secret + public; +} {75|175} + +# RETURNING with duplicate column names in result (should work) +do_execsql_test_on_specific_db {:memory:} insert-returning-duplicate-column-names { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING id, id, name, name; +} {1|1|test|test} + +do_execsql_test_on_specific_db {:memory:} update-returning-duplicate-column-names { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 10); + UPDATE t SET value = 20 WHERE id = 1 RETURNING value, value * 2, value; +} {20|40|20} + +# RETURNING with expressions using column names as strings (should not work, but test anyway) +do_execsql_test_on_specific_db {:memory:} insert-returning-column-name-as-string { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING 'id', 'name'; +} {id|name} + +# RETURNING with excluded in regular INSERT (should error) +do_execsql_test_in_memory_any_error insert-returning-excluded-not-upsert { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING excluded.name; +} + +# RETURNING with expressions referencing columns that were updated to NULL +do_execsql_test_on_specific_db {:memory:} update-returning-updated-to-null { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 42); + UPDATE t SET value = NULL WHERE id = 1 RETURNING value, COALESCE(value, 0); +} {|0} + +# RETURNING with expressions referencing columns that were updated from NULL +do_execsql_test_on_specific_db {:memory:} update-returning-updated-from-null { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, NULL); + UPDATE t SET value = 42 WHERE id = 1 RETURNING value, COALESCE(value, 0); +} {42|42} + +# RETURNING with expressions that use backticks +do_execsql_test_on_specific_db {:memory:} insert-returning-backtick-columns { + CREATE TABLE t (`id` INTEGER, `name` TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING `id`, `name`; +} {1|test} + +# RETURNING with expressions that use double quotes (if DQS enabled) +do_execsql_test_skip_lines_on_specific_db 1 {:memory:} insert-returning-double-quote-columns { + .dbconfig dqs_dml on + CREATE TABLE t ("id" INTEGER, "name" TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING "id", "name"; +} {1|test} + +# RETURNING with expressions referencing columns in different cases (case sensitivity) +do_execsql_test_on_specific_db {:memory:} insert-returning-case-insensitive { + CREATE TABLE t (ID INTEGER, Name TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING id, name, ID, Name; +} {1|test|1|test} + +# RETURNING with expressions that reference columns that don't exist (should error) +do_execsql_test_in_memory_any_error insert-returning-nonexistent-column { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING nonexistent; +} + +do_execsql_test_in_memory_any_error update-returning-nonexistent-column { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1); + UPDATE t SET id = 2 RETURNING nonexistent; +} + +# RETURNING with expressions that reference wrong table (should error) +do_execsql_test_in_memory_any_error insert-returning-wrong-table { + CREATE TABLE t1 (id INTEGER); + CREATE TABLE t2 (id INTEGER); + INSERT INTO t1 VALUES (1) RETURNING t2.id; +} + +# RETURNING with expressions that mix table-qualified and unqualified +do_execsql_test_on_specific_db {:memory:} insert-returning-mixed-qualification { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING t.id, name, t.name, id; +} {1|test|test|1} + +# RETURNING with expressions that reference columns updated in SET clause +do_execsql_test_on_specific_db {:memory:} update-returning-set-clause-reference { + CREATE TABLE t (id INTEGER, x INTEGER, y INTEGER); + INSERT INTO t VALUES (1, 5, 10); + UPDATE t SET x = 20, y = x + 10 WHERE id = 1 RETURNING x, y; +} {20|15} + +# RETURNING with expressions that reference columns in order of update +do_execsql_test_on_specific_db {:memory:} update-returning-order-dependent { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER); + INSERT INTO t VALUES (1, 1, 2); + UPDATE t SET a = b, b = a WHERE id = 1 RETURNING a, b; +} {2|1} + +# RETURNING with empty result set (no rows affected) +do_execsql_test_on_specific_db {:memory:} update-returning-no-rows { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test'); + UPDATE t SET name = 'updated' WHERE id = 999 RETURNING id, name; +} {} + +# RETURNING with expressions using || operator on NULL +do_execsql_test_on_specific_db {:memory:} insert-returning-concat-null { + CREATE TABLE t (id INTEGER, first TEXT, last TEXT); + INSERT INTO t VALUES (1, NULL, 'Doe') RETURNING first || ' ' || last; +} {} + +do_execsql_test_on_specific_db {:memory:} insert-returning-concat-both-null { + CREATE TABLE t (id INTEGER, first TEXT, last TEXT); + INSERT INTO t VALUES (1, NULL, NULL) RETURNING first || ' ' || last; +} {} + +# RETURNING with expressions using IN operator +do_execsql_test_on_specific_db {:memory:} insert-returning-in-operator { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5) RETURNING value IN (1, 2, 3, 4, 5); +} {1} + +do_execsql_test_on_specific_db {:memory:} insert-returning-in-operator-false { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 10) RETURNING value IN (1, 2, 3, 4, 5); +} {0} + +# RETURNING with expressions using LIKE operator +do_execsql_test_on_specific_db {:memory:} insert-returning-like-operator { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'hello world') RETURNING name LIKE 'hello%'; +} {1} + +do_execsql_test_on_specific_db {:memory:} insert-returning-like-operator-false { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'hello world') RETURNING name LIKE 'goodbye%'; +} {0} + +# RETURNING with expressions using IS NULL +do_execsql_test_on_specific_db {:memory:} insert-returning-is-null { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, NULL) RETURNING value IS NULL; +} {1} + +do_execsql_test_on_specific_db {:memory:} insert-returning-is-not-null { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 42) RETURNING value IS NOT NULL; +} {1} + +# RETURNING with expressions using BETWEEN +do_execsql_test_on_specific_db {:memory:} insert-returning-between { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5) RETURNING value BETWEEN 1 AND 10; +} {1} + +do_execsql_test_on_specific_db {:memory:} insert-returning-between-false { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 15) RETURNING value BETWEEN 1 AND 10; +} {0} + +# RETURNING with expressions using GLOB (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-glob { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'hello.txt') RETURNING name GLOB '*.txt'; +} {1} + +# RETURNING with expressions that overflow (large numbers) +do_execsql_test_on_specific_db {:memory:} insert-returning-large-number { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 9223372036854775807) RETURNING value; +} {9223372036854775807} + +# RETURNING with expressions using ABS on negative +do_execsql_test_on_specific_db {:memory:} insert-returning-abs-negative { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, -42) RETURNING ABS(value); +} {42} + +# RETURNING with expressions using MAX/MIN (should work per-row, not aggregate) +do_execsql_test_on_specific_db {:memory:} insert-returning-max-min-per-row { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER); + INSERT INTO t VALUES (1, 5, 10) RETURNING MAX(a, b), MIN(a, b); +} {10|5} + +# RETURNING with expressions using ROUND +do_execsql_test_on_specific_db {:memory:} insert-returning-round { + CREATE TABLE t (id INTEGER, value REAL); + INSERT INTO t VALUES (1, 3.14159) RETURNING ROUND(value, 2); +} {3.14} + +# RETURNING with expressions using LENGTH on empty string +do_execsql_test_on_specific_db {:memory:} insert-returning-length-empty { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, '') RETURNING LENGTH(name); +} {0} + +# RETURNING with expressions using TRIM +do_execsql_test_on_specific_db {:memory:} insert-returning-trim { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, ' hello ') RETURNING TRIM(name); +} {hello} + +# RETURNING with expressions using REPLACE +do_execsql_test_on_specific_db {:memory:} insert-returning-replace { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'hello world') RETURNING REPLACE(name, 'world', 'universe'); +} {"hello universe"} + +# RETURNING with expressions using LOWER/UPPER on mixed case +do_execsql_test_on_specific_db {:memory:} insert-returning-lower-upper { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'HeLLo WoRLd') RETURNING LOWER(name), UPPER(name); +} {"hello world|HELLO WORLD"} + +# RETURNING with expressions using SUBSTR with negative start +do_execsql_test_on_specific_db {:memory:} insert-returning-substr-negative { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'hello') RETURNING SUBSTR(name, -3); +} {llo} + +# RETURNING with expressions using CHAR (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-char { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 65) RETURNING CHAR(value); +} {A} + +# RETURNING with expressions using UNICODE (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-unicode { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'A') RETURNING UNICODE(name); +} {65} + +# RETURNING with expressions using DATE functions (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-date { + CREATE TABLE t (id INTEGER, date_text TEXT); + INSERT INTO t VALUES (1, '2023-01-01') RETURNING DATE(date_text); +} {2023-01-01} + +# RETURNING with expressions using JULIANDAY (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-julianday { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING JULIANDAY('2023-01-01'); +} {2459945.5} + +# RETURNING with expressions using STRFTIME (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-strftime { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING STRFTIME('%Y-%m-%d', '2023-01-01'); +} {2023-01-01} + +# RETURNING with expressions using JSON functions (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-json { + CREATE TABLE t (id INTEGER, data TEXT); + INSERT INTO t VALUES (1, '{"key": "value"}') RETURNING JSON_EXTRACT(data, '$.key'); +} {"value"} + +# RETURNING with expressions using typeof +do_execsql_test_on_specific_db {:memory:} insert-returning-typeof { + CREATE TABLE t (id INTEGER, name TEXT, value REAL); + INSERT INTO t VALUES (1, 'test', 10.5) RETURNING typeof(id), typeof(name), typeof(value); +} {integer|text|real} + +# RETURNING with expressions using typeof on NULL +do_execsql_test_on_specific_db {:memory:} insert-returning-typeof-null { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, NULL) RETURNING typeof(value); +} {null} + +# RETURNING with expressions using printf (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-printf { + CREATE TABLE t (id INTEGER, name TEXT, value INTEGER); + INSERT INTO t VALUES (1, 'test', 42) RETURNING printf('id=%d name=%s value=%d', id, name, value); +} {"id=1 name=test value=42"} + +# RETURNING with expressions using randomblob/zeroblob (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-zeroblob { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING LENGTH(zeroblob(100)); +} {100} + +# RETURNING with expressions using changes (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-changes { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING changes(); +} {0} + +# RETURNING with expressions using last_insert_rowid (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-last-insert-rowid { + CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT); + INSERT INTO t (name) VALUES ('test') RETURNING last_insert_rowid(); +} {1} + +# RETURNING with expressions using total_changes (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-total-changes { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING total_changes(); +} {0} + +# RETURNING with expressions using likely/unlikely (optimization hints) +do_execsql_test_on_specific_db {:memory:} insert-returning-likely { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5) RETURNING likely(value > 0); +} {1} + +# RETURNING with expressions using iif (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-iif { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5) RETURNING IIF(value > 0, 'positive', 'negative'); +} {positive} + +# RETURNING with expressions using json_array/json_object (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-json-array { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER); + INSERT INTO t VALUES (1, 2, 3) RETURNING JSON_ARRAY(a, b); +} {[2,3]} + +# RETURNING with expressions using aggregate functions (should error) +do_execsql_test_in_memory_any_error insert-returning-aggregate { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 42) RETURNING SUM(value); +} + +do_execsql_test_in_memory_any_error insert-returning-aggregate-count { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING COUNT(*); +} \ No newline at end of file