Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 49 additions & 12 deletions sqlite3.carp
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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))
Expand Down
58 changes: 54 additions & 4 deletions sqlite3_helper.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
#include "sqlite3.h"

// --- BEGIN HELPERS ---

typedef struct {
sqlite3* handle;
} SQLite;
Expand Down Expand Up @@ -145,8 +143,6 @@ char* SQLiteRes_error(SQLiteRes r) {
return (char*)r.err;
}

// --- END HELPERS ---

SQLite SQLite3_init() {
SQLite res;
res.handle = NULL;
Expand Down Expand Up @@ -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);
}
113 changes: 112 additions & 1 deletion test/sqlite3.carp
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Loading