733 lines
20 KiB
C++
733 lines
20 KiB
C++
// Copyright 2025 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
#include <assert.h>
|
|
#include <list>
|
|
|
|
#include "addon.h"
|
|
|
|
#include "napi.h"
|
|
#include "signal-tokenizer.h"
|
|
#include "sqlite3.h"
|
|
|
|
// Signal Tokenizer
|
|
|
|
class SignalTokenizerModule {
|
|
public:
|
|
static void Destroy(void* p_ctx) {
|
|
delete static_cast<SignalTokenizerModule*>(p_ctx);
|
|
}
|
|
|
|
static fts5_tokenizer api_object;
|
|
|
|
private:
|
|
static int Create(void* p_ctx, char const**, int, Fts5Tokenizer** pp_out) {
|
|
SignalTokenizerModule* m = static_cast<SignalTokenizerModule*>(p_ctx);
|
|
*pp_out = reinterpret_cast<Fts5Tokenizer*>(m);
|
|
return SQLITE_OK;
|
|
}
|
|
|
|
static void Delete(Fts5Tokenizer* tokenizer) {}
|
|
};
|
|
|
|
fts5_tokenizer SignalTokenizerModule::api_object = {
|
|
&Create,
|
|
&Delete,
|
|
signal_fts5_tokenize,
|
|
};
|
|
|
|
static int SignalTokenizeCallback(void* tokens_ptr,
|
|
int _flags,
|
|
char const* token,
|
|
int len,
|
|
int _start,
|
|
int _end) {
|
|
std::vector<std::string>* tokens =
|
|
reinterpret_cast<std::vector<std::string>*>(tokens_ptr);
|
|
tokens->push_back(std::string(token, len));
|
|
return SQLITE_OK;
|
|
}
|
|
|
|
static Napi::Value SignalTokenize(const Napi::CallbackInfo& info) {
|
|
auto env = info.Env();
|
|
|
|
auto value = info[0].As<Napi::String>();
|
|
assert(value.IsString());
|
|
|
|
auto utf8 = value.Utf8Value();
|
|
|
|
std::vector<std::string> tokens;
|
|
int status =
|
|
signal_fts5_tokenize(nullptr, reinterpret_cast<void*>(&tokens), 0,
|
|
utf8.c_str(), utf8.length(), SignalTokenizeCallback);
|
|
if (status != SQLITE_OK) {
|
|
NAPI_THROW(Napi::Error::New(env, "Failed to tokenize"), Napi::Value());
|
|
}
|
|
|
|
auto result = Napi::Array::New(env, tokens.size());
|
|
int i = 0;
|
|
for (auto& str : tokens) {
|
|
result[i++] = str.c_str();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Utils
|
|
|
|
Napi::Error FormatError(Napi::Env env, const char* format, ...) {
|
|
va_list args;
|
|
|
|
// Get buffer size
|
|
va_start(args, format);
|
|
auto size = vsnprintf(nullptr, 0, format, args);
|
|
va_end(args);
|
|
|
|
// Allocate and fill the string
|
|
auto buf = new char[size + 1];
|
|
va_start(args, format);
|
|
vsnprintf(buf, size + 1, format, args);
|
|
va_end(args);
|
|
|
|
auto err = Napi::Error::New(env, std::string(buf, size));
|
|
delete[] buf;
|
|
return err;
|
|
}
|
|
|
|
// Database
|
|
|
|
Napi::Object Database::Init(Napi::Env env, Napi::Object exports) {
|
|
exports["databaseOpen"] = Napi::Function::New(env, &Database::Open);
|
|
exports["databaseInitTokenizer"] =
|
|
Napi::Function::New(env, &Database::InitTokenizer);
|
|
exports["databaseClose"] = Napi::Function::New(env, &Database::Close);
|
|
exports["databaseExec"] = Napi::Function::New(env, &Database::Exec);
|
|
return exports;
|
|
}
|
|
|
|
Database::Database(Napi::Env env, sqlite3* handle) : handle_(handle) {
|
|
auto external = Napi::External<Database>::New(
|
|
env, this, [](Napi::Env env, Database* db) { delete db; });
|
|
self_ref_ = Napi::Persistent(external);
|
|
}
|
|
|
|
Database::~Database() {
|
|
// Manually closed
|
|
if (handle_ == nullptr) {
|
|
return;
|
|
}
|
|
|
|
int r = sqlite3_close(handle_);
|
|
if (r != SQLITE_OK) {
|
|
fprintf(stderr, "Cleanup: sqlite3_close failure\n");
|
|
abort();
|
|
}
|
|
handle_ = nullptr;
|
|
}
|
|
|
|
Database* Database::FromExternal(const Napi::Value value) {
|
|
auto external = value.As<Napi::External<Database>>();
|
|
|
|
auto db = external.Data();
|
|
|
|
if (db->handle_ == nullptr) {
|
|
NAPI_THROW(Napi::Error::New(value.Env(), "Database closed"), nullptr);
|
|
}
|
|
|
|
return db;
|
|
}
|
|
|
|
Napi::Value Database::Open(const Napi::CallbackInfo& info) {
|
|
auto env = info.Env();
|
|
|
|
auto path = info[0].As<Napi::String>();
|
|
assert(path.IsString());
|
|
|
|
auto path_utf8 = path.Utf8Value();
|
|
|
|
int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
|
|
|
|
sqlite3* handle = nullptr;
|
|
int r = sqlite3_open_v2(path_utf8.c_str(), &handle, flags, nullptr);
|
|
if (r != SQLITE_OK) {
|
|
NAPI_THROW(FormatError(env, "sqlite open error: %s", sqlite3_errstr(r)),
|
|
Napi::Value());
|
|
}
|
|
|
|
auto db = new Database(env, handle);
|
|
|
|
r = sqlite3_extended_result_codes(handle, 1);
|
|
if (r != SQLITE_OK) {
|
|
return db->ThrowSqliteError(env, r);
|
|
}
|
|
|
|
return db->self_ref_.Value();
|
|
}
|
|
|
|
Napi::Value Database::InitTokenizer(const Napi::CallbackInfo& info) {
|
|
auto env = info.Env();
|
|
|
|
auto db = FromExternal(info[0]);
|
|
if (db == nullptr) {
|
|
return Napi::Value();
|
|
}
|
|
|
|
fts5_api* fts5 = db->GetFTS5API(env);
|
|
|
|
if (fts5 == nullptr) {
|
|
return Napi::Value();
|
|
}
|
|
SignalTokenizerModule* icu = new SignalTokenizerModule();
|
|
int r =
|
|
fts5->xCreateTokenizer(fts5, "signal_tokenizer", icu, &icu->api_object,
|
|
&SignalTokenizerModule::Destroy);
|
|
if (r != SQLITE_OK) {
|
|
delete icu;
|
|
return db->ThrowSqliteError(env, r);
|
|
}
|
|
|
|
return Napi::Value();
|
|
}
|
|
|
|
Napi::Value Database::Close(const Napi::CallbackInfo& info) {
|
|
auto env = info.Env();
|
|
|
|
auto db = FromExternal(info[0]);
|
|
if (db == nullptr) {
|
|
return Napi::Value();
|
|
}
|
|
|
|
// Close all active statements (otherwise `sqlite3_close()` is going to error)
|
|
for (const auto& stmt : db->statements_) {
|
|
int r = sqlite3_finalize(stmt->handle_);
|
|
if (r != SQLITE_OK) {
|
|
return db->ThrowSqliteError(env, r);
|
|
}
|
|
stmt->handle_ = nullptr;
|
|
stmt->db_ = nullptr;
|
|
}
|
|
db->statements_.clear();
|
|
|
|
int r = sqlite3_close(db->handle_);
|
|
if (r != SQLITE_OK) {
|
|
return db->ThrowSqliteError(env, r);
|
|
}
|
|
db->handle_ = nullptr;
|
|
return Napi::Value();
|
|
}
|
|
|
|
Napi::Value Database::Exec(const Napi::CallbackInfo& info) {
|
|
auto env = info.Env();
|
|
|
|
auto db = FromExternal(info[0]);
|
|
auto query = info[1].As<Napi::String>();
|
|
assert(query.IsString());
|
|
|
|
if (db == nullptr) {
|
|
return Napi::Value();
|
|
}
|
|
|
|
auto query_utf8 = query.Utf8Value();
|
|
|
|
if (db->handle_ == nullptr) {
|
|
NAPI_THROW(Napi::Error::New(env, "Database closed"), Napi::Value());
|
|
}
|
|
|
|
int r =
|
|
sqlite3_exec(db->handle_, query_utf8.c_str(), nullptr, nullptr, nullptr);
|
|
if (r != SQLITE_OK) {
|
|
return db->ThrowSqliteError(env, r);
|
|
}
|
|
return Napi::Value();
|
|
}
|
|
|
|
Napi::Value Database::ThrowSqliteError(Napi::Env env, int error) {
|
|
assert(handle_ != nullptr);
|
|
const char* msg = sqlite3_errmsg(handle_);
|
|
int offset = sqlite3_error_offset(handle_);
|
|
int extended = sqlite3_extended_errcode(handle_);
|
|
if (offset == -1) {
|
|
NAPI_THROW(FormatError(env, "sqlite error(%d): %s", extended, msg),
|
|
Napi::Value());
|
|
} else {
|
|
NAPI_THROW(FormatError(env, "sqlite error(%d): %s, offset: %d", extended,
|
|
msg, offset),
|
|
Napi::Value());
|
|
}
|
|
}
|
|
|
|
fts5_api* Database::GetFTS5API(Napi::Env env) {
|
|
sqlite3_stmt* stmt_ = nullptr;
|
|
|
|
int r = sqlite3_prepare(handle_, "SELECT fts5(?1)", -1, &stmt_, 0);
|
|
if (r != SQLITE_OK) {
|
|
ThrowSqliteError(env, r);
|
|
return nullptr;
|
|
}
|
|
|
|
fts5_api* fts5 = nullptr;
|
|
sqlite3_bind_pointer(stmt_, 1, reinterpret_cast<void*>(&fts5), "fts5_api_ptr",
|
|
nullptr);
|
|
sqlite3_step(stmt_);
|
|
r = sqlite3_finalize(stmt_);
|
|
if (r != SQLITE_OK) {
|
|
ThrowSqliteError(env, r);
|
|
return nullptr;
|
|
}
|
|
|
|
assert(fts5 != nullptr);
|
|
return fts5;
|
|
}
|
|
|
|
std::list<Statement*>::const_iterator Database::TrackStatement(
|
|
Statement* stmt) {
|
|
// Keep database instance alive while any statement is
|
|
self_ref_.Ref();
|
|
|
|
statements_.emplace_back(stmt);
|
|
auto end = statements_.end();
|
|
end--;
|
|
return end;
|
|
}
|
|
|
|
void Database::UntrackStatement(std::list<Statement*>::const_iterator iter) {
|
|
self_ref_.Unref();
|
|
statements_.erase(iter);
|
|
}
|
|
|
|
// Statement
|
|
|
|
Napi::Object Statement::Init(Napi::Env env, Napi::Object exports) {
|
|
exports["statementNew"] = Napi::Function::New(env, &Statement::New);
|
|
exports["statementClose"] = Napi::Function::New(env, &Statement::Close);
|
|
exports["statementRun"] = Napi::Function::New(env, &Statement::Run);
|
|
exports["statementStep"] = Napi::Function::New(env, &Statement::Step);
|
|
return exports;
|
|
}
|
|
|
|
Statement::Statement(Database* db,
|
|
Napi::Value db_obj,
|
|
sqlite3_stmt* handle,
|
|
bool is_persistent,
|
|
bool is_pluck,
|
|
bool is_bigint)
|
|
: db_(db),
|
|
handle_(handle),
|
|
is_persistent_(is_persistent),
|
|
is_pluck_(is_pluck),
|
|
is_bigint_(is_bigint) {
|
|
db_iter_ = db_->TrackStatement(this);
|
|
}
|
|
|
|
Statement::~Statement() {
|
|
// Manually closed
|
|
if (handle_ == nullptr) {
|
|
return;
|
|
}
|
|
|
|
int r = sqlite3_finalize(handle_);
|
|
if (r != SQLITE_OK) {
|
|
fprintf(stderr, "Cleanup: sqlite3_finalize failure\n");
|
|
abort();
|
|
}
|
|
db_->UntrackStatement(db_iter_);
|
|
db_ = nullptr;
|
|
handle_ = nullptr;
|
|
}
|
|
|
|
Napi::Value Statement::New(const Napi::CallbackInfo& info) {
|
|
auto env = info.Env();
|
|
|
|
auto db_external = info[0].As<Napi::External<Database>>();
|
|
auto query = info[1].As<Napi::String>();
|
|
auto is_persistent = info[2].As<Napi::Boolean>();
|
|
auto is_pluck = info[3].As<Napi::Boolean>();
|
|
auto is_bigint = info[4].As<Napi::Boolean>();
|
|
|
|
assert(db_external.IsExternal());
|
|
assert(query.IsString());
|
|
assert(is_persistent.IsBoolean());
|
|
assert(is_pluck.IsBoolean());
|
|
assert(is_bigint.IsBoolean());
|
|
|
|
auto db = db_external.Data();
|
|
|
|
auto utf8 = query.Utf8Value();
|
|
sqlite3_stmt* handle = nullptr;
|
|
|
|
const char* tail;
|
|
int r = sqlite3_prepare_v3(db->handle(), utf8.c_str(), utf8.length(),
|
|
is_persistent ? SQLITE_PREPARE_PERSISTENT : 0,
|
|
&handle, &tail);
|
|
if (r != SQLITE_OK) {
|
|
return db->ThrowSqliteError(env, r);
|
|
}
|
|
|
|
// Verify no further statements
|
|
if (HasTail(tail)) {
|
|
r = sqlite3_finalize(handle);
|
|
if (r == SQLITE_OK) {
|
|
NAPI_THROW(Napi::Error::New(env, "Can't prepare more than one statement"),
|
|
Napi::Value());
|
|
} else {
|
|
return db->ThrowSqliteError(env, r);
|
|
}
|
|
}
|
|
|
|
auto stmt = new Statement(db, db_external, handle, is_persistent, is_pluck,
|
|
is_bigint);
|
|
|
|
return Napi::External<Statement>::New(
|
|
env, stmt, [](Napi::Env env, Statement* stmt) { delete stmt; });
|
|
}
|
|
|
|
Statement* Statement::FromExternal(const Napi::Value& value) {
|
|
auto external = value.As<Napi::External<Statement>>();
|
|
assert(external.IsExternal());
|
|
|
|
auto stmt = external.Data();
|
|
|
|
if (stmt->handle_ == nullptr) {
|
|
NAPI_THROW(Napi::Error::New(external.Env(), "Statement closed"), nullptr);
|
|
}
|
|
|
|
return stmt;
|
|
}
|
|
|
|
Napi::Value Statement::Close(const Napi::CallbackInfo& info) {
|
|
auto env = info.Env();
|
|
|
|
auto stmt = FromExternal(info[0]);
|
|
if (stmt == nullptr) {
|
|
return Napi::Value();
|
|
}
|
|
|
|
int r = sqlite3_finalize(stmt->handle_);
|
|
if (r != SQLITE_OK) {
|
|
return stmt->db_->ThrowSqliteError(env, r);
|
|
}
|
|
stmt->handle_ = nullptr;
|
|
stmt->db_->UntrackStatement(stmt->db_iter_);
|
|
stmt->db_ = nullptr;
|
|
return Napi::Value();
|
|
}
|
|
|
|
Napi::Value Statement::Run(const Napi::CallbackInfo& info) {
|
|
auto env = info.Env();
|
|
|
|
auto stmt = FromExternal(info[0]);
|
|
if (stmt == nullptr) {
|
|
return Napi::Value();
|
|
}
|
|
|
|
auto params = info[1];
|
|
auto result = info[2].As<Napi::Array>();
|
|
|
|
assert(params.IsObject() || params.IsUndefined());
|
|
assert(result.IsArray());
|
|
|
|
if (stmt->handle_ == nullptr) {
|
|
NAPI_THROW(Napi::Error::New(env, "Statement closed"), Napi::Value());
|
|
}
|
|
|
|
if (!stmt->BindParams(env, params)) {
|
|
// BindParams threw an exception
|
|
return Napi::Value();
|
|
}
|
|
|
|
int total_changes_before = sqlite3_total_changes(stmt->db_->handle());
|
|
|
|
int r = sqlite3_step(stmt->handle_);
|
|
stmt->Reset();
|
|
if (r != SQLITE_DONE && r != SQLITE_ROW) {
|
|
return stmt->db_->ThrowSqliteError(env, r);
|
|
}
|
|
|
|
int total_changes_after = sqlite3_total_changes(stmt->db_->handle());
|
|
int64_t last_rowid = sqlite3_last_insert_rowid(stmt->db_->handle());
|
|
|
|
result[static_cast<uint32_t>(0)] = total_changes_after == total_changes_before
|
|
? 0
|
|
: sqlite3_changes(stmt->db_->handle());
|
|
result[static_cast<uint32_t>(1)] = last_rowid;
|
|
|
|
return Napi::Value();
|
|
}
|
|
|
|
Napi::Value Statement::Step(const Napi::CallbackInfo& info) {
|
|
auto env = info.Env();
|
|
|
|
auto stmt = FromExternal(info[0]);
|
|
if (stmt == nullptr) {
|
|
return Napi::Value();
|
|
}
|
|
|
|
auto params = info[1];
|
|
auto cache = info[2];
|
|
auto is_get = info[3].As<Napi::Boolean>();
|
|
|
|
// Note: `null` is only allowed in `run` to keep the bound parameters
|
|
assert(params.IsObject() || params.IsUndefined() || params.IsNull());
|
|
assert(cache.IsArray() || cache.IsUndefined());
|
|
assert(is_get.IsBoolean());
|
|
|
|
if (stmt->handle_ == nullptr) {
|
|
NAPI_THROW(Napi::Error::New(env, "Statement closed"), Napi::Value());
|
|
}
|
|
|
|
if (!stmt->BindParams(env, params)) {
|
|
// BindParams threw an exception
|
|
return Napi::Value();
|
|
}
|
|
|
|
int r = sqlite3_step(stmt->handle_);
|
|
|
|
// No more rows
|
|
if (r == SQLITE_DONE) {
|
|
stmt->Reset();
|
|
return Napi::Value();
|
|
}
|
|
|
|
AutoResetStatement _(stmt, is_get.Value());
|
|
if (r != SQLITE_ROW) {
|
|
return stmt->db_->ThrowSqliteError(env, r);
|
|
}
|
|
|
|
int column_count = sqlite3_column_count(stmt->handle_);
|
|
|
|
// In pluck mode - return the value of the first column
|
|
if (stmt->is_pluck_) {
|
|
if (column_count != 1) {
|
|
NAPI_THROW(Napi::Error::New(env, "Invalid column count for pluck"),
|
|
Napi::Value());
|
|
}
|
|
|
|
auto result = stmt->GetColumnValue(env, 0);
|
|
return result;
|
|
}
|
|
|
|
// In non-persistent mode - construct the JS object with column names as keys
|
|
// and row values as values.
|
|
if (!stmt->is_persistent_) {
|
|
auto result = Napi::Object::New(env);
|
|
for (int i = 0; i < column_count; i++) {
|
|
result[sqlite3_column_name(stmt->handle_, i)] =
|
|
stmt->GetColumnValue(env, i);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Track when the statement gets recompiled due to a schema change. When it
|
|
// happens - we need to invalidate the cached JS wrapper function that
|
|
// translates an array of column names and values into a JS object.
|
|
auto recompiled =
|
|
sqlite3_stmt_status(stmt->handle_, SQLITE_STMTSTATUS_REPREPARE, 1);
|
|
|
|
Napi::Array result;
|
|
if (recompiled || cache.IsUndefined()) {
|
|
result = Napi::Array::New(env, 2 * column_count);
|
|
for (int i = 0; i < column_count; i++) {
|
|
result[i] = sqlite3_column_name(stmt->handle_, i);
|
|
}
|
|
} else {
|
|
result = cache.As<Napi::Array>();
|
|
}
|
|
|
|
for (int i = 0; i < column_count; i++) {
|
|
result[column_count + i] = stmt->GetColumnValue(env, i);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool Statement::BindParams(Napi::Env env, Napi::Value params) {
|
|
int key_count = sqlite3_bind_parameter_count(handle_);
|
|
|
|
if (params.IsNull()) {
|
|
// `.all()` executes `Step()` multiple times, but only binds `params` once.
|
|
// Passing `null` allows to keep bound params as is until the last `Step()`
|
|
// where they will get reset.
|
|
} else if (params.IsUndefined()) {
|
|
if (key_count == 0) {
|
|
return true;
|
|
}
|
|
|
|
NAPI_THROW(FormatError(env, "Expected %d parameters, got 0", key_count),
|
|
false);
|
|
} else if (params.IsArray()) {
|
|
auto list = params.As<Napi::Array>();
|
|
auto list_len = static_cast<int>(list.Length());
|
|
if (list_len != key_count) {
|
|
NAPI_THROW(FormatError(env, "Expected %d parameters, got %d", key_count,
|
|
list_len),
|
|
false);
|
|
}
|
|
|
|
for (int i = 1; i <= list_len; i++) {
|
|
auto name = sqlite3_bind_parameter_name(handle_, i);
|
|
if (name != nullptr) {
|
|
NAPI_THROW(FormatError(env, "Unexpected named param %s at %d", name, i),
|
|
false);
|
|
}
|
|
|
|
auto error = BindParam(env, i, list[i - 1]);
|
|
if (error != nullptr) {
|
|
NAPI_THROW(
|
|
FormatError(env, "Failed to bind param %d, error %s", i, error),
|
|
false);
|
|
}
|
|
}
|
|
} else {
|
|
auto obj = params.As<Napi::Object>();
|
|
|
|
for (int i = 1; i <= key_count; i++) {
|
|
auto name = sqlite3_bind_parameter_name(handle_, i);
|
|
if (name == nullptr) {
|
|
NAPI_THROW(FormatError(env, "Unexpected anonymous param at %d", i),
|
|
false);
|
|
}
|
|
|
|
// Skip "$"
|
|
name = name + 1;
|
|
auto value = obj[name];
|
|
auto error = BindParam(env, i, value);
|
|
if (error != nullptr) {
|
|
NAPI_THROW(
|
|
FormatError(env, "Failed to bind param %s, error %s", name, error),
|
|
false);
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const char* Statement::BindParam(Napi::Env env, int column, Napi::Value param) {
|
|
int r;
|
|
switch (param.Type()) {
|
|
case napi_null:
|
|
r = sqlite3_bind_null(handle_, column);
|
|
break;
|
|
case napi_number:
|
|
r = sqlite3_bind_double(handle_, column,
|
|
param.As<Napi::Number>().DoubleValue());
|
|
break;
|
|
case napi_string: {
|
|
auto val = napi_value(param.As<Napi::String>());
|
|
|
|
size_t length;
|
|
napi_status status =
|
|
napi_get_value_string_utf8(env, val, nullptr, 0, &length);
|
|
if (status != napi_ok) {
|
|
return "failed to get string length";
|
|
}
|
|
|
|
char* data = new char[length + 1];
|
|
status = napi_get_value_string_utf8(env, val, data, length + 1, nullptr);
|
|
if (status != napi_ok) {
|
|
delete[] data;
|
|
return "failed to copy string data";
|
|
}
|
|
|
|
r = sqlite3_bind_text(handle_, column, data, length, DestroyString);
|
|
break;
|
|
}
|
|
case napi_bigint: {
|
|
bool lossless;
|
|
auto value = param.As<Napi::BigInt>().Int64Value(&lossless);
|
|
if (!lossless) {
|
|
return "failed to convert bigint to int64";
|
|
}
|
|
r = sqlite3_bind_int64(handle_, column, value);
|
|
break;
|
|
}
|
|
case napi_object:
|
|
if (param.IsTypedArray()) {
|
|
auto val = param.As<Napi::TypedArray>();
|
|
|
|
auto data = val.ArrayBuffer();
|
|
const uint8_t* view = reinterpret_cast<const uint8_t*>(data.Data());
|
|
|
|
r = sqlite3_bind_blob(handle_, column, view + val.ByteOffset(),
|
|
val.ByteLength(), SQLITE_TRANSIENT);
|
|
break;
|
|
} else {
|
|
return "unexpected type `object`";
|
|
}
|
|
case napi_boolean:
|
|
return "unexpected type `boolean`";
|
|
case napi_external:
|
|
return "unexpected type `external`";
|
|
case napi_function:
|
|
return "unexpected type `function`";
|
|
case napi_undefined:
|
|
return "unexpected type `undefined`";
|
|
case napi_symbol:
|
|
return "unexpected type `symbol`";
|
|
default:
|
|
return "unknown parameter type";
|
|
}
|
|
if (r != SQLITE_OK) {
|
|
return sqlite3_errmsg(db_->handle());
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void Statement::DestroyString(void* param) {
|
|
delete[] reinterpret_cast<char*>(param);
|
|
}
|
|
|
|
Napi::Value Statement::GetColumnValue(Napi::Env env, int column) {
|
|
int type = sqlite3_column_type(handle_, column);
|
|
switch (type) {
|
|
case SQLITE_INTEGER: {
|
|
auto val = sqlite3_column_int64(handle_, column);
|
|
if (is_bigint_) {
|
|
return Napi::BigInt::New(env, static_cast<int64_t>(val));
|
|
}
|
|
if (static_cast<int64_t>(INT32_MIN) <= val &&
|
|
val <= static_cast<int64_t>(INT32_MAX)) {
|
|
napi_value n_value;
|
|
NAPI_THROW_IF_FAILED(
|
|
env, napi_create_int32(env, static_cast<int32_t>(val), &n_value),
|
|
Napi::Value());
|
|
return Napi::Value(env, n_value);
|
|
} else {
|
|
return Napi::Number::New(env, val);
|
|
}
|
|
}
|
|
case SQLITE_TEXT:
|
|
return Napi::String::New(
|
|
env,
|
|
reinterpret_cast<const char*>(sqlite3_column_text(handle_, column)),
|
|
sqlite3_column_bytes(handle_, column));
|
|
case SQLITE_FLOAT:
|
|
return Napi::Number::New(env, sqlite3_column_double(handle_, column));
|
|
case SQLITE_BLOB:
|
|
return Napi::Buffer<uint8_t>::Copy(
|
|
env,
|
|
reinterpret_cast<const uint8_t*>(
|
|
sqlite3_column_blob(handle_, column)),
|
|
sqlite3_column_bytes(handle_, column));
|
|
case SQLITE_NULL:
|
|
return env.Null();
|
|
}
|
|
return Napi::Value();
|
|
}
|
|
|
|
AutoResetStatement::~AutoResetStatement() {
|
|
if (enabled_) {
|
|
stmt_->Reset();
|
|
}
|
|
}
|
|
|
|
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
sqlite3_initialize();
|
|
|
|
Database::Init(env, exports);
|
|
Statement::Init(env, exports);
|
|
exports["signalTokenize"] = Napi::Function::New(env, &SignalTokenize);
|
|
return exports;
|
|
}
|
|
|
|
NODE_API_MODULE(node_sqlcipher, Init)
|