Packages
exqlite
0.38.0
0.39.0
0.38.0
0.37.0
0.36.0
0.35.0
0.34.0
0.33.1
0.33.0
0.32.1
0.32.0
0.31.0
0.30.1
0.30.0
0.29.0
0.28.0
0.27.1
0.27.0
0.26.0
0.25.0
0.24.2
0.24.1
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.19.0
0.18.0
0.17.0
0.16.2
0.16.1
0.16.0
0.15.0
0.14.0
0.13.15
0.13.14
0.13.13
0.13.12
0.13.11
0.13.10
0.13.9
0.13.8
0.13.7
0.13.6
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.9
0.11.8
0.11.7
0.11.6
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.2
0.4.1
0.4.0
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.2
0.2.1
0.2.0
0.1.1
0.1.0
An Elixir SQLite3 library
Current section
Files
Jump to
Current section
Files
c_src/sqlite3_nif.c
#include <assert.h>
#include <string.h>
#include <stdio.h>
// Elixir workaround for . in module names
#ifdef STATIC_ERLANG_NIF
#define STATIC_ERLANG_NIF_LIBNAME sqlite3_nif
#endif
#include <erl_nif.h>
#include <sqlite3.h>
static ERL_NIF_TERM am_ok;
static ERL_NIF_TERM am_error;
static ERL_NIF_TERM am_badarg;
static ERL_NIF_TERM am_nil;
static ERL_NIF_TERM am_out_of_memory;
static ERL_NIF_TERM am_done;
static ERL_NIF_TERM am_row;
static ERL_NIF_TERM am_rows;
static ERL_NIF_TERM am_invalid_filename;
static ERL_NIF_TERM am_invalid_flags;
static ERL_NIF_TERM am_database_open_failed;
static ERL_NIF_TERM am_failed_to_create_mutex;
static ERL_NIF_TERM am_invalid_connection;
static ERL_NIF_TERM am_sql_not_iolist;
static ERL_NIF_TERM am_connection_closed;
static ERL_NIF_TERM am_invalid_statement;
static ERL_NIF_TERM am_invalid_chunk_size;
static ERL_NIF_TERM am_busy;
static ERL_NIF_TERM am_invalid_column_count;
static ERL_NIF_TERM am_transaction;
static ERL_NIF_TERM am_idle;
static ERL_NIF_TERM am_database_name_not_iolist;
static ERL_NIF_TERM am_serialization_failed;
static ERL_NIF_TERM am_deserialization_failed;
static ERL_NIF_TERM am_invalid_enable_load_extension_value;
static ERL_NIF_TERM am_insert;
static ERL_NIF_TERM am_delete;
static ERL_NIF_TERM am_update;
static ERL_NIF_TERM am_invalid_pid;
static ERL_NIF_TERM am_log;
static ErlNifResourceType* connection_type = NULL;
static ErlNifResourceType* statement_type = NULL;
static sqlite3_mem_methods default_alloc_methods = {0};
ErlNifPid* log_hook_pid = NULL;
ErlNifMutex* log_hook_mutex = NULL;
// Denied authorizer action codes. Sized to 64 for margin — highest
// currently defined SQLite action code is SQLITE_RECURSIVE (33).
#define AUTHORIZER_DENY_SIZE 64
typedef struct connection
{
sqlite3* db;
ErlNifMutex* mutex;
ErlNifMutex* interrupt_mutex;
ErlNifPid update_hook_pid;
int authorizer_deny[AUTHORIZER_DENY_SIZE];
// Custom busy handler state
int cancelled; // guarded by interrupt_mutex
int busy_timeout_ms;
int progress_handler_steps;
ErlNifEnv* callback_env; // for enif_is_process_alive
ErlNifPid caller_pid;
} connection_t;
typedef struct statement
{
connection_t* conn;
sqlite3_stmt* statement;
} statement_t;
static int exqlite_progress_handler(void* arg);
static void*
exqlite_malloc(int bytes)
{
assert(bytes > 0);
size_t* p = enif_alloc(bytes + sizeof(size_t));
if (p) {
p[0] = bytes;
p++;
}
return p;
}
static void
exqlite_free(void* prior)
{
if (!prior) {
return;
}
size_t* p = prior;
// Shift the pointer back to free the proper block of data
p--;
enif_free(p);
}
static void*
exqlite_realloc(void* prior, int bytes)
{
assert(prior);
assert(bytes > 0);
size_t* p = prior;
p--;
p = enif_realloc(p, bytes + sizeof(size_t));
if (p) {
p[0] = bytes;
p++;
}
return p;
}
static int
exqlite_mem_size(void* prior)
{
if (!prior) {
return 0;
}
size_t* p = prior;
p--;
return p[0];
}
static int
exqlite_mem_round_up(int bytes)
{
return (bytes + 7) & ~7;
}
static int
exqlite_mem_init(void* ptr)
{
return SQLITE_OK;
}
static void
exqlite_mem_shutdown(void* ptr)
{
}
static const char*
get_sqlite3_error_msg(int rc, sqlite3* db)
{
if (rc == SQLITE_MISUSE) {
return "Sqlite3 was invoked incorrectly.";
}
const char* message = sqlite3_errmsg(db);
if (!message) {
return "No error message available.";
}
return message;
}
static ERL_NIF_TERM
make_ok_tuple(ErlNifEnv* env, ERL_NIF_TERM value)
{
assert(env);
assert(value);
return enif_make_tuple2(env, am_ok, value);
}
static ERL_NIF_TERM
make_error_tuple(ErlNifEnv* env, ERL_NIF_TERM reason)
{
assert(env);
assert(reason);
return enif_make_tuple2(env, am_error, reason);
}
static ERL_NIF_TERM
make_binary(ErlNifEnv* env, const void* bytes, unsigned int size)
{
ErlNifBinary blob;
ERL_NIF_TERM term;
if (!enif_alloc_binary(size, &blob)) {
return am_out_of_memory;
}
memcpy(blob.data, bytes, size);
term = enif_make_binary(env, &blob);
enif_release_binary(&blob);
return term;
}
static ERL_NIF_TERM
make_sqlite3_error_tuple(ErlNifEnv* env, int rc, sqlite3* db)
{
const char* msg = get_sqlite3_error_msg(rc, db);
size_t len = strlen(msg);
return make_error_tuple(env, make_binary(env, msg, len));
}
static ERL_NIF_TERM
raise_badarg(ErlNifEnv* env, ERL_NIF_TERM term)
{
ERL_NIF_TERM badarg = enif_make_tuple2(env, am_badarg, term);
return enif_raise_exception(env, badarg);
}
static ERL_NIF_TERM
make_cell(ErlNifEnv* env, sqlite3_stmt* statement, unsigned int i)
{
switch (sqlite3_column_type(statement, i)) {
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_NULL:
return am_nil;
case SQLITE_BLOB:
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 am_nil;
}
}
static ERL_NIF_TERM
make_row(ErlNifEnv* env, sqlite3_stmt* statement)
{
assert(env);
assert(statement);
ERL_NIF_TERM* columns = NULL;
ERL_NIF_TERM row;
unsigned int count = sqlite3_column_count(statement);
columns = enif_alloc(sizeof(ERL_NIF_TERM) * count);
if (!columns) {
return make_error_tuple(env, am_out_of_memory);
}
for (unsigned int i = 0; i < count; i++) {
columns[i] = make_cell(env, statement, i);
}
row = enif_make_list_from_array(env, columns, count);
enif_free(columns);
return row;
}
static inline void
connection_acquire_lock(connection_t* conn)
{
assert(conn);
enif_mutex_lock(conn->mutex);
}
static inline void
connection_release_lock(connection_t* conn)
{
assert(conn);
enif_mutex_unlock(conn->mutex);
}
static inline void
statement_acquire_lock(statement_t* statement)
{
assert(statement);
connection_acquire_lock(statement->conn);
}
static inline void
statement_release_lock(statement_t* statement)
{
assert(statement);
connection_release_lock(statement->conn);
}
static inline void
connection_configure_progress_handler(connection_t* conn)
{
assert(conn);
assert(conn->db);
if (conn->progress_handler_steps < 1) {
sqlite3_progress_handler(conn->db, 0, NULL, NULL);
return;
}
sqlite3_progress_handler(
conn->db,
conn->progress_handler_steps,
exqlite_progress_handler,
conn);
}
// ---------------------------------------------------------------------------
// Custom busy handler
//
// Replaces SQLite's default busy handler (which sleeps via sqlite3OsSleep and
// cannot be interrupted) with one that polls conn->cancelled between each
// sqlite3_sleep() call. cancel() sets the flag and calls sqlite3_interrupt()
// so disconnect() wakes within at most one sleep interval (~10ms).
// ---------------------------------------------------------------------------
static int
exqlite_busy_handler(void* arg, int count)
{
connection_t* conn = (connection_t*)arg;
int cancelled;
int timeout_ms;
ErlNifEnv* callback_env;
ErlNifPid caller_pid;
enif_mutex_lock(conn->interrupt_mutex);
cancelled = conn->cancelled;
timeout_ms = conn->busy_timeout_ms;
callback_env = conn->callback_env;
caller_pid = conn->caller_pid;
enif_mutex_unlock(conn->interrupt_mutex);
if (cancelled) {
return 0;
}
// Check if the calling process is still alive
if (callback_env != NULL && !enif_is_process_alive(callback_env, &caller_pid)) {
enif_mutex_lock(conn->interrupt_mutex);
conn->cancelled = 1;
enif_mutex_unlock(conn->interrupt_mutex);
return 0;
}
if (timeout_ms <= 0) {
return 0;
}
static const int delays[] = {1, 2, 5, 10, 15, 20, 25, 25, 25, 50, 50};
static const int ndelay = sizeof(delays) / sizeof(delays[0]);
int total_waited = 0;
for (int i = 0; i < count && i < ndelay; i++) {
total_waited += delays[i];
}
if (count >= ndelay) {
total_waited += (count - ndelay) * 50;
}
if (total_waited >= timeout_ms) {
return 0;
}
int sleep_ms = (count < ndelay) ? delays[count] : 50;
int remaining = timeout_ms - total_waited;
if (sleep_ms > remaining) {
sleep_ms = remaining;
}
sqlite3_sleep(sleep_ms);
enif_mutex_lock(conn->interrupt_mutex);
cancelled = conn->cancelled;
enif_mutex_unlock(conn->interrupt_mutex);
return cancelled ? 0 : 1;
}
// Progress handler: fires every N VDBE opcodes.
// Returns non-zero to interrupt execution when cancelled.
static int
exqlite_progress_handler(void* arg)
{
connection_t* conn = (connection_t*)arg;
int cancelled;
enif_mutex_lock(conn->interrupt_mutex);
cancelled = conn->cancelled;
enif_mutex_unlock(conn->interrupt_mutex);
return cancelled ? 1 : 0;
}
// Stash the current env + caller pid before a db operation.
// Must be called while holding conn->mutex.
static inline void
connection_stash_caller(connection_t* conn, ErlNifEnv* env)
{
enif_mutex_lock(conn->interrupt_mutex);
conn->callback_env = env;
enif_self(env, &conn->caller_pid);
conn->cancelled = 0;
enif_mutex_unlock(conn->interrupt_mutex);
}
// Clear the stashed caller after a db operation completes.
// Assumes that the `conn` has been locked for clearing the
// caller.
static inline void
connection_clear_caller(connection_t* conn)
{
enif_mutex_lock(conn->interrupt_mutex);
conn->callback_env = NULL;
enif_mutex_unlock(conn->interrupt_mutex);
}
///
/// Opens a new SQLite database
///
ERL_NIF_TERM
exqlite_open(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
int flags;
int rc = 0;
int size = 0;
connection_t* conn = NULL;
sqlite3* db = NULL;
ErlNifMutex* mutex = NULL;
ERL_NIF_TERM result;
ErlNifBinary bin;
ERL_NIF_TERM eos = enif_make_int(env, 0);
if (argc != 2) {
return enif_make_badarg(env);
}
if (!enif_inspect_iolist_as_binary(env, enif_make_list2(env, argv[0], eos), &bin)) {
return make_error_tuple(env, am_invalid_filename);
}
if (!enif_get_int(env, argv[1], &flags)) {
return make_error_tuple(env, am_invalid_flags);
}
rc = sqlite3_open_v2((char*)bin.data, &db, flags, NULL);
if (rc != SQLITE_OK) {
return make_error_tuple(env, am_database_open_failed);
}
mutex = enif_mutex_create("exqlite:connection");
if (mutex == NULL) {
sqlite3_close_v2(db);
return make_error_tuple(env, am_failed_to_create_mutex);
}
conn = enif_alloc_resource(connection_type, sizeof(connection_t));
if (!conn) {
sqlite3_close_v2(db);
enif_mutex_destroy(mutex);
return make_error_tuple(env, am_out_of_memory);
}
conn->db = db;
conn->mutex = mutex;
conn->interrupt_mutex = NULL;
memset(conn->authorizer_deny, 0, sizeof(conn->authorizer_deny));
// Initialize busy handler fields
conn->cancelled = 0;
conn->busy_timeout_ms = 2000; // default matches sqlite3_busy_timeout(db, 2000)
conn->progress_handler_steps = 1000;
conn->callback_env = NULL;
conn->interrupt_mutex = enif_mutex_create("exqlite:interrupt");
if (conn->interrupt_mutex == NULL) {
enif_release_resource(conn);
return make_error_tuple(env, am_failed_to_create_mutex);
}
// Install our custom busy handler + progress handler
sqlite3_busy_handler(db, exqlite_busy_handler, conn);
connection_configure_progress_handler(conn);
result = enif_make_resource(env, conn);
enif_release_resource(conn);
return make_ok_tuple(env, result);
}
///
/// Closes an SQLite database
///
ERL_NIF_TERM
exqlite_close(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
connection_t* conn = NULL;
int rc = SQLITE_OK;
if (argc != 1) {
return enif_make_badarg(env);
}
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
return make_error_tuple(env, am_invalid_connection);
}
// close connection in critical section to avoid race-condition
// cases. Cases such as query timeout and connection pooling
// attempting to close the connection
connection_acquire_lock(conn);
connection_stash_caller(conn, env);
// DB is already closed, nothing to do here.
if (conn->db == NULL) {
connection_clear_caller(conn);
connection_release_lock(conn);
return am_ok;
}
int autocommit = sqlite3_get_autocommit(conn->db);
if (autocommit == 0) {
rc = sqlite3_exec(conn->db, "ROLLBACK;", NULL, NULL, NULL);
if (rc != SQLITE_OK) {
connection_clear_caller(conn);
connection_release_lock(conn);
return make_sqlite3_error_tuple(env, rc, conn->db);
}
}
// Hold interrupt_mutex across close+NULL so that any concurrent
// exqlite_interrupt() either completes its sqlite3_interrupt() call
// before we start closing, or blocks until we've both closed and
// NULLed conn->db (then sees NULL and skips).
//
// note: _v2 may not fully close the connection, hence why we check if
// any transaction is open above, to make sure other connections aren't blocked.
// v1 is guaranteed to close or error, but will return error if any
// unfinalized statements, which we likely have, as we rely on the destructors
// to later run to clean those up
enif_mutex_lock(conn->interrupt_mutex);
rc = sqlite3_close_v2(conn->db);
if (rc != SQLITE_OK) {
enif_mutex_unlock(conn->interrupt_mutex);
connection_release_lock(conn);
return make_sqlite3_error_tuple(env, rc, conn->db);
}
conn->db = NULL;
enif_mutex_unlock(conn->interrupt_mutex);
connection_clear_caller(conn);
connection_release_lock(conn);
return am_ok;
}
///
/// Executes an SQL string.
///
ERL_NIF_TERM
exqlite_execute(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
ErlNifBinary bin;
connection_t* conn = NULL;
ERL_NIF_TERM eos = enif_make_int(env, 0);
int rc = SQLITE_OK;
if (argc != 2) {
return enif_make_badarg(env);
}
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
return make_error_tuple(env, am_invalid_connection);
}
if (!enif_inspect_iolist_as_binary(env, enif_make_list2(env, argv[1], eos), &bin)) {
return make_error_tuple(env, am_sql_not_iolist);
}
connection_acquire_lock(conn);
connection_stash_caller(conn, env);
if (conn->db == NULL) {
connection_clear_caller(conn);
connection_release_lock(conn);
return make_error_tuple(env, am_connection_closed);
}
rc = sqlite3_exec(conn->db, (char*)bin.data, NULL, NULL, NULL);
if (rc != SQLITE_OK) {
connection_clear_caller(conn);
connection_release_lock(conn);
return make_sqlite3_error_tuple(env, rc, conn->db);
}
connection_clear_caller(conn);
connection_release_lock(conn);
return am_ok;
}
///
/// Get the number of changes recently done to the database.
///
ERL_NIF_TERM
exqlite_changes(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
connection_t* conn = NULL;
if (argc != 1) {
return enif_make_badarg(env);
}
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
return make_error_tuple(env, am_invalid_connection);
}
connection_acquire_lock(conn);
if (conn->db == NULL) {
connection_release_lock(conn);
return make_error_tuple(env, am_connection_closed);
}
int changes = sqlite3_changes(conn->db);
connection_release_lock(conn);
return make_ok_tuple(env, enif_make_int(env, changes));
}
///
/// Prepares an Sqlite3 statement for execution
///
ERL_NIF_TERM
exqlite_prepare(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
ErlNifBinary bin;
connection_t* conn = NULL;
statement_t* statement = NULL;
ERL_NIF_TERM result;
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], connection_type, (void**)&conn)) {
return make_error_tuple(env, am_invalid_connection);
}
if (!enif_inspect_iolist_as_binary(env, enif_make_list2(env, argv[1], eos), &bin)) {
return make_error_tuple(env, am_sql_not_iolist);
}
statement = enif_alloc_resource(statement_type, sizeof(statement_t));
if (!statement) {
return make_error_tuple(env, am_out_of_memory);
}
statement->statement = NULL;
enif_keep_resource(conn);
statement->conn = conn;
// ensure connection is not getting closed by parallel thread
connection_acquire_lock(conn);
connection_stash_caller(conn, env);
if (conn->db == NULL) {
connection_clear_caller(conn);
connection_release_lock(conn);
enif_release_resource(statement);
return make_error_tuple(env, am_connection_closed);
}
rc = sqlite3_prepare_v3(conn->db, (char*)bin.data, bin.size, 0, &statement->statement, NULL);
if (rc != SQLITE_OK) {
result = make_sqlite3_error_tuple(env, rc, conn->db);
connection_clear_caller(conn);
connection_release_lock(conn);
enif_release_resource(statement);
return result;
}
connection_clear_caller(conn);
connection_release_lock(conn);
result = enif_make_resource(env, statement);
enif_release_resource(statement);
return make_ok_tuple(env, result);
}
///
/// Reset the prepared statement
///
ERL_NIF_TERM
exqlite_reset(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
statement_t* statement;
if (!enif_get_resource(env, argv[0], statement_type, (void**)&statement)) {
return raise_badarg(env, argv[0]);
}
statement_acquire_lock(statement);
if (statement->statement == NULL) {
statement_release_lock(statement);
return make_error_tuple(env, am_invalid_statement);
}
sqlite3_reset(statement->statement);
statement_release_lock(statement);
return am_ok;
}
///
/// Get the bind parameter count
///
ERL_NIF_TERM
exqlite_bind_parameter_count(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
statement_t* statement;
if (!enif_get_resource(env, argv[0], statement_type, (void**)&statement)) {
return raise_badarg(env, argv[0]);
}
statement_acquire_lock(statement);
if (statement->statement == NULL) {
statement_release_lock(statement);
return make_error_tuple(env, am_invalid_statement);
}
int bind_parameter_count = sqlite3_bind_parameter_count(statement->statement);
statement_release_lock(statement);
return enif_make_int(env, bind_parameter_count);
}
///
/// Get the bind parameter index
///
ERL_NIF_TERM
exqlite_bind_parameter_index(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
statement_t* statement;
if (!enif_get_resource(env, argv[0], statement_type, (void**)&statement)) {
return raise_badarg(env, argv[0]);
}
ERL_NIF_TERM eos = enif_make_int(env, 0);
ErlNifBinary name;
if (!enif_inspect_iolist_as_binary(env, enif_make_list2(env, argv[1], eos), &name)) {
return raise_badarg(env, argv[1]);
}
statement_acquire_lock(statement);
if (statement->statement == NULL) {
statement_release_lock(statement);
return make_error_tuple(env, am_invalid_statement);
}
int index = sqlite3_bind_parameter_index(statement->statement, (const char*)name.data);
statement_release_lock(statement);
return enif_make_int(env, index);
}
///
/// Binds a text parameter
///
ERL_NIF_TERM
exqlite_bind_text(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
statement_t* statement;
if (!enif_get_resource(env, argv[0], statement_type, (void**)&statement)) {
return raise_badarg(env, argv[0]);
}
unsigned int idx;
if (!enif_get_uint(env, argv[1], &idx)) {
return raise_badarg(env, argv[1]);
}
ErlNifBinary text;
if (!enif_inspect_binary(env, argv[2], &text)) {
return raise_badarg(env, argv[2]);
}
statement_acquire_lock(statement);
if (statement->statement == NULL) {
statement_release_lock(statement);
return make_error_tuple(env, am_invalid_statement);
}
int rc = sqlite3_bind_text(statement->statement, idx, (char*)text.data, text.size, SQLITE_TRANSIENT);
statement_release_lock(statement);
return enif_make_int(env, rc);
}
///
/// Binds a blob parameter
///
ERL_NIF_TERM
exqlite_bind_blob(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
statement_t* statement;
if (!enif_get_resource(env, argv[0], statement_type, (void**)&statement)) {
return raise_badarg(env, argv[0]);
}
unsigned int idx;
if (!enif_get_uint(env, argv[1], &idx)) {
return raise_badarg(env, argv[1]);
}
ErlNifBinary blob;
if (!enif_inspect_binary(env, argv[2], &blob)) {
return raise_badarg(env, argv[2]);
}
statement_acquire_lock(statement);
if (statement->statement == NULL) {
statement_release_lock(statement);
return make_error_tuple(env, am_invalid_statement);
}
int rc = sqlite3_bind_blob(statement->statement, idx, (char*)blob.data, blob.size, SQLITE_TRANSIENT);
statement_release_lock(statement);
return enif_make_int(env, rc);
}
///
/// Binds an integer parameter
///
ERL_NIF_TERM
exqlite_bind_integer(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
statement_t* statement;
if (!enif_get_resource(env, argv[0], statement_type, (void**)&statement)) {
return raise_badarg(env, argv[0]);
}
unsigned int idx;
if (!enif_get_uint(env, argv[1], &idx)) {
return raise_badarg(env, argv[1]);
}
ErlNifSInt64 i;
if (!enif_get_int64(env, argv[2], &i)) {
return raise_badarg(env, argv[2]);
}
statement_acquire_lock(statement);
if (statement->statement == NULL) {
statement_release_lock(statement);
return make_error_tuple(env, am_invalid_statement);
}
int rc = sqlite3_bind_int64(statement->statement, idx, i);
statement_release_lock(statement);
return enif_make_int(env, rc);
}
///
/// Binds a float parameter
///
ERL_NIF_TERM
exqlite_bind_float(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
statement_t* statement;
if (!enif_get_resource(env, argv[0], statement_type, (void**)&statement)) {
return raise_badarg(env, argv[0]);
}
unsigned int idx;
if (!enif_get_uint(env, argv[1], &idx)) {
return raise_badarg(env, argv[1]);
}
double f;
if (!enif_get_double(env, argv[2], &f)) {
return raise_badarg(env, argv[2]);
}
statement_acquire_lock(statement);
if (statement->statement == NULL) {
statement_release_lock(statement);
return make_error_tuple(env, am_invalid_statement);
}
int rc = sqlite3_bind_double(statement->statement, idx, f);
statement_release_lock(statement);
return enif_make_int(env, rc);
}
///
/// Binds a null parameter
///
ERL_NIF_TERM
exqlite_bind_null(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
statement_t* statement;
if (!enif_get_resource(env, argv[0], statement_type, (void**)&statement)) {
return raise_badarg(env, argv[0]);
}
unsigned int idx;
if (!enif_get_uint(env, argv[1], &idx)) {
return raise_badarg(env, argv[1]);
}
statement_acquire_lock(statement);
if (statement->statement == NULL) {
statement_release_lock(statement);
return make_error_tuple(env, am_invalid_statement);
}
int rc = sqlite3_bind_null(statement->statement, idx);
statement_release_lock(statement);
return enif_make_int(env, rc);
}
///
/// Steps the sqlite prepared statement multiple times.
///
/// This is to reduce the back and forth between the BEAM and sqlite in
/// fetching data. Without using this, throughput can suffer.
///
ERL_NIF_TERM
exqlite_multi_step(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
statement_t* statement = NULL;
connection_t* conn = NULL;
int chunk_size;
if (argc != 3) {
return enif_make_badarg(env);
}
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
return make_error_tuple(env, am_invalid_connection);
}
if (!enif_get_resource(env, argv[1], statement_type, (void**)&statement)) {
return make_error_tuple(env, am_invalid_statement);
}
if (!statement || !statement->statement) {
return make_error_tuple(env, am_invalid_statement);
}
if (!enif_get_int(env, argv[2], &chunk_size)) {
return make_error_tuple(env, am_invalid_chunk_size);
}
if (chunk_size < 1) {
return make_error_tuple(env, am_invalid_chunk_size);
}
connection_acquire_lock(conn);
connection_stash_caller(conn, env);
if (statement->statement == NULL) {
connection_clear_caller(conn);
connection_release_lock(conn);
return make_error_tuple(env, am_invalid_statement);
}
ERL_NIF_TERM rows = enif_make_list_from_array(env, NULL, 0);
for (int i = 0; i < chunk_size; i++) {
ERL_NIF_TERM row;
int rc = sqlite3_step(statement->statement);
switch (rc) {
case SQLITE_BUSY:
sqlite3_reset(statement->statement);
connection_clear_caller(conn);
connection_release_lock(conn);
return am_busy;
case SQLITE_DONE:
sqlite3_reset(statement->statement);
connection_clear_caller(conn);
connection_release_lock(conn);
return enif_make_tuple2(env, am_done, rows);
case SQLITE_ROW:
row = make_row(env, statement->statement);
rows = enif_make_list_cell(env, row, rows);
break;
default:
sqlite3_reset(statement->statement);
connection_clear_caller(conn);
connection_release_lock(conn);
return make_sqlite3_error_tuple(env, rc, conn->db);
}
}
connection_clear_caller(conn);
connection_release_lock(conn);
return enif_make_tuple2(env, am_rows, rows);
}
///
/// Invokes one step on the SQLite prepared statement's results. If multiple
/// steps are being taken, throughput may suffer, but this does allow for
/// better interleaved calls to a NIF and letting the VM do more bookkeeping
///
ERL_NIF_TERM
exqlite_step(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
ERL_NIF_TERM result;
statement_t* statement = NULL;
connection_t* conn = NULL;
if (argc != 2) {
return enif_make_badarg(env);
}
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
return make_error_tuple(env, am_invalid_connection);
}
if (!enif_get_resource(env, argv[1], statement_type, (void**)&statement)) {
return make_error_tuple(env, am_invalid_statement);
}
connection_acquire_lock(conn);
connection_stash_caller(conn, env);
if (statement->statement == NULL) {
connection_clear_caller(conn);
connection_release_lock(conn);
return make_error_tuple(env, am_invalid_statement);
}
int rc = sqlite3_step(statement->statement);
switch (rc) {
case SQLITE_ROW:
result = enif_make_tuple2(env, am_row, make_row(env, statement->statement));
connection_clear_caller(conn);
connection_release_lock(conn);
return result;
case SQLITE_BUSY:
sqlite3_reset(statement->statement);
connection_clear_caller(conn);
connection_release_lock(conn);
return am_busy;
case SQLITE_DONE:
sqlite3_reset(statement->statement);
connection_clear_caller(conn);
connection_release_lock(conn);
return am_done;
default:
sqlite3_reset(statement->statement);
result = make_sqlite3_error_tuple(env, rc, conn->db);
connection_clear_caller(conn);
connection_release_lock(conn);
return result;
}
}
///
/// Get the columns requested in a prepared statement
///
ERL_NIF_TERM
exqlite_columns(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
int size = 0;
statement_t* statement = NULL;
connection_t* conn = NULL;
ERL_NIF_TERM* columns;
ERL_NIF_TERM result;
if (argc != 2) {
return enif_make_badarg(env);
}
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
return make_error_tuple(env, am_invalid_connection);
}
if (!enif_get_resource(env, argv[1], statement_type, (void**)&statement)) {
return make_error_tuple(env, am_invalid_statement);
}
statement_acquire_lock(statement);
if (statement->statement == NULL) {
statement_release_lock(statement);
return make_error_tuple(env, am_invalid_statement);
}
size = sqlite3_column_count(statement->statement);
if (size == 0) {
statement_release_lock(statement);
return make_ok_tuple(env, enif_make_list(env, 0));
} else if (size < 0) {
statement_release_lock(statement);
return make_error_tuple(env, am_invalid_column_count);
}
columns = enif_alloc(sizeof(ERL_NIF_TERM) * size);
if (!columns) {
statement_release_lock(statement);
return make_error_tuple(env, am_out_of_memory);
}
for (int i = 0; i < size; i++) {
const char* name = sqlite3_column_name(statement->statement, i);
if (!name) {
enif_free(columns);
statement_release_lock(statement);
return make_error_tuple(env, am_out_of_memory);
}
columns[i] = make_binary(env, name, strlen(name));
}
statement_release_lock(statement);
result = enif_make_list_from_array(env, columns, size);
enif_free(columns);
return make_ok_tuple(env, result);
}
///
/// Get the last inserted row id.
///
ERL_NIF_TERM
exqlite_last_insert_rowid(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
connection_t* conn = NULL;
if (argc != 1) {
return enif_make_badarg(env);
}
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
return make_error_tuple(env, am_invalid_connection);
}
connection_acquire_lock(conn);
if (conn->db == NULL) {
connection_release_lock(conn);
return make_error_tuple(env, am_connection_closed);
}
sqlite3_int64 last_rowid = sqlite3_last_insert_rowid(conn->db);
connection_release_lock(conn);
return make_ok_tuple(env, enif_make_int64(env, last_rowid));
}
///
/// Get the current transaction status
///
ERL_NIF_TERM
exqlite_transaction_status(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
connection_t* conn = NULL;
if (argc != 1) {
return enif_make_badarg(env);
}
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
return make_error_tuple(env, am_invalid_connection);
}
// If the connection times out, DbConnection disconnects the client
// and then re-opens a new connection. There is a condition where by
// the connection's database is not set but the calling elixir / erlang
// pass an incomplete reference.
// Check must be inside the lock: a concurrent close() can set conn->db = NULL
// between the pre-lock check and the sqlite3_get_autocommit() call → segfault.
connection_acquire_lock(conn);
if (!conn->db) {
connection_release_lock(conn);
return make_ok_tuple(env, am_error);
}
int autocommit = sqlite3_get_autocommit(conn->db);
connection_release_lock(conn);
return make_ok_tuple(
env,
autocommit == 0 ? am_transaction : am_idle);
}
ERL_NIF_TERM
exqlite_serialize(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
connection_t* conn = NULL;
ErlNifBinary database_name;
ERL_NIF_TERM eos = enif_make_int(env, 0);
unsigned char* buffer = NULL;
sqlite3_int64 buffer_size = 0;
ERL_NIF_TERM serialized;
if (argc != 2) {
return enif_make_badarg(env);
}
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
return make_error_tuple(env, am_invalid_connection);
}
if (!enif_inspect_iolist_as_binary(env, enif_make_list2(env, argv[1], eos), &database_name)) {
return make_error_tuple(env, am_database_name_not_iolist);
}
connection_acquire_lock(conn);
if (conn->db == NULL) {
connection_release_lock(conn);
return make_error_tuple(env, am_connection_closed);
}
buffer = sqlite3_serialize(conn->db, (char*)database_name.data, &buffer_size, 0);
if (!buffer) {
connection_release_lock(conn);
return make_error_tuple(env, am_serialization_failed);
}
serialized = make_binary(env, buffer, buffer_size);
sqlite3_free(buffer);
connection_release_lock(conn);
return make_ok_tuple(env, serialized);
}
ERL_NIF_TERM
exqlite_deserialize(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
connection_t* conn = NULL;
unsigned char* buffer = NULL;
ErlNifBinary database_name;
ERL_NIF_TERM eos = enif_make_int(env, 0);
ErlNifBinary serialized;
int size = 0;
int rc = 0;
int flags = SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_RESIZEABLE;
if (argc != 3) {
return enif_make_badarg(env);
}
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
return make_error_tuple(env, am_invalid_connection);
}
if (!enif_inspect_iolist_as_binary(env, enif_make_list2(env, argv[1], eos), &database_name)) {
return make_error_tuple(env, am_database_name_not_iolist);
}
if (!enif_inspect_binary(env, argv[2], &serialized)) {
return enif_make_badarg(env);
}
connection_acquire_lock(conn);
if (conn->db == NULL) {
connection_release_lock(conn);
return make_error_tuple(env, am_connection_closed);
}
size = serialized.size;
buffer = sqlite3_malloc(size);
if (!buffer) {
connection_release_lock(conn);
return make_error_tuple(env, am_deserialization_failed);
}
memcpy(buffer, serialized.data, size);
rc = sqlite3_deserialize(conn->db, (const char*)database_name.data, buffer, size, size, flags);
if (rc != SQLITE_OK) {
sqlite3_free(buffer);
connection_release_lock(conn);
return make_sqlite3_error_tuple(env, rc, conn->db);
}
connection_release_lock(conn);
return am_ok;
}
///
/// Releases a prepared statement's consumed memory and allows the system to
/// reclaim it.
///
ERL_NIF_TERM
exqlite_release(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
statement_t* statement = NULL;
connection_t* conn = NULL;
if (argc != 2) {
return enif_make_badarg(env);
}
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
return make_error_tuple(env, am_invalid_connection);
}
if (!enif_get_resource(env, argv[1], statement_type, (void**)&statement)) {
return make_error_tuple(env, am_invalid_statement);
}
statement_acquire_lock(statement);
if (statement->statement) {
sqlite3_finalize(statement->statement);
statement->statement = NULL;
}
statement_release_lock(statement);
return am_ok;
}
void
connection_type_destructor(ErlNifEnv* env, void* arg)
{
assert(env);
assert(arg);
connection_t* conn = (connection_t*)arg;
if (conn->mutex) {
connection_acquire_lock(conn);
}
if (conn->interrupt_mutex) {
enif_mutex_lock(conn->interrupt_mutex);
// Signal cancel to wake any busy handler that might still be sleeping,
// so it returns and releases SQLite's db->mutex before we close.
conn->cancelled = 1;
}
if (conn->db) {
sqlite3_close_v2(conn->db);
conn->db = NULL;
}
if (conn->interrupt_mutex) {
enif_mutex_unlock(conn->interrupt_mutex);
}
if (conn->mutex) {
connection_release_lock(conn);
}
if (conn->mutex) {
enif_mutex_destroy(conn->mutex);
conn->mutex = NULL;
}
if (conn->interrupt_mutex) {
enif_mutex_destroy(conn->interrupt_mutex);
conn->interrupt_mutex = NULL;
}
}
void
statement_type_destructor(ErlNifEnv* env, void* arg)
{
assert(env);
assert(arg);
statement_t* statement = (statement_t*)arg;
statement_acquire_lock(statement);
if (statement->statement) {
sqlite3_finalize(statement->statement);
statement->statement = NULL;
}
statement_release_lock(statement);
enif_release_resource(statement->conn);
statement->conn = NULL;
}
int
on_load(ErlNifEnv* env, void** priv, ERL_NIF_TERM info)
{
assert(env);
static const sqlite3_mem_methods methods = {
exqlite_malloc,
exqlite_free,
exqlite_realloc,
exqlite_mem_size,
exqlite_mem_round_up,
exqlite_mem_init,
exqlite_mem_shutdown,
0};
sqlite3_config(SQLITE_CONFIG_GETMALLOC, &default_alloc_methods);
sqlite3_config(SQLITE_CONFIG_MALLOC, &methods);
am_ok = enif_make_atom(env, "ok");
am_error = enif_make_atom(env, "error");
am_badarg = enif_make_atom(env, "badarg");
am_nil = enif_make_atom(env, "nil");
am_out_of_memory = enif_make_atom(env, "out_of_memory");
am_done = enif_make_atom(env, "done");
am_row = enif_make_atom(env, "row");
am_rows = enif_make_atom(env, "rows");
am_invalid_filename = enif_make_atom(env, "invalid_filename");
am_invalid_flags = enif_make_atom(env, "invalid_flags");
am_database_open_failed = enif_make_atom(env, "database_open_failed");
am_failed_to_create_mutex = enif_make_atom(env, "failed_to_create_mutex");
am_invalid_connection = enif_make_atom(env, "invalid_connection");
am_sql_not_iolist = enif_make_atom(env, "sql_not_iolist");
am_connection_closed = enif_make_atom(env, "connection_closed");
am_invalid_statement = enif_make_atom(env, "invalid_statement");
am_invalid_chunk_size = enif_make_atom(env, "invalid_chunk_size");
am_busy = enif_make_atom(env, "busy");
am_invalid_column_count = enif_make_atom(env, "invalid_column_count");
am_transaction = enif_make_atom(env, "transaction");
am_idle = enif_make_atom(env, "idle");
am_database_name_not_iolist = enif_make_atom(env, "database_name_not_iolist");
am_serialization_failed = enif_make_atom(env, "serialization_failed");
am_deserialization_failed = enif_make_atom(env, "deserialization_failed");
am_invalid_enable_load_extension_value = enif_make_atom(env, "invalid_enable_load_extension_value");
am_insert = enif_make_atom(env, "insert");
am_delete = enif_make_atom(env, "delete");
am_update = enif_make_atom(env, "update");
am_invalid_pid = enif_make_atom(env, "invalid_pid");
am_log = enif_make_atom(env, "log");
connection_type = enif_open_resource_type(
env,
NULL,
"connection_type",
connection_type_destructor,
ERL_NIF_RT_CREATE,
NULL);
if (!connection_type) {
return -1;
}
statement_type = enif_open_resource_type(
env,
NULL,
"statement_type",
statement_type_destructor,
ERL_NIF_RT_CREATE,
NULL);
if (!statement_type) {
return -1;
}
log_hook_mutex = enif_mutex_create("exqlite:log_hook");
if (!log_hook_mutex) {
return -1;
}
return 0;
}
static void
on_unload(ErlNifEnv* caller_env, void* priv_data)
{
assert(caller_env);
sqlite3_config(SQLITE_CONFIG_MALLOC, &default_alloc_methods);
enif_mutex_destroy(log_hook_mutex);
}
// We don't need to upgrade anything yet
// See: https://www.erlang.org/docs/28/apps/erts/erl_nif.html#initialization
static int
on_upgrade(ErlNifEnv* env, void** priv_data, void** old_priv_data, ERL_NIF_TERM load_info)
{
assert(env);
return 0;
}
//
// Enable extension loading
//
ERL_NIF_TERM
exqlite_enable_load_extension(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
connection_t* conn = NULL;
int rc = SQLITE_OK;
int enable_load_extension_value;
if (argc != 2) {
return enif_make_badarg(env);
}
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
return make_error_tuple(env, am_invalid_connection);
}
if (!enif_get_int(env, argv[1], &enable_load_extension_value)) {
return make_error_tuple(env, am_invalid_enable_load_extension_value);
}
connection_acquire_lock(conn);
if (conn->db == NULL) {
connection_release_lock(conn);
return make_error_tuple(env, am_connection_closed);
}
rc = sqlite3_enable_load_extension(conn->db, enable_load_extension_value);
if (rc != SQLITE_OK) {
ERL_NIF_TERM err = make_sqlite3_error_tuple(env, rc, conn->db);
connection_release_lock(conn);
return err;
}
connection_release_lock(conn);
return am_ok;
}
///
/// Data Change Notifications
///
void
update_callback(void* arg, int sqlite_operation_type, char const* sqlite_database, char const* sqlite_table, sqlite3_int64 sqlite_rowid)
{
connection_t* conn = (connection_t*)arg;
if (conn == NULL) {
return;
}
ErlNifEnv* msg_env = enif_alloc_env();
ERL_NIF_TERM change_type;
switch (sqlite_operation_type) {
case SQLITE_INSERT:
change_type = am_insert;
break;
case SQLITE_DELETE:
change_type = am_delete;
break;
case SQLITE_UPDATE:
change_type = am_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, change_type, database, table, rowid);
if (!enif_send(NULL, &conn->update_hook_pid, msg_env, msg)) {
sqlite3_update_hook(conn->db, NULL, NULL);
}
enif_free_env(msg_env);
}
ERL_NIF_TERM
exqlite_set_update_hook(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
connection_t* conn = NULL;
if (argc != 2) {
return enif_make_badarg(env);
}
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
return am_invalid_connection;
}
if (!enif_get_local_pid(env, argv[1], &conn->update_hook_pid)) {
return am_invalid_pid;
}
connection_acquire_lock(conn);
if (conn->db == NULL) {
connection_release_lock(conn);
return make_error_tuple(env, am_connection_closed);
}
// Passing the connection as the third argument causes it to be
// passed as the first argument to update_callback. This allows us
// to extract the hook pid and reset the hook if the pid is not alive.
sqlite3_update_hook(conn->db, update_callback, conn);
connection_release_lock(conn);
return am_ok;
}
//
// Authorizer
//
static int
authorizer_callback(void* user_data, int action, const char* arg1, const char* arg2, const char* db_name, const char* trigger)
{
connection_t* conn = (connection_t*)user_data;
if (action >= 0 && action < AUTHORIZER_DENY_SIZE && conn->authorizer_deny[action]) {
return SQLITE_DENY;
}
return SQLITE_OK;
}
// Maps atom names to SQLite authorizer action codes
static unsigned int
action_code_from_atom(ErlNifEnv* env, ERL_NIF_TERM atom)
{
// NOTE: `SQLITE_COPY` is no longer used, this is assigned the code 0, we
// can safely ignore it here and avoid the pesky signed integer UB
char buf[32];
const size_t buffsize = sizeof(buf);
if (!enif_get_atom(env, atom, buf, buffsize, ERL_NIF_LATIN1)) {
return 0;
}
buf[buffsize - 1] = 0;
if (strncmp(buf, "create_index", buffsize) == 0) {
return SQLITE_CREATE_INDEX;
}
if (strncmp(buf, "create_table", buffsize) == 0) {
return SQLITE_CREATE_TABLE;
}
if (strncmp(buf, "create_temp_index", buffsize) == 0) {
return SQLITE_CREATE_TEMP_INDEX;
}
if (strncmp(buf, "create_temp_table", buffsize) == 0) {
return SQLITE_CREATE_TEMP_TABLE;
}
if (strncmp(buf, "create_temp_trigger", buffsize) == 0) {
return SQLITE_CREATE_TEMP_TRIGGER;
}
if (strncmp(buf, "create_temp_view", buffsize) == 0) {
return SQLITE_CREATE_TEMP_VIEW;
}
if (strncmp(buf, "create_trigger", buffsize) == 0) {
return SQLITE_CREATE_TRIGGER;
}
if (strncmp(buf, "create_view", buffsize) == 0) {
return SQLITE_CREATE_VIEW;
}
if (strncmp(buf, "delete", buffsize) == 0) {
return SQLITE_DELETE;
}
if (strncmp(buf, "drop_index", buffsize) == 0) {
return SQLITE_DROP_INDEX;
}
if (strncmp(buf, "drop_table", buffsize) == 0) {
return SQLITE_DROP_TABLE;
}
if (strncmp(buf, "drop_temp_index", buffsize) == 0) {
return SQLITE_DROP_TEMP_INDEX;
}
if (strncmp(buf, "drop_temp_table", buffsize) == 0) {
return SQLITE_DROP_TEMP_TABLE;
}
if (strncmp(buf, "drop_temp_trigger", buffsize) == 0) {
return SQLITE_DROP_TEMP_TRIGGER;
}
if (strncmp(buf, "drop_temp_view", buffsize) == 0) {
return SQLITE_DROP_TEMP_VIEW;
}
if (strncmp(buf, "drop_trigger", buffsize) == 0) {
return SQLITE_DROP_TRIGGER;
}
if (strncmp(buf, "drop_view", buffsize) == 0) {
return SQLITE_DROP_VIEW;
}
if (strncmp(buf, "insert", buffsize) == 0) {
return SQLITE_INSERT;
}
if (strncmp(buf, "pragma", buffsize) == 0) {
return SQLITE_PRAGMA;
}
if (strncmp(buf, "read", buffsize) == 0) {
return SQLITE_READ;
}
if (strncmp(buf, "select", buffsize) == 0) {
return SQLITE_SELECT;
}
if (strncmp(buf, "transaction", buffsize) == 0) {
return SQLITE_TRANSACTION;
}
if (strncmp(buf, "update", buffsize) == 0) {
return SQLITE_UPDATE;
}
if (strncmp(buf, "attach", buffsize) == 0) {
return SQLITE_ATTACH;
}
if (strncmp(buf, "detach", buffsize) == 0) {
return SQLITE_DETACH;
}
if (strncmp(buf, "alter_table", buffsize) == 0) {
return SQLITE_ALTER_TABLE;
}
if (strncmp(buf, "reindex", buffsize) == 0) {
return SQLITE_REINDEX;
}
if (strncmp(buf, "analyze", buffsize) == 0) {
return SQLITE_ANALYZE;
}
if (strncmp(buf, "create_vtable", buffsize) == 0) {
return SQLITE_CREATE_VTABLE;
}
if (strncmp(buf, "drop_vtable", buffsize) == 0) {
return SQLITE_DROP_VTABLE;
}
if (strncmp(buf, "function", buffsize) == 0) {
return SQLITE_FUNCTION;
}
if (strncmp(buf, "savepoint", buffsize) == 0) {
return SQLITE_SAVEPOINT;
}
if (strncmp(buf, "recursive", buffsize) == 0) {
return SQLITE_RECURSIVE;
}
return 0;
}
// set_authorizer(conn, deny_list) -> :ok | {:error, reason}
// deny_list is a list of atoms: [:attach, :detach, :pragma, ...]
// Pass an empty list to clear the authorizer.
ERL_NIF_TERM
exqlite_set_authorizer(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
connection_t* conn = NULL;
if (argc != 2) {
return enif_make_badarg(env);
}
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
return am_invalid_connection;
}
connection_acquire_lock(conn);
if (conn->db == NULL) {
connection_release_lock(conn);
return make_error_tuple(env, am_connection_closed);
}
// Parse the deny list
unsigned int list_len;
if (!enif_get_list_length(env, argv[1], &list_len)) {
connection_release_lock(conn);
return enif_make_badarg(env);
}
if (list_len == 0) {
// Empty list: clear the authorizer
memset(conn->authorizer_deny, 0, sizeof(conn->authorizer_deny));
sqlite3_set_authorizer(conn->db, NULL, NULL);
connection_release_lock(conn);
return am_ok;
}
// Validate all atoms before mutating state — a bad atom in the list
// should not clear an existing authorizer as a side effect.
int new_deny[AUTHORIZER_DENY_SIZE] = {0};
ERL_NIF_TERM head;
ERL_NIF_TERM tail = argv[1];
while (enif_get_list_cell(env, tail, &head, &tail)) {
unsigned int code = action_code_from_atom(env, head);
if (code == 0) {
connection_release_lock(conn);
return enif_make_badarg(env);
}
new_deny[code] = 1;
}
// Validation passed — apply atomically
memcpy(conn->authorizer_deny, new_deny, sizeof(conn->authorizer_deny));
sqlite3_set_authorizer(conn->db, authorizer_callback, conn);
connection_release_lock(conn);
return am_ok;
}
//
// Log Notifications
//
void
log_callback(void* arg, int iErrCode, const char* zMsg)
{
if (log_hook_pid == NULL) {
return;
}
ErlNifEnv* msg_env = enif_alloc_env();
ERL_NIF_TERM error = make_binary(msg_env, zMsg, strlen(zMsg));
ERL_NIF_TERM msg = enif_make_tuple3(msg_env, am_log, enif_make_int(msg_env, iErrCode), error);
if (!enif_send(NULL, log_hook_pid, msg_env, msg)) {
enif_mutex_lock(log_hook_mutex);
sqlite3_config(SQLITE_CONFIG_LOG, NULL, NULL);
enif_free(log_hook_pid);
log_hook_pid = NULL;
enif_mutex_unlock(log_hook_mutex);
}
enif_free_env(msg_env);
}
ERL_NIF_TERM
exqlite_set_log_hook(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
if (argc != 1) {
return enif_make_badarg(env);
}
ErlNifPid* pid = (ErlNifPid*)enif_alloc(sizeof(ErlNifPid));
if (!enif_get_local_pid(env, argv[0], pid)) {
enif_free(pid);
return make_error_tuple(env, am_invalid_pid);
}
enif_mutex_lock(log_hook_mutex);
if (log_hook_pid) {
enif_free(log_hook_pid);
}
log_hook_pid = pid;
sqlite3_config(SQLITE_CONFIG_LOG, log_callback, NULL);
enif_mutex_unlock(log_hook_mutex);
return am_ok;
}
///
/// Interrupt a long-running query.
///
ERL_NIF_TERM
exqlite_interrupt(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
connection_t* conn = NULL;
if (argc != 1) {
return enif_make_badarg(env);
}
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
return make_error_tuple(env, am_invalid_connection);
}
// We deliberately do NOT hold the connection lock here. A running
// query holds the lock for its entire duration; acquiring it in
// interrupt() would block until the query finishes, which defeats
// the purpose of interrupting it.
//
// interrupt_mutex is a dedicated lightweight lock shared with close().
// close() holds the connection lock (so any running query has already
// released it) then acquires interrupt_mutex before nulling conn->db.
// interrupt() acquires interrupt_mutex here, so the two cannot overlap.
enif_mutex_lock(conn->interrupt_mutex);
if (conn->db != NULL) {
sqlite3_interrupt(conn->db);
}
enif_mutex_unlock(conn->interrupt_mutex);
return am_ok;
}
///
/// Set busy timeout without destroying the custom handler.
/// (PRAGMA busy_timeout calls sqlite3_busy_timeout() which replaces handlers)
///
ERL_NIF_TERM
exqlite_set_busy_timeout(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
connection_t* conn = NULL;
int timeout_ms;
if (argc != 2) {
return enif_make_badarg(env);
}
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
return make_error_tuple(env, am_invalid_connection);
}
if (!enif_get_int(env, argv[1], &timeout_ms)) {
return enif_make_badarg(env);
}
connection_acquire_lock(conn);
if (conn->db == NULL) {
connection_release_lock(conn);
return make_error_tuple(env, am_connection_closed);
}
enif_mutex_lock(conn->interrupt_mutex);
conn->busy_timeout_ms = timeout_ms;
enif_mutex_unlock(conn->interrupt_mutex);
connection_release_lock(conn);
return am_ok;
}
///
/// Configure how often SQLite invokes the progress handler.
/// Values less than 1 disable the progress handler entirely.
///
ERL_NIF_TERM
exqlite_set_progress_handler_steps(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
connection_t* conn = NULL;
int steps;
if (argc != 2) {
return enif_make_badarg(env);
}
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
return make_error_tuple(env, am_invalid_connection);
}
if (!enif_get_int(env, argv[1], &steps)) {
return enif_make_badarg(env);
}
connection_acquire_lock(conn);
if (conn->db == NULL) {
connection_release_lock(conn);
return make_error_tuple(env, am_connection_closed);
}
enif_mutex_lock(conn->interrupt_mutex);
conn->progress_handler_steps = steps;
connection_configure_progress_handler(conn);
enif_mutex_unlock(conn->interrupt_mutex);
connection_release_lock(conn);
return am_ok;
}
/// Cancel: wake busy handler + interrupt VDBE.
/// Superset of interrupt/1: sets cancelled flag + calls sqlite3_interrupt().
///
ERL_NIF_TERM
exqlite_cancel(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
assert(env);
connection_t* conn = NULL;
if (argc != 1) {
return enif_make_badarg(env);
}
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
return make_error_tuple(env, am_invalid_connection);
}
// We deliberately avoid conn->mutex here: the running query holds it for
// the duration of the SQLite call, so taking it would block cancellation.
enif_mutex_lock(conn->interrupt_mutex);
conn->cancelled = 1;
if (conn->db != NULL) {
sqlite3_interrupt(conn->db);
}
enif_mutex_unlock(conn->interrupt_mutex);
return am_ok;
}
ERL_NIF_TERM
exqlite_errmsg(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
connection_t* conn;
statement_t* statement;
const char* msg;
if (enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
connection_acquire_lock(conn);
if (conn->db == NULL) {
connection_release_lock(conn);
return make_error_tuple(env, am_connection_closed);
}
msg = sqlite3_errmsg(conn->db);
connection_release_lock(conn);
} else if (enif_get_resource(env, argv[0], statement_type, (void**)&statement)) {
statement_acquire_lock(statement);
if (statement->statement == NULL) {
statement_release_lock(statement);
return am_nil;
}
msg = sqlite3_errmsg(sqlite3_db_handle(statement->statement));
statement_release_lock(statement);
} else {
return raise_badarg(env, argv[0]);
}
if (!msg) {
return am_nil;
}
return make_binary(env, msg, strlen(msg));
}
ERL_NIF_TERM
exqlite_errstr(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
int rc;
if (!enif_get_int(env, argv[0], &rc)) {
return raise_badarg(env, argv[0]);
}
const char* msg = sqlite3_errstr(rc);
return make_binary(env, msg, strlen(msg));
}
//
// Most of our nif functions are going to be IO bounded
//
static ErlNifFunc nif_funcs[] = {
{"open", 2, exqlite_open, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"close", 1, exqlite_close, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"execute", 2, exqlite_execute, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"changes", 1, exqlite_changes, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"prepare", 2, exqlite_prepare, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"reset", 1, exqlite_reset, ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"bind_parameter_count", 1, exqlite_bind_parameter_count},
{"bind_parameter_index", 2, exqlite_bind_parameter_index},
{"bind_text", 3, exqlite_bind_text},
{"bind_blob", 3, exqlite_bind_blob},
{"bind_integer", 3, exqlite_bind_integer},
{"bind_float", 3, exqlite_bind_float},
{"bind_null", 2, exqlite_bind_null},
{"step", 2, exqlite_step, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"multi_step", 3, exqlite_multi_step, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"columns", 2, exqlite_columns, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"last_insert_rowid", 1, exqlite_last_insert_rowid, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"transaction_status", 1, exqlite_transaction_status, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"serialize", 2, exqlite_serialize, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"deserialize", 3, exqlite_deserialize, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"release", 2, exqlite_release, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"enable_load_extension", 2, exqlite_enable_load_extension, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"set_update_hook", 2, exqlite_set_update_hook, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"set_authorizer", 2, exqlite_set_authorizer, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"set_log_hook", 1, exqlite_set_log_hook, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"interrupt", 1, exqlite_interrupt, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"set_busy_timeout", 2, exqlite_set_busy_timeout, 0},
{"set_progress_handler_steps", 2, exqlite_set_progress_handler_steps, 0},
{"cancel", 1, exqlite_cancel, 0},
{"errmsg", 1, exqlite_errmsg},
{"errstr", 1, exqlite_errstr},
};
ERL_NIF_INIT(Elixir.Exqlite.Sqlite3NIF, nif_funcs, on_load, NULL, on_upgrade, on_unload)