#include #include #include // Elixir workaround for . in module names #ifdef STATIC_ERLANG_NIF #define STATIC_ERLANG_NIF_LIBNAME sqlite3_nif #endif #include #include 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]; } connection_t; typedef struct statement { connection_t* conn; sqlite3_stmt* statement; } statement_t; 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); } /// /// 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); } sqlite3_busy_timeout(db, 2000); 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 = enif_mutex_create("exqlite:interrupt"); memset(conn->authorizer_deny, 0, sizeof(conn->authorizer_deny)); if (conn->interrupt_mutex == NULL) { // conn->db and conn->mutex are set; the destructor will clean them up. enif_release_resource(conn); return make_error_tuple(env, am_failed_to_create_mutex); } 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); // DB is already closed, nothing to do here. if (conn->db == NULL) { 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_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_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); if (conn->db == NULL) { 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_release_lock(conn); return make_sqlite3_error_tuple(env, rc, conn->db); } 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); if (conn->db == NULL) { 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_release_lock(conn); enif_release_resource(statement); return result; } 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); if (statement->statement == NULL) { 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_release_lock(conn); return am_busy; case SQLITE_DONE: sqlite3_reset(statement->statement); 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_release_lock(conn); return make_sqlite3_error_tuple(env, rc, conn->db); } } 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); if (statement->statement == NULL) { 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_release_lock(conn); return result; case SQLITE_BUSY: sqlite3_reset(statement->statement); connection_release_lock(conn); return am_busy; case SQLITE_DONE: sqlite3_reset(statement->statement); connection_release_lock(conn); return am_done; default: sqlite3_reset(statement->statement); result = make_sqlite3_error_tuple(env, rc, conn->db); 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->db) { sqlite3_close_v2(conn->db); conn->db = NULL; } 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; } 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}, {"errmsg", 1, exqlite_errmsg}, {"errstr", 1, exqlite_errstr}, }; ERL_NIF_INIT(Elixir.Exqlite.Sqlite3NIF, nif_funcs, on_load, NULL, on_upgrade, on_unload)