package SQLiteTests import SQLite import ArrayList function _createTestDb() returns SqliteDb let db = new SqliteDb(":memory:") db.exec("CREATE TABLE heroes (id INTEGER PRIMARY KEY, name TEXT, role TEXT, power_level INTEGER, alive INTEGER)") db.exec("INSERT INTO heroes (name, role, power_level, alive) VALUES ('Arthur', 'Paladin', 9000, 1)") db.exec("INSERT INTO heroes (name, role, power_level, alive) VALUES ('Merlin', 'Mage', 8500, 1)") db.exec("INSERT INTO heroes (name, role, power_level, alive) VALUES ('Robin', 'Archer', 7200, 0)") return db // --- SqlResult tests --- @test function test_SqlResult_col_returnsStringValue() let db = _createTestDb() let row = db.selectFirst("SELECT name, role FROM heroes WHERE name = 'Arthur'") row.col(0).assertEquals("Arthur") row.col(1).assertEquals("Paladin") destroy row destroy db @test function test_SqlResult_colInt_parsesInteger() let db = _createTestDb() let row = db.selectFirst("SELECT power_level FROM heroes WHERE name = 'Arthur'") row.colInt(0).assertEquals(9000) destroy row destroy db @test function test_SqlResult_colReal_parsesReal() let db = _createTestDb() db.exec("CREATE TABLE stats (val REAL)") db.exec("INSERT INTO stats VALUES (3.14)") let row = db.selectFirst("SELECT val FROM stats") row.colReal(0).assertEquals(3.14, 0.01) destroy row destroy db @test function test_SqlResult_colBool_parsesOneAsTrue() let db = _createTestDb() let row = db.selectFirst("SELECT alive FROM heroes WHERE name = 'Arthur'") row.colBool(0).assertEquals(true) destroy row destroy db @test function test_SqlResult_colBool_parsesZeroAsFalse() let db = _createTestDb() let row = db.selectFirst("SELECT alive FROM heroes WHERE name = 'Robin'") row.colBool(0).assertEquals(false) destroy row destroy db @test function test_SqlResult_size_matchesColumnCount() let db = _createTestDb() let row = db.selectFirst("SELECT name, role, power_level FROM heroes LIMIT 1") row.size().assertEquals(3) destroy row let row2 = db.selectFirst("SELECT name FROM heroes LIMIT 1") row2.size().assertEquals(1) destroy row2 destroy db // --- SqliteDb.select tests --- @test function test_SqliteDb_select_returnsAllRows() let db = _createTestDb() let rows = db.select("SELECT name FROM heroes ORDER BY name") rows.size().assertEquals(3) rows.get(0).col(0).assertEquals("Arthur") rows.get(1).col(0).assertEquals("Merlin") rows.get(2).col(0).assertEquals("Robin") for i = 0 to rows.size() - 1 destroy rows.get(i) destroy rows destroy db @test function test_SqliteDb_select_emptyResultReturnsEmptyList() let db = _createTestDb() let rows = db.select("SELECT name FROM heroes WHERE name = 'Nobody'") rows.size().assertEquals(0) destroy rows destroy db @test function test_SqliteDb_select_multipleColumns() let db = _createTestDb() let rows = db.select("SELECT name, role, power_level FROM heroes WHERE name = 'Merlin'") rows.size().assertEquals(1) let row = rows.get(0) row.col(0).assertEquals("Merlin") row.col(1).assertEquals("Mage") row.col(2).assertEquals("8500") destroy row destroy rows destroy db // --- SqliteDb.selectFirst tests --- @test function test_SqliteDb_selectFirst_returnsSingleRow() let db = _createTestDb() let row = db.selectFirst("SELECT name FROM heroes ORDER BY power_level DESC") row.col(0).assertEquals("Arthur") destroy row destroy db @test function test_SqliteDb_selectFirst_returnsNullWhenEmpty() let db = _createTestDb() let row = db.selectFirst("SELECT name FROM heroes WHERE name = 'Nobody'") (row == null).assertEquals(true) destroy db // --- SqliteDb.exists tests --- @test function test_SqliteDb_exists_trueWhenRowPresent() let db = _createTestDb() db.exists("SELECT 1 FROM heroes WHERE name = 'Arthur'").assertEquals(true) destroy db @test function test_SqliteDb_exists_falseWhenNoRows() let db = _createTestDb() db.exists("SELECT 1 FROM heroes WHERE name = 'Nobody'").assertEquals(false) destroy db // --- SqliteDb.count tests --- @test function test_SqliteDb_count_returnsRowCount() let db = _createTestDb() db.count("SELECT count(*) FROM heroes").assertEquals(3) destroy db @test function test_SqliteDb_count_withWhereClause() let db = _createTestDb() db.count("SELECT count(*) FROM heroes WHERE alive = 1").assertEquals(2) destroy db @test function test_SqliteDb_count_zeroWhenEmpty() let db = _createTestDb() db.count("SELECT count(*) FROM heroes WHERE name = 'Nobody'").assertEquals(0) destroy db // --- SqliteDb.exec tests --- @test function test_SqliteDb_exec_insert() let db = _createTestDb() db.exec("INSERT INTO heroes (name, role, power_level, alive) VALUES ('Lancelot', 'Knight', 8800, 1)") db.count("SELECT count(*) FROM heroes").assertEquals(4) let row = db.selectFirst("SELECT name FROM heroes WHERE name = 'Lancelot'") row.col(0).assertEquals("Lancelot") destroy row destroy db @test function test_SqliteDb_exec_update() let db = _createTestDb() db.exec("UPDATE heroes SET power_level = 9999 WHERE name = 'Arthur'") let row = db.selectFirst("SELECT power_level FROM heroes WHERE name = 'Arthur'") row.colInt(0).assertEquals(9999) destroy row destroy db @test function test_SqliteDb_exec_delete() let db = _createTestDb() db.exec("DELETE FROM heroes WHERE name = 'Robin'") db.count("SELECT count(*) FROM heroes").assertEquals(2) db.exists("SELECT 1 FROM heroes WHERE name = 'Robin'").assertEquals(false) destroy db @test function test_SqliteDb_exec_createAndDropTable() let db = new SqliteDb(":memory:") db.exec("CREATE TABLE temp (id INTEGER)") db.exists("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'temp'").assertEquals(true) db.exec("DROP TABLE temp") db.exists("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'temp'").assertEquals(false) destroy db // --- SqliteDb.getHandle escape hatch --- @test function test_SqliteDb_getHandle_worksWithNativeCalls() let db = _createTestDb() let h = db.getHandle() sqlite_exec(h, "INSERT INTO heroes (name, role, power_level, alive) VALUES ('Gawain', 'Knight', 7500, 1)") let row = db.selectFirst("SELECT name FROM heroes WHERE name = 'Gawain'") row.col(0).assertEquals("Gawain") destroy row destroy db // --- Parameterised query tests --- @test function test_SqliteDb_execPrepared_insertsWithParams() let db = _createTestDb() let params = new ArrayList() params.add("Lancelot") params.add("Knight") params.add("8800") params.add("1") db.execPrepared("INSERT INTO heroes (name, role, power_level, alive) VALUES (?, ?, ?, ?)", params) destroy params db.count("SELECT count(*) FROM heroes").assertEquals(4) let row = db.selectFirst("SELECT name, role FROM heroes WHERE name = 'Lancelot'") row.col(0).assertEquals("Lancelot") row.col(1).assertEquals("Knight") destroy row destroy db @test function test_SqliteDb_selectPrepared_returnsFilteredRows() let db = _createTestDb() let params = new ArrayList() params.add("Paladin") let rows = db.selectPrepared("SELECT name FROM heroes WHERE role = ?", params) destroy params rows.size().assertEquals(1) rows.get(0).col(0).assertEquals("Arthur") for i = 0 to rows.size() - 1 destroy rows.get(i) destroy rows destroy db @test function test_SqliteDb_selectFirstPrepared_returnsSingleRow() let db = _createTestDb() let params = new ArrayList() params.add("Arthur") let row = db.selectFirstPrepared("SELECT power_level FROM heroes WHERE name = ?", params) destroy params row.colInt(0).assertEquals(9000) destroy row destroy db @test function test_SqliteDb_selectFirstPrepared_returnsNullWhenNoMatch() let db = _createTestDb() let params = new ArrayList() params.add("Nobody") let row = db.selectFirstPrepared("SELECT name FROM heroes WHERE name = ?", params) destroy params (row == null).assertEquals(true) destroy db @test function test_SqliteDb_existsPrepared_trueWhenPresent() let db = _createTestDb() let params = new ArrayList() params.add("Arthur") db.existsPrepared("SELECT 1 FROM heroes WHERE name = ?", params).assertEquals(true) destroy params destroy db @test function test_SqliteDb_existsPrepared_falseWhenMissing() let db = _createTestDb() let params = new ArrayList() params.add("Nobody") db.existsPrepared("SELECT 1 FROM heroes WHERE name = ?", params).assertEquals(false) destroy params destroy db @test function test_SqliteDb_execPrepared_preventsInjection() let db = _createTestDb() // This malicious name would drop the table if interpolated as raw SQL let params = new ArrayList() params.add("'); DROP TABLE heroes; --") params.add("Evil") params.add("0") params.add("0") db.execPrepared("INSERT INTO heroes (name, role, power_level, alive) VALUES (?, ?, ?, ?)", params) destroy params // Table should still exist and have 4 rows (3 original + 1 injected-as-data) db.count("SELECT count(*) FROM heroes").assertEquals(4) db.exists("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'heroes'").assertEquals(true) destroy db // --- SQL singleton tests --- @test function test_SQL_singleton_execAndSelect() SQL.exec("DROP TABLE IF EXISTS _sgtest1") SQL.exec("CREATE TABLE _sgtest1 (id INTEGER PRIMARY KEY, val TEXT)") SQL.exec("INSERT INTO _sgtest1 VALUES (1, 'hello')") let rows = SQL.select("SELECT val FROM _sgtest1 WHERE id = 1") rows.size().assertEquals(1) rows.get(0).col(0).assertEquals("hello") for i = 0 to rows.size() - 1 destroy rows.get(i) destroy rows SQL.exec("DROP TABLE _sgtest1") SQL.close() @test function test_SQL_singleton_selectFirst() SQL.exec("DROP TABLE IF EXISTS _sgtest2") SQL.exec("CREATE TABLE _sgtest2 (n TEXT)") SQL.exec("INSERT INTO _sgtest2 VALUES ('alpha')") SQL.exec("INSERT INTO _sgtest2 VALUES ('beta')") let row = SQL.selectFirst("SELECT n FROM _sgtest2 ORDER BY n") row.col(0).assertEquals("alpha") destroy row SQL.exec("DROP TABLE _sgtest2") SQL.close() @test function test_SQL_singleton_exists() SQL.exec("DROP TABLE IF EXISTS _sgtest3") SQL.exec("CREATE TABLE _sgtest3 (x INTEGER)") SQL.exec("INSERT INTO _sgtest3 VALUES (42)") SQL.exists("SELECT 1 FROM _sgtest3 WHERE x = 42").assertEquals(true) SQL.exists("SELECT 1 FROM _sgtest3 WHERE x = 99").assertEquals(false) SQL.exec("DROP TABLE _sgtest3") SQL.close() @test function test_SQL_singleton_count() SQL.exec("DROP TABLE IF EXISTS _sgtest4") SQL.exec("CREATE TABLE _sgtest4 (x INTEGER)") SQL.exec("INSERT INTO _sgtest4 VALUES (1)") SQL.exec("INSERT INTO _sgtest4 VALUES (2)") SQL.exec("INSERT INTO _sgtest4 VALUES (3)") SQL.count("SELECT count(*) FROM _sgtest4").assertEquals(3) SQL.exec("DROP TABLE _sgtest4") SQL.close() @test function test_SQL_singleton_closeAndReopen() SQL.exec("CREATE TABLE _sgtest5 (v TEXT)") SQL.exec("INSERT INTO _sgtest5 VALUES ('before')") SQL.count("SELECT count(*) FROM _sgtest5").assertEquals(1) SQL.close() // After close on :memory:, previous data is gone — verify a fresh connection works SQL.exec("CREATE TABLE _sgtest5 (v TEXT)") SQL.count("SELECT count(*) FROM _sgtest5").assertEquals(0) SQL.exec("DROP TABLE _sgtest5") SQL.close() @test function test_SQL_singleton_execPrepared() SQL.exec("DROP TABLE IF EXISTS _sgtest6") SQL.exec("CREATE TABLE _sgtest6 (name TEXT, val TEXT)") let params = new ArrayList() params.add("key1") params.add("value1") SQL.execPrepared("INSERT INTO _sgtest6 VALUES (?, ?)", params) destroy params let row = SQL.selectFirst("SELECT val FROM _sgtest6 WHERE name = 'key1'") row.col(0).assertEquals("value1") destroy row SQL.exec("DROP TABLE _sgtest6") SQL.close() // --- Edge cases --- @test function test_SqliteDb_select_orderByDescending() let db = _createTestDb() let rows = db.select("SELECT name, power_level FROM heroes ORDER BY power_level DESC") rows.get(0).col(0).assertEquals("Arthur") rows.get(0).colInt(1).assertEquals(9000) rows.get(1).col(0).assertEquals("Merlin") rows.get(2).col(0).assertEquals("Robin") for i = 0 to rows.size() - 1 destroy rows.get(i) destroy rows destroy db @test function test_SqliteDb_select_withLimitAndOffset() let db = _createTestDb() let rows = db.select("SELECT name FROM heroes ORDER BY name LIMIT 2 OFFSET 1") rows.size().assertEquals(2) rows.get(0).col(0).assertEquals("Merlin") rows.get(1).col(0).assertEquals("Robin") for i = 0 to rows.size() - 1 destroy rows.get(i) destroy rows destroy db @test function test_SqliteDb_nullValueReturnsEmptyString() let db = new SqliteDb(":memory:") db.exec("CREATE TABLE nullable (a TEXT, b TEXT)") db.exec("INSERT INTO nullable (a) VALUES ('present')") let row = db.selectFirst("SELECT a, b FROM nullable") row.col(0).assertEquals("present") row.col(1).assertEquals("") destroy row destroy db @test function test_SqlResult_colIsNull_distinguishesNullFromEmptyString() let db = new SqliteDb(":memory:") db.exec("CREATE TABLE t (a TEXT, b TEXT, c TEXT)") // a = 'x' (value), b = '' (empty string, NOT null), c = NULL (omitted) db.exec("INSERT INTO t (a, b) VALUES ('x', '')") let row = db.selectFirst("SELECT a, b, c FROM t") // col() cannot tell '' apart from NULL — both come back as "" row.col(0).assertEquals("x") row.col(1).assertEquals("") row.col(2).assertEquals("") // colIsNull() is the only reliable discriminator row.colIsNull(0).assertEquals(false) row.colIsNull(1).assertEquals(false) row.colIsNull(2).assertEquals(true) destroy row destroy db @test function test_SqliteDb_multipleTablesJoin() let db = new SqliteDb(":memory:") db.exec("CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT)") db.exec("CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, author_id INTEGER)") db.exec("INSERT INTO authors VALUES (1, 'Tolkien')") db.exec("INSERT INTO books VALUES (1, 'The Hobbit', 1)") let row = db.selectFirst("SELECT b.title, a.name FROM books b JOIN authors a ON a.id = b.author_id") row.col(0).assertEquals("The Hobbit") row.col(1).assertEquals("Tolkien") destroy row destroy db @test function test_SqliteDb_aggregateQueries() let db = _createTestDb() let row = db.selectFirst("SELECT min(power_level), max(power_level), avg(power_level) FROM heroes") row.colInt(0).assertEquals(7200) row.colInt(1).assertEquals(9000) row.size().assertEquals(3) destroy row destroy db // ============================================================================ // Native surface coverage — proves every @extern native behaves as documented. // These use the raw handle (db.getHandle()) so each native is exercised directly. // ============================================================================ // --- sqlite_bind_int / sqlite_bind_real / sqlite_column_int / sqlite_column_real --- @test function test_native_bindTypedAndReadBack() let db = new SqliteDb(":memory:") let h = db.getHandle() sqlite_exec(h, "CREATE TABLE t (i INTEGER, r REAL)") let ins = sqlite_prepare(h, "INSERT INTO t VALUES (?, ?)") sqlite_bind_int(ins, 1, 42) sqlite_bind_real(ins, 2, 2.5) // stepping an INSERT executes it and yields no row sqlite_step(ins).assertEquals(false) sqlite_finalize(ins) let sel = sqlite_prepare(h, "SELECT i, r FROM t") sqlite_step(sel).assertEquals(true) sqlite_column_int(sel, 0).assertEquals(42) sqlite_column_real(sel, 1).assertEquals(2.5, 0.0001) sqlite_finalize(sel) destroy db // --- sqlite_column_count --- @test function test_native_columnCount_matchesSelectedColumns() let db = new SqliteDb(":memory:") let h = db.getHandle() sqlite_exec(h, "CREATE TABLE t (a INTEGER, b INTEGER, c INTEGER)") sqlite_exec(h, "INSERT INTO t VALUES (1, 2, 3)") let stmt = sqlite_prepare(h, "SELECT a, b, c FROM t") sqlite_step(stmt).assertEquals(true) sqlite_column_count(stmt).assertEquals(3) sqlite_finalize(stmt) destroy db // --- sqlite_column_is_null (native) --- @test function test_native_columnIsNull() let db = new SqliteDb(":memory:") let h = db.getHandle() sqlite_exec(h, "CREATE TABLE t (a INTEGER, b INTEGER)") sqlite_exec(h, "INSERT INTO t (a) VALUES (7)") let stmt = sqlite_prepare(h, "SELECT a, b FROM t") sqlite_step(stmt).assertEquals(true) sqlite_column_is_null(stmt, 0).assertEquals(false) sqlite_column_is_null(stmt, 1).assertEquals(true) sqlite_finalize(stmt) destroy db // --- sqlite_step past the last row --- @test function test_native_stepReturnsFalsePastLastRow() let db = new SqliteDb(":memory:") let h = db.getHandle() sqlite_exec(h, "CREATE TABLE t (v INTEGER)") sqlite_exec(h, "INSERT INTO t VALUES (1)") let stmt = sqlite_prepare(h, "SELECT v FROM t") sqlite_step(stmt).assertEquals(true) sqlite_step(stmt).assertEquals(false) sqlite_finalize(stmt) destroy db // --- sqlite_reset: rewinds the cursor AND preserves bound parameters --- @test function test_native_reset_rewindsAndPreservesBindings() let db = new SqliteDb(":memory:") let h = db.getHandle() sqlite_exec(h, "CREATE TABLE t (v INTEGER)") sqlite_exec(h, "INSERT INTO t VALUES (10), (20), (30)") let stmt = sqlite_prepare(h, "SELECT v FROM t WHERE v >= ? ORDER BY v") sqlite_bind_int(stmt, 1, 20) // first pass: 20, 30 sqlite_step(stmt).assertEquals(true) sqlite_column_int(stmt, 0).assertEquals(20) sqlite_step(stmt).assertEquals(true) sqlite_column_int(stmt, 0).assertEquals(30) sqlite_step(stmt).assertEquals(false) // reset rewinds; the binding (20) must still be in effect sqlite_reset(stmt) sqlite_step(stmt).assertEquals(true) sqlite_column_int(stmt, 0).assertEquals(20) sqlite_finalize(stmt) destroy db // --- sqlite_clear_bindings: unbinds parameters (they become NULL) --- @test function test_native_clearBindings_resetsParamsToNull() let db = new SqliteDb(":memory:") let h = db.getHandle() let stmt = sqlite_prepare(h, "SELECT ?") sqlite_bind_string(stmt, 1, "bound") sqlite_step(stmt).assertEquals(true) sqlite_column_is_null(stmt, 0).assertEquals(false) sqlite_column_string(stmt, 0).assertEquals("bound") // clear the binding, rewind, step again — the parameter is now NULL sqlite_clear_bindings(stmt) sqlite_reset(stmt) sqlite_step(stmt).assertEquals(true) sqlite_column_is_null(stmt, 0).assertEquals(true) sqlite_finalize(stmt) destroy db // --- sqlite_reset then rebind allows reusing one statement with new values --- @test function test_native_resetAndRebind_reusesStatement() let db = new SqliteDb(":memory:") let h = db.getHandle() sqlite_exec(h, "CREATE TABLE t (v INTEGER)") let ins = sqlite_prepare(h, "INSERT INTO t VALUES (?)") sqlite_bind_int(ins, 1, 100) sqlite_step(ins).assertEquals(false) sqlite_reset(ins) sqlite_bind_int(ins, 1, 200) sqlite_step(ins).assertEquals(false) sqlite_finalize(ins) db.count("SELECT count(*) FROM t").assertEquals(2) db.count("SELECT count(*) FROM t WHERE v = 100").assertEquals(1) db.count("SELECT count(*) FROM t WHERE v = 200").assertEquals(1) destroy db // --- sqlite_exec runs a full multi-statement script (native sqlite3_exec) --- @test function test_native_exec_runsMultiStatementScript() let db = new SqliteDb(":memory:") let h = db.getHandle() sqlite_exec(h, "CREATE TABLE a (x INTEGER); CREATE TABLE b (y INTEGER); INSERT INTO a VALUES (1); INSERT INTO b VALUES (2);") db.count("SELECT count(*) FROM a").assertEquals(1) db.count("SELECT count(*) FROM b").assertEquals(1) destroy db // --- sqlite_exec runs a CREATE TRIGGER ... BEGIN ... END block (compound SQL) --- @test function test_native_exec_runsTriggerBlock() let db = new SqliteDb(":memory:") let h = db.getHandle() sqlite_exec(h, "CREATE TABLE src (v INTEGER); CREATE TABLE log (v INTEGER)") sqlite_exec(h, "CREATE TRIGGER cp AFTER INSERT ON src BEGIN INSERT INTO log VALUES (NEW.v); END;") sqlite_exec(h, "INSERT INTO src VALUES (5)") db.count("SELECT count(*) FROM log WHERE v = 5").assertEquals(1) destroy db // --- sqlite_finalize + fresh prepare on the same connection --- @test function test_native_finalizeThenReprepare() let db = new SqliteDb(":memory:") let h = db.getHandle() sqlite_exec(h, "CREATE TABLE t (v INTEGER)") sqlite_exec(h, "INSERT INTO t VALUES (1)") let s1 = sqlite_prepare(h, "SELECT v FROM t") sqlite_step(s1).assertEquals(true) sqlite_finalize(s1) // a second prepare/step on the same connection still works after finalize let s2 = sqlite_prepare(h, "SELECT v FROM t") sqlite_step(s2).assertEquals(true) sqlite_column_int(s2, 0).assertEquals(1) sqlite_finalize(s2) destroy db