Skip to content

Commit

Permalink
sqlite: add support for custom functions
Browse files Browse the repository at this point in the history
This commit adds support to node:sqlite for defining custom
functions that can be invoked from SQL.

Fixes: nodejs#54349
  • Loading branch information
cjihrig committed Dec 11, 2024
1 parent d1b8b5c commit e0435f0
Show file tree
Hide file tree
Showing 4 changed files with 667 additions and 0 deletions.
28 changes: 28 additions & 0 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,31 @@ This method allows one or more SQL statements to be executed without returning
any results. This method is useful when executing SQL statements read from a
file. This method is a wrapper around [`sqlite3_exec()`][].

### `database.function(name[, options], function)`

<!-- YAML
added: REPLACEME
-->

* `name` {string} The name of the SQLite function to create.
* `options` {Object} Optional configuration settings for the function. The
following properties are supported:
* `deterministic` {boolean} If `true`, the [`SQLITE_DETERMINISTIC`][] flag is
set on the created function. **Default:** `false`.
* `directOnly` {boolean} If `true`, the [`SQLITE_DIRECTONLY`][] flag is set on
the created function. **Default:** `false`.
* `useBigIntArguments` {boolean} If `true`, integer arguments to `function`
are converted to `BigInt`s. If `false`, integer arguments are passed as
JavaScript numbers. **Default:** `false`.
* `varargs` {boolean} If `true`, `function` can accept a variable number of
arguments. If `false`, `function` must be invoked with exactly
`function.length` arguments. **Default:** `false`.
* `function` {Function} The JavaScript function to call when the SQLite
function is invoked.

This method is used to create SQLite user-defined functions. This method is a
wrapper around [`sqlite3_create_function_v2()`][].

### `database.open()`

<!-- YAML
Expand Down Expand Up @@ -490,8 +515,11 @@ The following constants are meant for use with [`database.applyChangeset()`](#da
[SQL injection]: https://en.wikipedia.org/wiki/SQL_injection
[`ATTACH DATABASE`]: https://www.sqlite.org/lang_attach.html
[`PRAGMA foreign_keys`]: https://www.sqlite.org/pragma.html#pragma_foreign_keys
[`SQLITE_DETERMINISTIC`]: https://www.sqlite.org/c3ref/c_deterministic.html
[`SQLITE_DIRECTONLY`]: https://www.sqlite.org/c3ref/c_deterministic.html
[`sqlite3_changes64()`]: https://www.sqlite.org/c3ref/changes.html
[`sqlite3_close_v2()`]: https://www.sqlite.org/c3ref/close.html
[`sqlite3_create_function_v2()`]: https://www.sqlite.org/c3ref/create_function.html
[`sqlite3_exec()`]: https://www.sqlite.org/c3ref/exec.html
[`sqlite3_expanded_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
[`sqlite3_last_insert_rowid()`]: https://www.sqlite.org/c3ref/last_insert_rowid.html
Expand Down
265 changes: 265 additions & 0 deletions src/node_sqlite.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ using v8::Function;
using v8::FunctionCallback;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Global;
using v8::Int32;
using v8::Integer;
using v8::Isolate;
using v8::Local;
Expand Down Expand Up @@ -112,6 +114,123 @@ inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, const char* message) {
}
}

class UserDefinedFunction {
public:
explicit UserDefinedFunction(Environment* env,
Local<Function> fn,
bool use_bigint_args)
: env_(env), fn_(env->isolate(), fn), use_bigint_args_(use_bigint_args) {}
virtual ~UserDefinedFunction() {}

static void xFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv) {
UserDefinedFunction* self =
static_cast<UserDefinedFunction*>(sqlite3_user_data(ctx));
Environment* env = self->env_;
Isolate* isolate = env->isolate();
auto recv = Undefined(isolate);
auto fn = self->fn_.Get(isolate);
LocalVector<Value> js_argv(isolate);

for (int i = 0; i < argc; ++i) {
sqlite3_value* value = argv[i];
MaybeLocal<Value> js_val;

switch (sqlite3_value_type(value)) {
case SQLITE_INTEGER: {
sqlite3_int64 val = sqlite3_value_int64(value);
if (self->use_bigint_args_) {
js_val = BigInt::New(isolate, val);
} else if (std::abs(val) <= kMaxSafeJsInteger) {
js_val = Number::New(isolate, val);
} else {
THROW_ERR_OUT_OF_RANGE(isolate,
"Value is too large to be represented as a "
"JavaScript number: %" PRId64,
val);
return;
}
break;
}
case SQLITE_FLOAT:
js_val = Number::New(isolate, sqlite3_value_double(value));
break;
case SQLITE_TEXT: {
const char* v =
reinterpret_cast<const char*>(sqlite3_value_text(value));
js_val = String::NewFromUtf8(isolate, v).As<Value>();
break;
}
case SQLITE_NULL:
js_val = Null(isolate);
break;
case SQLITE_BLOB: {
size_t size = static_cast<size_t>(sqlite3_value_bytes(value));
auto data =
reinterpret_cast<const uint8_t*>(sqlite3_value_blob(value));
auto store = ArrayBuffer::NewBackingStore(isolate, size);
memcpy(store->Data(), data, size);
auto ab = ArrayBuffer::New(isolate, std::move(store));
js_val = Uint8Array::New(ab, 0, size);
break;
}
default:
UNREACHABLE("Bad SQLite value");
}

Local<Value> local;
if (!js_val.ToLocal(&local)) {
return;
}

js_argv.emplace_back(local);
}

MaybeLocal<Value> retval =
fn->Call(env->context(), recv, argc, js_argv.data());
Local<Value> result;
if (!retval.ToLocal(&result)) {
return;
}

if (result->IsUndefined() || result->IsNull()) {
sqlite3_result_null(ctx);
} else if (result->IsNumber()) {
sqlite3_result_double(ctx, result.As<Number>()->Value());
} else if (result->IsString()) {
Utf8Value val(isolate, result.As<String>());
sqlite3_result_text(ctx, *val, val.length(), SQLITE_TRANSIENT);
} else if (result->IsUint8Array()) {
ArrayBufferViewContents<uint8_t> buf(result);
sqlite3_result_blob(ctx, buf.data(), buf.length(), SQLITE_TRANSIENT);
} else if (result->IsBigInt()) {
bool lossless;
int64_t as_int = result.As<BigInt>()->Int64Value(&lossless);
if (!lossless) {
sqlite3_result_error(ctx, "BigInt value is too large for SQLite", -1);
return;
}
sqlite3_result_int64(ctx, as_int);
} else if (result->IsPromise()) {
sqlite3_result_error(
ctx, "Asynchronous user-defined functions are not supported", -1);
} else {
sqlite3_result_error(
ctx,
"Returned JavaScript value cannot be converted to a SQLite value",
-1);
}
}

static void xDestroy(void* self) {
delete static_cast<UserDefinedFunction*>(self);
}

private:
Environment* env_;
Global<Function> fn_;
bool use_bigint_args_;
};

DatabaseSync::DatabaseSync(Environment* env,
Local<Object> object,
DatabaseOpenConfiguration&& open_config,
Expand Down Expand Up @@ -400,6 +519,151 @@ void DatabaseSync::Exec(const FunctionCallbackInfo<Value>& args) {
CHECK_ERROR_OR_THROW(env->isolate(), db->connection_, r, SQLITE_OK, void());
}

void DatabaseSync::CustomFunction(const FunctionCallbackInfo<Value>& args) {
DatabaseSync* db;
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");

if (!args[0]->IsString()) {
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
"The \"name\" argument must be a string.");
return;
}

int fn_index = args.Length() < 3 ? 1 : 2;
bool use_bigint_args = false;
bool varargs = false;
bool deterministic = false;
bool direct_only = false;

if (fn_index > 1) {
if (!args[1]->IsObject()) {
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
"The \"options\" argument must be an object.");
return;
}

Local<Object> options = args[1].As<Object>();
Local<Value> use_bigint_args_v;
if (!options
->Get(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "useBigIntArguments"))
.ToLocal(&use_bigint_args_v)) {
return;
}

if (!use_bigint_args_v->IsUndefined()) {
if (!use_bigint_args_v->IsBoolean()) {
THROW_ERR_INVALID_ARG_TYPE(
env->isolate(),
"The \"options.useBigIntArguments\" argument must be a boolean.");
return;
}
use_bigint_args = use_bigint_args_v.As<Boolean>()->Value();
}

Local<Value> varargs_v;
if (!options
->Get(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "varargs"))
.ToLocal(&varargs_v)) {
return;
}

if (!varargs_v->IsUndefined()) {
if (!varargs_v->IsBoolean()) {
THROW_ERR_INVALID_ARG_TYPE(
env->isolate(),
"The \"options.varargs\" argument must be a boolean.");
return;
}
varargs = varargs_v.As<Boolean>()->Value();
}

Local<Value> deterministic_v;
if (!options
->Get(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "deterministic"))
.ToLocal(&deterministic_v)) {
return;
}

if (!deterministic_v->IsUndefined()) {
if (!deterministic_v->IsBoolean()) {
THROW_ERR_INVALID_ARG_TYPE(
env->isolate(),
"The \"options.deterministic\" argument must be a boolean.");
return;
}
deterministic = deterministic_v.As<Boolean>()->Value();
}

Local<Value> direct_only_v;
if (!options
->Get(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "directOnly"))
.ToLocal(&direct_only_v)) {
return;
}

if (!direct_only_v->IsUndefined()) {
if (!direct_only_v->IsBoolean()) {
THROW_ERR_INVALID_ARG_TYPE(
env->isolate(),
"The \"options.directOnly\" argument must be a boolean.");
return;
}
direct_only = direct_only_v.As<Boolean>()->Value();
}
}

if (!args[fn_index]->IsFunction()) {
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
"The \"function\" argument must be a function.");
return;
}

Utf8Value name(env->isolate(), args[0].As<String>());
Local<Function> fn = args[fn_index].As<Function>();

int argc = 0;
if (varargs) {
argc = -1;
} else {
Local<Value> js_len;
if (!fn->Get(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "length"))
.ToLocal(&js_len)) {
return;
}
argc = js_len.As<Int32>()->Value();
}

UserDefinedFunction* user_data =
new UserDefinedFunction(env, fn, use_bigint_args);
int text_rep = SQLITE_UTF8;

if (deterministic) {
text_rep |= SQLITE_DETERMINISTIC;
}

if (direct_only) {
text_rep |= SQLITE_DIRECTONLY;
}

int r = sqlite3_create_function_v2(db->connection_,
*name,
argc,
text_rep,
user_data,
UserDefinedFunction::xFunc,
nullptr,
nullptr,
UserDefinedFunction::xDestroy);
CHECK_ERROR_OR_THROW(env->isolate(), db->connection_, r, SQLITE_OK, void());
}

void DatabaseSync::CreateSession(const FunctionCallbackInfo<Value>& args) {
std::string table;
std::string db_name = "main";
Expand Down Expand Up @@ -1409,6 +1673,7 @@ static void Initialize(Local<Object> target,
SetProtoMethod(isolate, db_tmpl, "close", DatabaseSync::Close);
SetProtoMethod(isolate, db_tmpl, "prepare", DatabaseSync::Prepare);
SetProtoMethod(isolate, db_tmpl, "exec", DatabaseSync::Exec);
SetProtoMethod(isolate, db_tmpl, "function", DatabaseSync::CustomFunction);
SetProtoMethod(
isolate, db_tmpl, "createSession", DatabaseSync::CreateSession);
SetProtoMethod(
Expand Down
1 change: 1 addition & 0 deletions src/node_sqlite.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class DatabaseSync : public BaseObject {
static void Close(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Prepare(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Exec(const v8::FunctionCallbackInfo<v8::Value>& args);
static void CustomFunction(const v8::FunctionCallbackInfo<v8::Value>& args);
static void CreateSession(const v8::FunctionCallbackInfo<v8::Value>& args);
static void ApplyChangeset(const v8::FunctionCallbackInfo<v8::Value>& args);
static void EnableLoadExtension(
Expand Down
Loading

0 comments on commit e0435f0

Please sign in to comment.