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
72 changes: 67 additions & 5 deletions sqlite3.carp
Original file line number Diff line number Diff line change
Expand Up @@ -170,17 +170,15 @@ 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`.

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`.
Expand All @@ -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))

Expand Down
17 changes: 15 additions & 2 deletions sqlite3_helper.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
124 changes: 122 additions & 2 deletions test/sqlite3.carp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"))
Loading