diff --git a/sqlite3.carp b/sqlite3.carp index c67109d..369df35 100644 --- a/sqlite3.carp +++ b/sqlite3.carp @@ -198,6 +198,53 @@ If it fails, we return an error message using `Result.Error`.") (doc close "closes a database.") (register close (Fn [SQLite] ()) "SQLite3_close_c") + (doc Stmt "is an opaque prepared statement type. Prepare once with +[prepare](#prepare), execute with [exec-prepared](#exec-prepared), and +release with [finalize-stmt](#finalize-stmt).") + (register-type Stmt) + + (private stmt-init) + (hidden stmt-init) + (register stmt-init (Fn [] Stmt) "SQLite3_stmt_init") + (private errmsg) + (hidden errmsg) + (register errmsg (Fn [&SQLite] String) "SQLite3_errmsg_c") + (private exec-prepared-) + (hidden exec-prepared-) + (register exec-prepared- + (Fn [&Stmt &(Array SQLiteColumn)] SQLiteRes) + "SQLite3_exec_prepared_c") + (private prepare-) + (hidden prepare-) + (register prepare- (Fn [&SQLite (Ptr CChar) &Stmt] Int) "SQLite3_prepare_c") + + (doc prepare "prepares a SQL statement for repeated execution via +[exec-prepared](#exec-prepared). Release with [finalize-stmt](#finalize-stmt) +when done.") + (defn prepare [db sql] + (let [stmt (stmt-init) + res (prepare- db (cstr sql) &stmt)] + (if (= res sql_ok) (Result.Success stmt) (Result.Error (errmsg db))))) + + (doc exec-prepared "executes a prepared statement with the given parameters. +Automatically resets the statement afterward for reuse.") + (defn exec-prepared [stmt p] + (let [r (exec-prepared- stmt + &(Array.copy-map &(fn [x] + (Type.to-sqlite3-internal @x)) + p))] + (if (SQLiteRes.ok? &r) + (Result.Success (SQLiteRes.to-array r)) + (Result.Error (from-cstr (SQLiteRes.error r)))))) + + (doc reset-stmt "manually resets a prepared statement and clears its bindings. +Called automatically by [exec-prepared](#exec-prepared).") + (register reset-stmt (Fn [&Stmt] ()) "SQLite3_reset_stmt_c") + + (doc finalize-stmt "releases a prepared statement’s resources. Must not be +used afterward.") + (register finalize-stmt (Fn [Stmt] ()) "SQLite3_finalize_stmt_c") + (doc last-insert-rowid "returns the row ID of the last successful INSERT.") (register last-insert-rowid (Fn [&SQLite] Long) "SQLite3_last_insert_rowid") @@ -223,18 +270,8 @@ or DELETE.") (Result.Success _) (Result.Success ()) (Result.Error e) (Result.Error e))) - (doc with-transaction "executes `body` inside a transaction. If `body` -evaluates to a `Result.Error`, the transaction is rolled back; otherwise it is -committed. Returns the result of `body`, or the error from `begin`/`commit` -if those fail. - -Example: -``` -(SQLite3.with-transaction &db - (do - (ignore (SQLite3.query &db \"INSERT INTO t VALUES (1);\" &[])) - (SQLite3.query &db \"SELECT * FROM t;\" &[]))) -```") + (doc with-transaction "executes `body` inside a transaction. Rolls back on +`Result.Error`, commits on success.") (defmacro with-transaction [db body] (list 'let (array '__wtx_begin (list 'SQLite3.begin db)) diff --git a/sqlite3_helper.h b/sqlite3_helper.h index bb96920..bf0bb46 100644 --- a/sqlite3_helper.h +++ b/sqlite3_helper.h @@ -1,7 +1,5 @@ #include "sqlite3.h" -// --- BEGIN HELPERS --- - typedef struct { sqlite3* handle; } SQLite; @@ -145,8 +143,6 @@ char* SQLiteRes_error(SQLiteRes r) { return (char*)r.err; } -// --- END HELPERS --- - SQLite SQLite3_init() { SQLite res; res.handle = NULL; @@ -319,3 +315,57 @@ char* SQLite3_error_and_close(SQLite db) { sqlite3_close_v2(db.handle); return copy; } + +typedef struct { + sqlite3_stmt* handle; +} Stmt; + +Stmt SQLite3_stmt_init() { + Stmt res; + res.handle = NULL; + return res; +} + +int SQLite3_prepare_c(SQLite* db, const char* sql, Stmt* stmt) { + return sqlite3_prepare_v2(db->handle, sql, -1, &stmt->handle, NULL); +} + +char* SQLite3_errmsg_c(SQLite* db) { + const char* msg = sqlite3_errmsg(db->handle); + size_t len = strlen(msg); + char* copy = CARP_MALLOC(len + 1); + memcpy(copy, msg, len + 1); + return copy; +} + +SQLiteRes SQLite3_exec_prepared_c(Stmt* stmt, Array* p) { + SQLiteRes res; + res.is = OK; + res.rows = SQLiteRows_new_rows(); + + const char* err = SQLite3_bind(stmt->handle, p); + if (err) goto fail; + + err = SQLite3_exec_internal(stmt->handle, &res.rows); + if (err) goto fail; + + sqlite3_reset(stmt->handle); + sqlite3_clear_bindings(stmt->handle); + return res; + +fail: + sqlite3_reset(stmt->handle); + sqlite3_clear_bindings(stmt->handle); + res.is = ERR; + res.err = err; + return res; +} + +void SQLite3_reset_stmt_c(Stmt* stmt) { + sqlite3_reset(stmt->handle); + sqlite3_clear_bindings(stmt->handle); +} + +void SQLite3_finalize_stmt_c(Stmt stmt) { + if (stmt.handle) sqlite3_finalize(stmt.handle); +} diff --git a/test/sqlite3.carp b/test/sqlite3.carp index f803dea..0b54adc 100644 --- a/test/sqlite3.carp +++ b/test/sqlite3.carp @@ -303,4 +303,115 @@ (SQLite3.query &db "SELECT * FROM t;" &[]))] (SQLite3.close db) r)) - "blob with embedded nulls round-trips correctly")) + "blob with embedded nulls round-trips correctly") + + (assert-true test + (let [db (open-memory)] + (let-do [r (SQLite3.prepare &db "SELECT 1;")] + (match r + (Result.Success stmt) + (do (SQLite3.finalize-stmt stmt) (SQLite3.close db) true) + (Result.Error _) (do (SQLite3.close db) false)))) + "prepare succeeds for valid SQL") + + (assert-true test + (let [db (open-memory)] + (let-do [r (SQLite3.prepare &db "NOT VALID SQL")] + (SQLite3.close db) + (Result.error? &r))) + "prepare fails for invalid SQL") + + (assert-equal test + &(Result.Success [[(SQLite3.Type.Integer 42l)]]) + &(let [db (open-memory)] + (match (SQLite3.prepare &db "SELECT ?1;") + (Result.Success stmt) + (let-do [r (SQLite3.exec-prepared &stmt &[(to-sqlite3 42)])] + (SQLite3.finalize-stmt stmt) + (SQLite3.close db) + r) + (Result.Error e) (do (SQLite3.close db) (Result.Error e)))) + "exec-prepared returns results") + + (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-do [db (open-memory)] + (ignore (SQLite3.query &db "CREATE TABLE t (id INT, name TEXT);" &[])) + (match (SQLite3.prepare &db "INSERT INTO t VALUES (?1, ?2);") + (Result.Success stmt) + (do + (ignore + (SQLite3.exec-prepared &stmt &[(to-sqlite3 1) (to-sqlite3 @"a")])) + (ignore + (SQLite3.exec-prepared &stmt &[(to-sqlite3 2) (to-sqlite3 @"b")])) + (ignore + (SQLite3.exec-prepared &stmt &[(to-sqlite3 3) (to-sqlite3 @"c")])) + (SQLite3.finalize-stmt stmt) + (let-do [r (SQLite3.query &db "SELECT * FROM t ORDER BY id;" &[])] + (SQLite3.close db) + r)) + (Result.Error e) (do (SQLite3.close db) (Result.Error e)))) + "exec-prepared reuses statement for multiple inserts") + + (assert-equal test + &(Result.Success (the (Array (Array SQLite3.Type)) [])) + &(let-do [db (open-memory)] + (ignore (SQLite3.query &db "CREATE TABLE t (x INT);" &[])) + (match (SQLite3.prepare &db "SELECT * FROM t;") + (Result.Success stmt) + (let-do [r (SQLite3.exec-prepared &stmt &[])] + (SQLite3.finalize-stmt stmt) + (SQLite3.close db) + r) + (Result.Error e) (do (SQLite3.close db) (Result.Error e)))) + "exec-prepared with no rows returns empty array") + + (assert-equal test + &(Result.Success + [[(SQLite3.Type.Integer 1l)] + [(SQLite3.Type.Integer 2l)] + [(SQLite3.Type.Integer 3l)]]) + &(let-do [db (open-memory)] + (ignore (SQLite3.query &db "CREATE TABLE t (x INT);" &[])) + (ignore (SQLite3.query &db "INSERT INTO t VALUES (1);" &[])) + (ignore (SQLite3.query &db "INSERT INTO t VALUES (2);" &[])) + (ignore (SQLite3.query &db "INSERT INTO t VALUES (3);" &[])) + (match (SQLite3.prepare &db "SELECT * FROM t WHERE x > ?1;") + (Result.Success stmt) + (let-do [r (SQLite3.exec-prepared &stmt &[(to-sqlite3 0)])] + (SQLite3.finalize-stmt stmt) + (SQLite3.close db) + r) + (Result.Error e) (do (SQLite3.close db) (Result.Error e)))) + "exec-prepared with parameterized SELECT works") + + (assert-true test + (let-do [db (open-memory)] + (ignore (SQLite3.query &db "CREATE TABLE t (x INT);" &[])) + (match (SQLite3.prepare &db "INSERT INTO t VALUES (?1);") + (Result.Success stmt) + (do + (SQLite3.reset-stmt &stmt) + (let-do [r (SQLite3.exec-prepared &stmt &[(to-sqlite3 99)])] + (SQLite3.finalize-stmt stmt) + (SQLite3.close db) + (Result.success? &r))) + (Result.Error _) (do (SQLite3.close db) false))) + "reset-stmt allows re-execution") + + (assert-equal test + 2l + (let-do [db (open-memory)] + (ignore (SQLite3.query &db "CREATE TABLE t (id INTEGER PRIMARY KEY);" &[])) + (match (SQLite3.prepare &db "INSERT INTO t VALUES (NULL);") + (Result.Success stmt) + (do + (ignore (SQLite3.exec-prepared &stmt &[])) + (ignore (SQLite3.exec-prepared &stmt &[])) + (SQLite3.finalize-stmt stmt) + (let-do [r (SQLite3.last-insert-rowid &db)] (SQLite3.close db) r)) + (Result.Error _) (do (SQLite3.close db) 0l))) + "last-insert-rowid works with prepared inserts"))