From de5fa19cf6dae5e7b612c4b9832592328d6343e2 Mon Sep 17 00:00:00 2001 From: "carpentry-heartbeat[bot]" Date: Mon, 25 May 2026 21:50:39 +0200 Subject: [PATCH 1/2] fix: use 64-bit integers for SQLite3.Type.Integer; add test suite The Integer variant of SQLite3.Type stored Int (32-bit), silently truncating Long values via to-int. This changes the storage to Long (64-bit) throughout: the Carp type, the C helper struct, and the sqlite3_bind/column calls now use the 64-bit variants. Also adds a comprehensive test suite (17 tests) covering open/close, DDL, all type conversions, null handling, multiple rows, empty results, parameterized queries, and the Long truncation regression. --- sqlite3.carp | 21 ++-- sqlite3_helper.h | 10 +- test/sqlite3.carp | 248 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 268 insertions(+), 11 deletions(-) create mode 100644 test/sqlite3.carp diff --git a/sqlite3.carp b/sqlite3.carp index f31e3df..793591b 100644 --- a/sqlite3.carp +++ b/sqlite3.carp @@ -71,7 +71,7 @@ primitive Carp types can be casted to appropriate SQLite types by using the `to-sqlite3` interface.") (deftype Type (Null []) - (Integer [Int]) + (Integer [Long]) (Floating [Double]) (Text [String]) (Blob [String])) @@ -83,11 +83,20 @@ primitive Carp types can be casted to appropriate SQLite types by using the (defmodule Type (defmodule SQLiteColumn (register nil (Fn [] SQLiteColumn) "SQLiteColumn_nil") - (register int (Fn [Int] SQLiteColumn) "SQLiteColumn_int") + (register int (Fn [Long] SQLiteColumn) "SQLiteColumn_int") (register float (Fn [Double] SQLiteColumn) "SQLiteColumn_float") (register text (Fn [String] SQLiteColumn) "SQLiteColumn_text") (register blob (Fn [String] SQLiteColumn) "SQLiteColumn_blob")) + (defn = [a b] + (match-ref a + (Null) (match-ref b (Null) true _ false) + (Integer ai) (match-ref b (Integer bi) (= ai bi) _ false) + (Floating af) (match-ref b (Floating bf) (= af bf) _ false) + (Text as) (match-ref b (Text bs) (= as bs) _ false) + (Blob ab) (match-ref b (Blob bb) (= ab bb) _ false))) + (implements = SQLite3.Type.=) + (defn prn [s] (SQLite3.Type.str s)) (implements prn SQLite3.Type.prn) @@ -102,7 +111,7 @@ primitive Carp types can be casted to appropriate SQLite types by using the (defmodule SQLiteColumn (register tag (Fn [&SQLiteColumn] Int) "SQLiteColumn_tag") - (register from-integer (Fn [SQLiteColumn] Int) "SQLiteColumn_from_int") + (register from-integer (Fn [SQLiteColumn] Long) "SQLiteColumn_from_int") (register from-floating (Fn [SQLiteColumn] Double) "SQLiteColumn_from_float") (register from-text (Fn [SQLiteColumn] String) "SQLiteColumn_from_str") @@ -191,15 +200,15 @@ If it fails, we return an error message using `Result.Error`.") (definterface to-sqlite3 (Fn [a] SQLite3.Type)) (defmodule Bool - (defn to-sqlite3 [b] (SQLite3.Type.Integer (if b 1 0))) + (defn to-sqlite3 [b] (SQLite3.Type.Integer (if b 1l 0l))) (implements to-sqlite3 Bool.to-sqlite3)) (defmodule Int - (defn to-sqlite3 [i] (SQLite3.Type.Integer i)) + (defn to-sqlite3 [i] (SQLite3.Type.Integer (Long.from-int i))) (implements to-sqlite3 Int.to-sqlite3)) (defmodule Long - (defn to-sqlite3 [l] (SQLite3.Type.Integer (to-int (the Long l)))) + (defn to-sqlite3 [l] (SQLite3.Type.Integer l)) (implements to-sqlite3 Long.to-sqlite3)) (defmodule Float diff --git a/sqlite3_helper.h b/sqlite3_helper.h index b571807..9344662 100644 --- a/sqlite3_helper.h +++ b/sqlite3_helper.h @@ -9,7 +9,7 @@ typedef struct { typedef struct { int tag; union { - int i; + long i; double f; char* s; }; @@ -19,7 +19,7 @@ int SQLiteColumn_tag(SQLiteColumn* col) { return col->tag; } -int SQLiteColumn_from_int(SQLiteColumn col) { +long SQLiteColumn_from_int(SQLiteColumn col) { return col.i; } @@ -37,7 +37,7 @@ SQLiteColumn SQLiteColumn_nil() { return res; } -SQLiteColumn SQLiteColumn_int(int i) { +SQLiteColumn SQLiteColumn_int(long i) { SQLiteColumn res; res.tag = SQLITE_INTEGER; res.i = i; @@ -169,7 +169,7 @@ const char* SQLite3_exec_internal(sqlite3_stmt* s, SQLiteRows* rows) { c->tag = sqlite3_column_type(s, i); switch(c->tag) { case SQLITE_INTEGER: - c->i = sqlite3_column_int(s, i); + c->i = (long)sqlite3_column_int64(s, i); break; case SQLITE_FLOAT: c->f = sqlite3_column_double(s, i); @@ -230,7 +230,7 @@ const char* SQLite3_bind(sqlite3_stmt* s, Array* p) { res = sqlite3_bind_null(s, i+1); break; case SQLITE_INTEGER: - res = sqlite3_bind_int(s, i+1, val.i); + res = sqlite3_bind_int64(s, i+1, (sqlite3_int64)val.i); break; case SQLITE_FLOAT: res = sqlite3_bind_double(s, i+1, val.f); diff --git a/test/sqlite3.carp b/test/sqlite3.carp new file mode 100644 index 0000000..c788105 --- /dev/null +++ b/test/sqlite3.carp @@ -0,0 +1,248 @@ +(load "../sqlite3.carp") +(load "Test.carp") +(use Test) + +; --------------------------------------------------------------------------- +; Helpers +; --------------------------------------------------------------------------- + +(defn open-memory [] (Result.unsafe-from-success (SQLite3.open ":memory:"))) + +; --------------------------------------------------------------------------- +; Tests +; --------------------------------------------------------------------------- + +(deftest test + ; ========================================================================= + ; Open / Close + ; ========================================================================= + + (assert-true test + (Result.success? &(SQLite3.open ":memory:")) + "open in-memory database succeeds") + + (assert-true test + (Result.error? &(SQLite3.open "/nonexistent/path/db")) + "open invalid path fails") + + ; ========================================================================= + ; DDL queries + ; ========================================================================= + + (assert-true test + (let [db (open-memory)] + (let-do [r (SQLite3.query &db "CREATE TABLE t (x INT);" &[])] + (SQLite3.close db) + (Result.success? &r))) + "CREATE TABLE succeeds") + + (assert-true test + (let [db (open-memory)] + (let-do [r (SQLite3.query &db "NOT VALID SQL" &[])] + (SQLite3.close db) + (Result.error? &r))) + "invalid SQL returns error") + + ; ========================================================================= + ; Insert and select - basic types + ; ========================================================================= + + (assert-equal test + &(Result.Success + [[(SQLite3.Type.Integer 42l) + (SQLite3.Type.Text @"hello") + (SQLite3.Type.Floating 3.14)]]) + &(let [db (open-memory)] + (let-do [r (do + (ignore + (SQLite3.query &db + "CREATE TABLE t (i INT, s TEXT, f REAL);" + &[])) + (ignore + (SQLite3.query &db + "INSERT INTO t VALUES (?1, ?2, ?3);" + &[(to-sqlite3 42) + (to-sqlite3 @"hello") + (to-sqlite3 3.14)])) + (SQLite3.query &db "SELECT * FROM t;" &[]))] + (SQLite3.close db) + r)) + "insert and select Int, String, Double") + + ; ========================================================================= + ; Null handling + ; ========================================================================= + + (assert-equal test + &(Result.Success [[(SQLite3.Type.Null)]]) + &(let [db (open-memory)] + (let-do [r (do + (ignore (SQLite3.query &db "CREATE TABLE t (x INT);" &[])) + (ignore + (SQLite3.query &db + "INSERT INTO t VALUES (?1);" + &[(SQLite3.Type.Null)])) + (SQLite3.query &db "SELECT * FROM t;" &[]))] + (SQLite3.close db) + r)) + "NULL round-trips correctly") + + ; ========================================================================= + ; Bool conversion + ; ========================================================================= + + (assert-equal test + &(Result.Success [[(SQLite3.Type.Integer 1l)] [(SQLite3.Type.Integer 0l)]]) + &(let [db (open-memory)] + (let-do [r (do + (ignore (SQLite3.query &db "CREATE TABLE t (b INT);" &[])) + (ignore + (SQLite3.query &db + "INSERT INTO t VALUES (?1);" + &[(to-sqlite3 true)])) + (ignore + (SQLite3.query &db + "INSERT INTO t VALUES (?1);" + &[(to-sqlite3 false)])) + (SQLite3.query &db "SELECT * FROM t;" &[]))] + (SQLite3.close db) + r)) + "Bool to-sqlite3 stores 1 for true, 0 for false") + + ; ========================================================================= + ; Float conversion + ; ========================================================================= + + (assert-equal test + &(Result.Success [[(SQLite3.Type.Floating 2.5)]]) + &(let [db (open-memory)] + (let-do [r (do + (ignore (SQLite3.query &db "CREATE TABLE t (f REAL);" &[])) + (ignore + (SQLite3.query &db + "INSERT INTO t VALUES (?1);" + &[(to-sqlite3 2.5f)])) + (SQLite3.query &db "SELECT * FROM t;" &[]))] + (SQLite3.close db) + r)) + "Float to-sqlite3 promotes to Double correctly") + + ; ========================================================================= + ; Long - large values (the bug fix) + ; ========================================================================= + + (assert-equal test + &(Result.Success [[(SQLite3.Type.Integer 3000000000l)]]) + &(let [db (open-memory)] + (let-do [r (do + (ignore (SQLite3.query &db "CREATE TABLE t (big INT);" &[])) + (ignore + (SQLite3.query &db + "INSERT INTO t VALUES (?1);" + &[(to-sqlite3 3000000000l)])) + (SQLite3.query &db "SELECT * FROM t;" &[]))] + (SQLite3.close db) + r)) + "Long values > 2^31 round-trip without truncation") + + (assert-equal test + &(Result.Success [[(SQLite3.Type.Integer -3000000000l)]]) + &(let [db (open-memory)] + (let-do [r (do + (ignore (SQLite3.query &db "CREATE TABLE t (big INT);" &[])) + (ignore + (SQLite3.query &db + "INSERT INTO t VALUES (?1);" + &[(to-sqlite3 -3000000000l)])) + (SQLite3.query &db "SELECT * FROM t;" &[]))] + (SQLite3.close db) + r)) + "negative Long values > 2^31 round-trip without truncation") + + ; ========================================================================= + ; Multiple rows + ; ========================================================================= + + (assert-equal test + &(Result.Success + [[(SQLite3.Type.Integer 1l) (SQLite3.Type.Text @"a")] + [(SQLite3.Type.Integer 2l) (SQLite3.Type.Text @"b")] + [(SQLite3.Type.Integer 3l) (SQLite3.Type.Text @"c")]]) + &(let [db (open-memory)] + (let-do [r (do + (ignore + (SQLite3.query &db "CREATE TABLE t (id INT, name TEXT);" &[])) + (ignore + (SQLite3.query &db + "INSERT INTO t VALUES (?1, ?2);" + &[(to-sqlite3 1) (to-sqlite3 @"a")])) + (ignore + (SQLite3.query &db + "INSERT INTO t VALUES (?1, ?2);" + &[(to-sqlite3 2) (to-sqlite3 @"b")])) + (ignore + (SQLite3.query &db + "INSERT INTO t VALUES (?1, ?2);" + &[(to-sqlite3 3) (to-sqlite3 @"c")])) + (SQLite3.query &db "SELECT * FROM t ORDER BY id;" &[]))] + (SQLite3.close db) + r)) + "multiple rows returned correctly") + + ; ========================================================================= + ; Empty result set + ; ========================================================================= + + (assert-equal test + &(Result.Success (the (Array (Array SQLite3.Type)) [])) + &(let [db (open-memory)] + (let-do [r (do + (ignore (SQLite3.query &db "CREATE TABLE t (x INT);" &[])) + (SQLite3.query &db "SELECT * FROM t;" &[]))] + (SQLite3.close db) + r)) + "empty result set returns empty array") + + ; ========================================================================= + ; Prepared statement parameters + ; ========================================================================= + + (assert-equal test + &(Result.Success [[(SQLite3.Type.Integer 2l) (SQLite3.Type.Text @"bob")]]) + &(let [db (open-memory)] + (let-do [r (do + (ignore + (SQLite3.query &db "CREATE TABLE t (id INT, name TEXT);" &[])) + (ignore + (SQLite3.query &db "INSERT INTO t VALUES (1, 'alice');" &[])) + (ignore + (SQLite3.query &db "INSERT INTO t VALUES (2, 'bob');" &[])) + (ignore + (SQLite3.query &db "INSERT INTO t VALUES (3, 'carol');" &[])) + (SQLite3.query &db + "SELECT * FROM t WHERE id = ?1;" + &[(to-sqlite3 2)]))] + (SQLite3.close db) + r)) + "parameterized WHERE clause works") + + ; ========================================================================= + ; Type.str / show + ; ========================================================================= + + (assert-equal test + "(Integer 7)" + &(str &(SQLite3.Type.Integer 7l)) + "Type.Integer str") + + (assert-equal test + "(Text @\"hi\")" + &(str &(SQLite3.Type.Text @"hi")) + "Type.Text str") + + (assert-equal test + "(Floating 1.5)" + &(str &(SQLite3.Type.Floating 1.5)) + "Type.Floating str") + + (assert-equal test "(Null)" &(str &(SQLite3.Type.Null)) "Type.Null str")) From 7be640d46647ed98ba54cb1ffa41f9edbf79c002 Mon Sep 17 00:00:00 2001 From: "carpentry-heartbeat[bot]" Date: Tue, 26 May 2026 04:48:42 +0200 Subject: [PATCH 2/2] Fix long to int64_t in C helper for LLP64 portability Replace bare `long` with `int64_t` in SQLiteColumn union field, SQLiteColumn_from_int return type, and SQLiteColumn_int parameter. Remove explicit (long) cast on sqlite3_column_int64 result. This ensures correct 64-bit integer handling on Windows (LLP64) where long is only 32 bits. --- sqlite3_helper.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sqlite3_helper.h b/sqlite3_helper.h index 9344662..420ce38 100644 --- a/sqlite3_helper.h +++ b/sqlite3_helper.h @@ -9,7 +9,7 @@ typedef struct { typedef struct { int tag; union { - long i; + int64_t i; double f; char* s; }; @@ -19,7 +19,7 @@ int SQLiteColumn_tag(SQLiteColumn* col) { return col->tag; } -long SQLiteColumn_from_int(SQLiteColumn col) { +int64_t SQLiteColumn_from_int(SQLiteColumn col) { return col.i; } @@ -37,7 +37,7 @@ SQLiteColumn SQLiteColumn_nil() { return res; } -SQLiteColumn SQLiteColumn_int(long i) { +SQLiteColumn SQLiteColumn_int(int64_t i) { SQLiteColumn res; res.tag = SQLITE_INTEGER; res.i = i; @@ -169,7 +169,7 @@ const char* SQLite3_exec_internal(sqlite3_stmt* s, SQLiteRows* rows) { c->tag = sqlite3_column_type(s, i); switch(c->tag) { case SQLITE_INTEGER: - c->i = (long)sqlite3_column_int64(s, i); + c->i = sqlite3_column_int64(s, i); break; case SQLITE_FLOAT: c->f = sqlite3_column_double(s, i);