From 0bc51d649418dd62629c888154a29d164818006c Mon Sep 17 00:00:00 2001 From: "carpentry-heartbeat[bot]" Date: Sat, 20 Jun 2026 06:23:17 +0200 Subject: [PATCH] Fix blob column handling to preserve binary data with embedded nulls Change Type.Blob from String to (Array Byte) so binary data with embedded null bytes is not truncated. Add SQLiteColumn_from_blob to return blob data as a length-aware Carp Array, and use the stored blob_len in sqlite3_bind_blob instead of strlen. --- sqlite3.carp | 9 ++++++--- sqlite3_helper.h | 20 +++++++++++++++----- test/sqlite3.carp | 16 +++++++++++++++- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/sqlite3.carp b/sqlite3.carp index fd150e1..c67109d 100644 --- a/sqlite3.carp +++ b/sqlite3.carp @@ -74,7 +74,7 @@ primitive Carp types can be casted to appropriate SQLite types by using the (Integer [Long]) (Floating [Double]) (Text [String]) - (Blob [String])) + (Blob [(Array Byte)])) (private SQLiteColumn) (hidden SQLiteColumn) @@ -86,7 +86,7 @@ primitive Carp types can be casted to appropriate SQLite types by using the (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")) + (register blob (Fn [(Array Byte)] SQLiteColumn) "SQLiteColumn_blob")) (defn = [a b] (match-ref a @@ -114,6 +114,9 @@ primitive Carp types can be casted to appropriate SQLite types by using the (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") + (register from-blob + (Fn [SQLiteColumn] (Array Byte)) + "SQLiteColumn_from_blob") (defn to-carp [c] (case (tag &c) @@ -124,7 +127,7 @@ primitive Carp types can be casted to appropriate SQLite types by using the sql_text (Type.Text (from-text c)) sql_blob - (Type.Blob (from-text c)) + (Type.Blob (from-blob c)) (Type.Null)))) (private SQLiteRow) diff --git a/sqlite3_helper.h b/sqlite3_helper.h index 2be2d43..bb96920 100644 --- a/sqlite3_helper.h +++ b/sqlite3_helper.h @@ -8,6 +8,7 @@ typedef struct { typedef struct { int tag; + int blob_len; union { int64_t i; double f; @@ -58,10 +59,19 @@ SQLiteColumn SQLiteColumn_text(char* s) { return res; } -SQLiteColumn SQLiteColumn_blob(char* s) { +SQLiteColumn SQLiteColumn_blob(Array a) { SQLiteColumn res; res.tag = SQLITE_BLOB; - res.s = s; + res.blob_len = a.len; + res.s = a.data; + return res; +} + +Array SQLiteColumn_from_blob(SQLiteColumn col) { + Array res; + res.len = col.blob_len; + res.capacity = col.blob_len; + res.data = col.s; return res; } @@ -183,9 +193,9 @@ const char* SQLite3_exec_internal(sqlite3_stmt* s, SQLiteRows* rows) { } case SQLITE_BLOB: { len = sqlite3_column_bytes(s, i); - c->s = CARP_MALLOC(len+1); + c->blob_len = len; + c->s = CARP_MALLOC(len); memcpy(c->s, sqlite3_column_blob(s, i), len); - c->s[len] = '\0'; break; } case SQLITE_NULL: @@ -239,7 +249,7 @@ const char* SQLite3_bind(sqlite3_stmt* s, Array* p) { res = sqlite3_bind_text(s, i+1, val.s, strlen(val.s), SQLITE_STATIC); break; case SQLITE_BLOB: - res = sqlite3_bind_blob(s, i+1, val.s, strlen(val.s), SQLITE_STATIC); + res = sqlite3_bind_blob(s, i+1, val.s, val.blob_len, SQLITE_STATIC); break; } if (res != SQLITE_OK) { diff --git a/test/sqlite3.carp b/test/sqlite3.carp index c5fcb6e..f803dea 100644 --- a/test/sqlite3.carp +++ b/test/sqlite3.carp @@ -289,4 +289,18 @@ (let-do [r (SQLite3.query &db "SELECT * FROM t;" &[])] (SQLite3.close db) r)) - "with-transaction rolls back on error")) + "with-transaction rolls back on error") + + (assert-equal test + &(Result.Success [[(SQLite3.Type.Blob [0b 1b 0b 2b 3b])]]) + &(let [db (open-memory)] + (let-do [r (do + (ignore (SQLite3.query &db "CREATE TABLE t (data BLOB);" &[])) + (ignore + (SQLite3.query &db + "INSERT INTO t VALUES (?1);" + &[(SQLite3.Type.Blob [0b 1b 0b 2b 3b])])) + (SQLite3.query &db "SELECT * FROM t;" &[]))] + (SQLite3.close db) + r)) + "blob with embedded nulls round-trips correctly"))