Skip to content

Commit 6bb9155

Browse files
committed
sqlite,test,doc: allow Buffer and URL as database location
PR-URL: nodejs#56991 Reviewed-By: Colin Ihrig <[email protected]>
1 parent be79f4a commit 6bb9155

File tree

5 files changed

+212
-17
lines changed

5 files changed

+212
-17
lines changed

doc/api/sqlite.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,20 +77,24 @@ console.log(query.all());
7777

7878
<!-- YAML
7979
added: v22.5.0
80+
changes:
81+
- version: REPLACEME
82+
pr-url: https://github.com/nodejs/node/pull/56991
83+
description: The `path` argument now supports Buffer and URL objects.
8084
-->
8185

8286
This class represents a single [connection][] to a SQLite database. All APIs
8387
exposed by this class execute synchronously.
8488

85-
### `new DatabaseSync(location[, options])`
89+
### `new DatabaseSync(path[, options])`
8690

8791
<!-- YAML
8892
added: v22.5.0
8993
-->
9094

91-
* `location` {string} The location of the database. A SQLite database can be
95+
* `path` {string | Buffer | URL} The path of the database. A SQLite database can be
9296
stored in a file or completely [in memory][]. To use a file-backed database,
93-
the location should be a file path. To use an in-memory database, the location
97+
the path should be a file path. To use an in-memory database, the path
9498
should be the special name `':memory:'`.
9599
* `options` {Object} Configuration options for the database connection. The
96100
following options are supported:
@@ -191,7 +195,7 @@ wrapper around [`sqlite3_create_function_v2()`][].
191195
added: v22.5.0
192196
-->
193197

194-
Opens the database specified in the `location` argument of the `DatabaseSync`
198+
Opens the database specified in the `path` argument of the `DatabaseSync`
195199
constructor. This method should only be used when the database is not opened via
196200
the constructor. An exception is thrown if the database is already open.
197201

src/env_properties.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@
186186
V(homedir_string, "homedir") \
187187
V(host_string, "host") \
188188
V(hostmaster_string, "hostmaster") \
189+
V(hostname_string, "hostname") \
190+
V(href_string, "href") \
189191
V(http_1_1_string, "http/1.1") \
190192
V(id_string, "id") \
191193
V(identity_string, "identity") \
@@ -295,6 +297,7 @@
295297
V(priority_string, "priority") \
296298
V(process_string, "process") \
297299
V(promise_string, "promise") \
300+
V(protocol_string, "protocol") \
298301
V(prototype_string, "prototype") \
299302
V(psk_string, "psk") \
300303
V(pubkey_string, "pubkey") \

src/node_sqlite.cc

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "node.h"
88
#include "node_errors.h"
99
#include "node_mem-inl.h"
10+
#include "node_url.h"
1011
#include "sqlite3.h"
1112
#include "util-inl.h"
1213

@@ -292,11 +293,14 @@ bool DatabaseSync::Open() {
292293
}
293294

294295
// TODO(cjihrig): Support additional flags.
296+
int default_flags = SQLITE_OPEN_URI;
295297
int flags = open_config_.get_read_only()
296298
? SQLITE_OPEN_READONLY
297299
: SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
298-
int r = sqlite3_open_v2(
299-
open_config_.location().c_str(), &connection_, flags, nullptr);
300+
int r = sqlite3_open_v2(open_config_.location().c_str(),
301+
&connection_,
302+
flags | default_flags,
303+
nullptr);
300304
CHECK_ERROR_OR_THROW(env()->isolate(), connection_, r, SQLITE_OK, false);
301305

302306
r = sqlite3_db_config(connection_,
@@ -358,27 +362,85 @@ inline sqlite3* DatabaseSync::Connection() {
358362
return connection_;
359363
}
360364

365+
std::optional<std::string> ValidateDatabasePath(Environment* env,
366+
Local<Value> path,
367+
const std::string& field_name) {
368+
auto has_null_bytes = [](const std::string& str) {
369+
return str.find('\0') != std::string::npos;
370+
};
371+
std::string location;
372+
if (path->IsString()) {
373+
location = Utf8Value(env->isolate(), path.As<String>()).ToString();
374+
if (!has_null_bytes(location)) {
375+
return location;
376+
}
377+
}
378+
379+
if (path->IsUint8Array()) {
380+
Local<Uint8Array> buffer = path.As<Uint8Array>();
381+
size_t byteOffset = buffer->ByteOffset();
382+
size_t byteLength = buffer->ByteLength();
383+
auto data =
384+
static_cast<const uint8_t*>(buffer->Buffer()->Data()) + byteOffset;
385+
if (!(std::find(data, data + byteLength, 0) != data + byteLength)) {
386+
Local<Value> out;
387+
if (String::NewFromUtf8(env->isolate(),
388+
reinterpret_cast<const char*>(data),
389+
NewStringType::kNormal,
390+
static_cast<int>(byteLength))
391+
.ToLocal(&out)) {
392+
return Utf8Value(env->isolate(), out.As<String>()).ToString();
393+
}
394+
}
395+
}
396+
397+
// When is URL
398+
if (path->IsObject()) {
399+
Local<Object> url = path.As<Object>();
400+
Local<Value> href;
401+
Local<Value> protocol;
402+
if (url->Get(env->context(), env->href_string()).ToLocal(&href) &&
403+
href->IsString() &&
404+
url->Get(env->context(), env->protocol_string()).ToLocal(&protocol) &&
405+
protocol->IsString()) {
406+
location = Utf8Value(env->isolate(), href.As<String>()).ToString();
407+
if (!has_null_bytes(location)) {
408+
auto file_url = ada::parse(location);
409+
CHECK(file_url);
410+
if (file_url->type != ada::scheme::FILE) {
411+
THROW_ERR_INVALID_URL_SCHEME(env->isolate());
412+
return std::nullopt;
413+
}
414+
415+
return location;
416+
}
417+
}
418+
}
419+
420+
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
421+
"The \"%s\" argument must be a string, "
422+
"Uint8Array, or URL without null bytes.",
423+
field_name.c_str());
424+
425+
return std::nullopt;
426+
}
427+
361428
void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
362429
Environment* env = Environment::GetCurrent(args);
363-
364430
if (!args.IsConstructCall()) {
365431
THROW_ERR_CONSTRUCT_CALL_REQUIRED(env);
366432
return;
367433
}
368434

369-
if (!args[0]->IsString()) {
370-
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
371-
"The \"path\" argument must be a string.");
435+
std::optional<std::string> location =
436+
ValidateDatabasePath(env, args[0], "path");
437+
if (!location.has_value()) {
372438
return;
373439
}
374440

375-
std::string location =
376-
Utf8Value(env->isolate(), args[0].As<String>()).ToString();
377-
DatabaseOpenConfiguration open_config(std::move(location));
378-
441+
DatabaseOpenConfiguration open_config(std::move(location.value()));
379442
bool open = true;
380443
bool allow_load_extension = false;
381-
382444
if (args.Length() > 1) {
383445
if (!args[1]->IsObject()) {
384446
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),

test/parallel/test-sqlite-database-sync.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,30 @@ suite('DatabaseSync() constructor', () => {
2323
});
2424
});
2525

26-
test('throws if database path is not a string', (t) => {
26+
test('throws if database path is not a string, Uint8Array, or URL', (t) => {
2727
t.assert.throws(() => {
2828
new DatabaseSync();
2929
}, {
3030
code: 'ERR_INVALID_ARG_TYPE',
31-
message: /The "path" argument must be a string/,
31+
message: /The "path" argument must be a string, Uint8Array, or URL without null bytes/,
32+
});
33+
});
34+
35+
test('throws if the database location as Buffer contains null bytes', (t) => {
36+
t.assert.throws(() => {
37+
new DatabaseSync(Buffer.from('l\0cation'));
38+
}, {
39+
code: 'ERR_INVALID_ARG_TYPE',
40+
message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.',
41+
});
42+
});
43+
44+
test('throws if the database location as string contains null bytes', (t) => {
45+
t.assert.throws(() => {
46+
new DatabaseSync('l\0cation');
47+
}, {
48+
code: 'ERR_INVALID_ARG_TYPE',
49+
message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.',
3250
});
3351
});
3452

@@ -256,6 +274,15 @@ suite('DatabaseSync.prototype.exec()', () => {
256274
});
257275
});
258276

277+
test('throws if the URL does not have the file: scheme', (t) => {
278+
t.assert.throws(() => {
279+
new DatabaseSync(new URL('http://example.com'));
280+
}, {
281+
code: 'ERR_INVALID_URL_SCHEME',
282+
message: 'The URL must be of scheme file:',
283+
});
284+
});
285+
259286
test('throws if database is not open', (t) => {
260287
const db = new DatabaseSync(nextDb(), { open: false });
261288

test/parallel/test-sqlite.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const tmpdir = require('../common/tmpdir');
44
const { join } = require('node:path');
55
const { DatabaseSync, constants } = require('node:sqlite');
66
const { suite, test } = require('node:test');
7+
const { pathToFileURL } = require('node:url');
78
let cnt = 0;
89

910
tmpdir.refresh();
@@ -111,3 +112,101 @@ test('math functions are enabled', (t) => {
111112
{ __proto__: null, pi: 3.141592653589793 },
112113
);
113114
});
115+
116+
test('Buffer is supported as the database path', (t) => {
117+
const db = new DatabaseSync(Buffer.from(nextDb()));
118+
t.after(() => { db.close(); });
119+
db.exec(`
120+
CREATE TABLE data(key INTEGER PRIMARY KEY);
121+
INSERT INTO data (key) VALUES (1);
122+
`);
123+
124+
t.assert.deepStrictEqual(
125+
db.prepare('SELECT * FROM data').all(),
126+
[{ __proto__: null, key: 1 }]
127+
);
128+
});
129+
130+
test('URL is supported as the database path', (t) => {
131+
const url = pathToFileURL(nextDb());
132+
const db = new DatabaseSync(url);
133+
t.after(() => { db.close(); });
134+
db.exec(`
135+
CREATE TABLE data(key INTEGER PRIMARY KEY);
136+
INSERT INTO data (key) VALUES (1);
137+
`);
138+
139+
t.assert.deepStrictEqual(
140+
db.prepare('SELECT * FROM data').all(),
141+
[{ __proto__: null, key: 1 }]
142+
);
143+
});
144+
145+
146+
suite('URI query params', () => {
147+
const baseDbPath = nextDb();
148+
const baseDb = new DatabaseSync(baseDbPath);
149+
baseDb.exec(`
150+
CREATE TABLE data(key INTEGER PRIMARY KEY);
151+
INSERT INTO data (key) VALUES (1);
152+
`);
153+
baseDb.close();
154+
155+
test('query params are supported with URL objects', (t) => {
156+
const url = pathToFileURL(baseDbPath);
157+
url.searchParams.set('mode', 'ro');
158+
const readOnlyDB = new DatabaseSync(url);
159+
t.after(() => { readOnlyDB.close(); });
160+
161+
t.assert.deepStrictEqual(
162+
readOnlyDB.prepare('SELECT * FROM data').all(),
163+
[{ __proto__: null, key: 1 }]
164+
);
165+
t.assert.throws(() => {
166+
readOnlyDB.exec('INSERT INTO data (key) VALUES (1);');
167+
}, {
168+
code: 'ERR_SQLITE_ERROR',
169+
message: 'attempt to write a readonly database',
170+
});
171+
});
172+
173+
test('query params are supported with string', (t) => {
174+
const url = pathToFileURL(baseDbPath);
175+
url.searchParams.set('mode', 'ro');
176+
177+
// Ensures a valid URI passed as a string is supported
178+
const readOnlyDB = new DatabaseSync(url.toString());
179+
t.after(() => { readOnlyDB.close(); });
180+
181+
t.assert.deepStrictEqual(
182+
readOnlyDB.prepare('SELECT * FROM data').all(),
183+
[{ __proto__: null, key: 1 }]
184+
);
185+
t.assert.throws(() => {
186+
readOnlyDB.exec('INSERT INTO data (key) VALUES (1);');
187+
}, {
188+
code: 'ERR_SQLITE_ERROR',
189+
message: 'attempt to write a readonly database',
190+
});
191+
});
192+
193+
test('query params are supported with Buffer', (t) => {
194+
const url = pathToFileURL(baseDbPath);
195+
url.searchParams.set('mode', 'ro');
196+
197+
// Ensures a valid URI passed as a Buffer is supported
198+
const readOnlyDB = new DatabaseSync(Buffer.from(url.toString()));
199+
t.after(() => { readOnlyDB.close(); });
200+
201+
t.assert.deepStrictEqual(
202+
readOnlyDB.prepare('SELECT * FROM data').all(),
203+
[{ __proto__: null, key: 1 }]
204+
);
205+
t.assert.throws(() => {
206+
readOnlyDB.exec('INSERT INTO data (key) VALUES (1);');
207+
}, {
208+
code: 'ERR_SQLITE_ERROR',
209+
message: 'attempt to write a readonly database',
210+
});
211+
});
212+
});

0 commit comments

Comments
 (0)