mirror of
https://github.com/janet-lang/janet
synced 2024-11-28 11:09:54 +00:00
429 lines
14 KiB
C
429 lines
14 KiB
C
/*
|
|
* Copyright (c) 2018 Calvin Rose
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to
|
|
* deal in the Software without restriction, including without limitation the
|
|
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
* sell copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
* IN THE SOFTWARE.
|
|
*/
|
|
|
|
#include "sqlite3.h"
|
|
#include <janet/janet.h>
|
|
|
|
#define FLAG_CLOSED 1
|
|
|
|
#define MSG_DB_CLOSED "database already closed"
|
|
|
|
typedef struct {
|
|
sqlite3* handle;
|
|
int flags;
|
|
} Db;
|
|
|
|
/* Close a db, noop if already closed */
|
|
static void closedb(Db *db) {
|
|
if (!(db->flags & FLAG_CLOSED)) {
|
|
db->flags |= FLAG_CLOSED;
|
|
sqlite3_close_v2(db->handle);
|
|
}
|
|
}
|
|
|
|
/* Called to garbage collect a sqlite3 connection */
|
|
static int gcsqlite(void *p, size_t s) {
|
|
(void) s;
|
|
Db *db = (Db *)p;
|
|
closedb(db);
|
|
return 0;
|
|
}
|
|
|
|
static const JanetAbstractType sql_conn_type = {
|
|
":sqlite3.connection",
|
|
gcsqlite,
|
|
NULL,
|
|
};
|
|
|
|
/* Open a new database connection */
|
|
static int sql_open(JanetArgs args) {
|
|
sqlite3 *conn;
|
|
const uint8_t *filename;
|
|
int status;
|
|
JANET_FIXARITY(args, 1);
|
|
JANET_ARG_STRING(filename, args, 0);
|
|
status = sqlite3_open((const char *)filename, &conn);
|
|
if (status == SQLITE_OK) {
|
|
Db *db = (Db *) janet_abstract(&sql_conn_type, sizeof(Db));
|
|
db->handle = conn;
|
|
db->flags = 0;
|
|
JANET_RETURN_ABSTRACT(args, db);
|
|
} else {
|
|
const char *err = sqlite3_errmsg(conn);
|
|
JANET_THROW(args, err);
|
|
}
|
|
}
|
|
|
|
/* Close a database connection */
|
|
static int sql_close(JanetArgs args) {
|
|
Db *db;
|
|
JANET_FIXARITY(args, 1);
|
|
JANET_ARG_ABSTRACT(db, args, 0, &sql_conn_type);
|
|
closedb(db);
|
|
JANET_RETURN_NIL(args);
|
|
}
|
|
|
|
/* Check for embedded NULL bytes */
|
|
static int has_null(const uint8_t *str, int32_t len) {
|
|
while (len--) {
|
|
if (!str[len])
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Bind a single parameter */
|
|
static const char *bind1(sqlite3_stmt *stmt, int index, Janet value) {
|
|
int res;
|
|
switch (janet_type(value)) {
|
|
default:
|
|
return "invalid sql value";
|
|
case JANET_NIL:
|
|
res = sqlite3_bind_null(stmt, index);
|
|
break;
|
|
case JANET_FALSE:
|
|
res = sqlite3_bind_int(stmt, index, 0);
|
|
break;
|
|
case JANET_TRUE:
|
|
res = sqlite3_bind_int(stmt, index, 1);
|
|
break;
|
|
case JANET_REAL:
|
|
res = sqlite3_bind_double(stmt, index, janet_unwrap_real(value));
|
|
break;
|
|
case JANET_INTEGER:
|
|
res = sqlite3_bind_int64(stmt, index, janet_unwrap_integer(value));
|
|
break;
|
|
case JANET_STRING:
|
|
case JANET_SYMBOL:
|
|
{
|
|
const uint8_t *str = janet_unwrap_string(value);
|
|
int32_t len = janet_string_length(str);
|
|
if (has_null(str, len)) {
|
|
return "cannot have embedded nulls in text values";
|
|
} else {
|
|
res = sqlite3_bind_text(stmt, index, (const char *)str, len + 1, SQLITE_STATIC);
|
|
}
|
|
}
|
|
break;
|
|
case JANET_BUFFER:
|
|
{
|
|
JanetBuffer *buffer = janet_unwrap_buffer(value);
|
|
res = sqlite3_bind_blob(stmt, index, buffer->data, buffer->count, SQLITE_STATIC);
|
|
}
|
|
break;
|
|
}
|
|
if (res != SQLITE_OK) {
|
|
sqlite3 *db = sqlite3_db_handle(stmt);
|
|
return sqlite3_errmsg(db);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Bind many parameters */
|
|
static const char *bindmany(sqlite3_stmt *stmt, Janet params) {
|
|
/* parameters */
|
|
const Janet *seq;
|
|
const JanetKV *kvs;
|
|
int32_t len, cap;
|
|
int limitindex = sqlite3_bind_parameter_count(stmt);
|
|
if (janet_indexed_view(params, &seq, &len)) {
|
|
if (len > limitindex + 1) {
|
|
return "invalid index in sql parameters";
|
|
}
|
|
for (int i = 0; i < len; i++) {
|
|
const char *err = bind1(stmt, i + 1, seq[i]);
|
|
if (err) {
|
|
return err;
|
|
}
|
|
}
|
|
} else if (janet_dictionary_view(params, &kvs, &len, &cap)) {
|
|
for (int i = 0; i < cap; i++) {
|
|
int index = 0;
|
|
switch (janet_type(kvs[i].key)) {
|
|
default:
|
|
/* Will fail */
|
|
break;
|
|
case JANET_NIL:
|
|
/* Will skip as nil keys indicate empty hash table slot */
|
|
continue;
|
|
case JANET_INTEGER:
|
|
index = janet_unwrap_integer(kvs[i].key);
|
|
break;
|
|
case JANET_STRING:
|
|
case JANET_SYMBOL:
|
|
{
|
|
const uint8_t *s = janet_unwrap_string(kvs[i].key);
|
|
index = sqlite3_bind_parameter_index(
|
|
stmt,
|
|
(const char *)s);
|
|
}
|
|
break;
|
|
}
|
|
if (index <= 0 || index > limitindex) {
|
|
return "invalid index in sql parameters";
|
|
}
|
|
const char *err = bind1(stmt, index, kvs[i].value);
|
|
if (err) {
|
|
return err;
|
|
}
|
|
}
|
|
} else {
|
|
return "invalid type for sql parameters";
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Execute a statement but don't collect results */
|
|
static const char *execute(sqlite3_stmt *stmt) {
|
|
int status;
|
|
const char *ret = NULL;
|
|
do {
|
|
status = sqlite3_step(stmt);
|
|
} while (status == SQLITE_ROW);
|
|
/* Check for errors */
|
|
if (status != SQLITE_DONE) {
|
|
sqlite3 *db = sqlite3_db_handle(stmt);
|
|
ret = sqlite3_errmsg(db);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/* Execute and return values from prepared statement */
|
|
static const char *execute_collect(sqlite3_stmt *stmt, JanetArray *rows) {
|
|
/* Count number of columns in result */
|
|
int ncol = sqlite3_column_count(stmt);
|
|
int status;
|
|
const char *ret = NULL;
|
|
|
|
/* Get column names */
|
|
Janet *tupstart = janet_tuple_begin(ncol);
|
|
for (int i = 0; i < ncol; i++) {
|
|
tupstart[i] = janet_cstringv(sqlite3_column_name(stmt, i));
|
|
}
|
|
const Janet *colnames = janet_tuple_end(tupstart);
|
|
|
|
do {
|
|
status = sqlite3_step(stmt);
|
|
if (status == SQLITE_ROW) {
|
|
JanetKV *row = janet_struct_begin(ncol);
|
|
for (int i = 0; i < ncol; i++) {
|
|
int t = sqlite3_column_type(stmt, i);
|
|
Janet value;
|
|
switch (t) {
|
|
case SQLITE_NULL:
|
|
value = janet_wrap_nil();
|
|
break;
|
|
case SQLITE_INTEGER:
|
|
value = janet_wrap_integer(sqlite3_column_int(stmt, i));
|
|
break;
|
|
case SQLITE_FLOAT:
|
|
value = janet_wrap_real(sqlite3_column_double(stmt, i));
|
|
break;
|
|
case SQLITE_TEXT:
|
|
{
|
|
int nbytes = sqlite3_column_bytes(stmt, i);
|
|
uint8_t *str = janet_string_begin(nbytes);
|
|
memcpy(str, sqlite3_column_text(stmt, i), nbytes);
|
|
value = janet_wrap_string(janet_string_end(str));
|
|
}
|
|
break;
|
|
case SQLITE_BLOB:
|
|
{
|
|
int nbytes = sqlite3_column_bytes(stmt, i);
|
|
JanetBuffer *b = janet_buffer(nbytes);
|
|
memcpy(b->data, sqlite3_column_blob(stmt, i), nbytes);
|
|
b->count = nbytes;
|
|
value = janet_wrap_buffer(b);
|
|
}
|
|
break;
|
|
}
|
|
janet_struct_put(row, colnames[i], value);
|
|
}
|
|
janet_array_push(rows, janet_wrap_struct(janet_struct_end(row)));
|
|
}
|
|
} while (status == SQLITE_ROW);
|
|
|
|
/* Check for errors */
|
|
if (status != SQLITE_DONE) {
|
|
sqlite3 *db = sqlite3_db_handle(stmt);
|
|
ret = sqlite3_errmsg(db);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/* Evaluate a string of sql */
|
|
static int sql_eval(JanetArgs args) {
|
|
const char *err;
|
|
sqlite3_stmt *stmt = NULL, *stmt_next = NULL;
|
|
const uint8_t *query;
|
|
|
|
JANET_MINARITY(args, 2);
|
|
JANET_MAXARITY(args, 3);
|
|
JANET_CHECKABSTRACT(args, 0, &sql_conn_type);
|
|
Db *db = (Db *)janet_unwrap_abstract(args.v[0]);
|
|
if (db->flags & FLAG_CLOSED) {
|
|
JANET_THROW(args, MSG_DB_CLOSED);
|
|
}
|
|
JANET_ARG_STRING(query, args, 1);
|
|
if (has_null(query, janet_string_length(query))) {
|
|
err = "cannot have embedded NULL in sql statememts";
|
|
goto error;
|
|
}
|
|
JanetArray *rows = janet_array(10);
|
|
const char *c = (const char *)query;
|
|
|
|
/* Evaluate all statements in a loop */
|
|
do {
|
|
/* Compile the next statement */
|
|
if (sqlite3_prepare_v2(db->handle, c, -1, &stmt_next, &c) != SQLITE_OK) {
|
|
err = sqlite3_errmsg(db->handle);
|
|
goto error;
|
|
}
|
|
/* Check if we have found last statement */
|
|
if (NULL == stmt_next) {
|
|
/* Execute current statement and collect results */
|
|
if (stmt) {
|
|
err = execute_collect(stmt, rows);
|
|
if (err) goto error;
|
|
}
|
|
} else {
|
|
/* Execute current statement but don't collect results. */
|
|
if (stmt) {
|
|
err = execute(stmt);
|
|
if (err) goto error;
|
|
}
|
|
/* Bind params to next statement*/
|
|
if (args.n == 3) {
|
|
/* parameters */
|
|
err = bindmany(stmt_next, args.v[2]);
|
|
if (err) goto error;
|
|
}
|
|
}
|
|
/* rotate stmt and stmt_next */
|
|
if (stmt) sqlite3_finalize(stmt);
|
|
stmt = stmt_next;
|
|
stmt_next = NULL;
|
|
} while (NULL != stmt);
|
|
|
|
/* Good return path */
|
|
JANET_RETURN_ARRAY(args, rows);
|
|
|
|
error:
|
|
if (stmt) sqlite3_finalize(stmt);
|
|
if (stmt_next) sqlite3_finalize(stmt_next);
|
|
JANET_THROW(args, err);
|
|
}
|
|
|
|
/* Convert int64_t to a string */
|
|
static const uint8_t *coerce_int64(int64_t x) {
|
|
uint8_t bytes[40];
|
|
int i = 0;
|
|
/* Edge cases */
|
|
if (x == 0) return janet_cstring("0");
|
|
if (x == INT64_MIN) return janet_cstring("-9,223,372,036,854,775,808");
|
|
/* Negative becomes pos */
|
|
if (x < 0) {
|
|
bytes[i++] = '-';
|
|
x = -x;
|
|
}
|
|
while (x) {
|
|
bytes[i++] = x % 10;
|
|
x = x / 10;
|
|
}
|
|
bytes[i] = '\0';
|
|
return janet_string(bytes, i);
|
|
}
|
|
|
|
/* Gets the last inserted row id */
|
|
static int sql_last_insert_rowid(JanetArgs args) {
|
|
JANET_FIXARITY(args, 1);
|
|
JANET_CHECKABSTRACT(args, 0, &sql_conn_type);
|
|
Db *db = (Db *)janet_unwrap_abstract(args.v[0]);
|
|
if (db->flags & FLAG_CLOSED) {
|
|
JANET_THROW(args, MSG_DB_CLOSED);
|
|
}
|
|
sqlite3_int64 id = sqlite3_last_insert_rowid(db->handle);
|
|
if (id >= INT32_MIN && id <= INT32_MAX) {
|
|
JANET_RETURN_INTEGER(args, (int32_t) id);
|
|
}
|
|
/* Convert to string */
|
|
JANET_RETURN_STRING(args, coerce_int64(id));
|
|
}
|
|
|
|
/* Get the sqlite3 errcode */
|
|
static int sql_error_code(JanetArgs args) {
|
|
JANET_FIXARITY(args, 1);
|
|
JANET_CHECKABSTRACT(args, 0, &sql_conn_type);
|
|
Db *db = (Db *)janet_unwrap_abstract(args.v[0]);
|
|
if (db->flags & FLAG_CLOSED) {
|
|
JANET_THROW(args, MSG_DB_CLOSED);
|
|
}
|
|
int errcode = sqlite3_errcode(db->handle);
|
|
JANET_RETURN_INTEGER(args, errcode);
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
|
|
static const JanetReg cfuns[] = {
|
|
{"open", sql_open,
|
|
"(sqlite3.open path)\n\n"
|
|
"Opens a sqlite3 database on disk. Returns the database handle if the database was opened "
|
|
"successfully, and otheriwse throws an error."
|
|
},
|
|
{"close", sql_close,
|
|
"(sqlite3.close db)\n\n"
|
|
"Closes a database. Use this to free a database after use. Returns nil."
|
|
},
|
|
{"eval", sql_eval,
|
|
"(sqlite3.eval db sql [,params])\n\n"
|
|
"Evaluate sql in the context of database db. Multiple sql statements "
|
|
"can be changed together, and optionally parameters maybe passed in. "
|
|
"The optional parameters maybe either an indexed data type (tuple or array), or a dictionary "
|
|
"data type (struct or table). If params is a tuple or array, then sqlite "
|
|
"parameters are substituted using indices. For example:\n\n"
|
|
"\t(sqlite3.eval db `SELECT * FROM tab WHERE id = ?;` [123])\n\n"
|
|
"Will select rows from tab where id is equal to 123. Alternatively, "
|
|
"the programmer can use named parameters with tables or structs, like so:\n\n"
|
|
"\t(sqlite3.eval db `SELECT * FROM tab WHERE id = :id;` {:id 123})\n\n"
|
|
"Will return an array of rows, where each row contains a table where columns names "
|
|
"are keys for column values."
|
|
},
|
|
{"last-insert-rowid", sql_last_insert_rowid,
|
|
"(sqlite3.last-insert-rowid db)\n\n"
|
|
"Returns the id of the last inserted row. If the id will fit into a 32-bit"
|
|
"signed integer, will returned an integer, otherwise will return a string representation "
|
|
"of the id (an 8 bytes string containing a long integer)."
|
|
},
|
|
{"error-code", sql_error_code,
|
|
"(sqlite3.error-code db)\n\n"
|
|
"Returns the error number of the last sqlite3 command that threw an error. Cross "
|
|
"check these numbers with the SQLite documentation for more information."
|
|
},
|
|
{NULL, NULL, NULL}
|
|
};
|
|
|
|
JANET_MODULE_ENTRY(JanetArgs args) {
|
|
JanetTable *env = janet_env(args);
|
|
janet_cfuns(env, "sqlite3", cfuns);
|
|
return 0;
|
|
}
|