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}