-- Exercise of the sqlite stdlib module. local function assert_eq(actual, expected, msg) if actual ~= expected then error( string.format("%s: expected %s, got %s", msg or "assertion failed", tostring(expected), tostring(actual)), 2 ) end end print("=== connect (in-memory) ===") local con = sqlite.connect(":memory:") print("=== CREATE TABLE / INSERT ===") con:execute([[ CREATE TABLE people ( id INTEGER PRIMARY KEY, name TEXT, age INTEGER, score REAL, vip INTEGER, notes TEXT ) ]]) -- Positional params, every supported type. local changed = con:execute( "INSERT INTO people (name, age, score, vip, notes) VALUES (?, ?, ?, ?, ?)", { "Alice", 30, 9.5, true, "first" } ) assert_eq(changed, 1, "execute returns rows changed") assert_eq(con:changes(), 1, "changes() after insert") local first_id = con:lastInsertRowid() assert_eq(first_id, 1, "lastInsertRowid") -- Named params (mix of :name in SQL). con:execute( "INSERT INTO people (name, age, score, vip, notes) VALUES (:name, :age, :score, :vip, :notes)", { name = "Bob", age = 42, score = 7.25, vip = false, notes = utils.NULL } ) assert_eq(con:lastInsertRowid(), 2, "lastInsertRowid after second insert") print("=== SELECT (query) ===") local rows = con:query("SELECT * FROM people ORDER BY id") assert_eq(#rows, 2, "query returns 2 rows") assert_eq(rows[1].name, "Alice", "row 1 name") assert_eq(rows[1].age, 30, "row 1 age (integer round-trip)") assert_eq(rows[1].score, 9.5, "row 1 score (real round-trip)") assert_eq(rows[1].vip, 1, "row 1 vip (boolean stored as 1)") assert_eq(rows[1].notes, "first", "row 1 notes (text round-trip)") -- A SQL NULL comes back as nil (absent key). assert_eq(rows[2].notes, nil, "row 2 notes is nil (NULL round-trip)") print("=== queryOne ===") local bob = con:queryOne("SELECT * FROM people WHERE name = ?", { "Bob" }) assert(bob ~= nil, "queryOne found Bob") assert_eq(bob.age, 42, "queryOne Bob age") local missing = con:queryOne("SELECT * FROM people WHERE name = ?", { "Nobody" }) assert_eq(missing, nil, "queryOne returns nil for no match") print("=== UPDATE / DELETE ===") local n = con:execute("UPDATE people SET age = age + 1 WHERE name = :who", { who = "Alice" }) assert_eq(n, 1, "update affected 1 row") assert_eq(con:queryOne("SELECT age FROM people WHERE name = ?", { "Alice" }).age, 31, "age incremented") n = con:execute("DELETE FROM people WHERE name = ?", { "Bob" }) assert_eq(n, 1, "delete affected 1 row") assert_eq(#con:query("SELECT * FROM people"), 1, "one row left after delete") print("=== transaction(fn) — commit ===") con:transaction(function() con:execute("INSERT INTO people (name, age) VALUES (?, ?)", { "Carol", 25 }) con:execute("INSERT INTO people (name, age) VALUES (?, ?)", { "Dave", 28 }) end) assert_eq(#con:query("SELECT * FROM people"), 3, "two rows committed") print("=== transaction(fn) — rollback on error ===") local ok, err = pcall(function() con:transaction(function() con:execute("INSERT INTO people (name, age) VALUES (?, ?)", { "Eve", 99 }) error("boom") end) end) assert_eq(ok, false, "transaction propagated the error") assert(tostring(err):find("boom"), "original error message preserved") assert_eq(#con:query("SELECT * FROM people"), 3, "failed transaction rolled back") print("=== manual begin / commit ===") con:begin() con:execute("INSERT INTO people (name, age) VALUES (?, ?)", { "Frank", 50 }) con:commit() assert_eq(#con:query("SELECT * FROM people"), 4, "manual commit persisted") print("=== manual begin / rollback ===") con:begin() con:execute("INSERT INTO people (name, age) VALUES (?, ?)", { "Grace", 60 }) con:rollback() assert_eq(#con:query("SELECT * FROM people"), 4, "manual rollback discarded") print("=== mixed param table is rejected ===") local mixed_ok = pcall(function() con:query("SELECT 1 WHERE 1 = ?", { 1, key = "x" }) end) assert_eq(mixed_ok, false, "mixed positional/named params error") print("=== close ===") con:close() local closed_ok = pcall(function() con:query("SELECT 1") end) assert_eq(closed_ok, false, "use after close errors") print("\nAll sqlite checks passed.")