Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,24 @@

<!-- YAML
added: v22.5.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/56991

Check warning on line 82 in doc/api/sqlite.md

View workflow job for this annotation

GitHub Actions / lint-pr-url

pr-url doesn't match the URL of the current PR.
description: The `path` argument now supports Buffer and URL objects.
-->

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

### `new DatabaseSync(location[, options])`
### `new DatabaseSync(path[, options])`

<!-- YAML
added: v22.5.0
-->

* `location` {string} The location of the database. A SQLite database can be
* `path` {string | Buffer | URL} The path of the database. A SQLite database can be
stored in a file or completely [in memory][]. To use a file-backed database,
the location should be a file path. To use an in-memory database, the location
the path should be a file path. To use an in-memory database, the path
should be the special name `':memory:'`.
* `options` {Object} Configuration options for the database connection. The
following options are supported:
Expand Down Expand Up @@ -191,7 +195,7 @@
added: v22.5.0
-->

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

Expand Down
3 changes: 3 additions & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@
V(homedir_string, "homedir") \
V(host_string, "host") \
V(hostmaster_string, "hostmaster") \
V(hostname_string, "hostname") \
V(href_string, "href") \
V(http_1_1_string, "http/1.1") \
V(id_string, "id") \
V(identity_string, "identity") \
Expand Down Expand Up @@ -295,6 +297,7 @@
V(priority_string, "priority") \
V(process_string, "process") \
V(promise_string, "promise") \
V(protocol_string, "protocol") \
V(prototype_string, "prototype") \
V(psk_string, "psk") \
V(pubkey_string, "pubkey") \
Expand Down
68 changes: 57 additions & 11 deletions src/node_sqlite.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "node.h"
#include "node_errors.h"
#include "node_mem-inl.h"
#include "node_url.h"
#include "sqlite3.h"
#include "util-inl.h"

Expand Down Expand Up @@ -292,11 +293,14 @@ bool DatabaseSync::Open() {
}

// TODO(cjihrig): Support additional flags.
int default_flags = SQLITE_OPEN_URI;
int flags = open_config_.get_read_only()
? SQLITE_OPEN_READONLY
: SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
int r = sqlite3_open_v2(
open_config_.location().c_str(), &connection_, flags, nullptr);
int r = sqlite3_open_v2(open_config_.location().c_str(),
&connection_,
flags | default_flags,
nullptr);
CHECK_ERROR_OR_THROW(env()->isolate(), connection_, r, SQLITE_OK, false);

r = sqlite3_db_config(connection_,
Expand Down Expand Up @@ -358,27 +362,69 @@ inline sqlite3* DatabaseSync::Connection() {
return connection_;
}

std::optional<std::string> ValidateDatabasePath(Environment* env,
Local<Value> path,
const std::string& field_name) {
constexpr auto has_null_bytes = [](std::string_view str) {
return str.find('\0') != std::string_view::npos;
};
if (path->IsString()) {
Utf8Value location(env->isolate(), path.As<String>());
if (!has_null_bytes(location.ToStringView())) {
return location.ToString();
}
} else if (path->IsUint8Array()) {
Local<Uint8Array> buffer = path.As<Uint8Array>();
size_t byteOffset = buffer->ByteOffset();
size_t byteLength = buffer->ByteLength();
auto data =
static_cast<const uint8_t*>(buffer->Buffer()->Data()) + byteOffset;
if (std::find(data, data + byteLength, 0) == data + byteLength) {
return std::string(reinterpret_cast<const char*>(data), byteLength);
}
} else if (path->IsObject()) { // When is URL
auto url = path.As<Object>();
Local<Value> href;
if (url->Get(env->context(), env->href_string()).ToLocal(&href) &&
href->IsString()) {
Utf8Value location_value(env->isolate(), href.As<String>());
auto location = location_value.ToStringView();
if (!has_null_bytes(location)) {
CHECK(ada::can_parse(location));
if (!location.starts_with("file:")) {
THROW_ERR_INVALID_URL_SCHEME(env->isolate());
return std::nullopt;
}

return location_value.ToString();
}
}
}

THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
"The \"%s\" argument must be a string, "
"Uint8Array, or URL without null bytes.",
field_name.c_str());

return std::nullopt;
}

void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);

if (!args.IsConstructCall()) {
THROW_ERR_CONSTRUCT_CALL_REQUIRED(env);
return;
}

if (!args[0]->IsString()) {
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
"The \"path\" argument must be a string.");
std::optional<std::string> location =
ValidateDatabasePath(env, args[0], "path");
if (!location.has_value()) {
return;
}

std::string location =
Utf8Value(env->isolate(), args[0].As<String>()).ToString();
DatabaseOpenConfiguration open_config(std::move(location));

DatabaseOpenConfiguration open_config(std::move(location.value()));
bool open = true;
bool allow_load_extension = false;

if (args.Length() > 1) {
if (!args[1]->IsObject()) {
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
Expand Down
31 changes: 29 additions & 2 deletions test/parallel/test-sqlite-database-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,30 @@ suite('DatabaseSync() constructor', () => {
});
});

test('throws if database path is not a string', (t) => {
test('throws if database path is not a string, Uint8Array, or URL', (t) => {
t.assert.throws(() => {
new DatabaseSync();
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "path" argument must be a string/,
message: /The "path" argument must be a string, Uint8Array, or URL without null bytes/,
});
});

test('throws if the database location as Buffer contains null bytes', (t) => {
t.assert.throws(() => {
new DatabaseSync(Buffer.from('l\0cation'));
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.',
});
});

test('throws if the database location as string contains null bytes', (t) => {
t.assert.throws(() => {
new DatabaseSync('l\0cation');
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.',
});
});

Expand Down Expand Up @@ -256,6 +274,15 @@ suite('DatabaseSync.prototype.exec()', () => {
});
});

test('throws if the URL does not have the file: scheme', (t) => {
t.assert.throws(() => {
new DatabaseSync(new URL('http://example.com'));
}, {
code: 'ERR_INVALID_URL_SCHEME',
message: 'The URL must be of scheme file:',
});
});

test('throws if database is not open', (t) => {
const db = new DatabaseSync(nextDb(), { open: false });

Expand Down
99 changes: 99 additions & 0 deletions test/parallel/test-sqlite.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const tmpdir = require('../common/tmpdir');
const { join } = require('node:path');
const { DatabaseSync, constants } = require('node:sqlite');
const { suite, test } = require('node:test');
const { pathToFileURL } = require('node:url');
let cnt = 0;

tmpdir.refresh();
Expand Down Expand Up @@ -111,3 +112,101 @@ test('math functions are enabled', (t) => {
{ __proto__: null, pi: 3.141592653589793 },
);
});

test('Buffer is supported as the database path', (t) => {
const db = new DatabaseSync(Buffer.from(nextDb()));
t.after(() => { db.close(); });
db.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY);
INSERT INTO data (key) VALUES (1);
`);

t.assert.deepStrictEqual(
db.prepare('SELECT * FROM data').all(),
[{ __proto__: null, key: 1 }]
);
});

test('URL is supported as the database path', (t) => {
const url = pathToFileURL(nextDb());
const db = new DatabaseSync(url);
t.after(() => { db.close(); });
db.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY);
INSERT INTO data (key) VALUES (1);
`);

t.assert.deepStrictEqual(
db.prepare('SELECT * FROM data').all(),
[{ __proto__: null, key: 1 }]
);
});


suite('URI query params', () => {
const baseDbPath = nextDb();
const baseDb = new DatabaseSync(baseDbPath);
baseDb.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY);
INSERT INTO data (key) VALUES (1);
`);
baseDb.close();

test('query params are supported with URL objects', (t) => {
const url = pathToFileURL(baseDbPath);
url.searchParams.set('mode', 'ro');
const readOnlyDB = new DatabaseSync(url);
t.after(() => { readOnlyDB.close(); });

t.assert.deepStrictEqual(
readOnlyDB.prepare('SELECT * FROM data').all(),
[{ __proto__: null, key: 1 }]
);
t.assert.throws(() => {
readOnlyDB.exec('INSERT INTO data (key) VALUES (1);');
}, {
code: 'ERR_SQLITE_ERROR',
message: 'attempt to write a readonly database',
});
});

test('query params are supported with string', (t) => {
const url = pathToFileURL(baseDbPath);
url.searchParams.set('mode', 'ro');

// Ensures a valid URI passed as a string is supported
const readOnlyDB = new DatabaseSync(url.toString());
t.after(() => { readOnlyDB.close(); });

t.assert.deepStrictEqual(
readOnlyDB.prepare('SELECT * FROM data').all(),
[{ __proto__: null, key: 1 }]
);
t.assert.throws(() => {
readOnlyDB.exec('INSERT INTO data (key) VALUES (1);');
}, {
code: 'ERR_SQLITE_ERROR',
message: 'attempt to write a readonly database',
});
});

test('query params are supported with Buffer', (t) => {
const url = pathToFileURL(baseDbPath);
url.searchParams.set('mode', 'ro');

// Ensures a valid URI passed as a Buffer is supported
const readOnlyDB = new DatabaseSync(Buffer.from(url.toString()));
t.after(() => { readOnlyDB.close(); });

t.assert.deepStrictEqual(
readOnlyDB.prepare('SELECT * FROM data').all(),
[{ __proto__: null, key: 1 }]
);
t.assert.throws(() => {
readOnlyDB.exec('INSERT INTO data (key) VALUES (1);');
}, {
code: 'ERR_SQLITE_ERROR',
message: 'attempt to write a readonly database',
});
});
});
Loading