mirror of
https://github.com/aljazceru/turso.git
synced 2026-01-06 17:54:20 +01:00
Merge 'Add framework for testing extensions in TCL' from Piotr Rżysko
There is a distinction between tests that verify extension-specific behavior and those that verify interactions between the database engine and extensions. Previously, both types of tests were kept in `extensions.py`. With this new framework, we can extract the latter type of tests from `extensions.py` into TCL. This cleans up `extensions.py` and enables compatibility testing with SQLite at no extra cost. I’m currently working on supporting outer joins involving TVFs and planning to add more tests that exercise the database’s handling of virtual tables, so I decided to do this refactoring first. In the future, we may consider moving extension-specific tests to TCL as well, especially those that have counterparts in SQLite or sqlean. Reviewed-by: Preston Thorpe <preston@turso.tech> Closes #2556
This commit is contained in:
@@ -1 +1,9 @@
|
||||
# Limbo Testing
|
||||
# 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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <table name>.* 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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
394
testing/vtab.test
Executable file
394
testing/vtab.test
Executable file
@@ -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 <table name>.* 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}
|
||||
Reference in New Issue
Block a user