Skip to content

Commit 1bf194b

Browse files
committed
sqlite: support db.loadExtension
1 parent 90d91ab commit 1bf194b

File tree

10 files changed

+218
-3
lines changed

10 files changed

+218
-3
lines changed

doc/api/errors.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2140,6 +2140,16 @@ An attempt was made to open an IPC communication channel with a synchronously
21402140
forked Node.js process. See the documentation for the [`child_process`][] module
21412141
for more information.
21422142

2143+
<a id="ERR_LOAD_SQLITE_EXTENSION"></a>
2144+
2145+
### `ERR_LOAD_SQLITE_EXTENSION`
2146+
2147+
<!-- YAML
2148+
added: REPLACEME
2149+
-->
2150+
2151+
An error occurred while loading a SQLite extension.
2152+
21432153
<a id="ERR_LOADER_CHAIN_INCOMPLETE"></a>
21442154

21452155
### `ERR_LOADER_CHAIN_INCOMPLETE`

doc/api/sqlite.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ added: v22.5.0
107107
* `open` {boolean} If `true`, the database is opened by the constructor. When
108108
this value is `false`, the database must be opened via the `open()` method.
109109
**Default:** `true`.
110+
* `allowLoadExtension` {boolean} If `true`, the `loadExtension` SQL function
111+
is enabled. **Default:** `false`.
110112

111113
Constructs a new `DatabaseSync` instance.
112114

@@ -119,6 +121,17 @@ added: v22.5.0
119121
Closes the database connection. An exception is thrown if the database is not
120122
open. This method is a wrapper around [`sqlite3_close_v2()`][].
121123

124+
### `database.loadExtension(path)`
125+
126+
<!-- YAML
127+
added: REPLACEME
128+
-->
129+
130+
* `path` {string} The path to the shared library to load.
131+
132+
Loads a shared library into the database connection. The `allowLoadExtension` option must be
133+
enabled when constructing the `DatabaseSync` instance.
134+
122135
### `database.exec(sql)`
123136

124137
<!-- YAML

src/node_errors.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
9191
V(ERR_INVALID_THIS, TypeError) \
9292
V(ERR_INVALID_URL, TypeError) \
9393
V(ERR_INVALID_URL_SCHEME, TypeError) \
94+
V(ERR_LOAD_SQLITE_EXTENSION, Error) \
9495
V(ERR_MEMORY_ALLOCATION_FAILED, Error) \
9596
V(ERR_MESSAGE_TARGET_CONTEXT_UNAVAILABLE, Error) \
9697
V(ERR_MISSING_ARGS, TypeError) \
@@ -186,6 +187,7 @@ ERRORS_WITH_CODE(V)
186187
V(ERR_INVALID_STATE, "Invalid state") \
187188
V(ERR_INVALID_THIS, "Value of \"this\" is the wrong type") \
188189
V(ERR_INVALID_URL_SCHEME, "The URL must be of scheme file:") \
190+
V(ERR_LOAD_SQLITE_EXTENSION, "Failed to load SQLite extension") \
189191
V(ERR_MEMORY_ALLOCATION_FAILED, "Failed to allocate memory") \
190192
V(ERR_OSSL_EVP_INVALID_DIGEST, "Invalid digest used") \
191193
V(ERR_MESSAGE_TARGET_CONTEXT_UNAVAILABLE, \

src/node_sqlite.cc

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "node.h"
77
#include "node_errors.h"
88
#include "node_mem-inl.h"
9+
#include "path.h"
910
#include "sqlite3.h"
1011
#include "util-inl.h"
1112

@@ -78,12 +79,14 @@ inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, sqlite3* db) {
7879
DatabaseSync::DatabaseSync(Environment* env,
7980
Local<Object> object,
8081
Local<String> location,
81-
bool open)
82+
bool open,
83+
bool allow_load_extension)
8284
: BaseObject(env, object) {
8385
MakeWeak();
8486
node::Utf8Value utf8_location(env->isolate(), location);
8587
location_ = utf8_location.ToString();
8688
connection_ = nullptr;
89+
allow_load_extension_ = allow_load_extension;
8790

8891
if (open) {
8992
Open();
@@ -109,6 +112,12 @@ bool DatabaseSync::Open() {
109112
int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
110113
int r = sqlite3_open_v2(location_.c_str(), &connection_, flags, nullptr);
111114
CHECK_ERROR_OR_THROW(env()->isolate(), connection_, r, SQLITE_OK, false);
115+
if (allow_load_extension_) {
116+
int load_extension_ret = sqlite3_db_config(
117+
connection_, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, nullptr);
118+
CHECK_ERROR_OR_THROW(
119+
env()->isolate(), connection_, load_extension_ret, SQLITE_OK, false);
120+
}
112121
return true;
113122
}
114123

@@ -127,6 +136,7 @@ void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
127136
}
128137

129138
bool open = true;
139+
bool allow_load_extension = false;
130140

131141
if (args.Length() > 1) {
132142
if (!args[1]->IsObject()) {
@@ -137,10 +147,17 @@ void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
137147

138148
Local<Object> options = args[1].As<Object>();
139149
Local<String> open_string = FIXED_ONE_BYTE_STRING(env->isolate(), "open");
150+
Local<String> allow_load_extension_string =
151+
FIXED_ONE_BYTE_STRING(env->isolate(), "allowLoadExtension");
140152
Local<Value> open_v;
153+
Local<Value> allow_load_extension_v;
141154
if (!options->Get(env->context(), open_string).ToLocal(&open_v)) {
142155
return;
143156
}
157+
if (!options->Get(env->context(), allow_load_extension_string)
158+
.ToLocal(&allow_load_extension_v)) {
159+
return;
160+
}
144161
if (!open_v->IsUndefined()) {
145162
if (!open_v->IsBoolean()) {
146163
node::THROW_ERR_INVALID_ARG_TYPE(
@@ -149,9 +166,19 @@ void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
149166
}
150167
open = open_v.As<Boolean>()->Value();
151168
}
169+
if (!allow_load_extension_v->IsUndefined()) {
170+
if (!allow_load_extension_v->IsBoolean()) {
171+
node::THROW_ERR_INVALID_ARG_TYPE(
172+
env->isolate(),
173+
"The \"options.allowLoadExtension\" argument must be a boolean.");
174+
return;
175+
}
176+
allow_load_extension = allow_load_extension_v.As<Boolean>()->Value();
177+
}
152178
}
153179

154-
new DatabaseSync(env, args.This(), args[0].As<String>(), open);
180+
new DatabaseSync(
181+
env, args.This(), args[0].As<String>(), open, allow_load_extension);
155182
}
156183

157184
void DatabaseSync::Open(const FunctionCallbackInfo<Value>& args) {
@@ -211,6 +238,35 @@ void DatabaseSync::Exec(const FunctionCallbackInfo<Value>& args) {
211238
CHECK_ERROR_OR_THROW(env->isolate(), db->connection_, r, SQLITE_OK, void());
212239
}
213240

241+
void DatabaseSync::LoadExtension(const FunctionCallbackInfo<Value>& args) {
242+
DatabaseSync* db;
243+
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
244+
Environment* env = Environment::GetCurrent(args);
245+
THROW_AND_RETURN_ON_BAD_STATE(
246+
env, db->connection_ == nullptr, "database is not open");
247+
THROW_AND_RETURN_ON_BAD_STATE(
248+
env, !db->allow_load_extension_, "load extension is not allowed");
249+
250+
if (!args[0]->IsString()) {
251+
node::THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
252+
"The \"path\" argument must be a string.");
253+
return;
254+
}
255+
256+
auto isolate = env->isolate();
257+
258+
BufferValue path(isolate, args[0]);
259+
CHECK_NOT_NULL(*path);
260+
ToNamespacedPath(env, &path);
261+
THROW_IF_INSUFFICIENT_PERMISSIONS(
262+
env, permission::PermissionScope::kFileSystemRead, path.ToStringView());
263+
char* errmsg = nullptr;
264+
int r = sqlite3_load_extension(db->connection_, *path, nullptr, &errmsg);
265+
if (r != SQLITE_OK) {
266+
isolate->ThrowException(node::ERR_LOAD_SQLITE_EXTENSION(isolate, errmsg));
267+
}
268+
}
269+
214270
StatementSync::StatementSync(Environment* env,
215271
Local<Object> object,
216272
sqlite3* db,
@@ -668,6 +724,8 @@ static void Initialize(Local<Object> target,
668724
SetProtoMethod(isolate, db_tmpl, "close", DatabaseSync::Close);
669725
SetProtoMethod(isolate, db_tmpl, "prepare", DatabaseSync::Prepare);
670726
SetProtoMethod(isolate, db_tmpl, "exec", DatabaseSync::Exec);
727+
SetProtoMethod(
728+
isolate, db_tmpl, "loadExtension", DatabaseSync::LoadExtension);
671729
SetConstructorFunction(context, target, "DatabaseSync", db_tmpl);
672730
SetConstructorFunction(context,
673731
target,

src/node_sqlite.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ class DatabaseSync : public BaseObject {
1818
DatabaseSync(Environment* env,
1919
v8::Local<v8::Object> object,
2020
v8::Local<v8::String> location,
21-
bool open);
21+
bool open,
22+
bool allow_load_extension);
2223
void MemoryInfo(MemoryTracker* tracker) const override;
2324
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
2425
static void Open(const v8::FunctionCallbackInfo<v8::Value>& args);
2526
static void Close(const v8::FunctionCallbackInfo<v8::Value>& args);
2627
static void Prepare(const v8::FunctionCallbackInfo<v8::Value>& args);
2728
static void Exec(const v8::FunctionCallbackInfo<v8::Value>& args);
29+
static void LoadExtension(const v8::FunctionCallbackInfo<v8::Value>& args);
2830

2931
SET_MEMORY_INFO_NAME(DatabaseSync)
3032
SET_SELF_SIZE(DatabaseSync)
@@ -34,6 +36,7 @@ class DatabaseSync : public BaseObject {
3436

3537
~DatabaseSync() override;
3638
std::string location_;
39+
bool allow_load_extension_;
3740
sqlite3* connection_;
3841
};
3942

124 KB
Binary file not shown.

test/fixtures/sqlite/vec0.dll

161 KB
Binary file not shown.

test/fixtures/sqlite/vec0.so

115 KB
Binary file not shown.
123 KB
Binary file not shown.

test/parallel/test-sqlite.js

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// Flags: --experimental-sqlite
22
'use strict';
33
const { spawnPromisified } = require('../common');
4+
const assert = require('node:assert');
45
const tmpdir = require('../common/tmpdir');
56
const { existsSync } = require('node:fs');
67
const { join } = require('node:path');
8+
const os = require('node:os');
9+
const { path } = require('../common/fixtures');
710
const { DatabaseSync, StatementSync } = require('node:sqlite');
811
const { suite, test } = require('node:test');
912
let cnt = 0;
@@ -78,6 +81,132 @@ suite('DatabaseSync() constructor', () => {
7881
message: /The "options\.open" argument must be a boolean/,
7982
});
8083
});
84+
85+
test('throws if options.allowLoadExtension is provided but is not a boolean', (t) => {
86+
t.assert.throws(() => {
87+
new DatabaseSync('foo', { allowLoadExtension: 5 });
88+
}, {
89+
code: 'ERR_INVALID_ARG_TYPE',
90+
message: /The "options\.allowLoadExtension" argument must be a boolean/,
91+
});
92+
});
93+
});
94+
95+
suite('DatabaseSync.prototype.loadExtension()', () => {
96+
test('throws if database is not open', (t) => {
97+
const db = new DatabaseSync(nextDb(), { open: false });
98+
t.assert.throws(() => {
99+
db.loadExtension();
100+
}, {
101+
code: 'ERR_INVALID_STATE',
102+
message: /database is not open/,
103+
});
104+
});
105+
106+
test('throws if path is not a valid sqlite extension', (t) => {
107+
const db = new DatabaseSync(nextDb(), {
108+
allowLoadExtension: true,
109+
});
110+
// Try to load a non-existent file
111+
const files = [
112+
'/dev/null',
113+
path('a.js'),
114+
path('shared-memory.wasm'),
115+
path('crash.wat'),
116+
path('doc_inc_1.md'),
117+
path('utf8-bom.json'),
118+
path('x.txt'),
119+
];
120+
for (const file of files) {
121+
t.assert.throws(() => {
122+
db.loadExtension(file);
123+
}, {
124+
code: 'ERR_LOAD_SQLITE_EXTENSION',
125+
}, `loadExtension("${file}") should throw an error`);
126+
}
127+
});
128+
129+
test('should load sqlite extension successfully', (t) => {
130+
const dbPath = nextDb();
131+
const db = new DatabaseSync(dbPath, { allowLoadExtension: true });
132+
const supportedPlatforms = [
133+
['macos', 'x86_64'],
134+
['windows', 'x86_64'],
135+
['linux', 'x86_64'],
136+
['macos', 'aarch64'],
137+
];
138+
function validPlatform(platform, arch) {
139+
return (
140+
supportedPlatforms.find(([p, a]) => platform === p && arch === a) !== null
141+
);
142+
}
143+
144+
function getExtension(platform, arch) {
145+
switch (platform) {
146+
case 'darwin':
147+
return arch === 'arm64' ? '.aarch64.dylib' : '.x86_64.dylib';
148+
case 'windows':
149+
return '.dll';
150+
case 'linux':
151+
return '.so';
152+
default:
153+
return null;
154+
}
155+
}
156+
const platform = os.platform();
157+
const arch = process.arch;
158+
if (!validPlatform(platform, arch)) {
159+
t.skip('Unsupported platform');
160+
return;
161+
}
162+
const ext = getExtension(platform, arch);
163+
const filePath = path('sqlite', `vec0${ext}`);
164+
t.assert.strictEqual(db.loadExtension(filePath), undefined);
165+
166+
const { vec_version } = db.prepare(
167+
'select vec_version() as vec_version;'
168+
).get();
169+
assert.strictEqual(vec_version, 'v0.1.1');
170+
171+
const items = [
172+
[1, [0.1, 0.1, 0.1, 0.1]],
173+
[2, [0.2, 0.2, 0.2, 0.2]],
174+
[3, [0.3, 0.3, 0.3, 0.3]],
175+
[4, [0.4, 0.4, 0.4, 0.4]],
176+
[5, [0.5, 0.5, 0.5, 0.5]],
177+
];
178+
const query = [0.3, 0.3, 0.3, 0.3];
179+
180+
db.exec('CREATE VIRTUAL TABLE vec_items USING vec0(embedding float[4])');
181+
182+
const insertStmt = db.prepare(
183+
'INSERT INTO vec_items(rowid, embedding) VALUES (?, ?)'
184+
);
185+
186+
for (const [id, vector] of items) {
187+
const rowId = BigInt(id);
188+
const embedding = new Uint8Array(Float32Array.from(vector).buffer);
189+
insertStmt.run(rowId, embedding);
190+
}
191+
192+
const rows = db.prepare(
193+
`
194+
SELECT
195+
rowid,
196+
distance
197+
FROM vec_items
198+
WHERE embedding MATCH ?
199+
ORDER BY distance
200+
LIMIT 3
201+
`
202+
).all(new Uint8Array(Float32Array.from(query).buffer));
203+
204+
assert.deepStrictEqual(rows, [
205+
{ rowid: 3, distance: 0 },
206+
{ rowid: 4, distance: 0.19999998807907104 },
207+
{ rowid: 2, distance: 0.20000001788139343 },
208+
]);
209+
});
81210
});
82211

83212
suite('DatabaseSync.prototype.open()', () => {

0 commit comments

Comments
 (0)