diff --git a/sqlite3.carp b/sqlite3.carp index 1282d6d..fd150e1 100644 --- a/sqlite3.carp +++ b/sqlite3.carp @@ -170,7 +170,7 @@ primitive Carp types can be casted to appropriate SQLite types by using the "SQLite3_exec_c") (private error-) (hidden error-) - (register error- (Fn [SQLite] (Ptr CChar)) "SQLite3_error") + (register error- (Fn [SQLite] String) "SQLite3_error_and_close") (doc open "opens a database with the filename `s`. @@ -178,9 +178,7 @@ If it fails, we return an error message using `Result.Error`.") (defn open [s] (let [db (SQLite3.init) res (open- &db (cstr s))] - (if (= res sql_ok) - (Result.Success db) - (Result.Error (from-cstr (error- db)))))) + (if (= res sql_ok) (Result.Success db) (Result.Error (error- db))))) (doc query "queries the database `db` using the query `s` and the parameters `p`. @@ -195,7 +193,71 @@ If it fails, we return an error message using `Result.Error`.") (Result.Error (from-cstr (SQLiteRes.error r)))))) (doc close "closes a database.") - (register close (Fn [SQLite] ()) "SQLite3_close_c")) + (register close (Fn [SQLite] ()) "SQLite3_close_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") + + (doc changes "returns the number of rows modified by the last INSERT, UPDATE, +or DELETE.") + (register changes (Fn [&SQLite] Int) "SQLite3_changes") + + (doc begin "begins a transaction.") + (defn begin [db] + (match (query db "BEGIN TRANSACTION;" &[]) + (Result.Success _) (Result.Success ()) + (Result.Error e) (Result.Error e))) + + (doc commit "commits the current transaction.") + (defn commit [db] + (match (query db "COMMIT;" &[]) + (Result.Success _) (Result.Success ()) + (Result.Error e) (Result.Error e))) + + (doc rollback "rolls back the current transaction.") + (defn rollback [db] + (match (query db "ROLLBACK;" &[]) + (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;\" &[]))) +```") + (defmacro with-transaction [db body] + (list 'let + (array '__wtx_begin (list 'SQLite3.begin db)) + (list 'match + '__wtx_begin + (list 'Result.Error '__wtx_err) + (list 'Result.Error '__wtx_err) + (list 'Result.Success '_) + (list 'let + (array '__wtx_result body) + (list 'match + '__wtx_result + (list 'Result.Error '__wtx_berr) + (list 'do + (list 'ignore (list 'SQLite3.rollback db)) + (list 'Result.Error '__wtx_berr)) + (list 'Result.Success '__wtx_val) + (list 'match + (list 'SQLite3.commit db) + (list 'Result.Error '__wtx_cerr) + (list 'do + (list 'ignore + (list 'SQLite3.rollback db)) + (list 'Result.Error '__wtx_cerr)) + (list 'Result.Success '_) + (list 'Result.Success '__wtx_val)))))))) (definterface to-sqlite3 (Fn [a] SQLite3.Type)) diff --git a/sqlite3_helper.h b/sqlite3_helper.h index 420ce38..2be2d43 100644 --- a/sqlite3_helper.h +++ b/sqlite3_helper.h @@ -293,6 +293,19 @@ void SQLite3_close_c(SQLite db) { sqlite3_close_v2(db.handle); } -char* SQLite3_error(SQLite db) { - return (char*)sqlite3_errmsg(db.handle); +int64_t SQLite3_last_insert_rowid(SQLite* db) { + return (int64_t)sqlite3_last_insert_rowid(db->handle); +} + +int SQLite3_changes(SQLite* db) { + return sqlite3_changes(db->handle); +} + +char* SQLite3_error_and_close(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); + sqlite3_close_v2(db.handle); + return copy; } diff --git a/test/sqlite3.carp b/test/sqlite3.carp index c788105..89e1443 100644 --- a/test/sqlite3.carp +++ b/test/sqlite3.carp @@ -6,7 +6,9 @@ ; Helpers ; --------------------------------------------------------------------------- -(defn open-memory [] (Result.unsafe-from-success (SQLite3.open ":memory:"))) +(defn open-memory [] + (Result.expect (SQLite3.open ":memory:") + "test setup failed: could not open :memory: db")) ; --------------------------------------------------------------------------- ; Tests @@ -245,4 +247,122 @@ &(str &(SQLite3.Type.Floating 1.5)) "Type.Floating str") - (assert-equal test "(Null)" &(str &(SQLite3.Type.Null)) "Type.Null str")) + (assert-equal test "(Null)" &(str &(SQLite3.Type.Null)) "Type.Null str") + + ; ========================================================================= + ; last-insert-rowid + ; ========================================================================= + + (assert-equal test + 1l + (let-do [db (open-memory)] + (ignore (SQLite3.query &db "CREATE TABLE t (id INTEGER PRIMARY KEY);" &[])) + (ignore (SQLite3.query &db "INSERT INTO t VALUES (NULL);" &[])) + (let-do [r (SQLite3.last-insert-rowid &db)] (SQLite3.close db) r)) + "last-insert-rowid returns 1 after first insert") + + (assert-equal test + 2l + (let-do [db (open-memory)] + (ignore (SQLite3.query &db "CREATE TABLE t (id INTEGER PRIMARY KEY);" &[])) + (ignore (SQLite3.query &db "INSERT INTO t VALUES (NULL);" &[])) + (ignore (SQLite3.query &db "INSERT INTO t VALUES (NULL);" &[])) + (let-do [r (SQLite3.last-insert-rowid &db)] (SQLite3.close db) r)) + "last-insert-rowid returns 2 after second insert") + + ; ========================================================================= + ; changes + ; ========================================================================= + + (assert-equal test + 3 + (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);" &[])) + (ignore (SQLite3.query &db "DELETE FROM t;" &[])) + (let-do [r (SQLite3.changes &db)] (SQLite3.close db) r)) + "changes returns 3 after deleting 3 rows") + + ; ========================================================================= + ; Transactions - begin / commit + ; ========================================================================= + + (assert-true test + (let-do [db (open-memory)] + (let-do [r (SQLite3.begin &db)] + (ignore (SQLite3.commit &db)) + (SQLite3.close db) + (Result.success? &r))) + "begin succeeds") + + (assert-equal test + &(Result.Success [[(SQLite3.Type.Integer 1l)]]) + &(let-do [db (open-memory)] + (ignore (SQLite3.query &db "CREATE TABLE t (x INT);" &[])) + (ignore (SQLite3.begin &db)) + (ignore (SQLite3.query &db "INSERT INTO t VALUES (1);" &[])) + (ignore (SQLite3.commit &db)) + (let-do [r (SQLite3.query &db "SELECT * FROM t;" &[])] + (SQLite3.close db) + r)) + "committed data persists") + + ; ========================================================================= + ; Transactions - rollback + ; ========================================================================= + + (assert-equal test + &(Result.Success (the (Array (Array SQLite3.Type)) [])) + &(let-do [db (open-memory)] + (ignore (SQLite3.query &db "CREATE TABLE t (x INT);" &[])) + (ignore (SQLite3.begin &db)) + (ignore (SQLite3.query &db "INSERT INTO t VALUES (1);" &[])) + (ignore (SQLite3.rollback &db)) + (let-do [r (SQLite3.query &db "SELECT * FROM t;" &[])] + (SQLite3.close db) + r)) + "rolled-back data is gone") + + ; ========================================================================= + ; with-transaction macro - success path + ; ========================================================================= + + (assert-equal test + &(Result.Success [[(SQLite3.Type.Integer 1l)]]) + &(let-do [db (open-memory)] + (ignore (SQLite3.query &db "CREATE TABLE t (x INT);" &[])) + (let-do [r (SQLite3.with-transaction &db + (do + (ignore + (SQLite3.query &db + "INSERT INTO t VALUES (1);" + &[])) + (SQLite3.query &db + "SELECT * FROM t;" + &[])))] + (SQLite3.close db) + r)) + "with-transaction commits on success") + + ; ========================================================================= + ; with-transaction macro - error path (rollback) + ; ========================================================================= + + (assert-equal test + &(Result.Success (the (Array (Array SQLite3.Type)) [])) + &(let-do [db (open-memory)] + (ignore (SQLite3.query &db "CREATE TABLE t (x INT);" &[])) + (ignore + (SQLite3.with-transaction &db + (do + (ignore + (SQLite3.query &db + "INSERT INTO t VALUES (1);" + &[])) + (SQLite3.query &db "NOT VALID SQL;" &[])))) + (let-do [r (SQLite3.query &db "SELECT * FROM t;" &[])] + (SQLite3.close db) + r)) + "with-transaction rolls back on error"))