Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sqlite,test,doc: accept Buffer and URL as paths #56991

Merged
merged 12 commits into from
Feb 27, 2025
22 changes: 15 additions & 7 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,24 @@ console.log(query.all());

<!-- YAML
added: v22.5.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/56991
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 @@ -200,7 +204,7 @@ wrapper around [`sqlite3_create_function_v2()`][].
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 Expand Up @@ -534,15 +538,19 @@ exception.
| `TEXT` | {string} |
| `BLOB` | {TypedArray} or {DataView} |

## `sqlite.backup(sourceDb, destination[, options])`
## `sqlite.backup(sourceDb, path[, options])`

<!-- YAML
added: v23.8.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/56991
description: The `path` argument now supports Buffer and URL objects.
-->

* `sourceDb` {DatabaseSync} The database to backup. The source database must be open.
* `destination` {string} The path where the backup will be created. If the file already exists, the contents will be
overwritten.
* `path` {string | Buffer | URL} The path where the backup will be created. If the file already exists,
the contents will be overwritten.
* `options` {Object} Optional configuration for the backup. The
following properties are supported:
* `source` {string} Name of the source database. This can be `'main'` (the default primary database) or any other
Expand Down
1 change: 1 addition & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@
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
104 changes: 82 additions & 22 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 "threadpoolwork-inl.h"
#include "util-inl.h"
Expand Down Expand Up @@ -181,10 +182,11 @@ class BackupJob : public ThreadPoolWork {
void ScheduleBackup() {
Isolate* isolate = env()->isolate();
HandleScope handle_scope(isolate);
backup_status_ = sqlite3_open_v2(destination_name_.c_str(),
&dest_,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
nullptr);
backup_status_ = sqlite3_open_v2(
destination_name_.c_str(),
&dest_,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_URI,
nullptr);
Local<Promise::Resolver> resolver =
Local<Promise::Resolver>::New(env()->isolate(), resolver_);
if (backup_status_ != SQLITE_OK) {
Expand Down Expand Up @@ -503,11 +505,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(), this, r, SQLITE_OK, false);

r = sqlite3_db_config(connection_,
Expand Down Expand Up @@ -585,27 +590,85 @@ bool DatabaseSync::ShouldIgnoreSQLiteError() {
return ignore_next_sqlite_error_;
}

std::optional<std::string> ValidateDatabasePath(Environment* env,
Local<Value> path,
const std::string& field_name) {
auto has_null_bytes = [](const std::string& str) {
return str.find('\0') != std::string::npos;
};
std::string location;
if (path->IsString()) {
location = Utf8Value(env->isolate(), path.As<String>()).ToString();
if (!has_null_bytes(location)) {
return location;
}
}

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)) {
Local<Value> out;
if (String::NewFromUtf8(env->isolate(),
reinterpret_cast<const char*>(data),
NewStringType::kNormal,
static_cast<int>(byteLength))
.ToLocal(&out)) {
return Utf8Value(env->isolate(), out.As<String>()).ToString();
}
}
}

// When is URL
if (path->IsObject()) {
Local<Object> url = path.As<Object>();
Local<Value> href;
Local<Value> protocol;
if (url->Get(env->context(), env->href_string()).ToLocal(&href) &&
href->IsString() &&
url->Get(env->context(), env->protocol_string()).ToLocal(&protocol) &&
protocol->IsString()) {
location = Utf8Value(env->isolate(), href.As<String>()).ToString();
if (!has_null_bytes(location)) {
auto file_url = ada::parse(location);
CHECK(file_url);
if (file_url->type != ada::scheme::FILE) {
THROW_ERR_INVALID_URL_SCHEME(env->isolate());
return std::nullopt;
}
Comment on lines +638 to +641
Copy link
Contributor Author

@geeksilva97 geeksilva97 Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the FileURLToPath because it was stripping the query params out. We should keep them since SQLite supports them.

Guaranteeing that a valid URL is given we can return the URL href it is.


return location;
}
}
}

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 Expand Up @@ -984,17 +1047,15 @@ void Backup(const FunctionCallbackInfo<Value>& args) {
DatabaseSync* db;
ASSIGN_OR_RETURN_UNWRAP(&db, args[0].As<Object>());
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
if (!args[1]->IsString()) {
THROW_ERR_INVALID_ARG_TYPE(
env->isolate(), "The \"destination\" argument must be a string.");
std::optional<std::string> dest_path =
ValidateDatabasePath(env, args[1], "path");
if (!dest_path.has_value()) {
return;
}

int rate = 100;
std::string source_db = "main";
std::string dest_db = "main";

Utf8Value dest_path(env->isolate(), args[1].As<String>());
Local<Function> progressFunc = Local<Function>();

if (args.Length() > 2) {
Expand Down Expand Up @@ -1077,12 +1138,11 @@ void Backup(const FunctionCallbackInfo<Value>& args) {
}

args.GetReturnValue().Set(resolver->GetPromise());

BackupJob* job = new BackupJob(env,
db,
resolver,
std::move(source_db),
*dest_path,
dest_path.value(),
std::move(dest_db),
rate,
progressFunc);
Expand Down
84 changes: 78 additions & 6 deletions test/parallel/test-sqlite-backup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { join } from 'node:path';
import { backup, DatabaseSync } from 'node:sqlite';
import { describe, test } from 'node:test';
import { writeFileSync } from 'node:fs';
import { pathToFileURL } from 'node:url';

let cnt = 0;

Expand All @@ -13,8 +14,8 @@ function nextDb() {
return join(tmpdir.path, `database-${cnt++}.db`);
}

function makeSourceDb() {
const database = new DatabaseSync(':memory:');
function makeSourceDb(dbPath = ':memory:') {
const database = new DatabaseSync(dbPath);

database.exec(`
CREATE TABLE data(
Expand Down Expand Up @@ -42,21 +43,39 @@ describe('backup()', () => {
});
});

test('throws if path is not a string', (t) => {
test('throws if path is not a string, URL, or Buffer', (t) => {
const database = makeSourceDb();

t.assert.throws(() => {
backup(database);
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "destination" argument must be a string.'
message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.'
});

t.assert.throws(() => {
backup(database, {});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "destination" argument must be a string.'
message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.'
});
});

test('throws if the database path contains null bytes', (t) => {
const database = makeSourceDb();

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

t.assert.throws(() => {
backup(database, '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 @@ -141,6 +160,46 @@ test('database backup', async (t) => {
});
});

test('backup database using location as URL', async (t) => {
const database = makeSourceDb();
const destDb = pathToFileURL(nextDb());

t.after(() => { database.close(); });

await backup(database, destDb);

const backupDb = new DatabaseSync(destDb);

t.after(() => { backupDb.close(); });

const rows = backupDb.prepare('SELECT * FROM data').all();

t.assert.deepStrictEqual(rows, [
{ __proto__: null, key: 1, value: 'value-1' },
{ __proto__: null, key: 2, value: 'value-2' },
]);
});

test('backup database using location as Buffer', async (t) => {
const database = makeSourceDb();
const destDb = Buffer.from(nextDb());

t.after(() => { database.close(); });

await backup(database, destDb);

const backupDb = new DatabaseSync(destDb);

t.after(() => { backupDb.close(); });

const rows = backupDb.prepare('SELECT * FROM data').all();

t.assert.deepStrictEqual(rows, [
{ __proto__: null, key: 1, value: 'value-1' },
{ __proto__: null, key: 2, value: 'value-2' },
]);
});

test('database backup in a single call', async (t) => {
const progressFn = t.mock.fn();
const database = makeSourceDb();
Expand Down Expand Up @@ -179,6 +238,19 @@ test('throws exception when trying to start backup from a closed database', (t)
});
});

test('throws if URL is not file: scheme', (t) => {
const database = new DatabaseSync(':memory:');

t.after(() => { database.close(); });

t.assert.throws(() => {
backup(database, new URL('http://example.com/backup.db'));
}, {
code: 'ERR_INVALID_URL_SCHEME',
message: 'The URL must be of scheme file:',
});
});

test('database backup fails when dest file is not writable', async (t) => {
const readonlyDestDb = nextDb();
writeFileSync(readonlyDestDb, '', { mode: 0o444 });
Expand Down Expand Up @@ -225,7 +297,7 @@ test('backup fails when source db is invalid', async (t) => {
});
});

test('backup fails when destination cannot be opened', async (t) => {
test('backup fails when path cannot be opened', async (t) => {
const database = makeSourceDb();

await t.assert.rejects(async () => {
Expand Down
Loading
Loading