diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/db.js | 60 | ||||
-rw-r--r-- | src/napi-sqlite.c | 807 | ||||
-rw-r--r-- | src/sql/config.sql | 34 | ||||
-rw-r--r-- | src/sql/migrations/2023-11-16T15:46:27-03:00-init-auth-data.sql | 0 | ||||
-rw-r--r-- | src/utils.js | 13 |
5 files changed, 813 insertions, 101 deletions
@@ -1,43 +1,49 @@ -const fs = require("node:fs"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); + const sqlite = require("./napi-sqlite.node"); +const { difference, log } = require("./utils"); -// const value = 8; -// console.log(`${value} time 2 =`, sqlite.my_function(value)); -const CONFIG_FILE = __dirname + "/sql/config.sql"; const MIGRATIONS_DIR = __dirname + "/sql/migrations/"; +const MIGRATION_FILENAMES = fs.readdirSync(MIGRATIONS_DIR, "UTF-8"); let db = null; -const init = async () => { - console.log({ - sqlite, - }); - console.log(`sqlite.myfn(2): ${sqlite.myfn(2)}`); - console.log(`sqlite.open(2): ${sqlite.open(2)}`); - /* - const config = fs.readFileSync(CONFIG_FILE, "UTF-8"); - const migrations = fs.readdirSync(MIGRATIONS_DIR, "UTF-8"); - - await exec(config); - await exec(` +const init = async (dbName = process.env.PAPO_DB_PATH) => { + assert(dbName); + + db = await sqlite.open(dbName); + await db.exec(` + BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS migrations ( filename TEXT PRIMARY KEY ); - `); - const done = await run(` - SELECT filename FROM migrations; + COMMIT TRANSACTION; `); - // FIXME: sort - const pending = new Set(migrations).difference(new Set(done)); + const done = await db.all(` + SELECT filename FROM migrations; + `).map(row => row.filename); + const pending = difference( + new Set(MIGRATION_FILENAMES), + new Set(done) + ); + const sortedPending = [...pending].sort((a, b) => a.localeCompare(b)); - await exec("BEGIN TRANSACTION;"); - for (const p of pending) { - await exec(fs.readFileSync(MIGRATIONS_DIR + p, "UTF-8")); - await exec(`INSERT INTO migrations (filename) VALUES (?)`, p); + await db.exec("BEGIN TRANSACTION;"); + for (const filename of sortedPending) { + log({ + log: "exec-migration", + filename, + }); + const sql = fs.readFileSync(MIGRATIONS_DIR + filename, "UTF-8"); + await db.exec(sql); + await db.run( + `INSERT INTO migrations (filename) VALUES ($filename);`, + { filename }, + ); } - await exec("COMMIT TRANSACTION;"); - */ + await db.exec("COMMIT TRANSACTION;"); }; module.exports = { diff --git a/src/napi-sqlite.c b/src/napi-sqlite.c index df3b042..74ab340 100644 --- a/src/napi-sqlite.c +++ b/src/napi-sqlite.c @@ -1,74 +1,801 @@ +#include <assert.h> #include <stdio.h> +#include <stdlib.h> #include <node/node_api.h> +#include <sqlite3.h> + + +static napi_value ffi_open(napi_env env, napi_callback_info info); +static napi_value ffi_exec(napi_env env, napi_callback_info info); +static napi_value ffi_all (napi_env env, napi_callback_info info); +static napi_value ffi_run (napi_env env, napi_callback_info info); + + +struct NAPIContext { + napi_env env; + napi_value value; + uint32_t index; +}; + +struct Fn { + const char *const label; + napi_value(*const handle)(napi_env env, napi_callback_info info); +}; + +static const struct Fn fns[] = { + { .label = "open", .handle = ffi_open, }, + { NULL, NULL }, +}; + +struct Fn methods[] = { + { .label = "exec", .handle = ffi_exec, }, + { .label = "all", .handle = ffi_all, }, + { .label = "run", .handle = ffi_run, }, + { NULL, NULL }, +}; -/* -FIXME static const napi_type_tag SQLITE_DB_TYPE_TAG = { 0x0e9614d459f746cc, 0x88b814a5dc5c4cf7 }; -*/ + +static const int +SQLITE_OPEN_FLAGS = + /* + From https://www.sqlite.org/c3ref/open.html: + + > The database is opened for reading and writing, and is created if + > it does not already exist. This is the behavior that is always + > used for sqlite3_open() and sqlite3_open16(). + */ + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | + + /* + From https://www.sqlite.org/c3ref/open.html: + + > The new database connection will use the "serialized" threading + > mode. This means the multiple threads can safely attempt to use + > the same database connection at the same time. (Mutexes will block + > any actual concurrency, but in this mode there is no harm in + > trying.) + */ + SQLITE_OPEN_FULLMUTEX | + + /* + From https://www.sqlite.org/c3ref/open.html: + + > The database connection comes up in "extended result code mode". + > In other words, the database behaves has if + > sqlite3_extended_result_codes(db,1) where called on the database + > connection as soon as the connection is created. In addition to + > setting the extended result code mode, this flag also causes + > sqlite3_open_v2() to return an extended result code. + + From https://www.sqlite.org/c3ref/extended_result_codes.html: + + > The sqlite3_extended_result_codes() routine enables or disables the + > extended result codes feature of SQLite. The extended result codes + > are disabled by default for historical compatibility. + */ + SQLITE_OPEN_EXRESCODE; + + +static const char *const +FIX_SQLITE_PRAGMAS = + /* + From https://research.cs.wisc.edu/adsl/Publications/alice-osdi14.pdf: + + > Similarly, SQLite does not provide durability under the default + > journal-mode (we became aware of this only after interacting with + > developers), but its documentation seems misleading. + > + > (...) + > + > The developers suggest the SQLite vulnerability is actually not a + > behavior guaranteed by SQLite (specifically, that durability cannot + > be achieved under rollback journaling); we believe the + > documentation is misleading. + > + > (...) + > + > Unclear documentation of application guarantees contributes to the + > confusion about crash vulnerabilities. During discussions with + > developers about durability vulnerabilities, we found that SQLite, + > which proclaims itself as fully ACID-complaint, does not provide + > durability (even optionally) with the default storage engine, + > though the documentation suggests it does. + */ + "PRAGMA journal_mode = WAL;\n" + "PRAGMA synchronous = EXTRA;\n" + + /* + From https://www.sqlite.org/foreignkeys.html: + + > Foreign key constraints are disabled by default (for backwards + > compatibility), so must be enabled separately for each database + > connection. + */ + "PRAGMA foreign_keys = ON;\n" + ; + + +// FIXME: make this async +static napi_value +ffi_exec(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1]; + int sqlite_rc; + napi_status napi_rc; + char *sql = NULL; + size_t sql_size1; + size_t sql_size2; + sqlite3 *db_handle = NULL; + char *error_msg = NULL; + + napi_rc = napi_get_cb_info( + env, + info, + &argc, + argv, + NULL, + (void *)&db_handle + ); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "111TODO ERRCODE", + "Failed to parse arguments TODO i18n" + ); + goto out; + } + + napi_rc = napi_get_value_string_utf8(env, argv[0], NULL, 0, &sql_size1); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "222TODO ERRCODE", + "Invalid number was passed as argument TODO i18n" + ); + goto out; + } + + if (sql_size1 == SIZE_MAX) { + napi_throw_error( + env, + "SQLITE_EOVERFLOW", + "TODO " + ); + goto out; + } + sql_size1++; // include the NULL-terminator in size measurement + + sql = malloc(sql_size1); + if (!sql) { + napi_throw_error( + env, + "SQLITE_ENOMEM", + "TODO i18n" + ); + goto out; + } + + napi_rc = napi_get_value_string_utf8( + env, + argv[0], + sql, + sql_size1, + &sql_size2 + ); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "TODO ERRCODE", + "Invalid number was passed as argument TODO i18n" + ); + goto out; + } + assert(sql_size1 == sql_size2 + 1 && + "Unstable behaviour from Node-API"); + assert(sql); + + sqlite_rc = sqlite3_exec( + db_handle, + sql, + NULL, + NULL, + &error_msg + ); + if (sqlite_rc != SQLITE_OK) { + napi_throw_error( + env, + "3iii33TODO ERRCODE", + sqlite3_errstr(sqlite_rc) + ); + goto out; + } + +out: + if (error_msg) { + sqlite3_free(error_msg); + } + if (sql) { + free(sql); + } + return NULL; +} + + +static int +accumulate_all_results( + void *ctxptr, + int argc, + char **argv, + char **column_names +) { + int rc = 0; + + napi_status napi_rc; + napi_value row = NULL; + + struct NAPIContext *ctx = ctxptr; + napi_env env = ctx->env; + napi_value results = ctx->value; + uint32_t index = ctx->index; + + napi_rc = napi_create_object(env, &row); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "TODO ERRCODE", + "TODO i18n" + ); + rc = -1; + goto out; + } + + for (int i = 0; i < argc; i++) { + const char *const column_name = column_names[i]; + const char *const column_value = argv[i]; + + // printf("column_name: %s\n", column_name); + // printf("column_value: %s\n", column_value); + + napi_value str; // FIXME: dispatch on the type + napi_rc = napi_create_string_utf8( + env, + column_value, + NAPI_AUTO_LENGTH, + &str + ); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "SQLITE_MiiiETHOD_CREATE", + "TODO i18n" + ); + rc = -1; + goto out; + } + + napi_rc = napi_set_named_property( + env, + row, + column_name, + str + ); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "iSQLITE_MiETHOD_CREATE", + "TODO i18n" + ); + rc = -1; + goto out; + } + } + + napi_rc = napi_set_element( + env, + results, + index, + row + ); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "TODO ERRCODE", + "TODO i18n" + ); + rc = -1; + goto out; + } + + ctx->index++; + +out: + return rc; +} static napi_value -myfn(napi_env env, napi_callback_info info) { +ffi_all(napi_env env, napi_callback_info info) { napi_value ret = NULL; - napi_status status; size_t argc = 1; - int number; napi_value argv[1]; - napi_value my_number; + int sqlite_rc; + napi_status napi_rc; + char *sql = NULL; + size_t sql_size1; + size_t sql_size2; + sqlite3 *db_handle = NULL; + char *error_msg = NULL; + napi_value results_array = NULL; + + napi_rc = napi_get_cb_info( + env, + info, + &argc, + argv, + NULL, + (void *)&db_handle + ); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "111TODO ERRCODE", + "Failed to parse arguments TODO i18n" + ); + goto out; + } + + napi_rc = napi_get_value_string_utf8(env, argv[0], NULL, 0, &sql_size1); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "222TODO ERRCODE", + "Invalid number was passed as argument TODO i18n" + ); + goto out; + } - status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL); - if (status != napi_ok) { - napi_throw_error(env, NULL, "Failed to parse arguments FIXME i18n"); + if (sql_size1 == SIZE_MAX) { + napi_throw_error( + env, + "SQLITE_EOVERFLOW", + "TODO " + ); goto out; } + sql_size1++; // include the NULL-terminator in size measurement - status = napi_get_value_int32(env, argv[0], &number); - if (status != napi_ok) { - napi_throw_error(env, NULL, "Invalid number was passed as argument FIXME i18n"); + sql = malloc(sql_size1); + if (!sql) { + napi_throw_error( + env, + "SQLITE_ENOMEM", + "TODO i18n" + ); goto out; } - number = number * 2; - status = napi_create_int32(env, number, &my_number); - if (status != napi_ok) { - napi_throw_error(env, NULL, "Unable to create return value FIXME i18n"); + napi_rc = napi_get_value_string_utf8( + env, + argv[0], + sql, + sql_size1, + &sql_size2 + ); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "TODO ERRCODE", + "Invalid number was passed as argument TODO i18n" + ); goto out; } - ret = my_number; + assert(sql_size1 == sql_size2 + 1 && + "Unstable behaviour from Node-API"); + assert(sql); + + napi_rc = napi_create_array(env, &results_array); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "TODO ERRCODE", + "Invalid number was passed as argument TODO i18n" + ); + goto out; + } + + struct NAPIContext ctx = { + .env = env, + .value = results_array, + .index = 0, + }; + sqlite_rc = sqlite3_exec( + db_handle, + sql, + accumulate_all_results, + &ctx, + &error_msg + ); + if (sqlite_rc != SQLITE_OK) { + napi_throw_error( + env, + "i3iiii33TODO ERRCODE", + sqlite3_errstr(sqlite_rc) + ); + goto out; + } + + ret = results_array; out: + if (error_msg) { + sqlite3_free(error_msg); + } + if (sql) { + free(sql); + } return ret; } static napi_value -open(napi_env env, napi_callback_info info) { - (void)env; - (void)info; - return NULL; +ffi_run(napi_env env, napi_callback_info info) { + napi_value ret = NULL; + + size_t argc = 1; + napi_value argv[1]; + int sqlite_rc; + napi_status napi_rc; + char *sql = NULL; + size_t sql_size1; + size_t sql_size2; + sqlite3 *db_handle = NULL; + char *error_msg = NULL; + napi_value results_array = NULL; + + napi_rc = napi_get_cb_info( + env, + info, + &argc, + argv, + NULL, + (void *)&db_handle + ); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "111TODO ERRCODE", + "Failed to parse arguments TODO i18n" + ); + goto out; + } + + napi_rc = napi_get_value_string_utf8(env, argv[0], NULL, 0, &sql_size1); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "222TODO ERRCODE", + "Invalid number was passed as argument TODO i18n" + ); + goto out; + } + + if (sql_size1 == SIZE_MAX) { + napi_throw_error( + env, + "SQLITE_EOVERFLOW", + "TODO " + ); + goto out; + } + sql_size1++; // include the NULL-terminator in size measurement + + sql = malloc(sql_size1); + if (!sql) { + napi_throw_error( + env, + "SQLITE_ENOMEM", + "TODO i18n" + ); + goto out; + } + + napi_rc = napi_get_value_string_utf8( + env, + argv[0], + sql, + sql_size1, + &sql_size2 + ); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "TODO ERRCODE", + "Invalid number was passed as argument TODO i18n" + ); + goto out; + } + assert(sql_size1 == sql_size2 + 1 && + "Unstable behaviour from Node-API"); + assert(sql); + + napi_rc = napi_create_array(env, &results_array); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "TODO ERRCODE", + "Invalid number was passed as argument TODO i18n" + ); + goto out; + } + + struct NAPIContext ctx = { + .env = env, + .value = results_array, + .index = 0, + }; + sqlite_rc = sqlite3_exec( + db_handle, + sql, + accumulate_all_results, + &ctx, + &error_msg + ); + if (sqlite_rc != SQLITE_OK) { + napi_throw_error( + env, + "i3iiii33TODO ERRCODE", + sqlite3_errstr(sqlite_rc) + ); + goto out; + } + + ret = results_array; + +out: + if (error_msg) { + sqlite3_free(error_msg); + } + if (sql) { + free(sql); + } + return ret; +} + +static void +finalize_db_handle(napi_env env, void *finalize_data, void *finalize_hint) { + (void)finalize_hint; + sqlite3 *db_handle = finalize_data; + int sqlite_rc; + + sqlite_rc = sqlite3_close(db_handle); + if (sqlite_rc != SQLITE_OK) { + napi_throw_error( + env, + "TODO ERRCODE", + sqlite3_errstr(sqlite_rc) + ); + goto out; + } + +out: + return; } static napi_value -close(napi_env env, napi_callback_info info) { - (void)env; - (void)info; - return NULL; +ffi_open(napi_env env, napi_callback_info info) { + napi_value ret = NULL; + + size_t argc = 1; + napi_value argv[1]; + int sqlite_rc; + napi_status napi_rc; + char *filename = NULL; + size_t filename_size1; + size_t filename_size2; + sqlite3 *db_handle = NULL; + napi_value wrapped_db_handle = NULL; + char *error_msg = NULL; + + napi_rc = napi_get_cb_info(env, info, &argc, argv, NULL, NULL); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "TODO ERRCODE", + "Failed to parse arguments TODO i18n" + ); + goto out; + } + + napi_rc = napi_get_value_string_utf8( + env, + argv[0], + NULL, // FIXME: what is this? + 0, // FIXME: what is this? + &filename_size1 + ); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "TODO ERRCODE", + "Invalid number was passed as argument TODO i18n" + ); + goto out; + } + + if (filename_size1 == SIZE_MAX) { + napi_throw_error( + env, + "SQLITE_EOVERFLOW", + "TODO " + ); + goto out; + } + filename_size1++; // include the NULL-terminator in size measurement + + filename = malloc(filename_size1); + if (!filename) { + napi_throw_error( + env, + "SQLITE_ENOMEM", + "TODO i18n" + ); + goto out; + } + + napi_rc = napi_get_value_string_utf8( + env, + argv[0], + filename, + filename_size1, + &filename_size2 + ); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "TODO ERRCODE", + "Invalid number was passed as argument TODO i18n" + ); + goto out; + } + assert(filename_size1 == filename_size2 + 1 && + "Unstable behaviour from Node-API"); + assert(filename); + + sqlite_rc = sqlite3_open_v2( + filename, + &db_handle, + SQLITE_OPEN_FLAGS, + NULL + ); + if (sqlite_rc != SQLITE_OK) { + napi_throw_error( + env, + "3i33TODO ERRCODE", + sqlite3_errstr(sqlite_rc) + ); + goto out; + } + + sqlite_rc = sqlite3_exec( + db_handle, + FIX_SQLITE_PRAGMAS, + NULL, + NULL, + &error_msg + ); + if (sqlite_rc != SQLITE_OK) { + napi_throw_error( + env, + "3ii33TODO ERRCODE", + sqlite3_errstr(sqlite_rc) + ); + goto out; + } + + + // FIXME: setup the global error log: + // https://www.sqlite.org/errlog.html + + napi_rc = napi_create_object(env, &wrapped_db_handle); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "TODO ERRCODE", + "TODO i18n" + ); + goto out; + } + + napi_rc = napi_type_tag_object( + env, + wrapped_db_handle, + &SQLITE_DB_TYPE_TAG + ); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "TODO ERRCODE", + "TODO i18n" + ); + goto out; + } + + napi_rc = napi_wrap( + env, + wrapped_db_handle, + db_handle, + finalize_db_handle, + NULL, + NULL + ); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "TODO ERRCODE", + "TODO i18n" + ); + goto out; + } + + for (size_t i = 0; methods[i].label && methods[i].handle; i++) { + napi_value fn; + napi_rc = napi_create_function( + env, + methods[i].label, + NAPI_AUTO_LENGTH, + methods[i].handle, + db_handle, + &fn + ); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "SQLITE_MiiETHOD_CREATE", + "TODO i18n" + ); + goto out; + } + + napi_rc = napi_set_named_property( + env, + wrapped_db_handle, + methods[i].label, + fn + ); + if (napi_rc != napi_ok) { + napi_throw_error( + env, + "SQLITE_METHOD_SETNAME", + "TODO i18n" + ); + goto out; + } + } + ret = wrapped_db_handle; + +out: + if (error_msg) { + sqlite3_free(error_msg); + } + if (filename) { + free(filename); + } + if (!ret) { + if (db_handle) { + if (sqlite3_close(db_handle)) { + // logerr(); + } + } + } + return ret; } -static const struct { - const char *label; - napi_value(*const handle)(napi_env env, napi_callback_info info); -} fns[] = { - { .label = "myfn", .handle = myfn, }, - { .label = "open", .handle = open, }, - { .label = "close", .handle = close, }, - { NULL, NULL }, -}; static napi_value -init(napi_env env, napi_value exports) { +ffi_init(napi_env env, napi_value exports) { napi_value ret = exports; napi_status status; @@ -80,7 +807,7 @@ init(napi_env env, napi_value exports) { fns[i].label, NAPI_AUTO_LENGTH, fns[i].handle, - "xucrutes", + NULL, &fn ); if (status != napi_ok) { @@ -88,7 +815,7 @@ init(napi_env env, napi_value exports) { napi_throw_error( env, "SQLITE_FN_CREATE", - "Unable to wrap native function FIXME i18n" + "Unable to wrap native function TODO i18n" ); goto out; } @@ -104,7 +831,7 @@ init(napi_env env, napi_value exports) { napi_throw_error( env, "SQLITE_FN_SETNAME", - "Unable to populate exports FIXME i18n" + "Unable to populate exports TODO i18n" ); goto out; } @@ -116,5 +843,5 @@ out: NAPI_MODULE_INIT() { - return init(env, exports); + return ffi_init(env, exports); } diff --git a/src/sql/config.sql b/src/sql/config.sql deleted file mode 100644 index 53eb279..0000000 --- a/src/sql/config.sql +++ /dev/null @@ -1,34 +0,0 @@ -; "Litestream requires periodic but short write locks on the database when -; checkpointing occurs": -; https://litestream.io/tips/#busy-timeout -PRAGMA busy_timeout = 5000; - -; "Litestream only works with the SQLite WAL journaling mode": -; https://litestream.io/tips/#wal-journal-mode -PRAGMA journal_mode = WAL; - -; "(...) change the synchronous mode to NORMAL (it typically defaults to FULL)": -; https://litestream.io/tips/#synchronous-pragma -; "WAL mode is safe from corruption with synchronous=NORMAL": -; https://www.sqlite.org/pragma.html#pragma_synchronous -PRAGMA synchronous = NORMAL; - -; "(...) can perform a checkpoint in between Litestream-initiated checkpoints -; and cause Litestream to miss a WAL file": -; https://litestream.io/tips/#disable-autocheckpoints-for-high-write-load-servers -PRAGMA wal_autocheckpoint = 0; - -; "This pragma does a low-level formatting and consistency check of the -; database": -; https://www.sqlite.org/pragma.html#pragma_integrity_check -PRAGMA integrity_check; - -; "The foreign_key_check pragma checks the database, or the table called - \"table-name\", for foreign key constraints that are violated": -; https://www.sqlite.org/pragma.html#pragma_foreign_key_check -PRAGMA foreign_key_check; - - -CREATE TABLE IF NO EXISTS migrations ( - name TEXT PRIMARY KEY -); diff --git a/src/sql/migrations/2023-11-16T15:46:27-03:00-init-auth-data.sql b/src/sql/migrations/2023-11-16T15:46:27-03:00-init-auth-data.sql new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/sql/migrations/2023-11-16T15:46:27-03:00-init-auth-data.sql diff --git a/src/utils.js b/src/utils.js index 2c3ea72..e1725ef 100644 --- a/src/utils.js +++ b/src/utils.js @@ -38,7 +38,20 @@ const keys = (ks, obj) => {}, ); +const difference = (a, b) => { + const diff = new Set(a); + for (const el of b) { + diff.delete(el); + } + return diff; +}; + +const log = o => console.error(JSON.stringify(o)); + + module.exports = { eq, keys, + difference, + log, }; |