commit f23d4ed49685330a059c4d1251c84d58daa1bb03 Author: hellerve Date: Sun Jan 26 16:02:11 2020 +0100 initial diff --git a/README.md b/README.md new file mode 100644 index 0000000..acedf45 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# sqlite3 + +is a simple high-level wrapper around SQLite3. It doesn’t intend to wrap +everything, but it tries to be useful. + +## Installation + +```clojure +(load "https://veitheller.de/git/carpentry/sqlite3@0.0.1") +``` + +## Usage + +The module `SQLite3` provides facilities for opening, closing, and querying +databases. + +```clojure +(load "https://veitheller.de/git/carpentry/sqlite3@0.0.1") + +; opening DBs can fail, for the purposes of this example we +; ignore that +(let-do [db (Result.unsafe-from-success (SQLite3.open "db"))] + ; we can prepare statements + (println* &(SQLite3.query &db "INSERT INTO mytable VALUES (?1, ?2);" + &[(to-sqlite3 @"hello") (to-sqlite3 100)])) + ; and query things + (println* &(SQLite3.query &db "SELECT * from mytable;" &[])) + (SQLite3.close db) +``` + +Because `open` and `query` return `Result` types, we could also use +combinators! + +For more information, check out [the +documentation](https://veitheller.de/sqlite3)! + +
+ +Have fun! diff --git a/docs/SQLite3.html b/docs/SQLite3.html new file mode 100644 index 0000000..bdadffc --- /dev/null +++ b/docs/SQLite3.html @@ -0,0 +1,166 @@ + + + + + + + + + +
+ +

+ SQLite3 +

+
+

is a simple high-level wrapper around SQLite3. It doesn’t intend +to wrap everything, but it tries to be useful.

+

Installation

+
(load "https://veitheller.de/git/carpentry/sqlite3@0.0.1")
+
+

Usage

+

The module SQLite3 provides facilities for opening, closing, and querying +databases.

+
(load "https://veitheller.de/git/carpentry/sqlite3@0.0.1")
+
+; opening DBs can fail, for the purposes of this example we
+; ignore that
+(let-do [db (Result.unsafe-from-success (SQLite3.open "db"))]
+  ; we can prepare statements
+  (println* &(SQLite3.query &db "INSERT INTO mytable VALUES (?1, ?2);"
+                            &[(to-sqlite3 @"hello") (to-sqlite3 100)]))
+  ; and query things
+  (println* &(SQLite3.query &db "SELECT * from mytable;" &[]))
+  (SQLite3.close db)
+
+

Because open and query return Result types, we could also use +combinators!

+ +
+
+ +

+ SQLite +

+
+
+ doc-stub +
+

+ a +

+ + + +

+

is the opaque database type. You’ll need one of those to query +anything.

+

It can be obtained by using open.

+ +

+
+
+ +

+ Type +

+
+
+ module +
+

+ Module +

+ + + +

+

represent all the SQLite types we can represent.

+

The constructors are Null, Integer, Floating, Text, and Blob. Most +primitive Carp types can be casted to appropriate SQLite types by using the +to-sqlite3 interface.

+ +

+
+
+ +

+ close +

+
+
+ external +
+

+ (λ [SQLite] ()) +

+ + + +

+

closes a database.

+ +

+
+
+ +

+ open +

+
+
+ defn +
+

+ (λ [&String] (Result SQLite String)) +

+
+                    (open s)
+                
+

+

opens a database with the filename s.

+

If it fails, we return an error message using Result.Error.

+ +

+
+
+ +

+ query +

+
+
+ defn +
+

+ (λ [(Ref SQLite), &String, (Ref (Array Type))] (Result (Array (Array Type)) String)) +

+
+                    (query db s p)
+                
+

+

queries the database db using the query s and the parameters +p.

+

If it fails, we return an error message using Result.Error.

+ +

+
+
+ + diff --git a/docs/style.css b/docs/style.css new file mode 100644 index 0000000..ed4f963 --- /dev/null +++ b/docs/style.css @@ -0,0 +1,110 @@ +html { + font-family: "Helvetica", sans-serif; + font-size: 16px; +} + +a { + color: #000; +} + +.logo { + display: none; +} + +ul { + list-style-type: none; + font-family: "Hasklig", "Lucida Console", monospace; + line-height: 1.4em; +} + +.module-description { + margin-bottom: 3em; +} + +.content { + margin: 3em auto auto auto; + width: 80%; + max-width: 610px; + min-width: 400px +} + +h1 { + margin-bottom: 1em; + font-weight: 400; +} + +h2 { + font-weight: 400; + margin-bottom: 0em; +} + +h3 { + margin: 0em; + font-weight: 400; +} + +.binder { + margin: 0em 0em 3.5em 0em; +} + +.sig { + font-family: "Hasklig", "Lucida Console", monospace; + margin: 0.5em 0em 0.5em 0em; +} + +.args { + background-color: #eee; + display: inline-block; + white-space: normal; + margin: 0; + margin-bottom: 1em; +} + +code { + background-color: #eee; +} + +pre { + background-color: #eee; + overflow-y: scroll; +} + +.description { + margin-top: 0.3em; + font-size: 0.8em; + color: #aaa; +} + +.huge { + font-size: 15em; + margin: 0em; +} + +/* Smaller screens */ +@media only screen and (max-width: 600px) { + .logo { + margin: 1em; + text-align: left; + float: left; + width: 100%; + } + .logo img { + display: block; + margin-left: auto; + margin-right: auto; + width: 50%; + } + .content { + margin: 0.5em; + } + .binder { + margin: 0em 0em 1.5em 0em; + } + .sig { + font-size: 0.9em; + } + ul { + padding: 0px; + } +} +.title, .index { display: none; } diff --git a/examples/simple.carp b/examples/simple.carp new file mode 100644 index 0000000..209e840 --- /dev/null +++ b/examples/simple.carp @@ -0,0 +1,19 @@ +(load "sqlite3.carp") +(use SQLite3) + +(defn main [] + (let-do [db (Result.unsafe-from-success (SQLite3.open "test.db"))] + ; we can create a table + (ignore + (query &db "CREATE TABLE people (id INTEGER PRIMARY KEY AUTOINCREMENT, firstname TEXT, lastname TEXT, age INT);" &[]) + ) + ; insert into it with a prepared statement + (ignore + (query &db "INSERT INTO people VALUES(?1, ?2, ?3, ?4);" + &[(to-sqlite3 1) (to-sqlite3 @"Veit") (to-sqlite3 @"Heller") (to-sqlite3 26)])) + ; using the index, we can even insert out of order + (ignore + (query &db "INSERT INTO people(firstname, lastname, age) VALUES(?3, ?2, ?1);" + &[(SQLite3.Type.Null) (to-sqlite3 @"Svedäng") (to-sqlite3 @"Erik")])) + (println* &(query &db "SELECT * FROM people;" &[])) + (close db))) diff --git a/gendocs.carp b/gendocs.carp new file mode 100644 index 0000000..3e688e5 --- /dev/null +++ b/gendocs.carp @@ -0,0 +1,13 @@ +(load "sqlite3.carp") + +(defndynamic gendocs [] + (do + (Project.config "title" "sqlite3") + (Project.config "docs-directory" "./docs/") + (Project.config "docs-logo" "") + (Project.config "docs-styling" "style.css") + (Project.config "docs-generate-index" false) + (save-docs SQLite3))) + +(gendocs) +(quit) diff --git a/sqlite3.carp b/sqlite3.carp new file mode 100644 index 0000000..24e6a0a --- /dev/null +++ b/sqlite3.carp @@ -0,0 +1,194 @@ +(relative-include "sqlite3_helper.h") +(add-cflag "-lsqlite3") + +(doc SQLite3 "is a simple high-level wrapper around SQLite3. It doesn’t intend +to wrap everything, but it tries to be useful. + +## Installation + +```clojure +(load \"https://veitheller.de/git/carpentry/sqlite3@0.0.1\") +``` + +## Usage + +The module `SQLite3` provides facilities for opening, closing, and querying +databases. + +```clojure +(load \"https://veitheller.de/git/carpentry/sqlite3@0.0.1\") + +; opening DBs can fail, for the purposes of this example we +; ignore that +(let-do [db (Result.unsafe-from-success (SQLite3.open \"db\"))] + ; we can prepare statements + (println* &(SQLite3.query &db \"INSERT INTO mytable VALUES (?1, ?2);\" + &[(to-sqlite3 @\"hello\") (to-sqlite3 100)])) + ; and query things + (println* &(SQLite3.query &db \"SELECT * from mytable;\" &[])) + (SQLite3.close db) +``` + +Because `open` and `query` return `Result` types, we could also use +combinators!") +(defmodule SQLite3 + (private sql_ok) + (hidden sql_ok) + (register sql_ok Int "SQLITE_OK") + (private sql_int) + (hidden sql_int) + (register sql_int Int "SQLITE_INTEGER") + (private sql_double) + (hidden sql_double) + (register sql_double Int "SQLITE_FLOAT") + (private sql_text) + (hidden sql_text) + (register sql_text Int "SQLITE_TEXT") + (private sql_blob) + (hidden sql_blob) + (register sql_blob Int "SQLITE_BLOB") + + (doc SQLite "is the opaque database type. You’ll need one of those to query +anything. + +It can be obtained by using [open](#open).") + (register-type SQLite) + + (doc Type "represent all the SQLite types we can represent. + +The constructors are `Null`, `Integer`, `Floating`, `Text`, and `Blob`. Most +primitive Carp types can be casted to appropriate SQLite types by using the +`to-sqlite3` interface.") + (deftype Type + (Null []) + (Integer [Int]) + (Floating [Double]) + (Text [String]) + (Blob [String]) + ) + + (private SQLiteColumn) + (hidden SQLiteColumn) + (register-type SQLiteColumn) + + (defmodule Type + (defmodule SQLiteColumn + (register nil (Fn [] SQLiteColumn) "SQLiteColumn_nil") + (register int (Fn [Int] SQLiteColumn) "SQLiteColumn_int") + (register float (Fn [Double] SQLiteColumn) "SQLiteColumn_float") + (register text (Fn [String] SQLiteColumn) "SQLiteColumn_text") + (register blob (Fn [String] SQLiteColumn) "SQLiteColumn_blob") + ) + + (defn to-sqlite3-internal [x] + (match x + (Null) (SQLiteColumn.nil) + (Integer i) (SQLiteColumn.int i) + (Floating f) (SQLiteColumn.float f) + (Text s) (SQLiteColumn.text s) + (Blob s) (SQLiteColumn.blob s))) + ) + + (defmodule SQLiteColumn + (register tag (Fn [&SQLiteColumn] Int) "SQLiteColumn_tag") + + (register from-integer (Fn [SQLiteColumn] Int) "SQLiteColumn_from_int") + (register from-floating (Fn [SQLiteColumn] Double) "SQLiteColumn_from_float") + (register from-text (Fn [SQLiteColumn] String) "SQLiteColumn_from_str") + + (defn to-carp [c] + (case (tag &c) + sql_int (Type.Integer (from-integer c)) + sql_double (Type.Floating (from-floating c)) + sql_text (Type.Text (from-text c)) + sql_blob (Type.Blob (from-text c)) + (Type.Null)))) + + (private SQLiteRow) + (hidden SQLiteRow) + (register-type SQLiteRow) + (defmodule SQLiteRow + (register length (Fn [&SQLiteRow] Int) "SQLiteRow_length") + (register nth (Fn [&SQLiteRow Int] SQLiteColumn) "SQLiteRow_nth") + + (defn to-carp [r] + (let-do [l (length &r) + a (Array.allocate l)] + (for [i 0 l] + (Array.aset-uninitialized! &a i (SQLiteColumn.to-carp (nth &r i)))) + a)) + ) + + (private SQLiteRes) + (hidden SQLiteRes) + (register-type SQLiteRes) + (defmodule SQLiteRes + (register ok? (Fn [&SQLiteRes] Bool) "SQLiteRes_is_ok") + (register length (Fn [&SQLiteRes] Int) "SQLiteRes_length") + (register nth (Fn [&SQLiteRes Int] SQLiteRow) "SQLiteRes_nth") + (register error (Fn [SQLiteRes] (Ptr Char)) "SQLiteRes_error") + + (defn to-array [r] + (let-do [l (length &r) + a (Array.allocate l)] + (for [i 0 l] + (Array.aset-uninitialized! &a i (SQLiteRow.to-carp (nth &r i)))) + a)) + ) + + (private init) + (hidden init) + (register init (Fn [] SQLite)) + (private open-) + (hidden open-) + (register open- (Fn [&SQLite (Ptr Char)] Int) "SQLite3_open_c") + (private exec-) + (hidden exec-) + (register exec- (Fn [&SQLite (Ptr Char) (Array SQLiteColumn)] SQLiteRes) "SQLite3_exec_c") + (private error-) + (hidden error-) + (register error- (Fn [SQLite] (Ptr Char)) "SQLite3_error") + + (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)))))) + + (doc query "queries the database `db` using the query `s` and the parameters +`p`. + +If it fails, we return an error message using `Result.Error`.") + (defn query [db s p] + (let [r (exec- db (cstr s) (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 close "closes a database.") + (register close (Fn [SQLite] ()) "SQLite3_close_c") +) + +(definterface to-sqlite3 (Fn [a] SQLIte3.Type)) + +(defmodule Bool + (defn to-sqlite3 [b] (SQLite3.Type.Integer (if b 1 0)))) + +(defmodule Int + (defn to-sqlite3 [i] (SQLite3.Type.Integer i))) + +(defmodule Long + (defn to-sqlite3 [l] (SQLite3.Type.Integer (to-int (the Long l))))) + +(defmodule Float + (defn to-sqlite3 [f] (SQLite3.Type.Floating (Double.from-float f)))) + +(defmodule Double + (defn to-sqlite3 [d] (SQLite3.Type.Floating d))) + +(defmodule String + (defn to-sqlite3 [s] (SQLite3.Type.Text s))) diff --git a/sqlite3_helper.h b/sqlite3_helper.h new file mode 100644 index 0000000..936e9c4 --- /dev/null +++ b/sqlite3_helper.h @@ -0,0 +1,298 @@ +#include "sqlite3.h" + +// --- BEGIN HELPERS --- + +typedef struct { + sqlite3* handle; +} SQLite; + +typedef struct { + int tag; + union { + int i; + double f; + char* s; + }; +} SQLiteColumn; + +int SQLiteColumn_tag(SQLiteColumn* col) { + return col->tag; +} + +int SQLiteColumn_from_int(SQLiteColumn col) { + return col.i; +} + +double SQLiteColumn_from_float(SQLiteColumn col) { + return col.f; +} + +char* SQLiteColumn_from_str(SQLiteColumn col) { + return col.s; +} + +SQLiteColumn SQLiteColumn_nil() { + SQLiteColumn res; + res.tag = SQLITE_NULL; + return res; +} + +SQLiteColumn SQLiteColumn_int(int i) { + SQLiteColumn res; + res.tag = SQLITE_INTEGER; + res.i = i; + return res; +} + +SQLiteColumn SQLiteColumn_float(double f) { + SQLiteColumn res; + res.tag = SQLITE_FLOAT; + res.f = f; + return res; +} + +SQLiteColumn SQLiteColumn_text(char* s) { + SQLiteColumn res; + res.tag = SQLITE_TEXT; + res.s = s; + return res; +} + +SQLiteColumn SQLiteColumn_blob(char* s) { + SQLiteColumn res; + res.tag = SQLITE_BLOB; + res.s = s; + return res; +} + +typedef struct { + int columns; + SQLiteColumn* data; +} SQLiteRow; + +int SQLiteRow_length(SQLiteRow* row) { + return row->columns; +} + +SQLiteColumn SQLiteRow_nth(SQLiteRow* row, int i) { + return row->data[i]; +} + +typedef struct { + int capacity; + int len; + SQLiteRow* rows; +} SQLiteRows; + +SQLiteRow* SQLiteRows_next_row(SQLiteRows* rows) { + SQLiteRow* res; + if (rows->capacity <= rows->len) { + if (!(rows->capacity)) rows->capacity = 10; + else rows->capacity *= 2; + rows->rows = realloc(rows->rows, (rows->capacity)*sizeof(SQLiteRow)); + } + res = (rows->rows)+(rows->len); + rows->len++; + return res; +} + +void SQLiteRows_finalize(SQLiteRows* rows) { + rows->capacity = rows->len; + rows->rows = realloc(rows->rows, (rows->capacity)*sizeof(SQLiteRow)); +} + +SQLiteRows SQLiteRows_new_rows() { + SQLiteRows res; + res.len = 0; + res.capacity = 0; + res.rows = NULL; + return res; +} + +typedef struct { + int is; + union { + const char* err; + SQLiteRows rows; + }; +} SQLiteRes; +#define OK 0 +#define ERR 1 + +int SQLiteRes_length(SQLiteRes* r) { + return r->rows.len; +} + +SQLiteRow SQLiteRes_nth(SQLiteRes* r, int i) { + return r->rows.rows[i]; +} + +bool SQLiteRes_is_ok(SQLiteRes* r) { + return r->is == OK; +} + +char* SQLiteRes_error(SQLiteRes r) { + return (char*)r.err; +} + +// --- END HELPERS --- + +SQLite SQLite3_init() { + SQLite res; + res.handle = NULL; + return res; +} + +int SQLite3_open_c(SQLite* db, const char* filename) { + sqlite3* c; + int res = sqlite3_open(filename, &c); + db->handle = c; + return res; +} + +const char* SQLite3_exec_internal(sqlite3_stmt* s, SQLiteRows* rows) { + int status; + int len; + const char* err = NULL; + int count = sqlite3_column_count(s); + + do { + status = sqlite3_step(s); + + if (status == SQLITE_ROW) { + SQLiteRow* row = SQLiteRows_next_row(rows); + row->columns = count; + row->data = CARP_MALLOC(count*sizeof(SQLiteColumn)); + + for (int i = 0; i < count; i++) { + SQLiteColumn* c = row->data+i; + c->tag = sqlite3_column_type(s, i); + switch(c->tag) { + case SQLITE_INTEGER: + c->i = sqlite3_column_int(s, i); + break; + case SQLITE_FLOAT: + c->f = sqlite3_column_double(s, i); + break; + case SQLITE_TEXT: { + len = sqlite3_column_bytes(s, i); + c->s = CARP_MALLOC(len); + memcpy(c->s, sqlite3_column_text(s, i), len); + c->s[len] = '\0'; + break; + } + case SQLITE_BLOB: { + len = sqlite3_column_bytes(s, i); + c->s = CARP_MALLOC(len); + memcpy(c->s, sqlite3_column_blob(s, i), len); + c->s[len] = '\0'; + break; + } + case SQLITE_NULL: + break; + } + } + } + } while (status == SQLITE_ROW); + + if (status != SQLITE_DONE) { + sqlite3* db = sqlite3_db_handle(s); + err = sqlite3_errmsg(db); + } + + SQLiteRows_finalize(rows); + + return err; +} + +static const char* SQLite3_exec_ignore(sqlite3_stmt* s) { + int status; + const char* ret = NULL; + do { status = sqlite3_step(s); } while (status == SQLITE_ROW); + + /* Check for errors */ + if (status != SQLITE_DONE) { + sqlite3* db = sqlite3_db_handle(s); + ret = sqlite3_errmsg(db); + } + return ret; +} + +const char* SQLite3_bind(sqlite3_stmt* s, Array p) { + int res; + const char* err = NULL; + + for (int i = 0; i < p.len; i++) { + SQLiteColumn val = ((SQLiteColumn*)p.data)[i]; + + switch (val.tag) { + case SQLITE_NULL: + res = sqlite3_bind_null(s, i+1); + break; + case SQLITE_INTEGER: + res = sqlite3_bind_int(s, i+1, val.i); + break; + case SQLITE_FLOAT: + res = sqlite3_bind_double(s, i+1, val.f); + break; + case SQLITE_TEXT: + 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); + break; + } + if (res != SQLITE_OK) { + sqlite3* db = sqlite3_db_handle(s); + err = sqlite3_errmsg(db); + } + if (err) break; + } + + return err; +} + +SQLiteRes SQLite3_exec_c(SQLite* db, const char* stmt, Array p) { + sqlite3_stmt* s = NULL; + sqlite3_stmt* n = NULL; + const char* err; + SQLiteRes res; + res.is = OK; + res.rows = SQLiteRows_new_rows(); + + do { + if (sqlite3_prepare_v2(db->handle, stmt, -1, &n, &stmt) != SQLITE_OK) { + err = sqlite3_errmsg(db->handle); + goto err; + } else { + if (n) { + err = SQLite3_bind(n, p); + if (err) goto err; + } + } + + if (s) { + err = n ? SQLite3_exec_ignore(s) : SQLite3_exec_internal(s, &(res.rows)); + if (err) goto err; + } + if (s) sqlite3_finalize(s); + s = n; + n = NULL; + } while (s); + + return res; +err: + if (s) sqlite3_finalize(s); + if (n) sqlite3_finalize(n); + res.is = ERR; + res.err = err; + return res; +} + +void SQLite3_close_c(SQLite db) { + sqlite3_close_v2(db.handle); +} + +char* SQLite3_error(SQLite db) { + return (char*)sqlite3_errmsg(db.handle); +}