diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3dc9f86..1a7079c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - otp_version: [21,22,23,24] + otp_version: [22,23,24] os: [ubuntu-latest] container: diff --git a/.gitignore b/.gitignore index 15348a3..91441e3 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ _build c_src/**/*.d erl_crash.dump tags +doc/* +!doc/style.css +!doc/overview.edoc +!doc/SQLite370.svg diff --git a/README.md b/README.md index 5cd8efc..6540c04 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,9 @@ in the nif library or the sqlite database can crash the entire Erlang VM. If you do not want to take this risk, it is always possible to access the sqlite nif from a separate erlang node. -Special care has been taken not to block the scheduler of the calling -process. This is done by handling all commands from erlang within a -lightweight thread. The erlang scheduler will get control back when -the command has been added to the command-queue of the thread. +Special care has been taken not to block the normal erlang scheduler +of the calling process. This is done by handling neccesary commands +from erlang by using a dirty scheduler. SQLite Compile Options ---------------------- @@ -66,3 +65,20 @@ INSERT INTO table VALUES("abcd", 1234); Will not work, because it sees the value 'abcd' as a SQL object value, and not a string literal. +Version 0.8.0 +------------- + +This version is a major derivation from previous versions. When +I started with this library it was implemented by using a separate +os level thread per connection. At the time this was the only way +to use functions in C which take longer to process than 1ms. +A lot has changed since then. The VM now has dirty schedulers +which make it possible to remove the thread per connection. +This makes it possible to open a lot more connections. On the +SQLite side some things have also changed. Extended error codes, +introspection into the internals. This release modernizes the +integration. In some places the API is no longer compatible and +will require small changes. In order to ease this process the +library now has typespecs, and the documentation was extended. + + diff --git a/c_src/esqlite3_nif.c b/c_src/esqlite3_nif.c index 1e27af6..361c425 100644 --- a/c_src/esqlite3_nif.c +++ b/c_src/esqlite3_nif.c @@ -1,5 +1,5 @@ /* - * Copyright 2011 - 2017 Maas-Maarten Zeeman + * Copyright 2011 - 2022 Maas-Maarten Zeeman * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,75 +23,37 @@ #include #include -#include "queue.h" #define MAX_ATOM_LENGTH 255 /* from atom.h, not exposed in erlang include */ -#define MAX_SQLITE_NAME_LENGTH 255 /* Maximum name length. Using longer will return misuse */ #define MAX_PATHNAME 512 /* unfortunately not in sqlite.h. */ -static ErlNifResourceType *esqlite_connection_type = NULL; -static ErlNifResourceType *esqlite_statement_type = NULL; -static ErlNifResourceType *esqlite_backup_type = NULL; +static ErlNifResourceType *esqlite3_type = NULL; +static ErlNifResourceType *esqlite3_stmt_type = NULL; +static ErlNifResourceType *esqlite3_backup_type = NULL; -/* database connection context */ +/* Database connection context */ typedef struct { - ErlNifTid tid; - ErlNifThreadOpts* opts; - ErlNifPid notification_pid; - sqlite3 *db; - queue *commands; -} esqlite_connection; + ErlNifPid update_hook_pid; +} esqlite3; -/* prepared statement */ +/* Prepared statement */ typedef struct { + esqlite3 *connection; + sqlite3_stmt *statement; -} esqlite_statement; + int column_count; +} esqlite3_stmt; -/* data associated with ongoing backup */ +/* Data associated with an ongoing backup */ typedef struct { + esqlite3 *source; + esqlite3 *destination; + sqlite3_backup *backup; -} esqlite_backup; +} esqlite3_backup; -typedef enum { - cmd_unknown, - cmd_open, - cmd_update_hook_set, - cmd_notification, - cmd_exec, - cmd_changes, - cmd_prepare, - cmd_bind, - cmd_multi_step, - cmd_reset, - cmd_column_names, - cmd_column_types, - cmd_backup_init, - cmd_backup_step, - cmd_backup_remaining, - cmd_backup_pagecount, - cmd_backup_finish, - cmd_close, - cmd_stop, - cmd_insert, - cmd_last_insert_rowid, - cmd_get_autocommit, -} command_type; - -typedef struct { - command_type type; - - ErlNifEnv *env; - ERL_NIF_TERM ref; - ErlNifPid pid; - ERL_NIF_TERM arg; - ERL_NIF_TERM stmt; -} esqlite_command; - -static ERL_NIF_TERM atom_esqlite3; - -static ERL_NIF_TERM push_command(ErlNifEnv *env, esqlite_connection *conn, esqlite_command *cmd); static ERL_NIF_TERM make_atom(ErlNifEnv *env, const char *atom_name) @@ -99,7 +61,7 @@ make_atom(ErlNifEnv *env, const char *atom_name) ERL_NIF_TERM atom; if(enif_make_existing_atom(env, atom_name, &atom, ERL_NIF_LATIN1)) - return atom; + return atom; return enif_make_atom(env, atom_name); } @@ -117,447 +79,55 @@ make_error_tuple(ErlNifEnv *env, const char *reason) } static ERL_NIF_TERM -make_row_tuple(ErlNifEnv *env, ERL_NIF_TERM value) -{ - return enif_make_tuple2(env, make_atom(env, "row"), value); -} - -static const char * -get_sqlite3_return_code_msg(int r) -{ - switch(r) { - case SQLITE_OK: return "ok"; - case SQLITE_ERROR : return "sqlite_error"; - case SQLITE_INTERNAL: return "internal"; - case SQLITE_PERM: return "perm"; - case SQLITE_ABORT: return "abort"; - case SQLITE_BUSY: return "busy"; - case SQLITE_LOCKED: return "locked"; - case SQLITE_NOMEM: return "nomem"; - case SQLITE_READONLY: return "readonly"; - case SQLITE_INTERRUPT: return "interrupt"; - case SQLITE_IOERR: return "ioerror"; - case SQLITE_CORRUPT: return "corrupt"; - case SQLITE_NOTFOUND: return "notfound"; - case SQLITE_FULL: return "full"; - case SQLITE_CANTOPEN: return "cantopen"; - case SQLITE_PROTOCOL: return "protocol"; - case SQLITE_EMPTY: return "empty"; - case SQLITE_SCHEMA: return "schema"; - case SQLITE_TOOBIG: return "toobig"; - case SQLITE_CONSTRAINT: return "constraint"; - case SQLITE_MISMATCH: return "mismatch"; - case SQLITE_MISUSE: return "misuse"; - case SQLITE_NOLFS: return "nolfs"; - case SQLITE_AUTH: return "auth"; - case SQLITE_FORMAT: return "format"; - case SQLITE_RANGE: return "range"; - case SQLITE_NOTADB: return "notadb"; - case SQLITE_ROW: return "row"; - case SQLITE_DONE: return "done"; - } - - return "unknown"; -} - -static const char * -get_sqlite3_error_msg(int error_code, sqlite3 *db) -{ - static const char *msg; - - if(error_code == SQLITE_MISUSE) - return "Sqlite3 was invoked incorrectly."; - - msg = sqlite3_errmsg(db); - if(!msg) - return "No sqlite3 error message found."; - - return msg; -} - -static ERL_NIF_TERM -make_sqlite3_error_tuple(ErlNifEnv *env, int error_code, sqlite3 *db) -{ - const char *error_code_msg = get_sqlite3_return_code_msg(error_code); - const char *msg = get_sqlite3_error_msg(error_code, db); - - return enif_make_tuple2(env, make_atom(env, "error"), - enif_make_tuple2(env, make_atom(env, error_code_msg), - enif_make_string(env, msg, ERL_NIF_LATIN1))); -} - -static void -command_destroy(void *obj) -{ - esqlite_command *cmd = (esqlite_command *) obj; - - if(cmd->env != NULL) - enif_free_env(cmd->env); - - enif_free(cmd); -} - -static esqlite_command * -command_create() -{ - esqlite_command *cmd = (esqlite_command *) enif_alloc(sizeof(esqlite_command)); - if(cmd == NULL) - return NULL; - - cmd->env = enif_alloc_env(); - if(cmd->env == NULL) { - command_destroy(cmd); - return NULL; - } - - cmd->type = cmd_unknown; - cmd->ref = 0; - cmd->arg = 0; - cmd->stmt = 0; - - return cmd; +make_sqlite3_error_tuple(ErlNifEnv *env, int error_code) { + return enif_make_tuple2(env, make_atom(env, "error"), enif_make_int(env, error_code)); } /* * */ static void -destruct_esqlite_connection(ErlNifEnv *env, void *arg) +destruct_esqlite3(ErlNifEnv *env, void *arg) { - esqlite_connection *db = (esqlite_connection *) arg; - esqlite_command *cmd = command_create(); - - /* Send the stop command - */ - cmd->type = cmd_stop; - queue_push(db->commands, cmd); - - /* Wait for the thread to finish - */ - enif_thread_join(db->tid, NULL); - - enif_thread_opts_destroy(db->opts); - - /* The thread has finished... now remove the command queue, and close - * the database (if it was still open). - */ - while(queue_has_item(db->commands)) { - command_destroy(queue_pop(db->commands)); - } - queue_destroy(db->commands); - + esqlite3 *db = (esqlite3 *) arg; sqlite3_close_v2(db->db); db->db = NULL; } static void -destruct_esqlite_statement(ErlNifEnv *env, void *arg) +destruct_esqlite3_stmt(ErlNifEnv *env, void *arg) { - esqlite_statement *stmt = (esqlite_statement *) arg; + esqlite3_stmt *stmt = (esqlite3_stmt *) arg; sqlite3_finalize(stmt->statement); stmt->statement = NULL; + + stmt->column_count = 0; + + if(stmt->connection) { + enif_release_resource(stmt->connection); + stmt->connection = NULL; + } } static void -destruct_esqlite_backup(ErlNifEnv *env, void *arg) +destruct_esqlite3_backup(ErlNifEnv *env, void *arg) { - esqlite_backup *backup = (esqlite_backup *) arg; - + esqlite3_backup *backup = (esqlite3_backup *) arg; + if(backup->backup) { sqlite3_backup_finish(backup->backup); } - backup->backup = NULL; -} -static ERL_NIF_TERM -do_open(ErlNifEnv *env, esqlite_connection *db, const ERL_NIF_TERM arg) -{ - char filename[MAX_PATHNAME]; - unsigned int size; - int rc; - ERL_NIF_TERM error; - - size = enif_get_string(env, arg, filename, MAX_PATHNAME, ERL_NIF_LATIN1); - if(size <= 0) - return make_error_tuple(env, "invalid_filename"); - - /* Open the database. - */ - rc = sqlite3_open(filename, &db->db); - if(rc != SQLITE_OK) { - error = make_sqlite3_error_tuple(env, rc, db->db); - sqlite3_close_v2(db->db); - db->db = NULL; - - return error; + if(backup->destination) { + enif_release_resource(backup->destination); + backup->destination = NULL; } - sqlite3_busy_timeout(db->db, 2000); - - return make_atom(env, "ok"); -} - -void -update_callback(void *arg, int sqlite_operation_type, char const *sqlite_database, char const *sqlite_table, sqlite3_int64 sqlite_rowid) -{ - esqlite_connection *db = (esqlite_connection *)arg; - esqlite_command *cmd = NULL; - ERL_NIF_TERM type, table, rowid; - - if(db == NULL) - return; - - cmd = command_create(); - if(!cmd) - return; - - rowid = enif_make_int64(cmd->env, sqlite_rowid); - table = enif_make_string(cmd->env, sqlite_table, ERL_NIF_LATIN1); - - switch(sqlite_operation_type) { - case SQLITE_INSERT: - type = make_atom(cmd->env, "insert"); - break; - case SQLITE_DELETE: - type = make_atom(cmd->env, "delete"); - break; - case SQLITE_UPDATE: - type = make_atom(cmd->env, "update"); - break; - default: - return; + if(backup->source) { + enif_release_resource(backup->source); + backup->source = NULL; } - - cmd->type = cmd_notification; - cmd->arg = enif_make_tuple3(cmd->env, type, table, rowid); - - push_command(cmd->env, db, cmd); -} - -static ERL_NIF_TERM -do_set_update_hook(ErlNifEnv *env, esqlite_connection *conn, const ERL_NIF_TERM arg) -{ - if(!enif_get_local_pid(env, arg, &conn->notification_pid)) { - return make_error_tuple(env, "invalid_pid"); - } - - if(!conn->db) { - return make_error_tuple(env, "closed"); - } - - sqlite3_update_hook(conn->db, update_callback, conn); - - return make_atom(env, "ok"); -} - -/* - */ -static ERL_NIF_TERM -do_exec(ErlNifEnv *env, esqlite_connection *conn, const ERL_NIF_TERM arg) -{ - ErlNifBinary bin; - int rc; - ERL_NIF_TERM eos = enif_make_int(env, 0); - - if(!enif_inspect_iolist_as_binary(env, enif_make_list2(env, arg, eos), &bin)) { - return make_error_tuple(env, "no_iodata"); - } - - rc = sqlite3_exec(conn->db, (char *) bin.data, NULL, NULL, NULL); - if(rc != SQLITE_OK) - return make_sqlite3_error_tuple(env, rc, conn->db); - - return make_atom(env, "ok"); -} - -/* - * Nr of changes - */ -static ERL_NIF_TERM -do_changes(ErlNifEnv *env, esqlite_connection *conn, const ERL_NIF_TERM arg) -{ - if(!conn->db) { - return make_error_tuple(env, "closed"); - } - - sqlite3_int64 changes = sqlite3_changes64(conn->db); - ERL_NIF_TERM changes_term = enif_make_int64(env, changes); - - return make_ok_tuple(env, changes_term); -} - -/* -* insert action -*/ -static ERL_NIF_TERM -do_insert(ErlNifEnv *env, esqlite_connection *conn, const ERL_NIF_TERM arg) -{ - ErlNifBinary bin; - int rc; - ERL_NIF_TERM eos = enif_make_int(env, 0); - - if(!enif_inspect_iolist_as_binary(env, enif_make_list2(env, arg, eos), &bin)) { - return make_error_tuple(env, "no_iodata"); - } - - rc = sqlite3_exec(conn->db, (char *) bin.data, NULL, NULL, NULL); - if(rc != SQLITE_OK) - return make_sqlite3_error_tuple(env, rc, conn->db); - sqlite3_int64 last_rowid = sqlite3_last_insert_rowid(conn->db); - ERL_NIF_TERM last_rowid_term = enif_make_int64(env, last_rowid); - return make_ok_tuple(env, last_rowid_term); -} - -/* - * Return the last inserted rowid - */ -static ERL_NIF_TERM -do_last_insert_rowid(ErlNifEnv *env, esqlite_connection *conn) -{ - if(!conn->db) { - return make_error_tuple(env, "closed"); - } - - sqlite3_int64 last_rowid = sqlite3_last_insert_rowid(conn->db); - ERL_NIF_TERM last_rowid_term = enif_make_int64(env, last_rowid); - - return make_ok_tuple(env, last_rowid_term); -} - -/* - * Compile a sql statement - */ -static ERL_NIF_TERM -do_prepare(ErlNifEnv *env, esqlite_connection *conn, const ERL_NIF_TERM arg) -{ - ErlNifBinary bin; - esqlite_statement *stmt; - ERL_NIF_TERM esqlite_stmt; - const char *tail; - int rc; - ERL_NIF_TERM eos = enif_make_int(env, 0); - - if(!enif_inspect_iolist_as_binary(env, enif_make_list2(env, arg, eos), &bin)) { - return make_error_tuple(env, "no_iodata"); - } - - stmt = enif_alloc_resource(esqlite_statement_type, sizeof(esqlite_statement)); - if(!stmt) - return make_error_tuple(env, "no_memory"); - - rc = sqlite3_prepare_v2(conn->db, (char *) bin.data, bin.size, &(stmt->statement), &tail); - if(rc != SQLITE_OK) { - enif_release_resource(stmt); - return make_sqlite3_error_tuple(env, rc, conn->db); - } - - esqlite_stmt = enif_make_resource(env, stmt); - enif_release_resource(stmt); - - return make_ok_tuple(env, esqlite_stmt); -} - -static int -bind_cell(ErlNifEnv *env, const ERL_NIF_TERM cell, sqlite3_stmt *stmt, unsigned int i) -{ - int the_int; - ErlNifSInt64 the_long_int; - double the_double; - char the_atom[MAX_ATOM_LENGTH+1]; - ErlNifBinary the_blob; - int arity; - const ERL_NIF_TERM* tuple; - - if(enif_get_int(env, cell, &the_int)) - return sqlite3_bind_int(stmt, i, the_int); - - if(enif_get_int64(env, cell, &the_long_int)) - return sqlite3_bind_int64(stmt, i, the_long_int); - - if(enif_get_double(env, cell, &the_double)) - return sqlite3_bind_double(stmt, i, the_double); - - if(enif_get_atom(env, cell, the_atom, sizeof(the_atom), ERL_NIF_LATIN1)) { - if(strncmp("undefined", the_atom, strlen("undefined")) == 0) { - return sqlite3_bind_null(stmt, i); - } - - if(strncmp("null", the_atom, strlen("null")) == 0) { - return sqlite3_bind_null(stmt, i); - } - - return sqlite3_bind_text(stmt, i, the_atom, strlen(the_atom), SQLITE_TRANSIENT); - } - - /* Bind as text assume it is utf-8 encoded text */ - if(enif_inspect_iolist_as_binary(env, cell, &the_blob)) { - return sqlite3_bind_text(stmt, i, (char *) the_blob.data, the_blob.size, SQLITE_TRANSIENT); - } - - /* Check for blob tuple */ - if(enif_get_tuple(env, cell, &arity, &tuple)) { - if(arity != 2) - return -1; - - /* length 2! */ - if(enif_get_atom(env, tuple[0], the_atom, sizeof(the_atom), ERL_NIF_LATIN1)) { - /* its a blob... */ - if(0 == strncmp("blob", the_atom, strlen("blob"))) { - /* with a iolist as argument */ - if(enif_inspect_iolist_as_binary(env, tuple[1], &the_blob)) { - /* kaboom... get the blob */ - return sqlite3_bind_blob(stmt, i, the_blob.data, the_blob.size, SQLITE_TRANSIENT); - } - } - } - } - - return -1; -} - -static ERL_NIF_TERM -do_bind(ErlNifEnv *env, sqlite3 *db, sqlite3_stmt *stmt, const ERL_NIF_TERM arg) -{ - int parameter_count = sqlite3_bind_parameter_count(stmt); - int i, is_list, r; - ERL_NIF_TERM list, head, tail; - unsigned int list_length; - - is_list = enif_get_list_length(env, arg, &list_length); - if(!is_list) - return make_error_tuple(env, "bad_arg_list"); - if(parameter_count != list_length) - return make_error_tuple(env, "args_wrong_length"); - - sqlite3_reset(stmt); - - list = arg; - for(i=0; i < list_length; i++) { - enif_get_list_cell(env, list, &head, &tail); - r = bind_cell(env, head, stmt, i+1); - if(r == -1) - return make_error_tuple(env, "wrong_type"); - if(r != SQLITE_OK) - return make_sqlite3_error_tuple(env, r, db); - list = tail; - } - - return make_atom(env, "ok"); -} - -static ERL_NIF_TERM -do_get_autocommit(ErlNifEnv *env, esqlite_connection *conn) -{ - if(!conn->db) { - return make_error_tuple(env, "closed"); - } - - if(sqlite3_get_autocommit(conn->db) != 0) { - return make_atom(env, "true"); - } - - return make_atom(env, "false"); } static ERL_NIF_TERM @@ -567,7 +137,6 @@ make_binary(ErlNifEnv *env, const void *bytes, unsigned int size) ERL_NIF_TERM term; if(!enif_alloc_binary(size, &blob)) { - /* TODO: fix this */ return make_atom(env, "error"); } @@ -584,470 +153,21 @@ make_cell(ErlNifEnv *env, sqlite3_stmt *statement, unsigned int i) int type = sqlite3_column_type(statement, i); switch(type) { + case SQLITE_NULL: + return make_atom(env, "undefined"); case SQLITE_INTEGER: return enif_make_int64(env, sqlite3_column_int64(statement, i)); case SQLITE_FLOAT: return enif_make_double(env, sqlite3_column_double(statement, i)); case SQLITE_BLOB: - return enif_make_tuple2(env, make_atom(env, "blob"), - make_binary(env, sqlite3_column_blob(statement, i), - sqlite3_column_bytes(statement, i))); - case SQLITE_NULL: - return make_atom(env, "undefined"); + return make_binary(env, sqlite3_column_blob(statement, i), + sqlite3_column_bytes(statement, i)); case SQLITE_TEXT: return make_binary(env, sqlite3_column_text(statement, i), sqlite3_column_bytes(statement, i)); - default: - return make_atom(env, "should_not_happen"); - } -} - -static ERL_NIF_TERM -make_row(ErlNifEnv *env, sqlite3_stmt *statement, ERL_NIF_TERM *array, int size) -{ - if(!array) - return make_error_tuple(env, "no_memory"); - - for(int i = 0; i < size; i++) - array[i] = make_cell(env, statement, i); - - return enif_make_tuple_from_array(env, array, size); -} - -static ERL_NIF_TERM -do_multi_step(ErlNifEnv *env, sqlite3 *db, sqlite3_stmt *stmt, const ERL_NIF_TERM arg) -{ - ERL_NIF_TERM status; - ERL_NIF_TERM rows = enif_make_list_from_array(env, NULL, 0); - ERL_NIF_TERM *rowBuffer = NULL; - int rowBufferSize = 0; - - int chunk_size = 0; - enif_get_int(env, arg, &chunk_size); - - int rc = sqlite3_step(stmt); - while (rc == SQLITE_ROW && chunk_size-- > 0) - { - if (!rowBufferSize) - rowBufferSize = sqlite3_column_count(stmt); - if (rowBuffer == NULL) - rowBuffer = (ERL_NIF_TERM *) enif_alloc(sizeof(ERL_NIF_TERM)*rowBufferSize); - - rows = enif_make_list_cell(env, make_row(env, stmt, rowBuffer, rowBufferSize), rows); - - if (chunk_size > 0) - rc = sqlite3_step(stmt); } - switch(rc) { - case SQLITE_ROW: - status = make_atom(env, "rows"); - break; - case SQLITE_BUSY: - status = make_atom(env, "$busy"); - break; - case SQLITE_DONE: - /* - * Automatically reset the statement after a done so - * column_names will work after the statement is done. - * - * Not resetting the statement can lead to vm crashes. - */ - sqlite3_reset(stmt); - status = make_atom(env, "$done"); - break; - default: - /* We use prepare_v2, so any error code can be returned. */ - return make_sqlite3_error_tuple(env, rc, db); - } - - enif_free(rowBuffer); - return enif_make_tuple2(env, status, rows); -} - -static ERL_NIF_TERM -do_reset(ErlNifEnv *env, sqlite3 *db, sqlite3_stmt *stmt) -{ - int rc = sqlite3_reset(stmt); - if(rc != SQLITE_OK) - return make_sqlite3_error_tuple(env, rc, db); - - return make_atom(env, "ok"); -} - -static ERL_NIF_TERM -do_column_names(ErlNifEnv *env, sqlite3_stmt *stmt) -{ - int i, size; - const char *name; - ERL_NIF_TERM *array; - ERL_NIF_TERM column_names; - - size = sqlite3_column_count(stmt); - if(size == 0) - return enif_make_tuple(env, 0); - else if(size < 0) - return make_error_tuple(env, "invalid_column_count"); - - array = (ERL_NIF_TERM *) enif_alloc(sizeof(ERL_NIF_TERM) * size); - if(!array) - return make_error_tuple(env, "no_memory"); - - for(i = 0; i < size; i++) { - name = sqlite3_column_name(stmt, i); - if(name == NULL) { - enif_free(array); - return make_error_tuple(env, "sqlite3_malloc_failure"); - } - - array[i] = make_atom(env, name); - } - - column_names = enif_make_tuple_from_array(env, array, size); - enif_free(array); - return column_names; -} - -static ERL_NIF_TERM -do_column_types(ErlNifEnv *env, sqlite3_stmt *stmt) -{ - int i, size; - const char *type; - ERL_NIF_TERM *array; - ERL_NIF_TERM column_types; - - size = sqlite3_column_count(stmt); - if(size == 0) - return enif_make_tuple(env, 0); - else if(size < 0) - return make_error_tuple(env, "invalid_column_count"); - - array = (ERL_NIF_TERM *) enif_alloc(sizeof(ERL_NIF_TERM) * size); - if(!array) - return make_error_tuple(env, "no_memory"); - - for(i = 0; i < size; i++) { - type = sqlite3_column_decltype(stmt, i); - if(type == NULL) { - type = "nil"; - } - - array[i] = make_atom(env, type); - } - - column_types = enif_make_tuple_from_array(env, array, size); - enif_free(array); - return column_types; -} - -static ERL_NIF_TERM -do_backup_init(ErlNifEnv *env, sqlite3 *db, const ERL_NIF_TERM arg) -{ - int tuple_arity; - const ERL_NIF_TERM *elements; - sqlite3_backup *backup; - unsigned int size; - char dst_name[MAX_SQLITE_NAME_LENGTH]; - char src_name[MAX_SQLITE_NAME_LENGTH]; - esqlite_connection *src; - esqlite_backup *esqlite_backup; - ERL_NIF_TERM erl_backup_term; - - if(db == NULL) { - return make_error_tuple(env, "dst_closed"); - } - - if(!enif_get_tuple(env, arg, &tuple_arity, &elements)) { - return make_error_tuple(env, "no_tuple"); - } - if(tuple_arity != 3) { - return make_error_tuple(env, "invalid_tuple"); - } - - size = enif_get_string(env, elements[0], dst_name, MAX_PATHNAME, ERL_NIF_LATIN1); - if(size <= 0) - return make_error_tuple(env, "invalid_dst_name"); - - if(!enif_get_resource(env, elements[1], esqlite_connection_type, (void **) &src)) { - return make_error_tuple(env, "invalid_src_db"); - } - if(!src->db) { - return make_error_tuple(env, "src_closed"); - } - - size = enif_get_string(env, elements[2], src_name, MAX_PATHNAME, ERL_NIF_LATIN1); - if(size <= 0) - return make_error_tuple(env, "invalid_src_name"); - - backup = sqlite3_backup_init(db, dst_name, src->db, src_name); - if(backup == NULL) { - return make_sqlite3_error_tuple(env, sqlite3_errcode(db), db); - } - - esqlite_backup = enif_alloc_resource(esqlite_backup_type, sizeof(esqlite_backup)); - if(!esqlite_backup) { - // Release backup resouces - (void) sqlite3_backup_finish(backup); - return make_error_tuple(env, "no_memory"); - } - - esqlite_backup->backup = backup; - erl_backup_term = enif_make_resource(env, esqlite_backup); - enif_release_resource(esqlite_backup); - - return make_ok_tuple(env, erl_backup_term); -} - -static ERL_NIF_TERM -do_backup_step(ErlNifEnv *env, sqlite3 *db, const ERL_NIF_TERM arg) -{ - int tuple_arity; - const ERL_NIF_TERM *elements; - esqlite_backup *esqlite_backup; - int n_page = 0; - int rc; - - if(db == NULL) { - return make_error_tuple(env, "closed"); - } - - if(!enif_get_tuple(env, arg, &tuple_arity, &elements)) { - return make_error_tuple(env, "no_tuple"); - } - if(tuple_arity != 2) { - return make_error_tuple(env, "invalid_tuple"); - } - - if(!enif_get_resource(env, elements[0], esqlite_backup_type, (void **) &esqlite_backup)) { - return make_error_tuple(env, "invalid"); - } - if(!esqlite_backup->backup) { - return make_error_tuple(env, "backup"); - } - - if(!enif_get_int(env, elements[1], &n_page)) { - return make_error_tuple(env, "n_page"); - } - - rc = sqlite3_backup_step(esqlite_backup->backup, n_page); - if(rc == SQLITE_DONE) { - return make_atom(env, "done"); - } - - if(rc != SQLITE_OK) { - return make_sqlite3_error_tuple(env, rc, db); - } - - return make_atom(env, "ok"); -} - -static ERL_NIF_TERM -do_backup_remaining(ErlNifEnv *env, const ERL_NIF_TERM arg) -{ - esqlite_backup *esqlite_backup; - int remaining; - ERL_NIF_TERM remaining_term; - - if(!enif_get_resource(env, arg, esqlite_backup_type, (void **) &esqlite_backup)) { - return make_error_tuple(env, "invalid"); - } - - remaining = sqlite3_backup_remaining(esqlite_backup->backup); - remaining_term = enif_make_int64(env, remaining); - - return make_ok_tuple(env, remaining_term); -} - -static ERL_NIF_TERM -do_backup_pagecount(ErlNifEnv *env, const ERL_NIF_TERM arg) -{ - esqlite_backup *esqlite_backup; - int pagecount; - ERL_NIF_TERM pagecount_term; - - if(!enif_get_resource(env, arg, esqlite_backup_type, (void **) &esqlite_backup)) { - return make_error_tuple(env, "invalid"); - } - - pagecount = sqlite3_backup_pagecount(esqlite_backup->backup); - pagecount_term = enif_make_int64(env, pagecount); - - return make_ok_tuple(env, pagecount_term); -} - -static ERL_NIF_TERM -do_backup_finish(ErlNifEnv *env, const ERL_NIF_TERM arg) -{ - esqlite_backup *esqlite_backup; - - if(!enif_get_resource(env, arg, esqlite_backup_type, (void **) &esqlite_backup)) { - return make_error_tuple(env, "invalid"); - } - - if(esqlite_backup->backup) { - (void) sqlite3_backup_finish(esqlite_backup->backup); - esqlite_backup->backup = NULL; - } - - return make_atom(env, "ok"); -} - -static ERL_NIF_TERM -do_close(ErlNifEnv *env, esqlite_connection *conn, const ERL_NIF_TERM arg) -{ - int rc; - - rc = sqlite3_close_v2(conn->db); - if(rc != SQLITE_OK) - return make_sqlite3_error_tuple(env, rc, conn->db); - - conn->db = NULL; - return make_atom(env, "ok"); -} - -static ERL_NIF_TERM -evaluate_command(esqlite_command *cmd, esqlite_connection *conn) -{ - esqlite_statement *stmt = NULL; - - if(cmd->stmt) { - if(!enif_get_resource(cmd->env, cmd->stmt, esqlite_statement_type, (void **) &stmt)) { - return make_error_tuple(cmd->env, "invalid_statement"); - } - } - - switch(cmd->type) { - case cmd_open: - return do_open(cmd->env, conn, cmd->arg); - case cmd_update_hook_set: - return do_set_update_hook(cmd->env, conn, cmd->arg); - case cmd_exec: - return do_exec(cmd->env, conn, cmd->arg); - case cmd_changes: - return do_changes(cmd->env, conn, cmd->arg); - case cmd_prepare: - return do_prepare(cmd->env, conn, cmd->arg); - case cmd_multi_step: - return do_multi_step(cmd->env, conn->db, stmt->statement, cmd->arg); - case cmd_reset: - return do_reset(cmd->env, conn->db, stmt->statement); - case cmd_bind: - return do_bind(cmd->env, conn->db, stmt->statement, cmd->arg); - case cmd_column_names: - return do_column_names(cmd->env, stmt->statement); - case cmd_column_types: - return do_column_types(cmd->env, stmt->statement); - case cmd_backup_init: - return do_backup_init(cmd->env, conn->db, cmd->arg); - case cmd_backup_step: - return do_backup_step(cmd->env, conn->db, cmd->arg); - case cmd_backup_remaining: - return do_backup_remaining(cmd->env, cmd->arg); - case cmd_backup_pagecount: - return do_backup_pagecount(cmd->env, cmd->arg); - case cmd_backup_finish: - return do_backup_finish(cmd->env, cmd->arg); - case cmd_close: - return do_close(cmd->env, conn, cmd->arg); - case cmd_last_insert_rowid: - return do_last_insert_rowid(cmd->env, conn); - case cmd_insert: - return do_insert(cmd->env, conn, cmd->arg); - case cmd_get_autocommit: - return do_get_autocommit(cmd->env, conn); - case cmd_unknown: // not handled - case cmd_stop: // not handled here - case cmd_notification: // not handled here. - break; - } - - return make_error_tuple(cmd->env, "invalid_command"); -} - -static ERL_NIF_TERM -push_command(ErlNifEnv *env, esqlite_connection *conn, esqlite_command *cmd) { - if(!queue_push(conn->commands, cmd)) - return make_error_tuple(env, "command_push_failed"); - - return make_atom(env, "ok"); -} - -static ERL_NIF_TERM -make_answer(esqlite_command *cmd, ERL_NIF_TERM answer) -{ - return enif_make_tuple3(cmd->env, atom_esqlite3, cmd->ref, answer); -} - -static void * -esqlite_connection_run(void *arg) -{ - esqlite_connection *db = (esqlite_connection *) arg; - esqlite_command *cmd; - int continue_running = 1; - - while(continue_running) { - cmd = queue_pop(db->commands); - - if(cmd->type == cmd_stop) { - continue_running = 0; - } else if(cmd->type == cmd_notification) { - enif_send(NULL, &db->notification_pid, cmd->env, cmd->arg); - } else { - enif_send(NULL, &cmd->pid, cmd->env, make_answer(cmd, evaluate_command(cmd, db))); - } - - command_destroy(cmd); - } - - return NULL; -} - -/* - * Start the processing thread - */ -static ERL_NIF_TERM -esqlite_start(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite_connection *conn; - ERL_NIF_TERM db_conn; - - /* Initialize the resource */ - conn = enif_alloc_resource(esqlite_connection_type, sizeof(esqlite_connection)); - if(!conn) - return make_error_tuple(env, "no_memory"); - - conn->db = NULL; - - /* Create command queue */ - conn->commands = queue_create(); - if(!conn->commands) { - enif_release_resource(conn); - return make_error_tuple(env, "command_queue_create_failed"); - } - - /* Start command processing thread */ - conn->opts = enif_thread_opts_create("esqlite_thread_opts"); - if(conn->opts == NULL) { - return make_error_tuple(env, "thread_opts_failed"); - } - - /* Configure a fixed sized stack, windows uses a default of 1Mb, which - * can be too small for complex queries. Linux and MacOS uses a stack of about - * 8Mb, which is a bit too large, since the largest sqlite query is about 1Mb - * in size. The stack size depends on that. A value of 3Mb is about right. - */ - conn->opts->suggested_stack_size = 3072; - - if(enif_thread_create("esqlite_connection", &conn->tid, esqlite_connection_run, conn, conn->opts) != 0) { - enif_thread_opts_destroy(conn->opts); - enif_release_resource(conn); - return make_error_tuple(env, "thread_create_failed"); - } - - db_conn = enif_make_resource(env, conn); - enif_release_resource(conn); - - return make_ok_tuple(env, db_conn); + return enif_raise_exception(env, make_atom(env, "internal_error")); } /* @@ -1056,624 +176,48 @@ esqlite_start(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) static ERL_NIF_TERM esqlite_open(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { - esqlite_connection *db; - esqlite_command *cmd = NULL; - ErlNifPid pid; + esqlite3 *conn; + char filename[MAX_PATHNAME]; - if(argc != 4) + if(argc != 1) { return enif_make_badarg(env); - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &db)) - return enif_make_badarg(env); - if(!enif_is_ref(env, argv[1])) - return make_error_tuple(env, "invalid_ref"); - if(!enif_get_local_pid(env, argv[2], &pid)) - return make_error_tuple(env, "invalid_pid"); - - if(!sqlite3_threadsafe()) - return make_error_tuple(env, "sqlite3 not thread safe."); - - /* Note, no check is made for the type of the argument */ - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); - - cmd->type = cmd_open; - cmd->ref = enif_make_copy(cmd->env, argv[1]); - cmd->pid = pid; - cmd->arg = enif_make_copy(cmd->env, argv[3]); - - return push_command(env, db, cmd); -} - -static ERL_NIF_TERM -set_update_hook(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite_connection *db; - esqlite_command *cmd = NULL; - ErlNifPid pid; - - if(argc != 4) - return enif_make_badarg(env); - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &db)) - return enif_make_badarg(env); - if(!enif_is_ref(env, argv[1])) - return make_error_tuple(env, "invalid_ref"); - if(!enif_get_local_pid(env, argv[2], &pid)) - return make_error_tuple(env, "invalid_pid"); - - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); - - /* command */ - cmd->type = cmd_update_hook_set; - cmd->ref = enif_make_copy(cmd->env, argv[1]); - cmd->pid = pid; - cmd->arg = enif_make_copy(cmd->env, argv[3]); - - return push_command(env, db, cmd); -} - -/* - * Execute the sql statement - */ -static ERL_NIF_TERM -esqlite_exec(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite_connection *db; - esqlite_command *cmd = NULL; - ErlNifPid pid; - - if(argc != 4) - return enif_make_badarg(env); - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &db)) - return enif_make_badarg(env); - if(!enif_is_ref(env, argv[1])) - return make_error_tuple(env, "invalid_ref"); - if(!enif_get_local_pid(env, argv[2], &pid)) - return make_error_tuple(env, "invalid_pid"); - - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); - - /* command */ - cmd->type = cmd_exec; - cmd->ref = enif_make_copy(cmd->env, argv[1]); - cmd->pid = pid; - cmd->arg = enif_make_copy(cmd->env, argv[3]); - - return push_command(env, db, cmd); -} - -/* - * Count the nr of changes of last statement - */ -static ERL_NIF_TERM -esqlite_changes(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite_connection *db; - esqlite_command *cmd = NULL; - ErlNifPid pid; - - if(argc != 3) - return enif_make_badarg(env); - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &db)) - return enif_make_badarg(env); - if(!enif_is_ref(env, argv[1])) - return make_error_tuple(env, "invalid_ref"); - if(!enif_get_local_pid(env, argv[2], &pid)) - return make_error_tuple(env, "invalid_pid"); - - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); - - /* command */ - cmd->type = cmd_changes; - cmd->ref = enif_make_copy(cmd->env, argv[1]); - cmd->pid = pid; - - return push_command(env, db, cmd); -} - -static ERL_NIF_TERM -esqlite_insert(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite_connection *db; - esqlite_command *cmd = NULL; - ErlNifPid pid; - - if(argc != 4) - return enif_make_badarg(env); - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &db)) - return enif_make_badarg(env); - if(!enif_is_ref(env, argv[1])) - return make_error_tuple(env, "invalid_ref"); - if(!enif_get_local_pid(env, argv[2], &pid)) - return make_error_tuple(env, "invalid_pid"); - - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); - - /* command */ - cmd->type = cmd_insert; - cmd->ref = enif_make_copy(cmd->env, argv[1]); - cmd->pid = pid; - cmd->arg = enif_make_copy(cmd->env, argv[3]); - - return push_command(env, db, cmd); -} - -static ERL_NIF_TERM -esqlite_last_insert_rowid(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite_connection *db; - esqlite_command *cmd = NULL; - ErlNifPid pid; - - if(argc != 3) - return enif_make_badarg(env); - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &db)) - return enif_make_badarg(env); - if(!enif_is_ref(env, argv[1])) - return make_error_tuple(env, "invalid_ref"); - if(!enif_get_local_pid(env, argv[2], &pid)) - return make_error_tuple(env, "invalid_pid"); - - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); - - /* command */ - cmd->type = cmd_last_insert_rowid; - cmd->ref = enif_make_copy(cmd->env, argv[1]); - cmd->pid = pid; - - return push_command(env, db, cmd); -} - -static ERL_NIF_TERM -esqlite_get_autocommit(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite_connection *db; - esqlite_command *cmd = NULL; - ErlNifPid pid; - - if(argc != 3) - return enif_make_badarg(env); - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &db)) - return enif_make_badarg(env); - if(!enif_is_ref(env, argv[1])) - return make_error_tuple(env, "invalid_ref"); - if(!enif_get_local_pid(env, argv[2], &pid)) - return make_error_tuple(env, "invalid_pid"); - - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); - - /* command */ - cmd->type = cmd_get_autocommit; - cmd->ref = enif_make_copy(cmd->env, argv[1]); - cmd->pid = pid; - - return push_command(env, db, cmd); -} - -/* - * Prepare the sql statement - */ -static ERL_NIF_TERM -esqlite_prepare(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite_connection *conn; - esqlite_command *cmd = NULL; - ErlNifPid pid; - - if(argc != 4) - return enif_make_badarg(env); - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &conn)) - return enif_make_badarg(env); - if(!enif_is_ref(env, argv[1])) - return make_error_tuple(env, "invalid_ref"); - if(!enif_get_local_pid(env, argv[2], &pid)) - return make_error_tuple(env, "invalid_pid"); - - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); - - cmd->type = cmd_prepare; - cmd->ref = enif_make_copy(cmd->env, argv[1]); - cmd->pid = pid; - cmd->arg = enif_make_copy(cmd->env, argv[3]); - - return push_command(env, conn, cmd); -} - -/* - * Bind a variable to a prepared statement - */ -static ERL_NIF_TERM -esqlite_bind(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite_connection *conn; - esqlite_statement *stmt; - esqlite_command *cmd = NULL; - ErlNifPid pid; - - if(argc != 5) - return enif_make_badarg(env); - - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &conn)) - return enif_make_badarg(env); - if(!enif_get_resource(env, argv[1], esqlite_statement_type, (void **) &stmt)) - return enif_make_badarg(env); - if(!enif_is_ref(env, argv[2])) - return make_error_tuple(env, "invalid_ref"); - if(!enif_get_local_pid(env, argv[3], &pid)) - return make_error_tuple(env, "invalid_pid"); - - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); - - cmd->type = cmd_bind; - cmd->ref = enif_make_copy(cmd->env, argv[2]); - cmd->pid = pid; - cmd->stmt = enif_make_copy(cmd->env, argv[1]); - cmd->arg = enif_make_copy(cmd->env, argv[4]); - - return push_command(env, conn, cmd); -} - -/* - * Multi step to a prepared statement - */ -static ERL_NIF_TERM -esqlite_multi_step(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite_connection *conn; - esqlite_statement *stmt; - esqlite_command *cmd = NULL; - ErlNifPid pid; - int chunk_size = 0; - - if(argc != 5) - return enif_make_badarg(env); - - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &conn)) - return enif_make_badarg(env); - - if(!enif_get_resource(env, argv[1], esqlite_statement_type, (void **) &stmt)) - return enif_make_badarg(env); - - if(!enif_get_int(env, argv[2], &chunk_size)) - return make_error_tuple(env, "invalid_chunk_size"); - - if(!enif_is_ref(env, argv[3])) - return make_error_tuple(env, "invalid_ref"); - - if(!enif_get_local_pid(env, argv[4], &pid)) - return make_error_tuple(env, "invalid_pid"); - - if(!stmt->statement) - return make_error_tuple(env, "no_prepared_statement"); - - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); - - cmd->type = cmd_multi_step; - cmd->ref = enif_make_copy(cmd->env, argv[3]); - cmd->pid = pid; - cmd->stmt = enif_make_copy(cmd->env, argv[1]); - cmd->arg = enif_make_copy(cmd->env, argv[2]); - - return push_command(env, conn, cmd); -} - -/* - * Reset a prepared statement to its initial state - */ -static ERL_NIF_TERM -esqlite_reset(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite_connection *conn; - esqlite_statement *stmt; - esqlite_command *cmd = NULL; - ErlNifPid pid; - - if(argc != 4) - return enif_make_badarg(env); - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &conn)) - return enif_make_badarg(env); - if(!enif_get_resource(env, argv[1], esqlite_statement_type, (void **) &stmt)) - return enif_make_badarg(env); - if(!enif_is_ref(env, argv[2])) - return make_error_tuple(env, "invalid_ref"); - if(!enif_get_local_pid(env, argv[3], &pid)) - return make_error_tuple(env, "invalid_pid"); - if(!stmt->statement) - return make_error_tuple(env, "no_prepared_statement"); - - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); - - cmd->type = cmd_reset; - cmd->ref = enif_make_copy(cmd->env, argv[2]); - cmd->pid = pid; - cmd->stmt = enif_make_copy(cmd->env, argv[1]); - - return push_command(env, conn, cmd); -} - -/* - * Get the column names of the prepared statement. - */ -static ERL_NIF_TERM -esqlite_column_names(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite_connection *conn; - esqlite_statement *stmt; - esqlite_command *cmd = NULL; - ErlNifPid pid; - - if(argc != 4) - return enif_make_badarg(env); - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &conn)) - return enif_make_badarg(env); - if(!enif_get_resource(env, argv[1], esqlite_statement_type, (void **) &stmt)) - return enif_make_badarg(env); - if(!enif_is_ref(env, argv[2])) - return make_error_tuple(env, "invalid_ref"); - if(!enif_get_local_pid(env, argv[3], &pid)) - return make_error_tuple(env, "invalid_pid"); - if(!stmt->statement) - return make_error_tuple(env, "no_prepared_statement"); - - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); - - cmd->type = cmd_column_names; - cmd->ref = enif_make_copy(cmd->env, argv[2]); - cmd->pid = pid; - cmd->stmt = enif_make_copy(cmd->env, argv[1]); - - return push_command(env, conn, cmd); -} - -/* - * Get the column types of the prepared statement. - */ -static ERL_NIF_TERM -esqlite_column_types(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite_connection *conn; - esqlite_statement *stmt; - esqlite_command *cmd = NULL; - ErlNifPid pid; - - if(argc != 4) - return enif_make_badarg(env); - - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &conn)) - return enif_make_badarg(env); - if(!enif_get_resource(env, argv[1], esqlite_statement_type, (void **) &stmt)) - return enif_make_badarg(env); - if(!enif_is_ref(env, argv[2])) - return make_error_tuple(env, "invalid_ref"); - if(!enif_get_local_pid(env, argv[3], &pid)) - return make_error_tuple(env, "invalid_pid"); - - if(!stmt->statement) - return make_error_tuple(env, "no_prepared_statement"); - - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); - - cmd->type = cmd_column_types; - cmd->ref = enif_make_copy(cmd->env, argv[2]); - cmd->pid = pid; - cmd->stmt = enif_make_copy(cmd->env, argv[1]); - - return push_command(env, conn, cmd); -} - -/* - * Backup functions - * - */ - - -static ERL_NIF_TERM -esqlite_backup_init(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite_connection *destination; - esqlite_command *cmd = NULL; - ErlNifPid pid; - - if(argc != 6) - return enif_make_badarg(env); - - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &destination)) - return enif_make_badarg(env); - // 1 destination name - // 2 source connection with database - // 3 source name - if(!enif_is_ref(env, argv[4])) - return make_error_tuple(env, "invalid_ref"); - if(!enif_get_local_pid(env, argv[5], &pid)) - return make_error_tuple(env, "invalid_pid"); - - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); - - cmd->type = cmd_backup_init; - cmd->ref = enif_make_copy(cmd->env, argv[4]); - cmd->pid = pid; - cmd->arg = enif_make_tuple3(cmd->env, argv[1], argv[2], argv[3]); - - /* Use the connection of the destination database */ - return push_command(env, destination, cmd); -} - -static ERL_NIF_TERM -esqlite_backup_finish(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite_connection *conn; - esqlite_backup *backup; - esqlite_command *cmd = NULL; - ErlNifPid pid; - - if(argc != 4) - return enif_make_badarg(env); - - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &conn)) - return enif_make_badarg(env); - - if(!enif_is_ref(env, argv[2])) - return make_error_tuple(env, "invalid_ref"); - if(!enif_get_local_pid(env, argv[3], &pid)) - return make_error_tuple(env, "invalid_pid"); - - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); - - cmd->type = cmd_backup_finish; - cmd->ref = enif_make_copy(cmd->env, argv[2]); - cmd->pid = pid; - cmd->arg = enif_make_copy(cmd->env, argv[1]); - - return push_command(env, conn, cmd); -} - -static ERL_NIF_TERM -esqlite_backup_step(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite_connection *conn; - esqlite_backup *backup; - esqlite_command *cmd = NULL; - ErlNifPid pid; - - if(argc != 5) - return enif_make_badarg(env); - - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &conn)) - return enif_make_badarg(env); - // 1 backup - if(!enif_is_number(env, argv[2])) - return make_error_tuple(env, "invalid_count"); - if(!enif_is_ref(env, argv[3])) - return make_error_tuple(env, "invalid_ref"); - if(!enif_get_local_pid(env, argv[4], &pid)) - return make_error_tuple(env, "invalid_pid"); - - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); - - cmd->type = cmd_backup_step; - cmd->ref = enif_make_copy(cmd->env, argv[3]); - cmd->pid = pid; - cmd->arg = enif_make_tuple2(cmd->env, argv[1], argv[2]); - - return push_command(env, conn, cmd); -} - -/* - * Get the remaining pagecount of the backup. - */ - -static ERL_NIF_TERM -esqlite_backup_remaining(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite_connection *conn; - esqlite_backup *backup; - esqlite_command *cmd = NULL; - ErlNifPid pid; - - if(argc != 4) - return enif_make_badarg(env); - - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &conn)) - return enif_make_badarg(env); - // backup 1 - if(!enif_is_ref(env, argv[2])) - return make_error_tuple(env, "invalid_ref"); - if(!enif_get_local_pid(env, argv[3], &pid)) - return make_error_tuple(env, "invalid_pid"); - - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); - - cmd->type = cmd_backup_remaining; - cmd->ref = enif_make_copy(cmd->env, argv[2]); - cmd->pid = pid; - cmd->arg = enif_make_copy(cmd->env, argv[1]); - - return push_command(env, conn, cmd); -} - -/* - * Get the total pagecount of the backup - */ - -static ERL_NIF_TERM -esqlite_backup_pagecount(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite_connection *conn; - esqlite_backup *backup; - esqlite_command *cmd = NULL; - ErlNifPid pid; - - if(argc != 4) - return enif_make_badarg(env); - - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &conn)) - return enif_make_badarg(env); - // backup 1 - if(!enif_is_ref(env, argv[2])) - return make_error_tuple(env, "invalid_ref"); - if(!enif_get_local_pid(env, argv[3], &pid)) - return make_error_tuple(env, "invalid_pid"); - - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); - - cmd->type = cmd_backup_pagecount; - cmd->ref = enif_make_copy(cmd->env, argv[2]); - cmd->pid = pid; - cmd->arg = enif_make_copy(cmd->env, argv[1]); - - return push_command(env, conn, cmd); -} - -/* - * Interrupt currently active query. - */ - -static ERL_NIF_TERM -esqlite_interrupt(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite_connection *conn; - - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &conn)) - return enif_make_badarg(env); - - esqlite_connection *db = (esqlite_connection *) conn; - sqlite3_interrupt(db->db); - - return enif_make_atom(env, "ok"); + } + + if(!sqlite3_threadsafe()) { + return enif_raise_exception(env, make_atom(env, "not_thread_safe")); + } + + int size = enif_get_string(env, argv[0], filename, MAX_PATHNAME, ERL_NIF_LATIN1); + if(size <= 0) { + return make_error_tuple(env, "invalid_filename"); + } + + /* Initialize the resource */ + conn = enif_alloc_resource(esqlite3_type, sizeof(esqlite3)); + if(!conn) { + return enif_raise_exception(env, make_atom(env, "no_memory")); + } + + /* Open the database. + */ + int rc = sqlite3_open(filename, &conn->db); + if(rc != SQLITE_OK) { + ERL_NIF_TERM error = make_sqlite3_error_tuple(env, rc); + sqlite3_close_v2(conn->db); + enif_release_resource(conn); + return error; + } + + /* Set a standard busy timeout of 2 seconds */ + sqlite3_busy_timeout(conn->db, 2000); + + /* Turn on extended error codes for better error reporting */ + sqlite3_extended_result_codes(conn->db, 1); + + ERL_NIF_TERM db_conn = enif_make_resource(env, conn); + enif_release_resource(conn); + + return make_ok_tuple(env, db_conn); } /* @@ -1682,26 +226,913 @@ esqlite_interrupt(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) static ERL_NIF_TERM esqlite_close(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { - esqlite_connection *conn; - esqlite_command *cmd = NULL; - ErlNifPid pid; + esqlite3 *conn; + int rc; - if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &conn)) + if(argc != 1) { return enif_make_badarg(env); - if(!enif_is_ref(env, argv[1])) - return make_error_tuple(env, "invalid_ref"); - if(!enif_get_local_pid(env, argv[2], &pid)) - return make_error_tuple(env, "invalid_pid"); + } - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); + if(!enif_get_resource(env, argv[0], esqlite3_type, (void **) &conn)) { + return enif_make_badarg(env); + } - cmd->type = cmd_close; - cmd->ref = enif_make_copy(cmd->env, argv[1]); - cmd->pid = pid; + if(conn->db == NULL) { + return make_atom(env, "ok"); + } - return push_command(env, conn, cmd); + /* + * close_v2 is not guaranteed to close the connection. There could be + * an open transaction. Rollback this transaction first, before trying + * to close the connection. + */ + if(!sqlite3_get_autocommit(conn->db)) { + rc = sqlite3_exec(conn->db, "ROLLBACK;", NULL, NULL, NULL); + if (rc != SQLITE_OK) { + return make_sqlite3_error_tuple(env, rc); + } + } + + rc = sqlite3_close_v2(conn->db); + if(rc != SQLITE_OK) { + return make_sqlite3_error_tuple(env, rc); + } + + conn->db = NULL; + return make_atom(env, "ok"); +} + +/* + * Return a description of the last occurred error. + */ +static ERL_NIF_TERM +esqlite_error_info(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3 *conn; + + if(argc != 1) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_type, (void **) &conn)) { + return enif_make_badarg(env); + } + + if(conn->db == NULL) { + enif_make_badarg(env); + } + + int code = sqlite3_errcode(conn->db); + int extended_code = sqlite3_extended_errcode(conn->db); + const char *errstr = sqlite3_errstr(extended_code); + const char *errmsg = sqlite3_errmsg(conn->db); + + ERL_NIF_TERM info = enif_make_new_map(env); + enif_make_map_put(env, info, make_atom(env, "errcode"), enif_make_int(env, code), &info); + enif_make_map_put(env, info, make_atom(env, "extended_errcode"), enif_make_int(env, extended_code), &info); + enif_make_map_put(env, info, make_atom(env, "errstr"), make_binary(env, errstr, strlen(errstr)), &info); + enif_make_map_put(env, info, make_atom(env, "errmsg"), make_binary(env, errmsg, strlen(errmsg)), &info); + enif_make_map_put(env, info, make_atom(env, "error_offset"), enif_make_int(env, sqlite3_error_offset(conn->db)), &info); + + return info; +} + +void +update_callback(void *arg, int sqlite_operation_type, char const *sqlite_database, char const *sqlite_table, sqlite3_int64 sqlite_rowid) +{ + esqlite3 *conn = (esqlite3 *)arg; + + if(conn == NULL) { + return; + } + + /* Create a message environment */ + ErlNifEnv *msg_env = enif_alloc_env(); + + ERL_NIF_TERM type; + + switch(sqlite_operation_type) { + case SQLITE_INSERT: + type = make_atom(msg_env, "insert"); + break; + case SQLITE_DELETE: + type = make_atom(msg_env, "delete"); + break; + case SQLITE_UPDATE: + type = make_atom(msg_env, "update"); + break; + default: + return; + } + ERL_NIF_TERM rowid = enif_make_int64(msg_env, sqlite_rowid); + ERL_NIF_TERM database = make_binary(msg_env, sqlite_database, strlen(sqlite_database)); + ERL_NIF_TERM table = make_binary(msg_env, sqlite_table, strlen(sqlite_table)); + + ERL_NIF_TERM msg = enif_make_tuple4(msg_env, type, database, table, rowid); + + if(!enif_send(NULL, &conn->update_hook_pid, msg_env, msg)) { + sqlite3_update_hook(conn->db, NULL, NULL); + } +} + + +static ERL_NIF_TERM +esqlite_set_update_hook(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3 *conn; + + if(argc != 2) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_type, (void **) &conn)) { + return enif_make_badarg(env); + } + + if(!conn->db) { + return enif_raise_exception(env, make_atom(env, "closed")); + } + + if(enif_is_atom(env, argv[1])) { + /* Reset the hook when an atom is passed */ + sqlite3_update_hook(conn->db, NULL, NULL); + } else { + if(!enif_get_local_pid(env, argv[1], &conn->update_hook_pid)) { + return enif_make_badarg(env); + } + + sqlite3_update_hook(conn->db, update_callback, conn); + } + + return make_atom(env, "ok"); +} + + +/* + * Exec a sql statement + */ +static ERL_NIF_TERM +esqlite_exec(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3 *conn; + ErlNifBinary bin; + int rc; + ERL_NIF_TERM eos = enif_make_int(env, 0); + + if(argc != 2) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_type, (void **) &conn)) { + return enif_make_badarg(env); + } + + if(!enif_inspect_iolist_as_binary(env, enif_make_list2(env, argv[1], eos), &bin)) { + return enif_make_badarg(env); + } + + rc = sqlite3_exec(conn->db, (char *) bin.data, NULL, NULL, NULL); + if(rc != SQLITE_OK) { + return make_sqlite3_error_tuple(env, rc); + } + + return make_atom(env, "ok"); +} + + +/* + * Prepare the sql statement + */ +static ERL_NIF_TERM +esqlite_prepare(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3 *conn; + + if(argc != 3) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_type, (void **) &conn)) { + return enif_make_badarg(env); + } + + ErlNifBinary bin; + esqlite3_stmt *stmt; + ERL_NIF_TERM esqlite_stmt; + const char *tail; + int rc; + ERL_NIF_TERM eos = enif_make_int(env, 0); + + if(!enif_inspect_iolist_as_binary(env, enif_make_list2(env, argv[1], eos), &bin)) { + return enif_make_badarg(env); + } + + unsigned int prep_flags; + if(!enif_get_uint(env, argv[2], &prep_flags)) { + return enif_make_badarg(env); + } + + stmt = enif_alloc_resource(esqlite3_stmt_type, sizeof(esqlite3_stmt)); + if(!stmt) { + return enif_raise_exception(env, make_atom(env, "no_memory")); + } + /* Keep a reference to the connection to prevent it from being garbage collected + * before the statement. + */ + enif_keep_resource((void *) conn); + stmt->connection = conn; + + rc = sqlite3_prepare_v3(conn->db, (char *) bin.data, bin.size, prep_flags, &(stmt->statement), &tail); + if(rc != SQLITE_OK) { + enif_release_resource(stmt); + return make_sqlite3_error_tuple(env, rc); + } + + stmt->column_count = sqlite3_column_count(stmt->statement); + + esqlite_stmt = enif_make_resource(env, stmt); + enif_release_resource(stmt); + + return make_ok_tuple(env, esqlite_stmt); +} + +/* + * Get the column names of the prepared statement. + */ +static ERL_NIF_TERM +esqlite_column_names(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3_stmt *stmt; + + if(argc != 1) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_stmt_type, (void **) &stmt)) { + return enif_make_badarg(env); + } + + if(!stmt->statement) { + return enif_raise_exception(env, make_atom(env, "no_prepared_statement")); + } + + ERL_NIF_TERM column_names = enif_make_list(env, 0); + + int size = sqlite3_column_count(stmt->statement); + if(size < 0) { + return enif_raise_exception(env, make_atom(env, "invalid_column_count")); + } + + for(int i=size; i-- > 0; ) { + const char *name = sqlite3_column_name(stmt->statement, i); + if(name == NULL) { + return enif_raise_exception(env, make_atom(env, "sqlite3_malloc_failure")); + } + + ERL_NIF_TERM ename = make_binary(env, name, strlen(name)); + column_names = enif_make_list_cell(env, ename, column_names); + } + + return column_names; +} + +/* + * Get the column's declared datatypes of a statement. + */ +static ERL_NIF_TERM +esqlite_column_decltypes(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3_stmt *stmt; + + if(argc != 1) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_stmt_type, (void **) &stmt)) { + return enif_make_badarg(env); + } + + if(!stmt->statement) { + return enif_raise_exception(env, make_atom(env, "no_prepared_statement")); + } + + ERL_NIF_TERM column_types = enif_make_list(env, 0); + + int size = sqlite3_column_count(stmt->statement); + if(size < 0) { + return enif_raise_exception(env, make_atom(env, "invalid_column_count")); + } + + for(int i=size; i-- > 0; ) { + ERL_NIF_TERM type_name; + + const char *type = sqlite3_column_decltype(stmt->statement, i); + + if(type == NULL) { + type_name = make_atom(env, "undefined"); + } else { + type_name = make_binary(env, type, strlen(type)); + } + + column_types = enif_make_list_cell(env, type_name, column_types); + } + + return column_types; +} + +static ERL_NIF_TERM +esqlite_bind_int(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3_stmt *stmt; + int index; + int value; + + if(argc != 3) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_stmt_type, (void **) &stmt)) { + return enif_make_badarg(env); + } + + if(!stmt->statement) { + return enif_raise_exception(env, make_atom(env, "no_prepared_statement")); + } + + if(!enif_get_int(env, argv[1], &index)) { + return enif_make_badarg(env); + } + + if(!enif_get_int(env, argv[2], &value)) { + return enif_make_badarg(env); + } + + int rc = sqlite3_bind_int(stmt->statement, index, value); + if(rc != SQLITE_OK) { + return make_sqlite3_error_tuple(env, rc); + } + + return make_atom(env, "ok"); +} + +static ERL_NIF_TERM +esqlite_bind_int64(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3_stmt *stmt; + int index; + ErlNifSInt64 value; + + if(argc != 3) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_stmt_type, (void **) &stmt)) { + return enif_make_badarg(env); + } + + if(!stmt->statement) { + return enif_raise_exception(env, make_atom(env, "no_prepared_statement")); + } + + if(!enif_get_int(env, argv[1], &index)) { + return enif_make_badarg(env); + } + + if(!enif_get_int64(env, argv[2], &value)) { + return enif_make_badarg(env); + } + + int rc = sqlite3_bind_int64(stmt->statement, index, value); + if(rc != SQLITE_OK) { + return make_sqlite3_error_tuple(env, rc); + } + + return make_atom(env, "ok"); +} + +static ERL_NIF_TERM +esqlite_bind_double(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3_stmt *stmt; + int index; + double value; + + if(argc != 3) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_stmt_type, (void **) &stmt)) { + return enif_make_badarg(env); + } + + if(!stmt->statement) { + return enif_raise_exception(env, make_atom(env, "no_prepared_statement")); + } + + if(!enif_get_int(env, argv[1], &index)) { + return enif_make_badarg(env); + } + + if(!enif_get_double(env, argv[2], &value)) { + return enif_make_badarg(env); + } + + int rc = sqlite3_bind_double(stmt->statement, index, value); + if(rc != SQLITE_OK) { + return make_sqlite3_error_tuple(env, rc); + } + + return make_atom(env, "ok"); +} + +static ERL_NIF_TERM +esqlite_bind_text(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3_stmt *stmt; + int index; + ErlNifBinary text; + + if(argc != 3) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_stmt_type, (void **) &stmt)) { + return enif_make_badarg(env); + } + + if(!stmt->statement) { + return enif_raise_exception(env, make_atom(env, "no_prepared_statement")); + } + + if(!enif_get_int(env, argv[1], &index)) { + return enif_make_badarg(env); + } + + if(!enif_inspect_iolist_as_binary(env, argv[2], &text)) { + return enif_make_badarg(env); + } + + /* + * Don't do any checks on the input data, sqlite handes all kinds of input. It is + * garbage-in, garbage-out. + * + */ + + int rc = sqlite3_bind_text64(stmt->statement, index, (char *) text.data, text.size, SQLITE_TRANSIENT, SQLITE_UTF8); + if(rc != SQLITE_OK) { + return make_sqlite3_error_tuple(env, rc); + } + + return make_atom(env, "ok"); +} + +static ERL_NIF_TERM +esqlite_bind_blob(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3_stmt *stmt; + int index; + ErlNifBinary blob; + + if(argc != 3) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_stmt_type, (void **) &stmt)) { + return enif_make_badarg(env); + } + + if(!stmt->statement) { + return enif_raise_exception(env, make_atom(env, "no_prepared_statement")); + } + + if(!enif_get_int(env, argv[1], &index)) { + return enif_make_badarg(env); + } + + if(!enif_inspect_iolist_as_binary(env, argv[2], &blob)) { + return enif_make_badarg(env); + } + + int rc = sqlite3_bind_blob64(stmt->statement, index, blob.data, blob.size, SQLITE_TRANSIENT); + if(rc != SQLITE_OK) { + return make_sqlite3_error_tuple(env, rc); + } + + return make_atom(env, "ok"); +} + +static ERL_NIF_TERM +esqlite_bind_null(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3_stmt *stmt; + int index; + + if(argc != 2) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_stmt_type, (void **) &stmt)) { + return enif_make_badarg(env); + } + + if(!stmt->statement) { + return enif_raise_exception(env, make_atom(env, "no_prepared_statement")); + } + + if(!enif_get_int(env, argv[1], &index)) { + return enif_make_badarg(env); + } + + int rc = sqlite3_bind_null(stmt->statement, index); + if(rc != SQLITE_OK) { + return make_sqlite3_error_tuple(env, rc); + } + + return make_atom(env, "ok"); +} + +static ERL_NIF_TERM +esqlite_step(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3_stmt *stmt; + + if(argc != 1) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_stmt_type, (void **) &stmt)) { + return enif_make_badarg(env); + } + + if(!stmt->statement) { + return enif_raise_exception(env, make_atom(env, "no_prepared_statement")); + } + + int rc = sqlite3_step(stmt->statement); + switch(rc) { + case SQLITE_ROW: + { + ERL_NIF_TERM row = enif_make_list(env, 0); + for(int i=stmt->column_count; i-- > 0; ) { + row = enif_make_list_cell(env, make_cell(env, stmt->statement, i), row); + } + return row; + } + case SQLITE_DONE: + /* + * Automatically reset the statement after a done so + * column_names will work after the statement is done. + * + * Not resetting the statement can lead to vm crashes. + */ + sqlite3_reset(stmt->statement); + return make_atom(env, "$done"); + case SQLITE_BUSY: + return make_atom(env, "$busy"); + } + + return make_sqlite3_error_tuple(env, rc); +} + +static ERL_NIF_TERM +esqlite_reset(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3_stmt *stmt; + + if(argc != 1) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_stmt_type, (void **) &stmt)) { + return enif_make_badarg(env); + } + + if(!stmt->statement) { + return enif_raise_exception(env, make_atom(env, "no_prepared_statement")); + } + + int rc = sqlite3_reset(stmt->statement); + if(rc != SQLITE_OK) { + return make_sqlite3_error_tuple(env, rc); + } + return make_atom(env, "ok"); +} + + +/* + * Backup functions + * + */ + +static ERL_NIF_TERM +esqlite_backup_init(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3 *destination; + ErlNifBinary destination_name; + esqlite3 *source; + ErlNifBinary source_name; + ERL_NIF_TERM eos = enif_make_int(env, 0); + + if(argc != 4) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_type, (void **) &destination)) { + return enif_make_badarg(env); + } + + if(!enif_inspect_iolist_as_binary(env, enif_make_list2(env, argv[1], eos), &destination_name)) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[2], esqlite3_type, (void **) &source)) { + return enif_make_badarg(env); + } + + if(!enif_inspect_iolist_as_binary(env, enif_make_list2(env, argv[3], eos), &source_name)) { + return enif_make_badarg(env); + } + + sqlite3_backup *backup = sqlite3_backup_init(destination->db, (const char *) destination_name.data, source->db, (const char *) source_name.data); + if(backup == NULL) { + return make_sqlite3_error_tuple(env, sqlite3_errcode(destination->db)); + } + + esqlite3_backup *ebackup = enif_alloc_resource(esqlite3_backup_type, sizeof(esqlite3_backup)); + if(!ebackup) { + (void) sqlite3_backup_finish(backup); + return enif_raise_exception(env, make_atom(env, "no_memory")); + } + + ebackup->backup = backup; + + /** + * Keep references to both database connections to prevent + * them from being garbage collected during the operation. + */ + enif_keep_resource((void *)destination); + ebackup->destination = destination; + enif_keep_resource((void *)source); + ebackup->source = source; + + ERL_NIF_TERM erl_backup_term = enif_make_resource(env, ebackup); + enif_release_resource(ebackup); + + return make_ok_tuple(env, erl_backup_term); +} + +/* + * Get the remaining pagecount of the backup. + */ +static ERL_NIF_TERM +esqlite_backup_remaining(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3_backup *backup; + + if(argc != 1) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_backup_type, (void **) &backup)) { + return enif_make_badarg(env); + } + + sqlite3_int64 remaining = sqlite3_backup_remaining(backup->backup); + + return enif_make_int64(env, remaining); +} + +/* + * Get the total pagecount of the backup + */ +static ERL_NIF_TERM +esqlite_backup_pagecount(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3_backup *backup; + + if(argc != 1) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_backup_type, (void **) &backup)) { + return enif_make_badarg(env); + } + + sqlite3_int64 pagecount = sqlite3_backup_pagecount(backup->backup); + + return enif_make_int64(env, pagecount); +} + +static ERL_NIF_TERM +esqlite_backup_step(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3_backup *backup; + int n_page; + + if(argc != 2) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_backup_type, (void **) &backup)) { + return enif_make_badarg(env); + } + + if(!enif_get_int(env, argv[1], &n_page)) { + return enif_make_badarg(env); + } + + int rc = sqlite3_backup_step(backup->backup, n_page); + if(rc == SQLITE_DONE) { + return make_atom(env, "$done"); + } + + if(rc != SQLITE_OK) { + return make_sqlite3_error_tuple(env, rc); + } + + return make_atom(env, "ok"); +} + +/* + * Explicitly finish the backup. + */ +static ERL_NIF_TERM +esqlite_backup_finish(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3_backup *backup; + + if(argc != 1) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_backup_type, (void **) &backup)) { + return enif_make_badarg(env); + } + + int rc = SQLITE_OK; + if(backup->backup) { + rc = sqlite3_backup_finish(backup->backup); + backup->backup = NULL; + } + + if(backup->source) { + enif_release_resource(backup->source); + backup->source = NULL; + } + + if(backup->destination) { + enif_release_resource(backup->destination); + backup->destination = NULL; + } + + if(rc != SQLITE_OK) { + return make_sqlite3_error_tuple(env, rc); + } + + return make_atom(env, "ok"); +} + +/* + * Interrupt any currently active query. + */ +static ERL_NIF_TERM +esqlite_interrupt(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3 *conn; + + if(argc != 1) + return enif_make_badarg(env); + + if(!enif_get_resource(env, argv[0], esqlite3_type, (void **) &conn)) + return enif_make_badarg(env); + + esqlite3 *db = (esqlite3 *) conn; + if(db->db == NULL) { + return enif_raise_exception(env, make_atom(env, "closed")); + } + + sqlite3_interrupt(db->db); + return enif_make_atom(env, "ok"); +} + +static ERL_NIF_TERM +esqlite_get_autocommit(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3 *conn; + + if(argc != 1) + return enif_make_badarg(env); + + if(!enif_get_resource(env, argv[0], esqlite3_type, (void **) &conn)) + return enif_make_badarg(env); + + esqlite3 *db = (esqlite3 *) conn; + + if(db->db == NULL) { + return enif_raise_exception(env, make_atom(env, "closed")); + } + + if(sqlite3_get_autocommit(db->db)) { + return make_atom(env, "true"); + } + + return make_atom(env, "false"); +} + +static ERL_NIF_TERM +esqlite_last_insert_rowid(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3 *conn; + + if(argc != 1) + return enif_make_badarg(env); + + if(!enif_get_resource(env, argv[0], esqlite3_type, (void **) &conn)) + return enif_make_badarg(env); + + esqlite3 *db = (esqlite3 *) conn; + + if(db->db == NULL) { + return enif_raise_exception(env, make_atom(env, "closed")); + } + + sqlite3_int64 last_rowid = sqlite3_last_insert_rowid(db->db); + return enif_make_int64(env, last_rowid); +} + +static ERL_NIF_TERM +esqlite_changes(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3 *conn; + + if(argc != 1) + return enif_make_badarg(env); + + if(!enif_get_resource(env, argv[0], esqlite3_type, (void **) &conn)) + return enif_make_badarg(env); + + esqlite3 *db = (esqlite3 *) conn; + + if(db->db == NULL) { + return enif_raise_exception(env, make_atom(env, "closed")); + } + + sqlite3_int64 changes = sqlite3_changes64(db->db); + return enif_make_int64(env, changes); +} + +static ERL_NIF_TERM +esqlite_memory_stats(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + int highwater_reset_flag = 0; + + if(argc != 1) + return enif_make_badarg(env); + + if(!enif_get_int(env, argv[0], &highwater_reset_flag)) { + return enif_make_badarg(env); + } + + int used = sqlite3_memory_used(); + int highwater = sqlite3_memory_highwater(highwater_reset_flag); + + ERL_NIF_TERM stats = enif_make_new_map(env); + enif_make_map_put(env, stats, make_atom(env, "used"), enif_make_int64(env, used), &stats); + enif_make_map_put(env, stats, make_atom(env, "highwater"), enif_make_int64(env, highwater), &stats); + + return stats; +} + +static ERL_NIF_TERM +esqlite_status(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + int op = 0; + int highwater_reset_flag = 0; + + if(argc != 2) + return enif_make_badarg(env); + + if(!enif_get_int(env, argv[0], &op)) { + return enif_make_badarg(env); + } + + if(!enif_get_int(env, argv[0], &highwater_reset_flag)) { + return enif_make_badarg(env); + } + + sqlite3_int64 used; + sqlite3_int64 highwater; + + int rc = sqlite3_status64(op, &used, &highwater, highwater_reset_flag); + if(rc != SQLITE_OK) { + return make_sqlite3_error_tuple(env, rc); + } + + ERL_NIF_TERM stats = enif_make_new_map(env); + enif_make_map_put(env, stats, make_atom(env, "used"), enif_make_int64(env, used), &stats); + enif_make_map_put(env, stats, make_atom(env, "highwater"), enif_make_int64(env, highwater), &stats); + + return stats; } /* @@ -1712,25 +1143,24 @@ on_load(ErlNifEnv* env, void** priv, ERL_NIF_TERM info) { ErlNifResourceType *rt; - rt = enif_open_resource_type(env, "esqlite3_nif", "esqlite_connection_type", destruct_esqlite_connection, + rt = enif_open_resource_type(env, "esqlite3_nif", "esqlite3_type", destruct_esqlite3, ERL_NIF_RT_CREATE, NULL); if(!rt) return -1; - esqlite_connection_type = rt; + esqlite3_type = rt; - rt = enif_open_resource_type(env, "esqlite3_nif", "esqlite_statement_type", destruct_esqlite_statement, + rt = enif_open_resource_type(env, "esqlite3_nif", "esqlite3_stmt_type", destruct_esqlite3_stmt, ERL_NIF_RT_CREATE, NULL); if(!rt) return -1; - esqlite_statement_type = rt; + esqlite3_stmt_type = rt; - rt = enif_open_resource_type(env, "esqlite3_nif", "esqlite_backup_type", destruct_esqlite_backup, + rt = enif_open_resource_type(env, "esqlite3_nif", "esqlite3_backup_type", destruct_esqlite3_backup, ERL_NIF_RT_CREATE, NULL); if(!rt) return -1; - esqlite_backup_type = rt; + esqlite3_backup_type = rt; - atom_esqlite3 = make_atom(env, "esqlite3"); - - if(SQLITE_OK != sqlite3_initialize()) + if(SQLITE_OK != sqlite3_initialize()) { return -1; + } return 0; } @@ -1746,30 +1176,47 @@ static int on_upgrade(ErlNifEnv* env, void** priv, void** old_priv_data, ERL_NIF } static ErlNifFunc nif_funcs[] = { - {"start", 0, esqlite_start}, - {"open", 4, esqlite_open}, - {"set_update_hook", 4, set_update_hook}, - {"exec", 4, esqlite_exec}, - {"changes", 3, esqlite_changes}, - {"prepare", 4, esqlite_prepare}, - {"insert", 4, esqlite_insert}, - {"last_insert_rowid", 3, esqlite_last_insert_rowid}, - {"get_autocommit", 3, esqlite_get_autocommit}, - {"multi_step", 5, esqlite_multi_step}, - {"reset", 4, esqlite_reset}, - // TODO: {"esqlite_bind", 3, esqlite_bind_named}, - {"bind", 5, esqlite_bind}, - {"column_names", 4, esqlite_column_names}, - {"column_types", 4, esqlite_column_types}, + {"open", 1, esqlite_open, ERL_NIF_DIRTY_JOB_IO_BOUND}, + {"close", 1, esqlite_close, ERL_NIF_DIRTY_JOB_IO_BOUND}, - {"backup_init", 6, esqlite_backup_init}, - {"backup_step", 5, esqlite_backup_step}, - {"backup_remaining", 4, esqlite_backup_remaining}, - {"backup_pagecount", 4, esqlite_backup_pagecount}, - {"backup_finish", 4, esqlite_backup_finish}, + {"error_info", 1, esqlite_error_info}, + + /* + * Other interesting additions... trace. + * wal_hook triggered after every commit. + * also interesting commit and rollback_hooks + */ + {"set_update_hook", 2, esqlite_set_update_hook}, + + {"exec", 2, esqlite_exec, ERL_NIF_DIRTY_JOB_IO_BOUND}, + {"prepare", 3, esqlite_prepare}, + + {"column_names", 1, esqlite_column_names}, + {"column_decltypes", 1, esqlite_column_decltypes}, + + {"bind_int", 3, esqlite_bind_int}, + {"bind_int64", 3, esqlite_bind_int64}, + {"bind_double", 3, esqlite_bind_double}, + {"bind_text", 3, esqlite_bind_text}, + {"bind_blob", 3, esqlite_bind_blob}, + {"bind_null", 2, esqlite_bind_null}, + + {"step", 1, esqlite_step, ERL_NIF_DIRTY_JOB_IO_BOUND}, + {"reset", 1, esqlite_reset}, {"interrupt", 1, esqlite_interrupt, ERL_NIF_DIRTY_JOB_IO_BOUND}, - {"close", 3, esqlite_close} + {"last_insert_rowid", 1, esqlite_last_insert_rowid}, + {"get_autocommit", 1, esqlite_get_autocommit}, + {"changes", 1, esqlite_changes}, + + {"backup_init", 4, esqlite_backup_init, ERL_NIF_DIRTY_JOB_IO_BOUND}, + {"backup_remaining", 1, esqlite_backup_remaining}, + {"backup_pagecount", 1, esqlite_backup_pagecount}, + {"backup_step", 2, esqlite_backup_step, ERL_NIF_DIRTY_JOB_IO_BOUND}, + {"backup_finish", 1, esqlite_backup_finish, ERL_NIF_DIRTY_JOB_IO_BOUND}, + + {"memory_stats", 1, esqlite_memory_stats}, + {"status", 2, esqlite_status} }; ERL_NIF_INIT(esqlite3_nif, nif_funcs, on_load, on_reload, on_upgrade, NULL); diff --git a/c_src/queue.c b/c_src/queue.c deleted file mode 100644 index 685e9c8..0000000 --- a/c_src/queue.c +++ /dev/null @@ -1,195 +0,0 @@ -// This file is part of Emonk released under the MIT license. -// See the LICENSE file for more information. - -/* Adapted by: Maas-Maarten Zeeman -#include - -#include "queue.h" - -struct qitem_t -{ - struct qitem_t* next; - void* data; -}; - -typedef struct qitem_t qitem; - -struct queue_t -{ - ErlNifMutex *lock; - ErlNifCond *cond; - qitem *head; - qitem *tail; - void *message; - int length; -}; - -queue * -queue_create() -{ - queue *ret; - - ret = (queue *) enif_alloc(sizeof(struct queue_t)); - if(ret == NULL) goto error; - - ret->lock = NULL; - ret->cond = NULL; - ret->head = NULL; - ret->tail = NULL; - ret->message = NULL; - ret->length = 0; - - ret->lock = enif_mutex_create("queue_lock"); - if(ret->lock == NULL) goto error; - - ret->cond = enif_cond_create("queue_cond"); - if(ret->cond == NULL) goto error; - - return ret; - -error: - if(ret->lock != NULL) - enif_mutex_destroy(ret->lock); - if(ret->cond != NULL) - enif_cond_destroy(ret->cond); - if(ret != NULL) - enif_free(ret); - return NULL; -} - -void -queue_destroy(queue *queue) -{ - ErlNifMutex *lock; - ErlNifCond *cond; - int length; - - enif_mutex_lock(queue->lock); - lock = queue->lock; - cond = queue->cond; - length = queue->length; - - queue->lock = NULL; - queue->cond = NULL; - queue->head = NULL; - queue->tail = NULL; - queue->length = -1; - enif_mutex_unlock(lock); - - assert(length == 0 && "Attempting to destroy a non-empty queue."); - enif_cond_destroy(cond); - enif_mutex_destroy(lock); - enif_free(queue); -} - -int -queue_has_item(queue *queue) -{ - int ret; - - enif_mutex_lock(queue->lock); - ret = (queue->head != NULL); - enif_mutex_unlock(queue->lock); - - return ret; -} - -int -queue_push(queue *queue, void *item) -{ - qitem * entry = (qitem *) enif_alloc(sizeof(struct qitem_t)); - if(entry == NULL) - return 0; - - entry->data = item; - entry->next = NULL; - - enif_mutex_lock(queue->lock); - - assert(queue->length >= 0 && "Invalid queue size at push"); - - if(queue->tail != NULL) - queue->tail->next = entry; - - queue->tail = entry; - - if(queue->head == NULL) - queue->head = queue->tail; - - queue->length += 1; - - enif_cond_signal(queue->cond); - enif_mutex_unlock(queue->lock); - - return 1; -} - -void* -queue_pop(queue *queue) -{ - qitem *entry; - void* item; - - enif_mutex_lock(queue->lock); - - /* Wait for an item to become available. - */ - while(queue->head == NULL) - enif_cond_wait(queue->cond, queue->lock); - - assert(queue->length >= 0 && "Invalid queue size at pop."); - - /* Woke up because queue->head != NULL - * Remove the entry and return the payload. - */ - entry = queue->head; - queue->head = entry->next; - entry->next = NULL; - - if(queue->head == NULL) { - assert(queue->tail == entry && "Invalid queue state: Bad tail."); - queue->tail = NULL; - } - - queue->length -= 1; - - enif_mutex_unlock(queue->lock); - - item = entry->data; - enif_free(entry); - - return item; -} - -int -queue_send(queue *queue, void *item) -{ - enif_mutex_lock(queue->lock); - assert(queue->message == NULL && "Attempting to send multiple messages."); - queue->message = item; - enif_cond_signal(queue->cond); - enif_mutex_unlock(queue->lock); - return 1; -} - -void * -queue_receive(queue *queue) -{ - void *item; - - enif_mutex_lock(queue->lock); - - /* Wait for an item to become available. - */ - while(queue->message == NULL) - enif_cond_wait(queue->cond, queue->lock); - - item = queue->message; - queue->message = NULL; - - enif_mutex_unlock(queue->lock); - - return item; -} diff --git a/c_src/queue.h b/c_src/queue.h deleted file mode 100644 index 5ac597d..0000000 --- a/c_src/queue.h +++ /dev/null @@ -1,24 +0,0 @@ -// This file is part of Emonk released under the MIT license. -// See the LICENSE file for more information. - -/* adapted by: Maas-Maarten Zeeman + + +SQLiteimage/svg+xmlSQLite \ No newline at end of file diff --git a/doc/overview.edoc b/doc/overview.edoc index 5428e27..8ca56be 100644 --- a/doc/overview.edoc +++ b/doc/overview.edoc @@ -1,10 +1,10 @@ @author Maas-Maarten Zeeman -@title eSqlite Documentation +@title ESQLite Documentation @doc -eSqlite is a library which makes it possible to use sqlite databases in erlang. It is implemented +ESQLite is a library which makes it possible to use sqlite databases in erlang. It is implemented as a NIF, which means that the sqlite database engine is linked to the erlang virtual machine.
diff --git a/doc/style.css b/doc/style.css new file mode 100644 index 0000000..ad59351 --- /dev/null +++ b/doc/style.css @@ -0,0 +1,76 @@ +/* standard EDoc style sheet */ +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + margin-left: .25in; + margin-right: .2in; + margin-top: 0.2in; + margin-bottom: 0.2in; + color: #000000; + background-color: #ffffff; +} +h1,h2 { + margin-left: -0.2in; +} +div.navbar { + background-color: #add8e6; + padding: 0.2em; +} +h2.indextitle { + padding: 0.4em; + background-color: #add8e6; +} +h3.function,h3.typedecl { + background-color: #add8e6; + padding-left: 1em; +} +div.spec { + margin-left: 2em; + + background-color: #eeeeee; +} +a.module { + text-decoration:none +} +a.module:hover { + background-color: #eeeeee; +} +ul.definitions { + list-style-type: none; +} +ul.index { + list-style-type: none; + background-color: #eeeeee; +} + +/* + * Minor style tweaks + */ +ul { + list-style-type: square; +} +table { + border-collapse: collapse; +} +td { + padding: 3px; + vertical-align: middle; +} + +/* +Tune styles +*/ + +table[summary="navigation bar"] { + background-image: url('SQLite370.svg'); + background-size: 110px; + background-repeat: no-repeat; + background-position: center; +} + +code, p>tt, a>tt { + font-size: 1.2em; +} + +p { + line-height: 1.5; +} diff --git a/rebar.config.script b/rebar.config.script index 2443850..435ce86 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -1,5 +1,5 @@ -NifSharedSources = ["c_src/esqlite3_nif.c", "c_src/queue.c"]. +NifSharedSources = ["c_src/esqlite3_nif.c"]. NifStaticSources = NifSharedSources ++ ["c_src/sqlite3/sqlite3.c"]. CFlagsDefault = "$CFLAGS -Os -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=1 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_MAX_EXPR_DEPTH=0 -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_USE_ALLOCA -DSQLITE_OMIT_AUTOINIT -DSQLITE_USE_URI -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_MATH_FUNCTIONS -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_RTREE -DSQLITE_ENABLE_GEOPOLY". DrvLdFlagsDefault = "-shared -lsqlite3". @@ -23,9 +23,9 @@ CFlags = end. [ - {minimum_otp_vsn, "21.0"}, + {minimum_otp_vsn, "22.0"}, - {erl_opts, [debug_info, warnings_as_errors]}, + {erl_opts, [debug_info]}, {xref_checks, [undefined_function_calls, undefined_functions, @@ -68,6 +68,7 @@ CFlags = ]}, {edoc_opts, [{preprocess, true}, + {stylesheet, "style.css"}, {sort_functions, false}]}, {hex, [{doc, edoc}]} diff --git a/src/esqlite.app.src b/src/esqlite.app.src index 06ae835..08af0ee 100644 --- a/src/esqlite.app.src +++ b/src/esqlite.app.src @@ -1,7 +1,7 @@ {application, esqlite, [ {description, "sqlite nif interface"}, - {vsn, "0.7.3"}, + {vsn, "0.8.0"}, {modules, [esqlite3, esqlite3_nif]}, {registered, []}, {licenses, ["Apache"]}, diff --git a/src/esqlite3.erl b/src/esqlite3.erl index e4250bf..8a4cdf0 100644 --- a/src/esqlite3.erl +++ b/src/esqlite3.erl @@ -19,69 +19,90 @@ %% higher-level export -export([ - open/1, open/2, - close/1, close/2, - set_update_hook/2, set_update_hook/3, - exec/2, exec/3, exec/4, - changes/1, changes/2, - insert/2, insert/3, - last_insert_rowid/1, - get_autocommit/1, get_autocommit/2, - prepare/2, prepare/3, - step/1, step/2, - reset/1, - bind/2, bind/3, - fetchone/1, - fetchall/1, fetchall/2, fetchall/3, - column_names/1, column_names/2, - column_types/1, column_types/2, - backup_init/4, backup_init/5, - backup_finish/1, backup_finish/2, - backup_remaining/1, backup_remaining/2, - backup_pagecount/1, backup_pagecount/2, - backup_step/2, backup_step/3, - flush/0 -]). + open/1, + close/1, --export([q/2, q/3, q/4, map/3, map/4, foreach/3, foreach/4]). + error_info/1, + + %% db connection functions + set_update_hook/2, + + get_autocommit/1, + last_insert_rowid/1, + changes/1, + + %% queries + exec/2, + prepare/2, + prepare/3, + + %% prepared statement functions + column_names/1, + column_decltypes/1, + + bind_int/3, + bind_int64/3, + bind_double/3, + bind_text/3, + bind_blob/3, + bind_null/2, + + step/1, + reset/1, + interrupt/1, + + backup_init/4, + backup_remaining/1, + backup_pagecount/1, + backup_step/2, + backup_finish/1, + + bind/2, + q/2, q/3, + fetchall/1, + + status/0, status/1, status/2 +]). -define(DEFAULT_TIMEOUT, infinity). -define(DEFAULT_CHUNK_SIZE, 5000). --record(connection, { - raw_connection :: esqlite3_nif:raw_connection() +-define(SQLITE_PREPARE_PERSISTENT, 16#01). +-define(SQLITE_PREPARE_NO_VTAB, 16#04). + +-record(esqlite3, { + db :: esqlite3_nif:esqlite3_ref() }). --record(statement, { - raw_connection :: esqlite3_nif:raw_connection(), - raw_statement :: esqlite3_nif:raw_statement() +-record(esqlite3_stmt, { + stmt :: esqlite3_nif:esqlite3_stmt_ref() }). --record(backup, { - raw_connection :: esqlite3_nif:raw_connection(), - raw_backup :: esqlite3_nif:raw_backup() +-record(esqlite3_backup, { + backup :: esqlite3_nif:esqlite3_backup_ref() }). --type connection() :: #connection{}. --type statement() :: #statement{}. --type backup() :: #backup{}. +-type esqlite3() :: #esqlite3{}. +-type esqlite3_stmt() :: #esqlite3_stmt{}. +-type esqlite3_backup() :: #esqlite3_backup{}. -type sql() :: esqlite3_nif:sql(). -%% erlang -> sqlite type conversions -%% -%% 'undefined' -> null -%% 'null' -> null -%% atom() -> text -%% int() -> int or int64 -%% float() -> double -%% string() -> text -%% binary() -> text +-type prepare_flags() :: persistent | no_vtab. --type rowid() :: integer(). --type row() :: tuple(). % tuple of cell_type --type cell_type() :: undefined | integer() | binary() | float(). +-type status_info() :: #{ memory_used => stats(), + pagecache_used => stats(), + pagecache_overflow => stats(), + malloc_size := stats(), + parser_stack := stats(), + pagecache_size := stats(), + malloc_count := stats() }. +-type stats() :: #{ used := non_neg_integer(), highwater := non_neg_integer() }. +-type rowid() :: esqlite3_nif:rowid(). +-type cell() :: esqlite3_nif:cell(). +-type row() :: esqlite3_nif:row(). +-type error() :: esqlite3_nif:error(). --export_type([connection/0, statement/0, sql/0, row/0, rowid/0, cell_type/0]). +-export_type([esqlite3/0, esqlite3_stmt/0, esqlite3_backup/0, prepare_flags/0, sql/0, row/0, rowid/0, cell/0]). %% @doc Opens a sqlite3 database mentioned in Filename. %% @@ -90,7 +111,7 @@ %% information about this can be found here: [https://sqlite.org/uri.html] %% %% Example: -%% +% %% ```open("file:data.db")''' %% Opens "data.db" in the current working directory %% ```open("file:data.db?mode=ro&cache=private")''' @@ -98,96 +119,85 @@ %% ```open("file:memdb1?mode=memory&cache=shared")''' %% Opens a shared memory database named memdb1 with a shared cache. %% --spec open(string()) -> {ok, connection()} | {error, _}. +-spec open(Filename) -> OpenResult + when Filename :: string(), + OpenResult :: {ok, esqlite3()} | error(). open(Filename) -> - open(Filename, ?DEFAULT_TIMEOUT). - -%% @doc Like open/1, but with an additional timeout. -%% --spec open(string(), timeout()) -> {ok, connection()} | {error, _}. -open(Filename, Timeout) -> - {ok, RawConnection} = esqlite3_nif:start(), - - Ref = make_ref(), - ok = esqlite3_nif:open(RawConnection, Ref, self(), Filename), - case receive_answer(RawConnection, Ref, Timeout) of - ok -> - {ok, #connection{raw_connection=RawConnection}}; - {error, _Msg}=Error -> + case esqlite3_nif:open(Filename) of + {ok, Connection} -> + {ok, #esqlite3{db=Connection}}; + {error, _}=Error -> Error end. %% @doc Close the database --spec close(connection()) -> ok | {error, _}. -close(Connection) -> - close(Connection, ?DEFAULT_TIMEOUT). +-spec close(Connection) -> CloseResult + when Connection :: esqlite3(), + CloseResult :: ok | {error, _}. +close(#esqlite3{db=Connection}) -> + esqlite3_nif:close(Connection). -%% @doc Close the database --spec close(connection(), timeout()) -> ok | {error, _}. -close(#connection{raw_connection=RawConnection}, Timeout) -> - Ref = make_ref(), - ok = esqlite3_nif:close(RawConnection, Ref, self()), - receive_answer(RawConnection, Ref, Timeout). - -%% @doc Flush any stale answers left in the mailbox of the current process. -%% This can happen if there has been a timeout. Normally the nif functions -%% are called with the default 'infinite' timeout, so calling this is not -%% needed. --spec flush() -> ok. -flush() -> - flush_answers(). +%% @doc Return a description of the last occurred error. +-spec error_info(Connection) -> ErrorInfo + when Connection :: esqlite3(), + ErrorInfo :: esqlite3_nif:error_info(). +error_info(#esqlite3{db=Connection}) -> + esqlite3_nif:error_info(Connection). +%% @doc Interrupt a long running query. See [https://sqlite.org/c3ref/interrupt.html] for more details. +-spec interrupt(Connection) -> Result + when Connection :: esqlite3(), + Result:: ok. +interrupt(#esqlite3{db=Db}) -> + esqlite3_nif:interrupt(Db). %% @doc Subscribe to database notifications. When rows are inserted deleted -%% or updates, the process will receive messages: -%% ```{insert, string(), rowid()}''' +%% or updates, the registered process will receive messages: +%% ```{insert, binary(), binary(), rowid()}''' %% When a new row has been inserted. -%% ```{delete, string(), rowid()}''' +%% ```{delete, binary(), binary(), rowid()}''' %% When a new row has been deleted. -%% ```{update, string(), rowid()}''' +%% ```{update, binary(), binary(), rowid()}''' %% When a row has been updated. %% --spec set_update_hook(pid(), connection()) -> ok | {error, term()}. -set_update_hook(Pid, Connection) -> - set_update_hook(Pid, Connection, ?DEFAULT_TIMEOUT). +-spec set_update_hook(Connection, Pid) -> Result when + Connection :: esqlite3(), + Pid :: pid(), + Result :: ok. +set_update_hook(#esqlite3{db=Connection}, Pid) -> + esqlite3_nif:set_update_hook(Connection, Pid). -%% @doc Same as set_update_hook/2, but with an additional timeout parameter. -%% --spec set_update_hook(pid(), connection(), timeout()) -> ok | {error, term()}. -set_update_hook(Pid, #connection{raw_connection=RawConnection}, Timeout) -> - Ref = make_ref(), - ok = esqlite3_nif:set_update_hook(RawConnection, Ref, self(), Pid), - receive_answer(RawConnection, Ref, Timeout). +%%% +%%% q +%%% -%% -%% q -%% +%% @doc Execute a sql statement, returns the result as list rows. +-spec q(Connection, Sql) -> Result when + Connection :: esqlite3(), + Sql :: sql(), + Result :: list(row()) | error(). +q(Connection, Sql) -> + q(Connection, Sql, []). -%% @doc Execute a sql statement, returns a list with tuples. --spec q(sql(), connection()) -> list(row()) | {error, _}. -q(Sql, Connection) -> - q(Sql, [], Connection, ?DEFAULT_TIMEOUT). - -%% @doc Execute statement, bind args and return a list with tuples as result. --spec q(sql(), list(), connection()) -> list(row()) | {error, _}. -q(Sql, Args, Connection) -> - q(Sql, Args, Connection, ?DEFAULT_TIMEOUT). - -%% @doc Execute statement, bind args and return a list with tuples as result restricted by timeout. --spec q(sql(), list(), connection(), timeout()) -> list(row()) | {error, _}. -q(Sql, [], Connection, Timeout) -> - case prepare(Sql, Connection, Timeout) of +%% @doc Execute statement, bind args and return a list rows. +-spec q(Connection, Sql, Args) -> Result when + Connection :: esqlite3(), + Sql :: sql(), + Args :: list(), + Result :: list(row()) | error(). +q(Connection, Sql, []) -> + case prepare(Connection, Sql) of {ok, Statement} -> - fetchall(Statement, ?DEFAULT_CHUNK_SIZE, Timeout); + fetchall(Statement); {error, _Msg}=Error -> Error end; -q(Sql, Args, Connection, Timeout) -> - case prepare(Sql, Connection, Timeout) of +q(Connection, Sql, Args) -> + case prepare(Connection, Sql) of {ok, Statement} -> - case bind(Statement, Args, Timeout) of + case bind(Statement, Args) of ok -> - fetchall(Statement, ?DEFAULT_CHUNK_SIZE, Timeout); + fetchall(Statement); {error, _}=Error -> Error end; @@ -195,480 +205,313 @@ q(Sql, Args, Connection, Timeout) -> Error end. -%% -%% map -%% - -%% @doc Execute statement and return a list with the result of F for each row. --spec map(Fun, sql(), connection()) -> list(Type) when - Fun :: fun((Row) -> Type) | fun((ColumnNames, Row) -> Type), - Row :: row(), - ColumnNames :: tuple(), - Type :: any(). -map(Fun, Sql, Connection) -> - case prepare(Sql, Connection) of - {ok, Statement} -> - map_s(Fun, Statement); - {error, _Msg}=Error -> - Error - end. - -%% @doc Execute statement, bind args and return a list with the result of F for each row. --spec map(F, sql(), list(), connection()) -> list(Type) when - F :: fun((Row) -> Type) | fun((ColumnNames, Row) -> Type), - Row :: tuple(), - ColumnNames :: tuple(), - Type :: any(). -map(Fun, Sql, [], Connection) -> - map(Fun, Sql, Connection); -map(Fun, Sql, Args, Connection) -> - case prepare(Sql, Connection) of - {ok, Statement} -> - case bind(Statement, Args) of - ok -> - map_s(Fun, Statement); - {error, _}=Error -> - Error - end; - {error, _Msg}=Error -> - Error - end. - -%% -%% foreach -%% - -%% @doc Execute statement and call F with each row. --spec foreach(Fun, sql(), connection()) -> ok when - Fun :: fun((Row) -> any()) | fun((ColumnNames, Row) -> any()), - Row :: tuple(), - ColumnNames :: tuple(). -foreach(Fun, Sql, Connection) -> - case prepare(Sql, Connection) of - {ok, Statement} -> - foreach_s(Fun, Statement); - {error, _Msg}=Error -> - Error - end. - -%% @doc Execute statement, bind args and call F with each row. --spec foreach(Fun, sql(), list(), connection()) -> ok when - Fun :: fun((Row) -> any()) | fun((ColumnNames, Row) -> any()), - Row :: row(), - ColumnNames :: tuple(). -foreach(F, Sql, [], Connection) -> - foreach(F, Sql, Connection); -foreach(F, Sql, Args, Connection) -> - case prepare(Sql, Connection) of - {ok, Statement} -> - case bind(Statement, Args) of - ok -> - foreach_s(F, Statement); - {error, _Msg}=Error -> - Error - end; - {error, _Msg}=Error -> - Error - end. %% %% fetchall %% -%% --spec fetchone(statement()) -> tuple(). -fetchone(Statement) -> - case try_multi_step(Statement, 1, [], 0) of - {'$done', []} -> ok; - {error, _} = E -> E; - {rows, [Row | []]} -> Row - end. - -%% @doc Fetch all records -%% @param Statement is prepared sql statement --spec fetchall(statement()) -> list(row()) | {error, _}. +% @doc Fetch all rows from the prepared statement. +-spec fetchall(Statement) -> Result when + Statement :: esqlite3_stmt(), + Result :: list(row()) | error(). fetchall(Statement) -> - fetchall(Statement, ?DEFAULT_CHUNK_SIZE, ?DEFAULT_TIMEOUT). + fetchall1(Statement, []). -%% @doc Fetch all records -%% @param Statement is prepared sql statement -%% @param ChunkSize is a count of rows to read from sqlite and send to erlang process in one bulk. -%% Decrease this value if rows are heavy. Default value is 5000 (DEFAULT_CHUNK_SIZE). --spec fetchall(statement(), pos_integer()) -> list(row()) | {error, _}. -fetchall(Statement, ChunkSize) -> - fetchall(Statement, ChunkSize, ?DEFAULT_TIMEOUT). - -%% @doc Fetch all records -%% @param Statement is prepared sql statement -%% @param ChunkSize is a count of rows to read from sqlite and send to erlang process in one bulk. -%% Decrease this value if rows are heavy. Default value is 5000 (DEFAULT_CHUNK_SIZE). -%% @param Timeout is timeout per each request of the one bulk --spec fetchall(statement(), pos_integer(), timeout()) -> list(row()) | {error, _}. -fetchall(Statement, ChunkSize, Timeout) -> - case fetchall_internal(Statement, ChunkSize, [], Timeout) of - {'$done', Rows} -> lists:reverse(Rows); - {error, _} = E -> E +fetchall1(Statement, Acc) -> + case step(Statement) of + Row when is_list(Row) -> + fetchall1(Statement, [Row|Acc]); + '$done' -> + lists:reverse(Acc); + {error, _} = E -> + E end. -%% @doc Execute Sql statement. -%% --spec exec(sql(), connection()) -> ok | {error, _}. -exec(Sql, Connection) -> - exec(Sql, [], Connection, ?DEFAULT_TIMEOUT). +%% @doc Get the last inserted rowid. +%% See [https://sqlite.org/c3ref/set_last_insert_rowid.html] for more details. +-spec last_insert_rowid(Connection) -> RowidResult when + Connection :: esqlite3(), + RowidResult :: integer(). +last_insert_rowid(#esqlite3{db=Connection}) -> + esqlite3_nif:last_insert_rowid(Connection). --spec exec(sql(), list(cell_type()) | connection(), connection() | timeout()) -> ok | {error, _}. -exec(Sql, #connection{}=Connection, Timeout) -> - exec(Sql, [], Connection, Timeout); -exec(Sql, Params, #connection{}=Connection) -> - exec(Sql, Params, Connection, ?DEFAULT_TIMEOUT). +%% @doc Get the number of changes in the most recent INSERT, UPDATE or DELETE. +%% See [https://sqlite.org/c3ref/changes.html] for more details. +-spec changes(Connection) -> ChangesResult + when Connection :: esqlite3(), + ChangesResult :: integer(). +changes(#esqlite3{db=Connection}) -> + esqlite3_nif:changes(Connection). --spec exec(sql(), list(cell_type()), connection(), timeout()) -> ok | {error, _}. -exec(Sql, [], #connection{raw_connection=RawConnection}, Timeout) -> - Ref = make_ref(), - ok = esqlite3_nif:exec(RawConnection, Ref, self(), Sql), - receive_answer(RawConnection, Ref, Timeout); -exec(Sql, Params, Connection, Timeout) -> - {ok, Statement} = prepare(Sql, Connection, Timeout), - case bind(Statement, Params) of - ok -> - step(Statement, Timeout); - {error, _}=Error -> - Error - end. - - -%% @doc Return the number of affected rows of last statement. --spec changes(connection()) -> non_neg_integer(). -changes(Connection) -> - changes(Connection, ?DEFAULT_TIMEOUT). - --spec changes(connection(), timeout()) -> non_neg_integer(). -changes(#connection{raw_connection=RawConnection}, Timeout) -> - Ref = make_ref(), - ok = esqlite3_nif:changes(RawConnection, Ref, self()), - receive_answer(RawConnection, Ref, Timeout). - -%% @doc Insert records, returns the last rowid. -%% --spec insert(sql(), connection()) -> {ok, rowid()} | {error, _}. -insert(Sql, Connection) -> - insert(Sql, Connection, ?DEFAULT_TIMEOUT). - -%% @doc Like insert/2, but with extra timeout parameter. --spec insert(sql(), connection(), timeout()) -> {ok, rowid()} | {error, _}. -insert(Sql, #connection{raw_connection=RawConnection}, Timeout) -> - Ref = make_ref(), - ok = esqlite3_nif:insert(RawConnection, Ref, self(), Sql), - receive_answer(RawConnection, Ref, Timeout). - -%% @doc Get the last insert rowid, using the default timeout. -%% --spec last_insert_rowid(connection()) -> {ok, rowid()} | {error, _}. -last_insert_rowid(Connection) -> - last_insert_rowid(Connection, ?DEFAULT_TIMEOUT). - -%% @doc Get the last insert rowid. -%% --spec last_insert_rowid(connection(), timeout()) -> {ok, rowid()} | {error, _}. -last_insert_rowid(#connection{raw_connection=RawConnection}, Timeout) -> - Ref = make_ref(), - ok = esqlite3_nif:last_insert_rowid(RawConnection, Ref, self()), - receive_answer(RawConnection, Ref, Timeout). %% @doc Check if the connection is in auto-commit mode. -%% See: [https://sqlite.org/c3ref/get_autocommit.html] for more details. +%% See: [https://sqlite.org/c3ref/get_autocommit.html] for more details. %% --spec get_autocommit(connection()) -> true | false. -get_autocommit(Connection) -> - get_autocommit(Connection, ?DEFAULT_TIMEOUT). - -%% @doc Like autocommit/1, but with an extra timeout attribute. --spec get_autocommit(connection(), timeout()) -> true | false. -get_autocommit(#connection{raw_connection=RawConnection}, Timeout) -> - Ref = make_ref(), - ok = esqlite3_nif:get_autocommit(RawConnection, Ref, self()), - receive_answer(RawConnection, Ref, Timeout). +-spec get_autocommit(Connection) -> AutocommitResult + when Connection :: esqlite3(), + AutocommitResult :: true | false. +get_autocommit(#esqlite3{db=Connection}) -> + esqlite3_nif:get_autocommit(Connection). %% @doc Compile a SQL statement. Returns a cached compiled statement which can be used in %% queries. %% --spec prepare(sql(), connection()) -> {ok, statement()} | {error, _}. -prepare(Sql, Connection) -> - prepare(Sql, Connection, ?DEFAULT_TIMEOUT). +-spec exec(Connection, Sql) -> ExecResult + when Connection :: esqlite3(), + Sql :: sql(), + ExecResult :: ok | error(). +exec(#esqlite3{db=Connection}, Sql) -> + esqlite3_nif:exec(Connection, Sql). -%% @doc Like prepare/2, but with an extra timeout value. --spec prepare(sql(), connection(), timeout()) -> {ok, statement()} | {error, _}. -prepare(Sql, #connection{raw_connection=RawConnection}, Timeout) -> - Ref = make_ref(), - ok = esqlite3_nif:prepare(RawConnection, Ref, self(), Sql), - case receive_answer(RawConnection, Ref, Timeout) of - {ok, Stmt} when is_reference(Stmt) -> - {ok, #statement{raw_statement=Stmt, raw_connection=RawConnection}}; +%% +%% Prepared Statements +%% + +%% @doc Compile a SQL statement. Returns a cached compiled statement which can be used in +%% queries. +%% +-spec prepare(Connection, Sql) -> PrepareResult + when Connection :: esqlite3(), + Sql :: sql(), + PrepareResult :: {ok, esqlite3_stmt()} | error(). +prepare(Connection, Sql) -> + prepare(Connection, Sql, []). + +%% @doc Compile a SQL statement. Returns a cached compiled statement which can be used in +%% queries. +-spec prepare(Connection, Sql, PrepareFlags) -> PrepareResult when + Connection :: esqlite3(), + Sql :: sql(), + PrepareFlags :: list(prepare_flags()), + PrepareResult :: {ok, esqlite3_stmt()} | {error, _}. +prepare(#esqlite3{db=Connection}, Sql, PrepareFlags) -> + case esqlite3_nif:prepare(Connection, Sql, props_to_prepare_flag(PrepareFlags)) of + {ok, Stmt} -> + {ok, #esqlite3_stmt{stmt=Stmt}}; {error, _}=Error -> - Error + Error end. -%% @doc Step -%% --spec step(statement()) -> tuple() | '$busy' | '$done'. -step(Stmt) -> - step(Stmt, ?DEFAULT_TIMEOUT). +%% @doc Bind an array of values as parameters of a prepared statement +-spec bind(Statement, Args) -> Result when + Statement :: esqlite3_stmt(), + Args :: list(), + Result :: ok | {error, _}. +bind(#esqlite3_stmt{}=Statement, Args) when is_list(Args) -> + bind1(Statement, 1, Args). -%% @doc -%% --spec step(statement(), timeout()) -> tuple() | '$busy' | '$done'. -step(#statement{raw_statement=RawStatement, raw_connection=RawConnection}, Timeout) -> - Ref = make_ref(), - ok = esqlite3_nif:multi_step(RawConnection, RawStatement, 1, Ref, self()), - case receive_answer(RawConnection, Ref, Timeout) of - {rows, [Row | []]} -> {row, Row}; - {'$done', []} -> '$done'; - {'$busy', []} -> '$busy'; - Else -> Else +bind1(_Statement, _Column, []) -> + ok; +bind1(Statement, Column, [Arg | Args]) -> + case bind_arg(Statement, Column, Arg) of + ok -> + bind1(Statement, Column + 1, Args); + {error, _}=Error -> + Error end. -%% @doc Reset the prepared statement back to its initial state. -%% --spec reset(statement()) -> ok | {error, _}. -reset(#statement{raw_statement=RawStatement, raw_connection=RawConnection}) -> - Ref = make_ref(), - ok = esqlite3_nif:reset(RawConnection, RawStatement, Ref, self()), - receive_answer(RawConnection, Ref, ?DEFAULT_TIMEOUT). +% Bind with automatic tyoe conversion +bind_arg(Statement, Column, undefined) -> + bind_null(Statement, Column); +bind_arg(Statement, Column, null) -> + bind_null(Statement, Column); +bind_arg(Statement, Column, Atom) when is_atom(Atom) -> + bind_text(Statement, Column, atom_to_binary(Atom, utf8)); +bind_arg(Statement, Column, Int) when is_integer(Int) -> + bind_int64(Statement, Column, Int); +bind_arg(Statement, Column, Float) when is_float(Float) -> + bind_double(Statement, Column, Float); +bind_arg(Statement, Column, Bin) when is_binary(Bin) -> + bind_text(Statement, Column, Bin); +bind_arg(Statement, Column, String) when is_list(String) -> + bind_text(Statement, Column, String); +%% Explicit type binds. +bind_arg(Statement, Column, {int, Value}) -> + bind_int(Statement, Column, Value); +bind_arg(Statement, Column, {int64, Value}) -> + bind_int64(Statement, Column, Value); +bind_arg(Statement, Column, {float, Value}) -> + bind_double(Statement, Column, Value); +bind_arg(Statement, Column, {text, Value}) -> + bind_text(Statement, Column, Value); +bind_arg(Statement, Column, {blob, Value}) -> + bind_blob(Statement, Column, Value). -%% @doc Bind values to prepared statements -%% --spec bind(statement(), list(cell_type())) -> ok | {error, _}. -bind(Stmt, Args) -> - bind(Stmt, Args, ?DEFAULT_TIMEOUT). +-spec bind_int(Statement, Index, Value) -> BindResult when + Statement :: esqlite3_stmt(), + Index :: integer(), + Value :: integer(), + BindResult :: ok | error(). +bind_int(#esqlite3_stmt{stmt=Stmt}, Index, Value) -> + esqlite3_nif:bind_int(Stmt, Index, Value). + +-spec bind_int64(Statement, Index, Value) -> BindResult when + Statement :: esqlite3_stmt(), + Index :: non_neg_integer(), + Value :: integer(), + BindResult :: ok | error(). +bind_int64(#esqlite3_stmt{stmt=Stmt}, Index, Value) -> + esqlite3_nif:bind_int64(Stmt, Index, Value). + +-spec bind_double(Statement, Index, Value) -> BindResult + when Statement :: esqlite3_stmt(), + Index :: integer(), + Value :: float(), + BindResult :: ok | error(). +bind_double(#esqlite3_stmt{stmt=Stmt}, Index, Value) -> + esqlite3_nif:bind_double(Stmt, Index, Value). + +-spec bind_text(Statement, Index, Value) -> BindResult + when Statement :: esqlite3_stmt(), + Index :: integer(), + Value :: iodata(), + BindResult :: ok | error(). +bind_text(#esqlite3_stmt{stmt=Stmt}, Index, Value) -> + esqlite3_nif:bind_text(Stmt, Index, Value). + +-spec bind_blob(Statement, Index, Value) -> BindResult + when Statement :: esqlite3_stmt(), + Index :: integer(), + Value :: iodata(), + BindResult :: ok | error(). +bind_blob(#esqlite3_stmt{stmt=Stmt}, Index, Value) -> + esqlite3_nif:bind_blob(Stmt, Index, Value). + +-spec bind_null(Statement, Index) -> BindResult + when Statement :: esqlite3_stmt(), + Index :: integer(), + BindResult :: ok | error(). +bind_null(#esqlite3_stmt{stmt=Stmt}, Index) -> + esqlite3_nif:bind_null(Stmt, Index). + +-spec step(Statement) -> StepResult + when Statement :: esqlite3_stmt(), + StepResult:: row() | '$done' | error(). +step(#esqlite3_stmt{stmt=Stmt}) -> + esqlite3_nif:step(Stmt). + +% @doc Reset the prepared statement. +-spec reset(Statement) -> ResetResult + when Statement :: esqlite3_stmt(), + ResetResult:: ok | error(). +reset(#esqlite3_stmt{stmt=Stmt}) -> + esqlite3_nif:reset(Stmt). -%% @doc Bind values to prepared statements --spec bind(statement(), list(cell_type()), timeout()) -> ok | {error, _}. -bind(#statement{raw_statement=RawStatement, raw_connection=RawConnection}, Args, Timeout) -> - Ref = make_ref(), - ok = esqlite3_nif:bind(RawConnection, RawStatement, Ref, self(), Args), - receive_answer(RawConnection, Ref, Timeout). %% @doc Return the column names of the prepared statement. %% --spec column_names(statement()) -> {atom()}. -column_names(Stmt) -> - column_names(Stmt, ?DEFAULT_TIMEOUT). - --spec column_names(statement(), timeout()) -> {atom()}. -column_names(#statement{raw_statement=RawStatement, raw_connection=RawConnection}, Timeout) -> - Ref = make_ref(), - ok = esqlite3_nif:column_names(RawConnection, RawStatement, Ref, self()), - receive_answer(RawConnection, Ref, Timeout). +-spec column_names(Statement) -> Names + when Statement :: esqlite3_stmt(), + Names :: list(binary()). +column_names(#esqlite3_stmt{stmt=Stmt}) -> + esqlite3_nif:column_names(Stmt). %% @doc Return the column types of the prepared statement. %% --spec column_types(statement()) -> {atom()}. -column_types(Statement) -> - column_types(Statement, ?DEFAULT_TIMEOUT). - --spec column_types(statement(), timeout()) -> {atom()}. -column_types(#statement{raw_statement=RawStatement, raw_connection=RawConnection}, Timeout) -> - Ref = make_ref(), - ok = esqlite3_nif:column_types(RawConnection, RawStatement, Ref, self()), - receive_answer(RawConnection, Ref, Timeout). - -%% @doc make multiple sqlite steps per call return rows in reverse order -%% --spec multi_step(term(), pos_integer(), timeout()) -> - {rows, list(tuple())} | - {'$busy', list(tuple())} | - {'$done', list(tuple())} | - {error, _}. -multi_step(#statement{raw_statement=RawStatement, raw_connection=RawConnection}, ChunkSize, Timeout) -> - Ref = make_ref(), - ok = esqlite3_nif:multi_step(RawConnection, RawStatement, ChunkSize, Ref, self()), - receive_answer(RawConnection, Ref, Timeout). +-spec column_decltypes(Statement) -> Types + when Statement :: esqlite3_stmt(), + Types :: list(binary() | undefined). +column_decltypes(#esqlite3_stmt{stmt=Stmt}) -> + esqlite3_nif:column_decltypes(Stmt). %% %% Backup API %% % @doc Initialize a backup procedure. -%% --spec backup_init(connection(), string(), connection(), string()) -> {ok, backup()} | {error, _}. -backup_init(Dest, DestName, Src, SrcName) -> - backup_init(Dest, DestName, Src, SrcName, ?DEFAULT_TIMEOUT). - -%% @doc Like backup_init/4, but with an extra timeout value. -%% --spec backup_init(connection(), string(), connection(), string(), timeout()) -> {ok, backup()} | {error, _}. -backup_init(#connection{raw_connection=Dest}, DestName, #connection{raw_connection=Src}, SrcName, Timeout) -> - Ref = make_ref(), - ok = esqlite3_nif:backup_init(Dest, DestName, Src, SrcName, Ref, self()), - case receive_answer(Dest, Ref, Timeout) of - {ok, RawBackup} when is_reference(RawBackup) -> - {ok, #backup{raw_connection=Dest, raw_backup=RawBackup}}; - {error, _} = Error -> +% See [https://sqlite.org/backup.html] for more details on the backup api. +-spec backup_init(esqlite3(), iodata(), esqlite3(), iodata()) -> {ok, esqlite3_backup()} | {error, _}. +backup_init(#esqlite3{db=Dest}, DestName, #esqlite3{db=Src}, SrcName) -> + case esqlite3_nif:backup_init(Dest, DestName, Src, SrcName) of + {ok, BackupRef} -> + {ok, #esqlite3_backup{backup=BackupRef}}; + {error, _}=Error -> Error end. - %% @doc Release the resources held by the backup. --spec backup_finish(backup()) -> ok | {error, _}. -backup_finish(Backup) -> - backup_finish(Backup, ?DEFAULT_TIMEOUT). -%% @doc Like backup_finish/1, but with an extra timeout. --spec backup_finish(backup(), timeout()) -> ok | {error, _}. -backup_finish(#backup{raw_connection=Conn, raw_backup=Back}, Timeout) -> - Ref = make_ref(), - ok = esqlite3_nif:backup_finish(Conn, Back, Ref, self()), - receive_answer(Conn, Ref, Timeout). +-spec backup_finish(Backup) -> Result when + Backup :: esqlite3_backup(), + Result :: ok | error(). +backup_finish(#esqlite3_backup{backup=Backup}) -> + esqlite3_nif:backup_finish(Backup). %% @doc Do a backup step. --spec backup_step(backup(), integer()) -> ok | {error, _}. -backup_step(Backup, NPage) -> - backup_step(Backup, NPage, ?DEFAULT_TIMEOUT). - -%% @doc Do a backup step. --spec backup_step(backup(), integer(), timeout()) -> ok | {error, _}. -backup_step(#backup{raw_connection=Conn, raw_backup=Back}, NPage, Timeout) -> - Ref = make_ref(), - ok = esqlite3_nif:backup_step(Conn, Back, NPage, Ref, self()), - receive_answer(Conn, Ref, Timeout). - +-spec backup_step(esqlite3_backup(), integer()) -> ok | '$done' | error(). +backup_step(#esqlite3_backup{backup=Backup}, NPage) -> + esqlite3_nif:backup_step(Backup, NPage). %% @doc Get the remaining number of pages which need to be backed up. --spec backup_remaining(backup()) -> {ok, pos_integer()} | {error, _}. -backup_remaining(Backup) -> - backup_remaining(Backup, ?DEFAULT_TIMEOUT). +-spec backup_remaining(Backup) -> Remaining when + Backup :: esqlite3_backup(), + Remaining :: integer(). +backup_remaining(#esqlite3_backup{backup=Backup}) -> + esqlite3_nif:backup_remaining(Backup). %% @doc Get the remaining number of pages which need to be backed up. --spec backup_remaining(backup(), timeout()) -> {ok, pos_integer()} | {error, _}. -backup_remaining(#backup{raw_connection=Conn, raw_backup=Back}, Timeout) -> - Ref = make_ref(), - ok = esqlite3_nif:backup_remaining(Conn, Back, Ref, self()), - case receive_answer(Conn, Ref, Timeout) of - {ok, R} when is_integer(R) -> - {ok, R}; - {error, _}=E -> - E - end. +-spec backup_pagecount(Backup) -> Pagecount when + Backup :: esqlite3_backup(), + Pagecount :: integer(). +backup_pagecount(#esqlite3_backup{backup=Backup}) -> + esqlite3_nif:backup_pagecount(Backup). -%% @doc Get the remaining number of pages which need to be backed up. --spec backup_pagecount(backup()) -> {ok, pos_integer()} | {error, _}. -backup_pagecount(Backup) -> - backup_pagecount(Backup, ?DEFAULT_TIMEOUT). +%% +%% Status +%% -%% @doc Get the remaining number of pages which need to be backed up. --spec backup_pagecount(backup(), timeout()) -> {ok, pos_integer()} | {error, _}. -backup_pagecount(#backup{raw_connection=Conn, raw_backup=Back}, Timeout) -> - Ref = make_ref(), - ok = esqlite3_nif:backup_pagecount(Conn, Back, Ref, self()), - case receive_answer(Conn, Ref, Timeout) of - {ok, R} when is_integer(R) -> - {ok, R}; - {error, _}=E -> - E - end. +%% @doc Get all internal status information from sqlite. +%% +-spec status() -> StatusInfo when + StatusInfo :: status_info(). +status() -> + status(false). + +%% @doc Specify which internal status information you need, when true +%% is passed, the highwater information from the status information +%% will be reset. +-spec status(ArgsOrResetHighWater) -> Status when + ArgsOrResetHighWater :: list(atom()) | boolean(), + Status :: status_info(). +status(Args) when is_list(Args) -> + status(Args, false); +status(ResetHighWater) when ResetHighWater =:= true orelse ResetHighWater =:= false -> + status([memory_used,pagecache_used, pagecache_overflow, malloc_size, + parser_stack, pagecache_size, malloc_count], ResetHighWater); +status(Op) when is_atom(Op) -> + status(Op, false). + +status(Args, ResetHighWater) when is_list(Args) -> + status1(Args, #{}, ResetHighWater); +status(Op, ResetHighWater) -> + esqlite3_nif:status(op_arg(Op), reset_arg(ResetHighWater)). %% %% Helpers %% --spec foreach_s(Fun, statement()) -> ok when - Fun :: fun((Row) -> any()) | fun((ColumnNames, Row) -> any()), - Row :: row(), - ColumnNames :: tuple(). -foreach_s(Fun, Statement) when is_function(Fun, 1) -> - case try_multi_step(Statement, 1, [], 0) of - {'$done', []} -> - ok; - {error, _} = Error -> - Error; - {rows, [Row | []]} -> - Fun(Row), - foreach_s(Fun, Statement) - end; -foreach_s(Fun, Statement) when is_function(Fun, 2) -> - ColumnNames = column_names(Statement), - case try_multi_step(Statement, 1, [], 0) of - {'$done', []} -> - ok; - {error, _} = Error -> - Error; - {rows, [Row | []]} -> - Fun(ColumnNames, Row), - foreach_s(Fun, Statement) +status1([], Acc, _ResetArg) -> Acc; +status1([S|Rest], Acc, ResetArg) -> + status1(Rest, Acc#{ S => status(S, ResetArg)}, ResetArg). + +reset_arg(true) -> 1; +reset_arg(false) -> 0. + +op_arg(memory_used) -> 0; +op_arg(pagecache_used) -> 1; +op_arg(pagecache_overflow) -> 2; +op_arg(malloc_size) -> 5; +op_arg(parser_stack) -> 6; +op_arg(pagecache_size) -> 7; +op_arg(malloc_count) -> 8. + +props_to_prepare_flag(Props) -> + Flag = case proplists:get_value(no_vtab, Props, false) of + true -> ?SQLITE_PREPARE_NO_VTAB; + false -> 0 + end, + case proplists:get_value(persistent, Props, false) of + true -> Flag bor ?SQLITE_PREPARE_PERSISTENT; + false -> Flag end. --spec map_s(Fun, statement()) -> list(Type) when - Fun :: fun((Row) -> Type) | fun((ColumnNames, Row) -> Type), - Row :: row(), - ColumnNames :: tuple(), - Type :: term(). -map_s(Fun, Statement) when is_function(Fun, 1) -> - case try_multi_step(Statement, 1, [], 0) of - {'$done', []} -> - []; - {error, _} = Error -> - Error; - {rows, [Row | []]} -> - [Fun(Row) | map_s(Fun, Statement)] - end; -map_s(Fun, Statement) when is_function(Fun, 2) -> - ColumnNames = column_names(Statement), - case try_multi_step(Statement, 1, [], 0) of - {'$done', []} -> - []; - {error, _} = Error -> - Error; - {rows, [Row | []]} -> - [Fun(ColumnNames, Row) | map_s(Fun, Statement)] - end. -%% return rows in reverse order --spec fetchall_internal(statement(), pos_integer(), list(row()), timeout()) -> - {'$done', list(row())} | - {error, _}. -fetchall_internal(Statement, ChunkSize, Rest, Timeout) -> - case try_multi_step(Statement, ChunkSize, Rest, 0, Timeout) of - {rows, Rows} -> fetchall_internal(Statement, ChunkSize, Rows, Timeout); - Else -> Else - end. - -%% Try a number of steps, when the database is busy, -%% return rows in revers order -try_multi_step(Statement, ChunkSize, Rest, Tries) -> - try_multi_step(Statement, ChunkSize, Rest, Tries, ?DEFAULT_TIMEOUT). - -%% Try a number of steps, when the database is busy, -%% return rows in revers order --spec try_multi_step(statement(), pos_integer(), list(tuple()), non_neg_integer(), timeout()) -> - {rows, list(tuple())} | - {'$done', list(tuple())} | - {error, term()}. -try_multi_step(_Statement, _ChunkSize, _Rest, Tries, _Timeout) when Tries > 5 -> - throw(too_many_tries); -try_multi_step(Statement, ChunkSize, Rest, Tries, Timeout) -> - case multi_step(Statement, ChunkSize, Timeout) of - {'$busy', Rows} -> %% core can fetch a number of rows (rows < ChunkSize) per 'multi_step' call and then get busy... - erlang:display({"busy", Tries}), - timer:sleep(100 * Tries), - try_multi_step(Statement, ChunkSize, Rows ++ Rest, Tries + 1, Timeout); - {rows, Rows} -> - {rows, Rows ++ Rest}; - {'$done', Rows} -> - {'$done', Rows ++ Rest}; - Else -> Else - end. - -receive_answer(RawConnection, Ref, Timeout) -> - receive - {esqlite3, Ref, Resp} -> Resp - after - Timeout -> - ok = esqlite3_nif:interrupt(RawConnection), - throw({error, timeout, Ref}) - end. - -flush_answers() -> - receive - {esqlite3, _, _} -> flush_answers() - after - 0 -> ok - end. diff --git a/src/esqlite3_nif.erl b/src/esqlite3_nif.erl index 2e7e3b6..e341f82 100644 --- a/src/esqlite3_nif.erl +++ b/src/esqlite3_nif.erl @@ -1,3 +1,9 @@ +%% @author Maas-Maarten Zeeman +%% @copyright 2011 - 2022 Maas-Maarten Zeeman +%% +%% @doc Low level Erlang API for sqlite3 databases. +%% @end + %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at @@ -9,47 +15,67 @@ %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. -%% -%% @author Maas-Maarten Zeeman -%% @copyright 2011 - 2022 Maas-Maarten Zeeman -%% -%% @doc Low level erlang API for sqlite3 databases. -module(esqlite3_nif). -author("Maas-Maarten Zeeman "). %% low-level exports -export([ - start/0, - open/4, - set_update_hook/4, - exec/4, - changes/3, - insert/4, - last_insert_rowid/3, - get_autocommit/3, - prepare/4, - multi_step/5, - reset/4, - finalize/4, - bind/5, - column_names/4, - column_types/4, - backup_init/6, - backup_step/5, - backup_remaining/4, - backup_pagecount/4, - backup_finish/4, + open/1, + close/1, + error_info/1, + + set_update_hook/2, + + get_autocommit/1, + last_insert_rowid/1, + changes/1, + exec/2, + prepare/3, + + column_names/1, + column_decltypes/1, + + bind_int/3, + bind_int64/3, + bind_double/3, + bind_text/3, + bind_blob/3, + bind_null/2, + + step/1, + + reset/1, + interrupt/1, - close/3 + + backup_init/4, + backup_remaining/1, + backup_pagecount/1, + backup_step/2, + backup_finish/1, + + memory_stats/1, + status/2 ]). --type raw_connection() :: reference(). --type raw_statement() :: reference(). --type raw_backup() :: reference(). --type sql() :: iodata(). +-type esqlite3_ref() :: reference(). % Reference to a database connection handle. See [https://sqlite.org/c3ref/sqlite3.html] for more details. +-type esqlite3_stmt_ref() :: reference(). % Reference to a prepared statement object. See [https://sqlite.org/c3ref/stmt.html] for more details. +-type esqlite3_backup_ref() :: reference(). % Reference to a online backup object. See [https://sqlite.org/c3ref/backup.html] for more details. +-type sql() :: iodata(). % Make sure the iodata contains utf-8 encoded data. +-type rowid() :: integer(). +-type cell() :: undefined | integer() | float() | binary(). +-type row() :: list(cell()). +-type extended_errcode() :: integer(). % Extended sqlite3 error code. See [https://sqlite.org/rescode.html] for more details. +-type error() :: {error, extended_errcode()}. +-type error_info() :: #{ errcode := integer(), + extended_errcode := extended_errcode(), + errstr := unicode:unicode_binary(), % English-language text that describes the result code, as UTF-8 + errmsg := unicode:unicode_binary(), % English-language text that describes the error, as UTF-8 + error_offset := integer() % The byte offset to the token in the input sql. + }. % See: [https://sqlite.org/c3ref/errcode.html] for more information. --export_type([raw_connection/0, raw_statement/0, raw_backup/0, sql/0]). +-export_type([esqlite3_ref/0, esqlite3_stmt_ref/0, esqlite3_backup_ref/0, sql/0, rowid/0, cell/0, row/0, error/0, error_info/0]). -on_load(init/0). @@ -61,138 +87,225 @@ init() -> end, ok = erlang:load_nif(NifFileName, 0). -%% @doc Start a low level thread which will can handle sqlite3 calls. -%% --spec start() -> {ok, raw_connection()} | {error, _}. -start() -> - erlang:nif_error(nif_library_not_loaded). -%% @doc Open the specified sqlite3 database. -%% -%% Sends an asynchronous open command over the connection and returns -%% ok immediately. When the database is opened -%% --spec open(raw_connection(), reference(), pid(), string()) -> ok | {error, _}. -open(_Db, _Ref, _Dest, _Filename) -> - erlang:nif_error(nif_library_not_loaded). - --spec set_update_hook(raw_connection(), reference(), pid(), pid()) -> ok | {error, _}. -set_update_hook(_Db, _Ref, _Dest, _Pid) -> - erlang:nif_error(nif_library_not_loaded). - -%% @doc Exec the query. -%% -%% Sends an asynchronous exec command over the connection and returns -%% ok immediately. -%% -%% When the statement is executed Dest will receive message {Ref, answer()} -%% with answer() integer | {error, reason()} -%% --spec exec(raw_connection(), reference(), pid(), sql()) -> ok | {error, _}. -exec(_Db, _Ref, _Dest, _Sql) -> - erlang:nif_error(nif_library_not_loaded). - -%% @doc Get the number of affected rows of last statement -%% -%% When the statement is executed Dest will receive message {Ref, answer()} -%% with answer() integer | {error, reason()} --spec changes(raw_connection(), reference(), pid()) -> ok | {error, _}. -changes(_Db, _Ref, _Dest) -> - erlang:nif_error(nif_library_not_loaded). - -%% @doc -%% --spec prepare(raw_connection(), reference(), pid(), sql()) -> ok | {error, _}. -prepare(_Db, _Ref, _Dest, _Sql) -> - erlang:nif_error(nif_library_not_loaded). - -%% @doc -%% --spec multi_step(raw_connection(), raw_statement(), pos_integer(), reference(), pid()) -> ok | {error, _}. -multi_step(_Db, _Stmt, _Chunk_Size, _Ref, _Dest) -> - erlang:nif_error(nif_library_not_loaded). - -%% @doc -%% --spec reset(raw_connection(), raw_statement(), reference(), pid()) -> ok | {error, _}. -reset(_Db, _Stmt, _Ref, _Dest) -> - erlang:nif_error(nif_library_not_loaded). - -%% @doc -%% --spec finalize(raw_connection(), raw_statement(), reference(), pid()) -> ok | {error, _}. -finalize(_Db, _Stmt, _Ref, _Dest) -> - erlang:nif_error(nif_library_not_loaded). - -%% @doc Bind parameters to a prepared statement. -%% --spec bind(raw_connection(), raw_statement(), reference(), pid(), list(any())) -> ok | {error, _}. -bind(_Db, _Stmt, _Ref, _Dest, _Args) -> - erlang:nif_error(nif_library_not_loaded). - -%% @doc Retrieve the column names of the prepared statement -%% --spec column_names(raw_connection(), raw_statement(), reference(), pid()) -> ok | {error, _}. -column_names(_Db, _Stmt, _Ref, _Dest) -> - erlang:nif_error(nif_library_not_loaded). - -%% @doc Retrieve the column types of the prepared statement -%% --spec column_types(raw_connection(), raw_statement(), reference(), pid()) -> ok | {error, _}. -column_types(_Db, _Stmt, _Ref, _Dest) -> - erlang:nif_error(nif_library_not_loaded). - -%% @doc Initialize a backup procedure of a database. --spec backup_init(raw_connection(), string(), raw_connection(), string(), reference(), pid()) -> ok | {error, _}. -backup_init(_DestDb, _DestName, _SourceDb, _SourceName, _Ref, _Dest) -> - erlang:nif_error(nif_library_not_loaded). - -%% @doc Do a backup step. --spec backup_step(raw_connection(), raw_backup(), integer(), reference(), pid()) -> ok | {error, _}. -backup_step(_Db, _Backup, _NPages, _Ref, _Dest) -> - erlang:nif_error(nif_library_not_loaded). - -%% @doc Get the amount of remaining pages which need to be backed up. --spec backup_remaining(raw_connection(), raw_backup(), reference(), pid()) -> ok | {error, _}. -backup_remaining(_Db, _Backup, _Ref, _Dest) -> - erlang:nif_error(nif_library_not_loaded). - -%% @doc Get the total number of pages which need to be backed up. --spec backup_pagecount(raw_connection(), raw_backup(), reference(), pid()) -> ok | {error, _}. -backup_pagecount(_Db, _Backup, _Ref, _Dest) -> - erlang:nif_error(nif_library_not_loaded). - -%% @doc Finish the backup. --spec backup_finish(raw_connection(), raw_backup(), reference(), pid()) -> ok | {error, _}. -backup_finish(_Db, _Backup, _Ref, _Dest) -> - erlang:nif_error(nif_library_not_loaded). - -%% @doc Interrupt all active queries. --spec interrupt(raw_connection()) -> ok. -interrupt(_Db) -> +%% @doc Open the specified sqlite3 database. +%% It is possible to use sqlite's uri filenames to open files. +%% See: [https://sqlite.org/uri.html] for more information. +-spec open(Filename) -> OpenResult when + Filename :: string(), + OpenResult :: {ok, esqlite3_ref()} | error(). +open(_Filename) -> erlang:nif_error(nif_library_not_loaded). %% @doc Close the connection. %% --spec close(raw_connection(), reference(), pid()) -> ok | {error, _}. -close(_Db, _Ref, _Dest) -> +-spec close(Connection) -> CloseResult + when Connection :: esqlite3_ref(), + CloseResult :: ok | {error, _}. +close(_Db) -> erlang:nif_error(nif_library_not_loaded). -%% @doc Insert record +%% @doc Get an error messages for the last occurred error. %% --spec insert(raw_connection(), reference(), pid(), sql()) -> ok | {error, _}. -insert(_Db, _Ref, _Dest, _Sql) -> +-spec error_info(Connection) -> ErrorInfo + when Connection :: esqlite3_ref(), + ErrorInfo :: error_info(). +error_info(_Db) -> + erlang:nif_error(nif_library_not_loaded). + + +%% @doc Set an update hook +-spec set_update_hook(Connection, Pid) -> Result + when Connection :: esqlite3_ref(), + Pid :: pid(), + Result :: ok. +set_update_hook(_Db, _Pid) -> + erlang:nif_error(nif_library_not_loaded). + + +%% @doc Execute a sql statement +-spec exec(Connection, Sql) -> ExecResult + when Connection :: esqlite3_ref(), + Sql :: sql(), + ExecResult :: ok | error(). +exec(_Connection, _Sql) -> + erlang:nif_error(nif_library_not_loaded). + + +%% @doc Compile a sql statement. +-spec prepare(Connection, Sql, PrepareFlags) -> PrepareResult + when Connection :: esqlite3_ref(), + Sql :: sql(), + PrepareFlags :: non_neg_integer(), + PrepareResult :: {ok, esqlite3_stmt_ref()} | error(). +prepare(_Connection, _Sql, _PrepareFlags) -> + erlang:nif_error(nif_library_not_loaded). + +% @doc Bind an integer to a position in a prepared statement. +-spec bind_int(Statement, Index, Value) -> Result when + Statement :: esqlite3_stmt_ref(), + Index :: integer(), + Value :: integer(), %% [todo] Should be a 32 bit integer range. + Result :: ok | error(). +bind_int(_Statement, _Index, _Value) -> + erlang:nif_error(nif_library_not_loaded). + +% @doc Bind a 64 bit integer to a position in a prepared statement. +-spec bind_int64(Statement, Index, Value) -> Result when + Statement :: esqlite3_stmt_ref(), + Index :: integer(), + Value :: integer(), %% [todo] Should be a 64 bit integer range. + Result :: ok | error(). +bind_int64(_Statement, _Index, _Value) -> + erlang:nif_error(nif_library_not_loaded). + +% @doc Bind an double/float to a position in a prepared statement. +-spec bind_double(Statement, Index, Value) -> Result when + Statement :: esqlite3_stmt_ref(), + Index :: integer(), + Value :: float(), %% [todo] Should be a 64 bit integer range. + Result :: ok | error(). +bind_double(_Statement, _Index, _Value) -> + erlang:nif_error(nif_library_not_loaded). + +% @doc Bind a utf-8 string to a position in a prepared statement. +-spec bind_text(Statement, Index, Value) -> Result when + Statement :: esqlite3_stmt_ref(), + Index :: integer(), + Value :: iodata(), %% [todo] Should be a utf-8 iodata. + Result :: ok | error(). +bind_text(_Statement, _Index, _Value) -> + erlang:nif_error(nif_library_not_loaded). + +% @doc Bind a blob to a position in a prepared statement. +-spec bind_blob(Statement, Index, Value) -> Result when + Statement :: esqlite3_stmt_ref(), + Index :: integer(), + Value :: iodata(), + Result :: ok | error(). +bind_blob(_Statement, _Index, _Value) -> + erlang:nif_error(nif_library_not_loaded). + +% @doc Bind a null to a position in a prepared statement. +-spec bind_null(Statement, Index) -> Result when + Statement :: esqlite3_stmt_ref(), + Index :: integer(), + Result :: ok | error(). +bind_null(_Statement, _Index) -> + erlang:nif_error(nif_library_not_loaded). + +-spec step(Statement) -> StepResult when + Statement :: esqlite3_stmt_ref(), + StepResult :: row() | '$done' | error(). +step(_Statement) -> + erlang:nif_error(nif_library_not_loaded). + +-spec reset(Statement) -> ResetResult when + Statement :: esqlite3_stmt_ref(), + ResetResult :: ok | error(). +reset(_Statement) -> + erlang:nif_error(nif_library_not_loaded). + +%% @doc Retrieve the column names of the prepared statement +%% +-spec column_names(Statement) -> Names when + Statement :: esqlite3_stmt_ref(), + Names :: list(unicode:unicode_binary()). +column_names(_Stmt) -> + erlang:nif_error(nif_library_not_loaded). + +%% @doc Retrieve the declared datatypes of all columns. +%% +-spec column_decltypes(Statement) -> Types when + Statement :: esqlite3_stmt_ref(), + Types :: list(undefined | unicode:unicode_binary()). +column_decltypes(_Stmt) -> + erlang:nif_error(nif_library_not_loaded). + + +%% @doc Initialize a backup procedure of a database. +-spec backup_init(Destination, DestinationName, Source, SourceName) -> InitResult when + Destination :: esqlite3_ref(), + DestinationName :: iodata(), + Source :: esqlite3_ref(), + SourceName :: iodata(), + InitResult :: {ok, esqlite3_backup_ref()} | error(). +backup_init(_Dest, _DestName, _Src, _SrcName) -> + erlang:nif_error(nif_library_not_loaded). + +-spec backup_remaining(Backup) -> Remaining when + Backup :: esqlite3_backup_ref(), + Remaining :: integer(). +backup_remaining(_Backup) -> + erlang:nif_error(nif_library_not_loaded). + +-spec backup_pagecount(Backup) -> Pagecount when + Backup :: esqlite3_backup_ref(), + Pagecount :: integer(). +backup_pagecount(_Backup) -> + erlang:nif_error(nif_library_not_loaded). + +-spec backup_step(Backup, NPage) -> Result when + Backup :: esqlite3_backup_ref(), + NPage :: integer(), + Result :: ok | '$done' | error(). +backup_step(_Backup, _PageCount) -> + erlang:nif_error(nif_library_not_loaded). + +-spec backup_finish(Backup) -> Result when + Backup :: esqlite3_backup_ref(), + Result :: ok | error(). +backup_finish(_Backup) -> + erlang:nif_error(nif_library_not_loaded). + +%% @doc Interrupt all active queries. +-spec interrupt(esqlite3_ref()) -> ok. +interrupt(_Db) -> erlang:nif_error(nif_library_not_loaded). %% @doc Get the last insert rowid. %% --spec last_insert_rowid(raw_connection(), reference(), pid()) -> ok | {error, _}. -last_insert_rowid(_Db, _Ref, _Dest) -> +-spec last_insert_rowid(esqlite3_ref()) -> rowid(). +last_insert_rowid(_Connection) -> erlang:nif_error(nif_library_not_loaded). -%% @doc Get automcommit +%% @doc Get number of changes insert, delete of the most recent completed +%% INSERT, DELETE or UPDATE statement. %% --spec get_autocommit(raw_connection(), reference(), pid()) -> ok | {error, _}. -get_autocommit(_Db, _Ref, _Dest) -> +-spec changes(esqlite3_ref()) -> integer(). +changes(_Connection) -> + erlang:nif_error(nif_library_not_loaded). + +%% @doc Get autocommit +%% +-spec get_autocommit(esqlite3_ref()) -> true | false. +get_autocommit(_Connection) -> + erlang:nif_error(nif_library_not_loaded). + +%% @doc Get memory statistics +%% +-spec memory_stats(HighwaterResetFlag) -> Stats when + HighwaterResetFlag :: integer(), + Stats :: #{ used := integer(), highwater := integer() }. +memory_stats(_Flag) -> + erlang:nif_error(nif_library_not_loaded). + +%% @doc Get sqlite status information. +%% +%% +%% MEMORY_USED 0 +%% PAGECACHE_USED 1 +%% PAGECACHE_OVERFLOW 2 +%% MALLOC_SIZE 5 +%% PARSER_STACK 6 +%% PAGECACHE_SIZE 7 +%% MALLOC_COUNT 8 +%% +%% +-spec status(Op, HighwaterResetFlag) -> Stats when + Op :: integer(), + HighwaterResetFlag :: integer(), + Stats :: #{ used := non_neg_integer(), highwater := non_neg_integer() }. +status(_Op, _Flag) -> erlang:nif_error(nif_library_not_loaded). diff --git a/test/esqlite_test.erl b/test/esqlite_test.erl index 8a0aab4..6cf8de7 100644 --- a/test/esqlite_test.erl +++ b/test/esqlite_test.erl @@ -19,27 +19,82 @@ close_test() -> {ok, C} = esqlite3:open(":memory:"), ok = esqlite3:close(C), - %% Check if functions still return sensible values. - {error, closed} = esqlite3:set_update_hook(self(), C), - {error, closed} = esqlite3:changes(C), - {error, closed} = esqlite3:get_autocommit(C), - {error, closed} = esqlite3:last_insert_rowid(C), + %% Double close should also work. + ok = esqlite3:close(C), - {error, _} = esqlite3:exec("create table test(one, two, three)", C), + %% Check if functions still return sensible values. + ?assertError(closed, esqlite3:set_update_hook(C, self())), + ?assertError(closed, esqlite3:changes(C)), + ?assertError(closed, esqlite3:get_autocommit(C)), + ?assertError(closed, esqlite3:last_insert_rowid(C)), + + ?assertEqual({error, 21}, + esqlite3:exec(C, "create table test(one, two, three)")), + + ok. + +prepare_test() -> + {ok, C} = esqlite3:open(":memory:"), + ?assertMatch({ok, {esqlite3_stmt, _}}, esqlite3:prepare(C, "select 1")), + ok = esqlite3:close(C), + ok. + +prepare_after_close_test() -> + {ok, C} = esqlite3:open(":memory:"), + ?assertEqual(ok, esqlite3:close(C)), + ?assertMatch({error, 21}, esqlite3:prepare(C, "select 1")), + ok. + +column_names_test() -> + {ok, C} = esqlite3:open(":memory:"), + + {ok, Stmt} = esqlite3:prepare(C, "select 1 as one"), + ?assertEqual([<<"one">>], esqlite3:column_names(Stmt)), + + {ok, Stmt1} = esqlite3:prepare(C, <<"select 1 as 😀"/utf8>>), + ?assertEqual([<<"😀"/utf8>>], esqlite3:column_names(Stmt1)), + + {ok, Stmt2} = esqlite3:prepare(C, <<"select 1">>), + ?assertEqual([<<"1">>], esqlite3:column_names(Stmt2)), + + {ok, Stmt3} = esqlite3:prepare(C, <<"select 1, 2, 3">>), + ?assertEqual([<<"1">>, <<"2">>, <<"3">>], esqlite3:column_names(Stmt3)), + + ok. + +column_decltypes_test() -> + {ok, C} = esqlite3:open(":memory:"), + {ok, Stmt} = esqlite3:prepare(C, "select 1, 2, 3"), + ?assertEqual([undefined, undefined, undefined], esqlite3:column_decltypes(Stmt)), + + ok. + +step_test() -> + {ok, C} = esqlite3:open(":memory:"), + {ok, Stmt} = esqlite3:prepare(C, "select 1, 2, 3;" ), + + ?assertEqual([1,2,3], esqlite3:step(Stmt)), + ?assertEqual('$done', esqlite3:step(Stmt)), + + %% After the done, the statement is reset and + ?assertEqual([1,2,3], esqlite3:step(Stmt)), + ?assertEqual('$done', esqlite3:step(Stmt)), ok. iodata_test() -> {ok, C} = esqlite3:open(":memory:"), - {error, no_iodata} = esqlite3:exec(1000, C), - {error, no_iodata} = esqlite3:insert(1000, C), + + ?assertError(badarg, esqlite3:exec(C, 1000)), + ?assertError(badarg, esqlite3:exec(C, 1000)), ok. - open_multiple_same_databases_test() -> cleanup(), + %% Sqlite allows opening the same file multiple + %% times {ok, _C1} = esqlite3:open(?DB1), {ok, _C2} = esqlite3:open(?DB1), @@ -55,71 +110,91 @@ open_multiple_different_databases_test() -> get_autocommit_test() -> {ok, Db} = esqlite3:open(":memory:"), - ok = esqlite3:exec("CREATE TABLE test (id INTEGER PRIMARY KEY, val STRING);", Db), + + %% By default, the database is in autocommit mode true = esqlite3:get_autocommit(Db), - ok = esqlite3:exec("BEGIN;", Db), + ok = esqlite3:exec(Db, "CREATE TABLE test (id INTEGER PRIMARY KEY, val STRING);"), + true = esqlite3:get_autocommit(Db), + + %% After a begin statement, the connection will not be in autocommit mode anymore + ok = esqlite3:exec(Db, "BEGIN;"), false = esqlite3:get_autocommit(Db), - ok = esqlite3:exec("INSERT INTO test (val) VALUES ('this is a test');", Db), - ok = esqlite3:exec("COMMIT;", Db), + ok = esqlite3:exec(Db, "INSERT INTO test (val) VALUES ('this is a test');"), + ok = esqlite3:exec(Db, "COMMIT;"), + + %% After a commit statement, the connection will be in autocommit mode true = esqlite3:get_autocommit(Db), + ok. last_insert_rowid_test() -> {ok, Db} = esqlite3:open(":memory:"), - ok = esqlite3:exec("CREATE TABLE test (id INTEGER PRIMARY KEY, val STRING);", Db), - ok = esqlite3:exec("INSERT INTO test (val) VALUES ('this is a test');", Db), - {ok, 1} = esqlite3:last_insert_rowid(Db), - ok = esqlite3:exec("INSERT INTO test (val) VALUES ('this is another test');", Db), - {ok, 2} = esqlite3:last_insert_rowid(Db), + ok = esqlite3:exec(Db, "CREATE TABLE test (id INTEGER PRIMARY KEY, val STRING);"), + ok = esqlite3:exec(Db, "INSERT INTO test (val) VALUES ('this is a test');"), + 1 = esqlite3:last_insert_rowid(Db), + ok = esqlite3:exec(Db, "INSERT INTO test (val) VALUES ('this is another test');"), + 2 = esqlite3:last_insert_rowid(Db), ok. update_hook_test() -> {ok, Db} = esqlite3:open(":memory:"), - ok = esqlite3:set_update_hook(self(), Db), - ok = esqlite3:exec("CREATE TABLE test (id INTEGER PRIMARY KEY, val STRING);", Db), - ok = esqlite3:exec("INSERT INTO test (val) VALUES ('this is a test');", Db), - ok = receive {insert, "test", 1} -> ok after 150 -> no_message end, - ok = esqlite3:exec("UPDATE test SET val = 'a new test' WHERE id = 1;", Db), - ok = receive {update, "test", 1} -> ok after 150 -> no_message end, - ok = esqlite3:exec("DELETE FROM test WHERE id = 1;", Db), - ok = receive {delete, "test", 1} -> ok after 150 -> no_message end, + ok = esqlite3:set_update_hook(Db, self()), + + ok = esqlite3:exec(Db, "CREATE TABLE test (id INTEGER PRIMARY KEY, val STRING);"), + ok = esqlite3:exec(Db, "INSERT INTO test (val) VALUES ('this is a test');"), + + ok = receive {insert, <<"main">>, <<"test">>, 1} -> ok after 150 -> no_message end, + + ok = esqlite3:exec(Db, "UPDATE test SET val = 'a new test' WHERE id = 1;"), + + ok = receive {update, <<"main">>, <<"test">>, 1} -> ok after 150 -> no_message end, + + ok = esqlite3:exec(Db, "DELETE FROM test WHERE id = 1;"), + + ok = receive {delete, <<"main">>, <<"test">>, 1} -> ok after 150 -> no_message end, + + ok = esqlite3:set_update_hook(Db, undefined), + + ok = esqlite3:exec(Db, "INSERT INTO test (val) VALUES ('this is a test');"), + no_message = receive {insert, <<"main">>, <<"test">>, 1} -> ok after 150 -> no_message end, + ok. simple_query_test() -> {ok, Db} = esqlite3:open(":memory:"), - ok = esqlite3:exec("begin;", Db), - ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db), - ok = esqlite3:exec("insert into test_table values('hello1', 10);", Db), - {ok, 1} = esqlite3:changes(Db), + ok = esqlite3:exec(Db, "begin;"), + ok = esqlite3:exec(Db, "create table test_table(one varchar(10), two int);"), + ok = esqlite3:exec(Db, "insert into test_table values('hello1', 10);"), + ?assertEqual(1, esqlite3:changes(Db)), - ok = esqlite3:exec("insert into test_table values('hello2', 11);", Db), - {ok, 1} = esqlite3:changes(Db), - ok = esqlite3:exec("insert into test_table values('hello3', 12);", Db), - {ok, 1} = esqlite3:changes(Db), - ok = esqlite3:exec("insert into test_table values('hello4', 13);", Db), - {ok, 1} = esqlite3:changes(Db), - ok = esqlite3:exec("commit;", Db), - ok = esqlite3:exec("select * from test_table;", Db), + ok = esqlite3:exec(Db, "insert into test_table values('hello2', 11);"), + ?assertEqual(1, esqlite3:changes(Db)), + ok = esqlite3:exec(Db, "insert into test_table values('hello3', 12);"), + ?assertEqual(1, esqlite3:changes(Db)), + ok = esqlite3:exec(Db, "insert into test_table values('hello4', 13);"), + ?assertEqual(1, esqlite3:changes(Db)), + ok = esqlite3:exec(Db, "commit;"), + ok = esqlite3:exec(Db, "select * from test_table;"), - ok = esqlite3:exec("delete from test_table;", Db), - {ok, 4} = esqlite3:changes(Db), + ok = esqlite3:exec(Db, "delete from test_table;"), + ?assertEqual(4, esqlite3:changes(Db)), ok. -prepare_test() -> +prepare2_test() -> {ok, Db} = esqlite3:open(":memory:"), - esqlite3:exec("begin;", Db), - esqlite3:exec("create table test_table(one varchar(10), two int);", Db), - {ok, Statement} = esqlite3:prepare("insert into test_table values('one', 2)", Db), + esqlite3:exec(Db, "begin;"), + esqlite3:exec(Db, "create table test_table(one varchar(10), two int);"), + {ok, Statement} = esqlite3:prepare(Db, "insert into test_table values('one', 2)"), '$done' = esqlite3:step(Statement), - {ok, 1} = esqlite3:changes(Db), + 1 = esqlite3:changes(Db), - ok = esqlite3:exec("insert into test_table values('hello4', 13);", Db), + ok = esqlite3:exec(Db, "insert into test_table values('hello4', 13);"), %% Check if the values are there. - [{<<"one">>, 2}, {<<"hello4">>, 13}] = esqlite3:q("select * from test_table order by two", Db), - esqlite3:exec("commit;", Db), + [[<<"one">>, 2], [<<"hello4">>, 13]] = esqlite3:q(Db, "select * from test_table order by two"), + esqlite3:exec(Db, "commit;"), esqlite3:close(Db), ok. @@ -127,178 +202,176 @@ prepare_test() -> bind_test() -> {ok, Db} = esqlite3:open(":memory:"), - ok = esqlite3:exec("begin;", Db), - ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db), - ok = esqlite3:exec("commit;", Db), + ok = esqlite3:exec(Db, "begin;"), + ok = esqlite3:exec(Db, "create table test_table(one varchar(10), two int);"), + ok = esqlite3:exec(Db, "commit;"), %% Create a prepared statement - {ok, Statement} = esqlite3:prepare("insert into test_table values(?1, ?2)", Db), - esqlite3:bind(Statement, [one, 2]), + {ok, Statement} = esqlite3:prepare(Db, "insert into test_table values(?1, ?2)"), + ok = esqlite3:bind(Statement, [one, 2]), + '$done' = esqlite3:step(Statement), + ok = esqlite3:bind(Statement, ["three", 4]), esqlite3:step(Statement), - esqlite3:bind(Statement, ["three", 4]), + ok = esqlite3:bind(Statement, ["five", 6]), esqlite3:step(Statement), - esqlite3:bind(Statement, ["five", 6]), + ok = esqlite3:bind(Statement, [[<<"se">>, $v, "en"], 8]), % iolist bound as text esqlite3:step(Statement), - esqlite3:bind(Statement, [[<<"se">>, $v, "en"], 8]), % iolist bound as text + ok = esqlite3:bind(Statement, [<<"nine">>, 10]), % iolist bound as text esqlite3:step(Statement), - esqlite3:bind(Statement, [<<"nine">>, 10]), % iolist bound as text + ok = esqlite3:bind(Statement, [{blob, [<<"eleven">>, 0]}, 12]), % iolist bound as blob with trailing eos. esqlite3:step(Statement), - esqlite3:bind(Statement, [{blob, [<<"eleven">>, 0]}, 12]), % iolist bound as blob with trailing eos. - esqlite3:step(Statement), - esqlite3:bind(Statement, ["empty", undefined]), % 'undefined' is converted to SQL null + ok = esqlite3:bind(Statement, ["empty", undefined]), % 'undefined' is converted to SQL null esqlite3:step(Statement), %% int64 - esqlite3:bind(Statement, [int64, 308553449069486081]), + ok = esqlite3:bind(Statement, [int64, 308553449069486081]), esqlite3:step(Statement), - +% %% negative int64 - esqlite3:bind(Statement, [negative_int64, -308553449069486081]), + ok = esqlite3:bind(Statement, [negative_int64, -308553449069486081]), esqlite3:step(Statement), - %% utf-8 - esqlite3:bind(Statement, [[<<228,184,138,230,181,183>>], 100]), + ok = esqlite3:bind(Statement, [[<<228,184,138,230,181,183>>], 100]), esqlite3:step(Statement), - ?assertEqual([{<<"one">>, 2}], - esqlite3:q("select one, two from test_table where two = '2'", Db)), - ?assertEqual([{<<"three">>, 4}], - esqlite3:q("select one, two from test_table where two = 4", Db)), - ?assertEqual([{<<"five">>, 6}], - esqlite3:q("select one, two from test_table where two = 6", Db)), - ?assertEqual([{<<"seven">>, 8}], - esqlite3:q("select one, two from test_table where two = 8", Db)), - ?assertEqual([{<<"nine">>, 10}], - esqlite3:q("select one, two from test_table where two = 10", Db)), - ?assertEqual([{{blob, <<$e,$l,$e,$v,$e,$n,0>>}, 12}], - esqlite3:q("select one, two from test_table where two = 12", Db)), - ?assertEqual([{<<"empty">>, undefined}], - esqlite3:q("select one, two from test_table where two is null", Db)), + ?assertEqual([[<<"one">>, 2]], + esqlite3:q(Db, "select one, two from test_table where two = '2'")), + ?assertEqual([[<<"three">>, 4]], + esqlite3:q(Db, "select one, two from test_table where two = 4")), + ?assertEqual([[<<"five">>, 6]], + esqlite3:q(Db, "select one, two from test_table where two = 6")), + ?assertEqual([[<<"seven">>, 8]], + esqlite3:q(Db, "select one, two from test_table where two = 8")), + ?assertEqual([[<<"nine">>, 10]], + esqlite3:q(Db, "select one, two from test_table where two = 10")), + ?assertEqual([[<<$e,$l,$e,$v,$e,$n,0>>, 12]], + esqlite3:q(Db, "select one, two from test_table where two = 12")), + ?assertEqual([[<<"empty">>, undefined]], + esqlite3:q(Db, "select one, two from test_table where two is null")), - ?assertEqual([{<<"int64">>, 308553449069486081}], - esqlite3:q("select one, two from test_table where one = 'int64';", Db)), - ?assertEqual([{<<"negative_int64">>, -308553449069486081}], - esqlite3:q("select one, two from test_table where one = 'negative_int64';", Db)), + ?assertEqual([[<<"int64">>, 308553449069486081]], + esqlite3:q(Db, "select one, two from test_table where one = 'int64';")), + ?assertEqual([[<<"negative_int64">>, -308553449069486081]], + esqlite3:q(Db, "select one, two from test_table where one = 'negative_int64';")), %% utf-8 - ?assertEqual([{<<228,184,138,230,181,183>>, 100}], - esqlite3:q("select one, two from test_table where two = 100", Db)), + ?assertEqual([[<<228,184,138,230,181,183>>, 100]], + esqlite3:q(Db, "select one, two from test_table where two = 100")), ok. bind_for_queries_test() -> {ok, Db} = esqlite3:open(":memory:"), - ok = esqlite3:exec("begin;", Db), - ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db), - ok = esqlite3:exec("commit;", Db), + ok = esqlite3:exec(Db, "begin;"), + ok = esqlite3:exec(Db, "create table test_table(one varchar(10), two int);"), + ok = esqlite3:exec(Db, "commit;"), - ?assertEqual([{1}], esqlite3:q(<<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>, - [test_table], Db)), - ?assertEqual([{1}], esqlite3:q(<<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>, - ["test_table"], Db)), - ?assertEqual([{1}], esqlite3:q(<<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>, - [<<"test_table">>], Db)), - ?assertEqual([{1}], esqlite3:q(<<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>, - [[<<"test_table">>]], Db)), + ?assertEqual([[1]], esqlite3:q(Db, <<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>, + [test_table])), + ?assertEqual([[1]], esqlite3:q(Db, <<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>, + ["test_table"])), + ?assertEqual([[1]], esqlite3:q(Db, <<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>, + [<<"test_table">>])), + ?assertEqual([[1]], esqlite3:q(Db, <<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>, + [[<<"test_table">>]])), ok. -column_names_test() -> +column_names2_test() -> {ok, Db} = esqlite3:open(":memory:"), - ok = esqlite3:exec("begin;", Db), - ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db), - ok = esqlite3:exec("insert into test_table values('hello1', 10);", Db), - ok = esqlite3:exec("insert into test_table values('hello2', 20);", Db), - ok = esqlite3:exec("commit;", Db), + ok = esqlite3:exec(Db, "begin;"), + ok = esqlite3:exec(Db, "create table test_table(one varchar(10), two int);"), + ok = esqlite3:exec(Db, "insert into test_table values('hello1', 10);"), + ok = esqlite3:exec(Db, "insert into test_table values('hello2', 20);"), + ok = esqlite3:exec(Db, "commit;"), %% All columns - {ok, Stmt} = esqlite3:prepare("select * from test_table", Db), - {one, two} = esqlite3:column_names(Stmt), - {row, {<<"hello1">>, 10}} = esqlite3:step(Stmt), - {one, two} = esqlite3:column_names(Stmt), - {row, {<<"hello2">>, 20}} = esqlite3:step(Stmt), - {one, two} = esqlite3:column_names(Stmt), + {ok, Stmt} = esqlite3:prepare(Db, "select * from test_table"), + [<<"one">>, <<"two">>] = esqlite3:column_names(Stmt), + [<<"hello1">>, 10] = esqlite3:step(Stmt), + [<<"one">>, <<"two">>] = esqlite3:column_names(Stmt), + [<<"hello2">>, 20] = esqlite3:step(Stmt), + [<<"one">>, <<"two">>] = esqlite3:column_names(Stmt), '$done' = esqlite3:step(Stmt), - {one, two} = esqlite3:column_names(Stmt), + [<<"one">>, <<"two">>] = esqlite3:column_names(Stmt), %% One column - {ok, Stmt2} = esqlite3:prepare("select two from test_table", Db), - {two} = esqlite3:column_names(Stmt2), - {row, {10}} = esqlite3:step(Stmt2), - {two} = esqlite3:column_names(Stmt2), - {row, {20}} = esqlite3:step(Stmt2), - {two} = esqlite3:column_names(Stmt2), + {ok, Stmt2} = esqlite3:prepare(Db, "select two from test_table"), + [<<"two">>] = esqlite3:column_names(Stmt2), + [10] = esqlite3:step(Stmt2), + [<<"two">>] = esqlite3:column_names(Stmt2), + [20] = esqlite3:step(Stmt2), + [<<"two">>] = esqlite3:column_names(Stmt2), '$done' = esqlite3:step(Stmt2), - {two} = esqlite3:column_names(Stmt2), + [<<"two">>] = esqlite3:column_names(Stmt2), %% No columns - {ok, Stmt3} = esqlite3:prepare("values(1);", Db), - {column1} = esqlite3:column_names(Stmt3), - {row, {1}} = esqlite3:step(Stmt3), - {column1} = esqlite3:column_names(Stmt3), + {ok, Stmt3} = esqlite3:prepare(Db, "values(1);"), + [<<"column1">>] = esqlite3:column_names(Stmt3), + [1] = esqlite3:step(Stmt3), + [<<"column1">>] = esqlite3:column_names(Stmt3), %% Things get a bit weird when you retrieve the column name %% when calling an aggragage function. - {ok, Stmt4} = esqlite3:prepare("select date('now');", Db), - {'date(\'now\')'} = esqlite3:column_names(Stmt4), - {row, {Date}} = esqlite3:step(Stmt4), + {ok, Stmt4} = esqlite3:prepare(Db, "select date('now');"), + [<<"date(\'now\')">>] = esqlite3:column_names(Stmt4), + [Date] = esqlite3:step(Stmt4), true = is_binary(Date), %% Some statements have no column names - {ok, Stmt5} = esqlite3:prepare("create table dummy(a, b, c);", Db), - {} = esqlite3:column_names(Stmt5), + {ok, Stmt5} = esqlite3:prepare(Db, "create table dummy(a, b, c);"), + [] = esqlite3:column_names(Stmt5), ok. column_types_test() -> {ok, Db} = esqlite3:open(":memory:"), - ok = esqlite3:exec("begin;", Db), - ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db), - ok = esqlite3:exec("insert into test_table values('hello1', 10);", Db), - ok = esqlite3:exec("insert into test_table values('hello2', 20);", Db), - ok = esqlite3:exec("commit;", Db), + ok = esqlite3:exec(Db, "begin;"), + ok = esqlite3:exec(Db, "create table test_table(one varchar(10), two int);"), + ok = esqlite3:exec(Db, "insert into test_table values('hello1', 10);"), + ok = esqlite3:exec(Db, "insert into test_table values('hello2', 20);"), + ok = esqlite3:exec(Db, "commit;"), %% All columns - {ok, Stmt} = esqlite3:prepare("select * from test_table", Db), - ?assertEqual({'varchar(10)', 'INT'}, esqlite3:column_types(Stmt)), - {row, {<<"hello1">>, 10}} = esqlite3:step(Stmt), - {'varchar(10)', 'INT'} = esqlite3:column_types(Stmt), - {row, {<<"hello2">>, 20}} = esqlite3:step(Stmt), - {'varchar(10)', 'INT'} = esqlite3:column_types(Stmt), - '$done' = esqlite3:step(Stmt), - {'varchar(10)', 'INT'} = esqlite3:column_types(Stmt), + {ok, Stmt} = esqlite3:prepare(Db, "select * from test_table"), + ?assertEqual([<<"varchar(10)">>, <<"INT">>], esqlite3:column_decltypes(Stmt)), %% Some statements have no column types - {ok, Stmt2} = esqlite3:prepare("create table dummy(a, b, c);", Db), - {} = esqlite3:column_types(Stmt2), + {ok, Stmt2} = esqlite3:prepare(Db, "create table dummy(a, b, c);"), + [] = esqlite3:column_decltypes(Stmt2), + + {ok, Stmt3} = esqlite3:prepare(Db, "select 1, 2, 3;"), + [undefined, undefined, undefined] = esqlite3:column_decltypes(Stmt3), ok. -nil_column_types_test() -> +nil_column_decltypes_test() -> {ok, Db} = esqlite3:open(":memory:"), - ok = esqlite3:exec("begin;", Db), - ok = esqlite3:exec("create table t1(c1 variant);", Db), - ok = esqlite3:exec("commit;", Db), + ok = esqlite3:exec(Db, "begin;"), + ok = esqlite3:exec(Db, "create table t1(c1 variant);"), + ok = esqlite3:exec(Db, "commit;"), + + {ok, Stmt} = esqlite3:prepare(Db, "select c1 + 1, c1 from t1"), + ?assertEqual([undefined, <<"variant">>], esqlite3:column_decltypes(Stmt)), - {ok, Stmt} = esqlite3:prepare("select c1 + 1, c1 from t1", Db), - {nil, variant} = esqlite3:column_types(Stmt), ok. reset_test() -> {ok, Db} = esqlite3:open(":memory:"), - {ok, Stmt} = esqlite3:prepare("select * from (values (1), (2));", Db), - {row, {1}} = esqlite3:step(Stmt), + {ok, Stmt} = esqlite3:prepare(Db, "select * from (values (1), (2));"), + [1] = esqlite3:step(Stmt), ok = esqlite3:reset(Stmt), - {row, {1}} = esqlite3:step(Stmt), - {row, {2}} = esqlite3:step(Stmt), + + [1] = esqlite3:step(Stmt), + [2] = esqlite3:step(Stmt), '$done' = esqlite3:step(Stmt), % After a done the statement is automatically reset. - {row, {1}} = esqlite3:step(Stmt), + [1] = esqlite3:step(Stmt), % Calling reset multiple times... ok = esqlite3:reset(Stmt), @@ -307,155 +380,41 @@ reset_test() -> ok = esqlite3:reset(Stmt), % The statement should still be reset. - {row, {1}} = esqlite3:step(Stmt), + [1] = esqlite3:step(Stmt), ok. -foreach_test() -> - {ok, Db} = esqlite3:open(":memory:"), - - ok = esqlite3:exec("begin;", Db), - ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db), - - ok = esqlite3:exec("insert into test_table values('hello1', 10);", Db), - ok = esqlite3:exec("insert into test_table values('hello2', 11);", Db), - ok = esqlite3:exec("insert into test_table values('hello3', 12);", Db), - ok = esqlite3:exec("insert into test_table values('hello4', 13);", Db), - - ok = esqlite3:exec("commit;", Db), - - F = fun(Row) -> - case Row of - {Key, Value} -> - put(Key, Value); - _ -> - ok - end - end, - - esqlite3:foreach(F, "select * from test_table;", Db), - - 10 = get(<<"hello1">>), - 11 = get(<<"hello2">>), - 12 = get(<<"hello3">>), - 13 = get(<<"hello4">>), - - ok. - -bind_for_foreach_test() -> - {ok, Db} = esqlite3:open(":memory:"), - - ok = esqlite3:exec("begin;", Db), - ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db), - ok = esqlite3:exec("insert into test_table values('hello1', 10);", Db), - ok = esqlite3:exec("insert into test_table values('hello2', 11);", Db), - ok = esqlite3:exec("insert into test_table values('hello3', 12);", Db), - ok = esqlite3:exec("insert into test_table values('hello4', 13);", Db), - ok = esqlite3:exec("commit;", Db), - - F = fun(Row) -> - case Row of - {Key, Value} -> - put(Key, Value); - _ -> - ok - end - end, - - esqlite3:foreach(F, "select * from test_table where one = ?;", ["hello1"], Db), - - 10 = get(<<"hello1">>), - - ok. - -map_test() -> - {ok, Db} = esqlite3:open(":memory:"), - - ok = esqlite3:exec("begin;", Db), - ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db), - ok = esqlite3:exec("insert into test_table values('hello1', 10);", Db), - ok = esqlite3:exec("insert into test_table values('hello2', 11);", Db), - ok = esqlite3:exec("insert into test_table values('hello3', 12);", Db), - ok = esqlite3:exec("insert into test_table values('hello4', 13);", Db), - ok = esqlite3:exec("commit;", Db), - - F = fun(Row) -> Row end, - - [{<<"hello1">>,10}, - {<<"hello2">>,11}, - {<<"hello3">>,12}, - {<<"hello4">>,13}] = esqlite3:map(F, "select * from test_table", Db), - - %% Test that when the row-names are added.. - Assoc = fun(Names, Row) -> - lists:zip(tuple_to_list(Names), tuple_to_list(Row)) - end, - - [[{one,<<"hello1">>},{two,10}], - [{one,<<"hello2">>},{two,11}], - [{one,<<"hello3">>},{two,12}], - [{one,<<"hello4">>},{two,13}]] = esqlite3:map(Assoc, "select * from test_table", Db), - - ok. - -bind_for_map_test() -> - {ok, Db} = esqlite3:open(":memory:"), - - ok = esqlite3:exec("begin;", Db), - ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db), - ok = esqlite3:exec("insert into test_table values('hello1', 10);", Db), - ok = esqlite3:exec("insert into test_table values('hello2', 11);", Db), - ok = esqlite3:exec("insert into test_table values('hello3', 12);", Db), - ok = esqlite3:exec("insert into test_table values('hello4', 13);", Db), - ok = esqlite3:exec("commit;", Db), - - F = fun(Row) -> Row end, - - [{<<"hello1">>,10}] - = esqlite3:map(F, "select * from test_table where one = ?", ["hello1"], Db), - - %% Test that when the row-names are added.. - Assoc = fun(Names, Row) -> - lists:zip(tuple_to_list(Names), tuple_to_list(Row)) - end, - - [[{one,<<"hello1">>},{two,10}]] = esqlite3:map(Assoc, "select * from test_table where one = ?", ["hello1"], Db), - - ok. - - error1_msg_test() -> {ok, Db} = esqlite3:open(":memory:"), %% Not sql. - {error, {sqlite_error, _Msg1}} = esqlite3:exec("dit is geen sql", Db), + {error, 1} = esqlite3:exec(Db, "dit is geen sql"), %% Database test does not exist. - {error, {sqlite_error, _Msg2}} = esqlite3:exec("select * from test;", Db), + {error, 1} = esqlite3:exec(Db, "select * from test;"), %% Opening non-existant database. - {error, {cantopen, _Msg3}} = esqlite3:open("/dit/bestaat/niet"), + {error, 14} = esqlite3:open("/dit/bestaat/niet"), ok. prepare_and_close_connection_test() -> {ok, Db} = esqlite3:open(":memory:"), - [] = esqlite3:q("create table test(one, two, three)", Db), - ok = esqlite3:exec(["insert into test values(1,2,3);"], Db), - {ok, Stmt} = esqlite3:prepare("select * from test", Db), + [] = esqlite3:q(Db, "create table test(one, two, three)"), + ok = esqlite3:exec(Db, ["insert into test values(1,2,3);"]), + {ok, Stmt} = esqlite3:prepare(Db, "select * from test"), %% The prepated statment works. - {row, {1,2,3}} = esqlite3:step(Stmt), + [1,2,3] = esqlite3:step(Stmt), '$done' = esqlite3:step(Stmt), ok = esqlite3:close(Db), - ok = esqlite3:reset(Stmt), %% Internally sqlite3_close_v2 is used by the nif. This will destruct the %% connection when the last perpared statement is finalized - {row, {1,2,3}} = esqlite3:step(Stmt), + [1,2,3] = esqlite3:step(Stmt), '$done' = esqlite3:step(Stmt), ok. @@ -466,10 +425,12 @@ backup_test() -> {ok, Dest} = esqlite3:open(?DB1), {ok, Source} = esqlite3:open(?DB2), - {ok, Backup} = esqlite3:backup_init(Dest, "main", Source, "main"), - {ok, 0} = esqlite3:backup_remaining(Backup), - {ok, 0} = esqlite3:backup_pagecount(Backup), - done = esqlite3:backup_step(Backup, 1), + {ok, Backup} = esqlite3:backup_init(Dest, <<"main">>, Source, <<"main">>), + + 0 = esqlite3:backup_remaining(Backup), + 0 = esqlite3:backup_pagecount(Backup), + + '$done' = esqlite3:backup_step(Backup, 1), cleanup(), @@ -481,38 +442,39 @@ backup1_test() -> {ok, Dest} = esqlite3:open(?DB1), {ok, Source} = esqlite3:open(?DB2), - [] = esqlite3:q("create table test(one, two)", Source), - [] = esqlite3:q("begin;", Source), - [] = esqlite3:q("insert into test values(randomblob(10000), randomblob(10000));", Source), - [] = esqlite3:q("insert into test values(randomblob(10000), randomblob(10000));", Source), - [] = esqlite3:q("insert into test values(randomblob(10000), randomblob(10000));", Source), - [] = esqlite3:q("insert into test values(randomblob(10000), randomblob(10000));", Source), - [] = esqlite3:q("insert into test values(randomblob(10000), randomblob(10000));", Source), - [] = esqlite3:q("commit;", Source), + [] = esqlite3:q(Source, "create table test(one, two)"), + [] = esqlite3:q(Source, "begin;"), + [] = esqlite3:q(Source, "insert into test values(randomblob(10000), randomblob(10000));"), + [] = esqlite3:q(Source, "insert into test values(randomblob(10000), randomblob(10000));"), + [] = esqlite3:q(Source, "insert into test values(randomblob(10000), randomblob(10000));"), + [] = esqlite3:q(Source, "insert into test values(randomblob(10000), randomblob(10000));"), + [] = esqlite3:q(Source, "insert into test values(randomblob(10000), randomblob(10000));"), + [] = esqlite3:q(Source, "commit;"), - [{5}] = esqlite3:q("select count(*) from test", Source), - {error, {sqlite_error, "no such table: test"}} = esqlite3:q("select count(*) from test", Dest), + [[5]] = esqlite3:q(Source, "select count(*) from test"), + {error, 1} = esqlite3:q(Dest, "select count(*) from test"), + #{ errmsg := <<"no such table: test">> } = esqlite3:error_info(Dest), {ok, Backup} = esqlite3:backup_init(Dest, "main", Source, "main"), - {ok, 0} = esqlite3:backup_remaining(Backup), - {ok, 0} = esqlite3:backup_pagecount(Backup), + 0 = esqlite3:backup_remaining(Backup), + 0 = esqlite3:backup_pagecount(Backup), %% Backup 1 page. ok = esqlite3:backup_step(Backup, 1), - {ok, 26} = esqlite3:backup_remaining(Backup), - {ok, 27} = esqlite3:backup_pagecount(Backup), + 26 = esqlite3:backup_remaining(Backup), + 27 = esqlite3:backup_pagecount(Backup), %% Do all the remaining pages. - done = esqlite3:backup_step(Backup, -1), + '$done' = esqlite3:backup_step(Backup, -1), - {ok, 0} = esqlite3:backup_remaining(Backup), - {ok, 27} = esqlite3:backup_pagecount(Backup), + 0 = esqlite3:backup_remaining(Backup), + 27 = esqlite3:backup_pagecount(Backup), ok = esqlite3:backup_finish(Backup), - [{5}] = esqlite3:q("select count(*) from test", Dest), + [[5]] = esqlite3:q(Dest, "select count(*) from test"), cleanup(), @@ -521,47 +483,58 @@ backup1_test() -> sqlite_version_test() -> {ok, Db} = esqlite3:open(":memory:"), - {ok, Stmt} = esqlite3:prepare("select sqlite_version() as sqlite_version;", Db), - {sqlite_version} = esqlite3:column_names(Stmt), - ?assertEqual({row, {<<"3.38.0">>}}, esqlite3:step(Stmt)), + {ok, Stmt} = esqlite3:prepare(Db, "select sqlite_version() as sqlite_version;"), + [<<"sqlite_version">>] = esqlite3:column_names(Stmt), + ?assertEqual([<<"3.38.0">>], esqlite3:step(Stmt)), ok. sqlite_source_id_test() -> {ok, Db} = esqlite3:open(":memory:"), - {ok, Stmt} = esqlite3:prepare("select sqlite_source_id() as sqlite_source_id;", Db), - {sqlite_source_id} = esqlite3:column_names(Stmt), - ?assertEqual({row, {<<"2022-02-22 18:58:40 40fa792d359f84c3b9e9d6623743e1a59826274e221df1bde8f47086968a1bab">>}}, + {ok, Stmt} = esqlite3:prepare(Db, "select sqlite_source_id() as sqlite_source_id;"), + [<<"sqlite_source_id">>] = esqlite3:column_names(Stmt), + ?assertEqual([<<"2022-02-22 18:58:40 40fa792d359f84c3b9e9d6623743e1a59826274e221df1bde8f47086968a1bab">>], esqlite3:step(Stmt)), ok. interrupt_on_timeout_test() -> - {ok, Db} = esqlite3:open(":memory:"), - CreateTableQuery = "CREATE TABLE all_numbers_in_the_world (number int not null);", - ok = esqlite3:exec(CreateTableQuery, Db), - VeryLongQuery = " - WITH RECURSIVE - for(i) AS (VALUES(1) UNION ALL SELECT i+1 FROM for WHERE i < 10000000) - INSERT INTO all_numbers_in_the_world SELECT i FROM for; - ", - try - ok = esqlite3:exec(VeryLongQuery, [], Db, 10) - catch - {error, timeout, _} -> - ?assertMatch([{0}], esqlite3:q("SELECT COUNT(*) FROM all_numbers_in_the_world", Db)), - %% There is now a stale answer, because the recursive query was interrupted. - receive - {esqlite3, _, {error, {interrupt, "interrupted"}}} -> - ok - end - end. + {ok, Db1} = esqlite3:open("file:memdb1?mode=memory&cache=shared"), + % {ok, Db2} = esqlite3:open("file:memdb1?mode=memory&cache=shared"), + + Self = self(), + + F = fun() -> + CreateTableQuery = "CREATE TABLE all_numbers_in_the_world (number int not null);", + ok = esqlite3:exec(Db1, CreateTableQuery), + VeryLongQuery = " + WITH RECURSIVE + for(i) AS (VALUES(1) UNION ALL SELECT i+1 FROM for WHERE i < 10000000) + INSERT INTO all_numbers_in_the_world SELECT i FROM for; + ", + + %% The query was interrupted + Self ! {msg, esqlite3:exec(Db1, VeryLongQuery)} + end, + + spawn(F), + timer:sleep(10), + ok = esqlite3:interrupt(Db1), + + %% The query was interrupted, so no result + ?assertEqual([[0]], esqlite3:q(Db1, "SELECT COUNT(*) FROM all_numbers_in_the_world")), + + %% We should have gotten an interrupt error. + Msg = receive {msg, M} -> M end, + ?assertEqual({error, 9}, Msg), + + ok. garbage_collect_test() -> F = fun() -> {ok, Db} = esqlite3:open(":memory:"), - [] = esqlite3:q("create table test(one, two, three)", Db), - [] = esqlite3:q("insert into test values(1, '2', 3.0)", Db), - {ok, Stmt} = esqlite3:prepare("select * from test", Db), - {row, {1, <<"2">>, 3.0}} = esqlite3:step(Stmt), + [] = esqlite3:q(Db, "create table test(one, two, three)"), + [] = esqlite3:q(Db, "insert into test values(1, '2', 3.0)"), + {ok, Stmt} = esqlite3:prepare(Db, "select * from test"), + [1, <<"2">>, 3.0] = esqlite3:step(Stmt), '$done' = esqlite3:step(Stmt), ok = esqlite3:close(Db) end, @@ -574,7 +547,6 @@ garbage_collect_test() -> receive after 500 -> ok end, erlang:garbage_collect(), - ok. %%