mirror of
https://github.com/nodejs/node.git
synced 2025-04-28 13:40:37 +00:00
sqlite,test,doc: allow Buffer and URL as database location
PR-URL: https://github.com/nodejs/node/pull/56991 Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
This commit is contained in:
parent
269c851240
commit
c7cf6778c7
@ -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:
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
@ -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") \
|
||||
|
@ -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"
|
||||
@ -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) {
|
||||
@ -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_,
|
||||
@ -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;
|
||||
}
|
||||
|
||||
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(),
|
||||
@ -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) {
|
||||
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
@ -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(
|
||||
@ -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.'
|
||||
});
|
||||
});
|
||||
|
||||
@ -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();
|
||||
@ -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 });
|
||||
@ -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 () => {
|
||||
|
@ -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.',
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 });
|
||||
|
||||
|
@ -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();
|
||||
@ -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…
Reference in New Issue
Block a user