diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9690558d0..c3becd317 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -86,5 +86,7 @@ jobs: steps: - uses: actions/checkout@v3 - uses: "./.github/shared/install_sqlite" + - name: build SQLite test extensions + run: cargo build --package limbo_sqlite_test_ext - name: Test run: SQLITE_EXEC="sqlite3" make test-compat diff --git a/extensions/tests/src/lib.rs b/extensions/tests/src/lib.rs index a4cb366ec..1a5685e30 100644 --- a/extensions/tests/src/lib.rs +++ b/extensions/tests/src/lib.rs @@ -260,7 +260,7 @@ impl VTable for KVStoreTable { } fn destroy(&mut self) -> Result<(), Self::Error> { - println!("VDestroy called"); + log::debug!("VDestroy called"); Ok(()) } } diff --git a/testing/README.md b/testing/README.md index ef4d07cde..674963282 100644 --- a/testing/README.md +++ b/testing/README.md @@ -1 +1,9 @@ -# Limbo Testing \ No newline at end of file +# Turso Testing + +## Testing Extensions +When adding tests for extensions, please follow these guidelines: +* Tests that verify the internal logic or behavior of a particular extension should go into `cli_tests/extensions.py`. +* Tests that verify how extensions interact with the database engine, such as virtual table handling, should be written +in TCL (see `vtab.test` as an example). + +To check which extensions are available in TCL, or to add a new one, refer to the `tester.tcl` file and look at the `extension_map`. diff --git a/testing/all.test b/testing/all.test index b3a1c60c6..61838a03a 100755 --- a/testing/all.test +++ b/testing/all.test @@ -40,3 +40,4 @@ source $testdir/values.test source $testdir/integrity_check.test source $testdir/rollback.test source $testdir/views.test +source $testdir/vtab.test diff --git a/testing/cli_tests/extensions.py b/testing/cli_tests/extensions.py index ebb856130..8b0e3c3a5 100755 --- a/testing/cli_tests/extensions.py +++ b/testing/cli_tests/extensions.py @@ -328,35 +328,6 @@ def _test_series(limbo: TestTursoShell): "SELECT * FROM generate_series(1, 10);", lambda res: res == "1\n2\n3\n4\n5\n6\n7\n8\n9\n10", ) - limbo.run_test_fn( - "SELECT * FROM generate_series WHERE start = 1 AND stop = 10;", - lambda res: res == "1\n2\n3\n4\n5\n6\n7\n8\n9\n10", - ) - limbo.run_test_fn( - "SELECT * FROM generate_series WHERE 1 = start AND 10 = stop;", - lambda res: res == "1\n2\n3\n4\n5\n6\n7\n8\n9\n10", - "Constraint with column on RHS used as TVF arg", - ) - limbo.run_test_fn( - "SELECT * FROM generate_series WHERE stop = 10 AND start = 1;", - lambda res: res == "1\n2\n3\n4\n5\n6\n7\n8\n9\n10", - ) - limbo.run_test_fn( - "SELECT * FROM generate_series(1, 10) WHERE value < 5;", - lambda res: res == "1\n2\n3\n4", - ) - limbo.run_test_fn( - "SELECT * FROM generate_series WHERE start = 1 AND stop = 10 AND value < 5;", - lambda res: res == "1\n2\n3\n4", - ) - limbo.run_test_fn( - "SELECT * FROM generate_series WHERE start = 1 AND stop = 10 AND start = 5;", - lambda res: res == "", - ) - limbo.run_test_fn( - "SELECT * FROM generate_series WHERE start = 1 AND stop = 10 AND start > 5;", - lambda res: res == "", - ) limbo.run_test_fn( "SELECT * FROM generate_series;", lambda res: "Invalid Argument" in res or 'first argument to "generate_series()" missing or unusable' in res, @@ -365,79 +336,10 @@ def _test_series(limbo: TestTursoShell): "SELECT * FROM generate_series(1, 10, 2);", lambda res: res == "1\n3\n5\n7\n9", ) - limbo.run_test_fn( - "SELECT * FROM generate_series WHERE start = 1 AND stop = 10 AND step = 2;", - lambda res: res == "1\n3\n5\n7\n9", - ) - limbo.run_test_fn( - "SELECT * FROM generate_series(1, 10, 2, 3);", - lambda res: "too many arguments" in res.lower(), - ) limbo.run_test_fn( "SELECT * FROM generate_series(10, 1, -2);", lambda res: res == "10\n8\n6\n4\n2", ) - limbo.run_test_fn( - "SELECT " - " a.value a_val, " - " b.value b_val " - "FROM " - " generate_series(1, 3) a " - "JOIN " - " generate_series(1, 1) b ON a.value = b.value;", - lambda res: res == "1|1", - ) - limbo.execute_dot("CREATE TABLE target (id integer primary key);") - limbo.execute_dot("INSERT INTO target SELECT * FROM generate_series(1, 5);") - limbo.run_test_fn( - "SELECT * FROM target;", - lambda res: res == "1\n2\n3\n4\n5", - ) - limbo.run_test_fn( - "SELECT t.id, series.value FROM target t, generate_series(t.id, 3) series;", - lambda res: res == "1|1\n1|2\n1|3\n2|2\n2|3\n3|3", - "Column reference from table on the left used as generate_series argument", - ) - limbo.run_test_fn( - "SELECT t.id, series.value FROM generate_series(t.id, 3) series, target t;", - lambda res: res == "1|1\n1|2\n1|3\n2|2\n2|3\n3|3", - "Column reference from table on the right used as generate_series argument", - ) - limbo.run_test_fn( - "SELECT one.value, series.value FROM (SELECT 1 AS value) one, generate_series(one.value, 3) series;", - lambda res: res == "1|1\n1|2\n1|3", - "Column reference from scalar subquery (left side)", - ) - limbo.run_test_fn( - "SELECT one.value, series.value FROM generate_series(one.value, 3) series, (SELECT 1 AS value) one;", - lambda res: res == "1|1\n1|2\n1|3", - "Column reference from scalar subquery (right side)", - ) - limbo.run_test_fn( - "SELECT " - " * " - "FROM " - " generate_series(a.start, a.stop) series " - "NATURAL JOIN " - " (SELECT 1 AS start, 3 AS stop, 2 AS value) a;", - lambda res: res == "2|1|3", - "Natural join where TVF arguments come from column references", - ) - limbo.run_test_fn( - "SELECT * FROM generate_series(a.start, a.stop) JOIN (SELECT 1 AS start, 3 AS stop) a USING (start, stop);", - lambda res: res == "1\n2\n3", - "Join USING where TVF arguments come from column references", - ) - limbo.run_test_fn( - "SELECT a.value, b.value FROM generate_series(b.value, b.value+1) a JOIN generate_series(1, 2) b;", - lambda res: res == "1|1\n2|1\n2|2\n3|2", - "TVF arguments come from another TVF", - ) - limbo.run_test_fn( - "SELECT * FROM generate_series(a.start, a.stop) b, generate_series(b.start, b.stop) a;", - lambda res: "No valid query plan found" in res or "no query solution" in res, - "circular column references between two generate_series", - ) limbo.run_test_fn( "SELECT * FROM generate_series(b.start, b.stop) b;", lambda res: "Invalid Argument" in res or 'first argument to "generate_series()" missing or unusable' in res, @@ -642,32 +544,6 @@ def test_vfs(): limbo.quit() -def test_drop_virtual_table(): - ext_path = "target/debug/libturso_ext_tests" - limbo = TestTursoShell() - limbo.execute_dot(f".load {ext_path}") - limbo.run_debug( - "create virtual table t using kv_store;", - ) - limbo.run_test_fn(".schema", lambda res: "CREATE VIRTUAL TABLE t" in res) - limbo.run_test_fn( - "insert into t values ('hello', 'world');", - null, - "can insert into kv_store vtable", - ) - limbo.run_test_fn( - "DROP TABLE t;", - lambda res: "VDestroy called" in res, - "can drop kv_store vtable", - ) - limbo.run_test_fn( - "DROP TABLE t;", - lambda res: "× Parse error: No such table: t" == res, - "should error when drop kv_store vtable", - ) - limbo.quit() - - def test_sqlite_vfs_compat(): sqlite = TestTursoShell( init_commands="", @@ -697,46 +573,6 @@ def test_sqlite_vfs_compat(): sqlite.quit() -def test_create_virtual_table(): - ext_path = "target/debug/libturso_ext_tests" - - limbo = TestTursoShell() - test_module_list(limbo, ext_path, "kv_store") - - limbo.run_debug("CREATE VIRTUAL TABLE t1 USING kv_store;") - limbo.run_test_fn( - "CREATE VIRTUAL TABLE t1 USING kv_store;", - lambda res: "× Parse error: Table t1 already exists" == res, - "create virtual table fails if virtual table with the same name already exists", - ) - limbo.run_test_fn( - "CREATE VIRTUAL TABLE IF NOT EXISTS t1 USING kv_store;", - null, - "create virtual table with IF NOT EXISTS succeeds", - ) - - limbo.run_debug("CREATE TABLE t2 (col INTEGER);") - limbo.run_test_fn( - "CREATE VIRTUAL TABLE t2 USING kv_store;", - lambda res: "× Parse error: Table t2 already exists" == res, - "create virtual table fails if regular table with the same name already exists", - ) - limbo.run_test_fn( - "CREATE VIRTUAL TABLE IF NOT EXISTS t2 USING kv_store;", - null, - "create virtual table with IF NOT EXISTS succeeds", - ) - - limbo.run_debug("CREATE VIRTUAL TABLE t3 USING kv_store;") - limbo.run_test_fn( - "CREATE TABLE t3 (col INTEGER);", - lambda res: "× Parse error: Table t3 already exists" == res, - "create table fails if virtual table with the same name already exists", - ) - - limbo.quit() - - def test_csv(): # open new empty connection explicitly to test whether we can load an extension # with brand new connection/uninitialized database. @@ -910,166 +746,6 @@ def test_tablestats(): limbo.quit() -def test_hidden_columns(): - _test_hidden_columns(exec_name=None, ext_path="target/debug/libturso_ext_tests") - _test_hidden_columns(exec_name="sqlite3", ext_path="target/debug/liblimbo_sqlite_test_ext") - - -def _test_hidden_columns(exec_name, ext_path): - console.info(f"Running test_hidden_columns for {ext_path}") - - limbo = TestTursoShell( - exec_name=exec_name, - ) - limbo.execute_dot(f".load {ext_path}") - limbo.execute_dot( - "create virtual table t using kv_store;", - ) - limbo.run_test_fn(".schema", lambda res: "CREATE VIRTUAL TABLE t" in res) - limbo.run_test_fn( - "insert into t(key, value) values ('k0', 'v0');", - null, - "can insert if hidden column is not specified explicitly", - ) - limbo.run_test_fn( - "insert into t(key, value) values ('k1', 'v1');", - null, - "can insert if hidden column is not specified explicitly", - ) - limbo.run_test_fn( - "select comment from t where key = 'k0';", - lambda res: "auto-generated" == res, - "can select a hidden column from kv_store", - ) - limbo.run_test_fn( - "select comment from (select * from t where key = 'k0');", - lambda res: "Column comment not found" in res or "no such column: comment" in res, - "hidden columns are not exposed by subqueries by default", - ) - limbo.run_test_fn( - "select * from (select comment from t where key = 'k0');", - lambda res: "auto-generated" == res, - "can select hidden column exposed by subquery", - ) - limbo.run_test_fn( - "insert into t(comment, key, value) values ('my comment', 'hidden', 'test');", - null, - "can insert if a hidden column is specified explicitly", - ) - limbo.run_test_fn( - "select comment from t where key = 'hidden';", - lambda res: "my comment" == res, - "can select a hidden column from kv_store", - ) - limbo.run_test_fn( - "select * from t where key = 'hidden';", - lambda res: "hidden|test" == res, - "hidden column is excluded from * expansion", - ) - limbo.run_test_fn( - "select t.* from t where key = 'hidden';", - lambda res: "hidden|test" == res, - "hidden column is excluded from .* expansion", - ) - limbo.run_test_fn( - "insert into t(comment, key, value) values ('insert_hidden', 'test');", - lambda res: "2 values for 3 columns" in res, - "fails when number of values does not match number of specified columns", - ) - limbo.run_test_fn( - "update t set comment = 'updated comment' where key = 'hidden';", - null, - "can update a hidden column if specified explicitly", - ) - limbo.run_test_fn( - "select comment from t where key = 'hidden';", - lambda res: "updated comment" == res, - ) - limbo.run_test_fn( - "PRAGMA table_info=t;", - lambda res: "0|key|TEXT|0|TURSO|1\n1|value|TEXT|0|TURSO|0" == res, - "hidden columns are not listed in the dataset returned by 'PRAGMA table_info'", - ) - limbo.run_test_fn( - "select comment, count(*) from t group by comment;", - lambda res: "auto-generated|2\nupdated comment|1" == res, - "can use hidden columns in aggregations", - ) - - # ORDER BY - limbo.execute_dot("CREATE VIRTUAL TABLE o USING kv_store;") - limbo.run_test_fn(".schema", lambda res: "CREATE VIRTUAL TABLE o" in res) - limbo.execute_dot("INSERT INTO o(comment, key, value) VALUES ('0', '5', 'a');") - limbo.execute_dot("INSERT INTO o(comment, key, value) VALUES ('1', '4', 'b');") - limbo.execute_dot("INSERT INTO o(comment, key, value) VALUES ('2', '3', 'c');") - limbo.run_test_fn( - "SELECT * FROM o ORDER BY comment;", - lambda res: "5|a\n4|b\n3|c" == res, - ) - limbo.run_test_fn( - "SELECT * FROM o ORDER BY 0;", - lambda res: "invalid column index: 0" in res or "term out of range - should be between 1 and 2" in res, - ) - limbo.run_test_fn( - "SELECT * FROM o ORDER BY 1;", - lambda res: "3|c\n4|b\n5|a" == res, - ) - - # JOINs - limbo.execute_dot("CREATE TABLE r (comment, key, value);") - limbo.execute_dot("INSERT INTO r VALUES ('comment0', '2', '3');") - limbo.execute_dot("INSERT INTO r VALUES ('comment1', '4', '5');") - limbo.execute_dot("CREATE VIRTUAL TABLE l USING kv_store;") - limbo.run_test_fn(".schema", lambda res: "CREATE VIRTUAL TABLE l" in res) - limbo.execute_dot("INSERT INTO l(comment, key, value) values ('comment1', '2', '3');") - limbo.run_test_fn( - "SELECT * FROM l NATURAL JOIN r;", - lambda res: "2|3|comment0" == res, - ) - limbo.run_test_fn( - "SELECT * FROM l JOIN r USING (comment);", - lambda res: "2|3|4|5" == res, - ) - limbo.run_test_fn( - "SELECT * FROM l JOIN r ON l.comment = r.comment;", - lambda res: "2|3|comment1|4|5" == res, - ) - limbo.run_test_fn( - "SELECT * FROM l NATURAL JOIN r NATURAL JOIN r;", - lambda res: "2|3|comment0" == res, - ) - limbo.run_test_fn( - "SELECT * FROM l NATURAL JOIN r NATURAL JOIN l;", - lambda res: "2|3|comment0" == res, - ) - limbo.run_test_fn( - "SELECT * FROM r NATURAL JOIN l;", - lambda res: "comment0|2|3" == res, - ) - limbo.run_test_fn( - "SELECT * FROM r NATURAL JOIN l NATURAL JOIN r;", - lambda res: "comment0|2|3" == res, - ) - limbo.run_test_fn( - "SELECT * FROM (SELECT * FROM l JOIN r USING(key, value)) JOIN r USING(comment, key, value);", - lambda res: "2|3|comment0" == res, - ) - limbo.run_test_fn( - "SELECT * FROM (SELECT * FROM l NATURAL JOIN r) JOIN r USING(comment, key, value);", - lambda res: "2|3|comment0" == res, - ) - limbo.run_test_fn( - "SELECT * FROM l JOIN r USING(key, value) JOIN r USING(comment, key, value);", - lambda res: "" == res, - ) - limbo.run_test_fn( - "SELECT * FROM l NATURAL JOIN r JOIN r USING(comment, key, value);", - lambda res: "" == res, - ) - - limbo.quit() - - def test_module_list(turso_shell, ext_path, module_name): """loads the extension at the provided path and asserts that 'PRAGMA module_list;' displays 'module_name'""" console.info(f"Running test_module_list for {ext_path}") @@ -1100,11 +776,8 @@ def main(): test_vfs() test_sqlite_vfs_compat() test_kv() - test_drop_virtual_table() - test_create_virtual_table() test_csv() test_tablestats() - test_hidden_columns() except Exception as e: console.error(f"Test FAILED: {e}") cleanup() diff --git a/testing/tester.tcl b/testing/tester.tcl index 6fbfa2dd5..434a21b1b 100644 --- a/testing/tester.tcl +++ b/testing/tester.tcl @@ -2,6 +2,32 @@ set sqlite_exec [expr {[info exists env(SQLITE_EXEC)] ? $env(SQLITE_EXEC) : "sql set test_dbs [list "testing/testing.db" "testing/testing_norowidalias.db"] set test_small_dbs [list "testing/testing_small.db" ] +# Array storing loaded extensions +array set extensions {} + +# Mapping of extension names to their respective library paths per database type +set extension_map { + test_ext { + sqlite "./target/debug/liblimbo_sqlite_test_ext" + turso "./target/debug/libturso_ext_tests" + } +} + +proc load_extension {extension_name} { + global extension_map + global extensions + + set version_output [exec $::sqlite_exec --version] + + set is_turso [string match "*Turso*" $version_output] + set db_type [expr {$is_turso ? "turso" : "sqlite"}] + + set ext_info [dict get $extension_map $extension_name] + set ext_path [dict get $ext_info $db_type] + + set extensions($extension_name) $ext_path +} + proc error_put {sql} { puts [format "\033\[1;31mTest FAILED:\033\[0m %s" $sql ] } @@ -11,8 +37,15 @@ proc test_put {msg db test_name} { } proc evaluate_sql {sqlite_exec db_name sql} { + global extensions + set load_commands "" + foreach name [array names extensions] { + append load_commands ".load $extensions($name)\n" + } + set statements "${load_commands}${sql}" + set command [list $sqlite_exec $db_name] - set output [exec echo $sql | {*}$command] + set output [exec echo $statements | {*}$command] return $output } @@ -127,11 +160,8 @@ proc do_execsql_test_tolerance {test_name sql_statements expected_outputs tolera } # This procedure passes the test if the output contains error messages proc run_test_expecting_any_error {sqlite_exec db_name sql} { - # Execute the SQL command and capture output - set command [list $sqlite_exec $db_name $sql] - # Use catch to handle both successful and error cases - catch {exec {*}$command} result options + catch {evaluate_sql $sqlite_exec $db_name $sql} result options # Check if the output contains error indicators (×, error, syntax error, etc.) if {[regexp {(error|ERROR|Error|×|syntax error|failed)} $result]} { @@ -148,11 +178,8 @@ proc run_test_expecting_any_error {sqlite_exec db_name sql} { # This procedure passes if error matches a specific pattern proc run_test_expecting_error {sqlite_exec db_name sql expected_error_pattern} { - # Execute the SQL command and capture output - set command [list $sqlite_exec $db_name $sql] - # Capture output whether command succeeds or fails - catch {exec {*}$command} result options + catch {evaluate_sql $sqlite_exec $db_name $sql} result options # Check if the output contains error indicators first if {![regexp {(error|ERROR|Error|×|syntax error|failed)} $result]} { @@ -177,11 +204,8 @@ proc run_test_expecting_error {sqlite_exec db_name sql expected_error_pattern} { # This version accepts exact error text, ignoring formatting proc run_test_expecting_error_content {sqlite_exec db_name sql expected_error_text} { - # Execute the SQL command and capture output - set command [list $sqlite_exec $db_name $sql] - # Capture output whether command succeeds or fails - catch {exec {*}$command} result options + catch {evaluate_sql $sqlite_exec $db_name $sql} result options # Check if the output contains error indicators first if {![regexp {(error|ERROR|Error|×|syntax error|failed)} $result]} { @@ -260,3 +284,13 @@ proc do_execsql_test_in_memory_error_content {test_name sql_statements expected_ set combined_sql [string trim $sql_statements] run_test_expecting_error_content $::sqlite_exec $db_name $combined_sql $expected_error_text } + +proc do_execsql_test_in_memory_error {test_name sql_statements expected_error_pattern} { + test_put "Running error test" in-memory $test_name + + # Use ":memory:" special filename for in-memory database + set db_name ":memory:" + + set combined_sql [string trim $sql_statements] + run_test_expecting_error $::sqlite_exec $db_name $combined_sql $expected_error_pattern +} diff --git a/testing/vtab.test b/testing/vtab.test new file mode 100755 index 000000000..3990a3e72 --- /dev/null +++ b/testing/vtab.test @@ -0,0 +1,394 @@ +#!/usr/bin/env tclsh + +set testdir [file dirname $argv0] +source $testdir/tester.tcl + +load_extension test_ext + +do_execsql_test_in_memory_error_content create-virtual-table-duplicate-name-1 { + CREATE VIRTUAL TABLE t1 USING kv_store; + CREATE VIRTUAL TABLE t1 USING kv_store; +} {Table t1 already exists} + +do_execsql_test_in_memory_error_content create-virtual-table-duplicate-name-2 { + CREATE TABLE t2 (col INTEGER); + CREATE VIRTUAL TABLE t2 USING kv_store; +} {Table t2 already exists} + +do_execsql_test_in_memory_error_content create-virtual-table-duplicate-name-3 { + CREATE VIRTUAL TABLE t3 USING kv_store; + CREATE TABLE t3 (col INTEGER); +} {Table t3 already exists} + +do_execsql_test_on_specific_db {:memory:} create-virtual-table-if-not-exists-1 { + CREATE TABLE t2 (col INTEGER); + CREATE VIRTUAL TABLE IF NOT EXISTS t2 USING kv_store; +} {} + +do_execsql_test_on_specific_db {:memory:} create-virtual-table-if-not-exists-2 { + CREATE VIRTUAL TABLE t1 USING kv_store; + CREATE VIRTUAL TABLE IF NOT EXISTS t1 USING kv_store; +} {} + +do_execsql_test_on_specific_db {:memory:} drop-virtual-table { + CREATE VIRTUAL TABLE t USING kv_store; + INSERT INTO t VALUES ('hello', 'world'); + DROP TABLE t; +} {} + +do_execsql_test_in_memory_error_content drop-virtual-table-twice { + CREATE VIRTUAL TABLE t USING kv_store; + INSERT INTO t VALUES ('hello', 'world'); + DROP TABLE t; + DROP TABLE t; +} {no such table: t} + +do_execsql_test_on_specific_db {:memory:} select-hidden-column { + create virtual table t using kv_store; + insert into t(key, value) values ('k0', 'v0'); + select comment from t where key = 'k0'; +} {auto-generated} + +# hidden columns are not exposed by subqueries by default +do_execsql_test_in_memory_error select-hidden-column-subquery-1 { + create virtual table t using kv_store; + insert into t(key, value) values ('k0', 'v0'); + select comment from (select * from t where key = 'k0'); +} {.*no such column.*} + +do_execsql_test_on_specific_db {:memory:} select-hidden-column-subquery-2 { + create virtual table t using kv_store; + insert into t(key, value) values ('k0', 'v0'); + select * from (select comment from t where key = 'k0'); +} {auto-generated} + +do_execsql_test_on_specific_db {:memory:} insert-hidden-column { + create virtual table t using kv_store; + insert into t(comment, key, value) values ('my comment', 'hidden', 'test'); + select comment from t where key = 'hidden'; +} {"my comment"} + +# hidden columns should be excluded from * expansion +do_execsql_test_on_specific_db {:memory:} select-star-hidden-column { + create virtual table t using kv_store; + insert into t(comment, key, value) values ('my comment', 'hidden', 'test'); + select * from t where key = 'hidden'; +} {hidden|test} + +# hidden columns should be excluded from
.* expansion +do_execsql_test_on_specific_db {:memory:} select-table-star-hidden-column { + create virtual table t using kv_store; + insert into t(comment, key, value) values ('my comment', 'hidden', 'test'); + select t.* from t where key = 'hidden'; +} {hidden|test} + +do_execsql_test_in_memory_error insert-values-column-count-mismatch { + create virtual table t using kv_store; + insert into t(comment, key, value) values ('insert_hidden', 'test'); +} {.*2 values for 3 columns.*} + +do_execsql_test_on_specific_db {:memory:} update-hidden-column { + create virtual table t using kv_store; + insert into t(comment, key, value) values ('my comment', 'hidden', 'test'); + update t set comment = 'updated comment' where key = 'hidden'; + select comment from t where key = 'hidden'; +} {"updated comment"} + +# hidden columns are not listed in the dataset returned by 'PRAGMA table_info' +do_execsql_test_on_specific_db {:memory:} pragma-table-info-hidden-columns { + create virtual table t using kv_store; + PRAGMA table_info=t; +} {0|key|TEXT|0||1 +1|value|TEXT|0||0} + +do_execsql_test_on_specific_db {:memory:} group-by-hidden-column { + create virtual table t using kv_store; + insert into t(key, value) values ('k0', 'v0'); + insert into t(key, value) values ('k1', 'v1'); + insert into t(comment, key, value) values ('updated_comment', 'hidden', 'test'); + select comment, count(*) from t group by comment order by comment; +} {auto-generated|2 +updated_comment|1} + +do_execsql_test_on_specific_db {:memory:} order-by-hidden-column { + CREATE VIRTUAL TABLE o USING kv_store; + INSERT INTO o(comment, key, value) VALUES ('0', '5', 'a'); + INSERT INTO o(comment, key, value) VALUES ('1', '4', 'b'); + INSERT INTO o(comment, key, value) VALUES ('2', '3', 'c'); + SELECT * FROM o ORDER BY comment; +} {5|a +4|b +3|c} + +do_execsql_test_in_memory_error order-by-hidden-column-index { + CREATE VIRTUAL TABLE o USING kv_store; + INSERT INTO o(comment, key, value) VALUES ('0', '5', 'a'); + SELECT * FROM o ORDER BY 0; +} {.*(invalid column index|term out of range).*} + +do_execsql_test_on_specific_db {:memory:} order-by-standard-column-index { + CREATE VIRTUAL TABLE o USING kv_store; + INSERT INTO o(comment, key, value) VALUES ('0', '5', 'a'); + INSERT INTO o(comment, key, value) VALUES ('1', '4', 'b'); + INSERT INTO o(comment, key, value) VALUES ('2', '3', 'c'); + SELECT * FROM o ORDER BY 1; +} {3|c +4|b +5|a} + +do_execsql_test_on_specific_db {:memory:} natural-join-hidden-column-1 { + CREATE TABLE r (comment, key, value); + INSERT INTO r VALUES ('comment0', '2', '3'); + INSERT INTO r VALUES ('comment1', '4', '5'); + CREATE VIRTUAL TABLE l USING kv_store; + INSERT INTO l(comment, key, value) values ('comment1', '2', '3'); + SELECT * FROM l NATURAL JOIN r; +} {2|3|comment0} + +do_execsql_test_on_specific_db {:memory:} natural-join-hidden-column-2 { + CREATE TABLE r (comment, key, value); + INSERT INTO r VALUES ('comment0', '2', '3'); + INSERT INTO r VALUES ('comment1', '4', '5'); + CREATE VIRTUAL TABLE l USING kv_store; + INSERT INTO l(comment, key, value) values ('comment1', '2', '3'); + SELECT * FROM r NATURAL JOIN l; +} {comment0|2|3} + +do_execsql_test_on_specific_db {:memory:} join-using-hidden-column { + CREATE TABLE r (comment, key, value); + INSERT INTO r VALUES ('comment0', '2', '3'); + INSERT INTO r VALUES ('comment1', '4', '5'); + CREATE VIRTUAL TABLE l USING kv_store; + INSERT INTO l(comment, key, value) values ('comment1', '2', '3'); + SELECT * FROM l JOIN r USING (comment); +} {2|3|4|5} + +do_execsql_test_on_specific_db {:memory:} join-on-hidden-column { + CREATE TABLE r (comment, key, value); + INSERT INTO r VALUES ('comment0', '2', '3'); + INSERT INTO r VALUES ('comment1', '4', '5'); + CREATE VIRTUAL TABLE l USING kv_store; + INSERT INTO l(comment, key, value) values ('comment1', '2', '3'); + SELECT * FROM l JOIN r ON l.comment = r.comment; +} {2|3|comment1|4|5} + +do_execsql_test_on_specific_db {:memory:} natural-join-hidden-column-multiple-vtabs-1 { + CREATE TABLE r (comment, key, value); + INSERT INTO r VALUES ('comment0', '2', '3'); + INSERT INTO r VALUES ('comment1', '4', '5'); + CREATE VIRTUAL TABLE l USING kv_store; + INSERT INTO l(comment, key, value) values ('comment1', '2', '3'); + SELECT * FROM l NATURAL JOIN r NATURAL JOIN r; +} {2|3|comment0} + +do_execsql_test_on_specific_db {:memory:} natural-join-hidden-column-multiple-vtabs-2 { + CREATE TABLE r (comment, key, value); + INSERT INTO r VALUES ('comment0', '2', '3'); + INSERT INTO r VALUES ('comment1', '4', '5'); + CREATE VIRTUAL TABLE l USING kv_store; + INSERT INTO l(comment, key, value) values ('comment1', '2', '3'); + SELECT * FROM l NATURAL JOIN r NATURAL JOIN l; +} {2|3|comment0} + +do_execsql_test_on_specific_db {:memory:} natural-join-hidden-column-multiple-vtabs-3 { + CREATE TABLE r (comment, key, value); + INSERT INTO r VALUES ('comment0', '2', '3'); + INSERT INTO r VALUES ('comment1', '4', '5'); + CREATE VIRTUAL TABLE l USING kv_store; + INSERT INTO l(comment, key, value) values ('comment1', '2', '3'); + SELECT * FROM r NATURAL JOIN l NATURAL JOIN r; +} {comment0|2|3} + +do_execsql_test_on_specific_db {:memory:} join-using-hidden-column-subquery-1 { + CREATE TABLE r (comment, key, value); + INSERT INTO r VALUES ('comment0', '2', '3'); + INSERT INTO r VALUES ('comment1', '4', '5'); + CREATE VIRTUAL TABLE l USING kv_store; + INSERT INTO l(comment, key, value) values ('comment1', '2', '3'); + SELECT * FROM (SELECT * FROM l JOIN r USING(key, value)) JOIN r USING(comment, key, value); +} {2|3|comment0} + +do_execsql_test_on_specific_db {:memory:} join-using-hidden-column-subquery-2 { + CREATE TABLE r (comment, key, value); + INSERT INTO r VALUES ('comment0', '2', '3'); + INSERT INTO r VALUES ('comment1', '4', '5'); + CREATE VIRTUAL TABLE l USING kv_store; + INSERT INTO l(comment, key, value) values ('comment1', '2', '3'); + SELECT * FROM (SELECT * FROM l NATURAL JOIN r) JOIN r USING(comment, key, value); +} {2|3|comment0} + +do_execsql_test_on_specific_db {:memory:} multiple-join-using-hidden-column { + CREATE TABLE r (comment, key, value); + INSERT INTO r VALUES ('comment0', '2', '3'); + INSERT INTO r VALUES ('comment1', '4', '5'); + CREATE VIRTUAL TABLE l USING kv_store; + INSERT INTO l(comment, key, value) values ('comment1', '2', '3'); + SELECT * FROM l JOIN r USING(key, value) JOIN r USING(comment, key, value); +} {} + +do_execsql_test_on_specific_db {:memory:} natural-join-using-hidden-column { + CREATE TABLE r (comment, key, value); + INSERT INTO r VALUES ('comment0', '2', '3'); + INSERT INTO r VALUES ('comment1', '4', '5'); + CREATE VIRTUAL TABLE l USING kv_store; + INSERT INTO l(comment, key, value) values ('comment1', '2', '3'); + SELECT * FROM l NATURAL JOIN r JOIN r USING(comment, key, value); +} {} + +do_execsql_test tvf-hidden-column-constraints-as-args { + SELECT * FROM generate_series WHERE start = 1 AND stop = 10; +} {1 +2 +3 +4 +5 +6 +7 +8 +9 +10} + +do_execsql_test tvf-hidden-column-constraints-as-args-rhs { + SELECT * FROM generate_series WHERE 1 = start AND 10 = stop; +} {1 +2 +3 +4 +5 +6 +7 +8 +9 +10} + +do_execsql_test tvf-hidden-column-constraints-as-args-reversed { + SELECT * FROM generate_series WHERE stop = 10 AND start = 1; +} {1 +2 +3 +4 +5 +6 +7 +8 +9 +10} + +do_execsql_test tvf-predicate-not-used-as-arg-1 { + SELECT * FROM generate_series(1, 10) WHERE value < 5; +} {1 +2 +3 +4} + +do_execsql_test tvf-predicate-not-used-as-arg-2 { + SELECT * FROM generate_series WHERE start = 1 AND stop = 10 AND value < 5; +} {1 +2 +3 +4} + +do_execsql_test tvf-multiple-constraints-on-same-column-1 { + SELECT * FROM generate_series WHERE start = 1 AND stop = 10 AND start = 5; +} {} + +do_execsql_test tvf-multiple-constraints-on-same-column-2 { + SELECT * FROM generate_series WHERE start = 1 AND stop = 10 AND start > 5; +} {} + +do_execsql_test tvf-multiple-constraints-on-same-column-3 { + SELECT * FROM generate_series WHERE start = 1 AND stop = 10 AND step = 2; +} {1 +3 +5 +7 +9} + +do_execsql_test_error_content tvf-too-many-args { + SELECT * FROM generate_series(1, 10, 2, 3); +} {too many arguments} + +do_execsql_test tvf-join-basic { + SELECT a.value a_val, b.value b_val + FROM generate_series(1, 3) a + JOIN generate_series(1, 1) b ON a.value = b.value; +} {1|1} + +do_execsql_test_on_specific_db {:memory:} insert-into-select-from-tvf { + CREATE TABLE target (id integer primary key); + INSERT INTO target SELECT * FROM generate_series(1, 5); + SELECT * FROM target; +} {1 +2 +3 +4 +5} + +do_execsql_test_on_specific_db {:memory:} tvf-arg-from-left-table-column { + CREATE TABLE target (id integer primary key); + INSERT INTO target SELECT * FROM generate_series(1, 5); + + SELECT t.id, series.value + FROM target t, generate_series(t.id, 3) series + WHERE t.id <= 3; +} {1|1 +1|2 +1|3 +2|2 +2|3 +3|3} + +do_execsql_test_on_specific_db {:memory:} tvf-arg-from-right-table-column { + CREATE TABLE target (id integer primary key); + INSERT INTO target SELECT * FROM generate_series(1, 5); + + SELECT t.id, series.value + FROM generate_series(t.id, 3) series, target t + WHERE t.id <= 3; +} {1|1 +1|2 +1|3 +2|2 +2|3 +3|3} + +do_execsql_test tvf-arg-from-left-subquery-column { + SELECT one.value, series.value + FROM (SELECT 1 AS value) one, generate_series(one.value, 3) series; +} {1|1 +1|2 +1|3} + +do_execsql_test tvf-arg-from-right-subquery-column { + SELECT one.value, series.value + FROM generate_series(one.value, 3) series, (SELECT 1 AS value) one; +} {1|1 +1|2 +1|3} + +do_execsql_test tvf-args-from-natural-join-columns { + SELECT * + FROM generate_series(a.start, a.stop) series + NATURAL JOIN (SELECT 1 AS start, 3 AS stop, 2 AS value) a; +} {2|1|3} + +do_execsql_test tvf-args-from-join-using-columns { + SELECT * + FROM generate_series(a.start, a.stop) + JOIN (SELECT 1 AS start, 3 AS stop) a USING (start, stop); +} {1 +2 +3} + +do_execsql_test tvf-args-from-another-tvf { + SELECT a.value, b.value + FROM generate_series(b.value, b.value+1) a + JOIN generate_series(1, 2) b; +} {1|1 +2|1 +2|2 +3|2} + +do_execsql_test_error tvf-circular-column-references { + SELECT * FROM generate_series(a.start, a.stop) b, generate_series(b.start, b.stop) a; +} {No valid query plan found|no query solution}