Skip to content

Commit e80bbb7

Browse files
geeksilva97aduh95
authored andcommitted
sqlite,test,doc: allow Buffer and URL as database location
PR-URL: #56991 Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
1 parent 83eed33 commit e80bbb7

6 files changed

+304
-37
lines changed

doc/api/sqlite.md

+15-7
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:
@@ -194,7 +198,7 @@ wrapper around [`sqlite3_create_function_v2()`][].
194198
added: v22.5.0
195199
-->
196200

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

@@ -508,15 +512,19 @@ exception.
508512
| `TEXT` | {string} |
509513
| `BLOB` | {TypedArray} or {DataView} |
510514

511-
## `sqlite.backup(sourceDb, destination[, options])`
515+
## `sqlite.backup(sourceDb, path[, options])`
512516

513517
<!-- YAML
514518
added: v23.8.0
519+
changes:
520+
- version: REPLACEME
521+
pr-url: https://github.com/nodejs/node/pull/56991
522+
description: The `path` argument now supports Buffer and URL objects.
515523
-->
516524

517525
* `sourceDb` {DatabaseSync} The database to backup. The source database must be open.
518-
* `destination` {string} The path where the backup will be created. If the file already exists, the contents will be
519-
overwritten.
526+
* `path` {string | Buffer | URL} The path where the backup will be created. If the file already exists,
527+
the contents will be overwritten.
520528
* `options` {Object} Optional configuration for the backup. The
521529
following properties are supported:
522530
* `source` {string} Name of the source database. This can be `'main'` (the default primary database) or any other

src/env_properties.h

+1
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@
194194
V(host_string, "host") \
195195
V(hostmaster_string, "hostmaster") \
196196
V(hostname_string, "hostname") \
197+
V(href_string, "href") \
197198
V(http_1_1_string, "http/1.1") \
198199
V(id_string, "id") \
199200
V(identity_string, "identity") \

src/node_sqlite.cc

+82-22
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 "threadpoolwork-inl.h"
1213
#include "util-inl.h"
@@ -181,10 +182,11 @@ class BackupJob : public ThreadPoolWork {
181182
void ScheduleBackup() {
182183
Isolate* isolate = env()->isolate();
183184
HandleScope handle_scope(isolate);
184-
backup_status_ = sqlite3_open_v2(destination_name_.c_str(),
185-
&dest_,
186-
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
187-
nullptr);
185+
backup_status_ = sqlite3_open_v2(
186+
destination_name_.c_str(),
187+
&dest_,
188+
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_URI,
189+
nullptr);
188190
Local<Promise::Resolver> resolver =
189191
Local<Promise::Resolver>::New(env()->isolate(), resolver_);
190192
if (backup_status_ != SQLITE_OK) {
@@ -503,11 +505,14 @@ bool DatabaseSync::Open() {
503505
}
504506

505507
// TODO(cjihrig): Support additional flags.
508+
int default_flags = SQLITE_OPEN_URI;
506509
int flags = open_config_.get_read_only()
507510
? SQLITE_OPEN_READONLY
508511
: SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
509-
int r = sqlite3_open_v2(
510-
open_config_.location().c_str(), &connection_, flags, nullptr);
512+
int r = sqlite3_open_v2(open_config_.location().c_str(),
513+
&connection_,
514+
flags | default_flags,
515+
nullptr);
511516
CHECK_ERROR_OR_THROW(env()->isolate(), this, r, SQLITE_OK, false);
512517

513518
r = sqlite3_db_config(connection_,
@@ -585,27 +590,85 @@ bool DatabaseSync::ShouldIgnoreSQLiteError() {
585590
return ignore_next_sqlite_error_;
586591
}
587592

593+
std::optional<std::string> ValidateDatabasePath(Environment* env,
594+
Local<Value> path,
595+
const std::string& field_name) {
596+
auto has_null_bytes = [](const std::string& str) {
597+
return str.find('\0') != std::string::npos;
598+
};
599+
std::string location;
600+
if (path->IsString()) {
601+
location = Utf8Value(env->isolate(), path.As<String>()).ToString();
602+
if (!has_null_bytes(location)) {
603+
return location;
604+
}
605+
}
606+
607+
if (path->IsUint8Array()) {
608+
Local<Uint8Array> buffer = path.As<Uint8Array>();
609+
size_t byteOffset = buffer->ByteOffset();
610+
size_t byteLength = buffer->ByteLength();
611+
auto data =
612+
static_cast<const uint8_t*>(buffer->Buffer()->Data()) + byteOffset;
613+
if (!(std::find(data, data + byteLength, 0) != data + byteLength)) {
614+
Local<Value> out;
615+
if (String::NewFromUtf8(env->isolate(),
616+
reinterpret_cast<const char*>(data),
617+
NewStringType::kNormal,
618+
static_cast<int>(byteLength))
619+
.ToLocal(&out)) {
620+
return Utf8Value(env->isolate(), out.As<String>()).ToString();
621+
}
622+
}
623+
}
624+
625+
// When is URL
626+
if (path->IsObject()) {
627+
Local<Object> url = path.As<Object>();
628+
Local<Value> href;
629+
Local<Value> protocol;
630+
if (url->Get(env->context(), env->href_string()).ToLocal(&href) &&
631+
href->IsString() &&
632+
url->Get(env->context(), env->protocol_string()).ToLocal(&protocol) &&
633+
protocol->IsString()) {
634+
location = Utf8Value(env->isolate(), href.As<String>()).ToString();
635+
if (!has_null_bytes(location)) {
636+
auto file_url = ada::parse(location);
637+
CHECK(file_url);
638+
if (file_url->type != ada::scheme::FILE) {
639+
THROW_ERR_INVALID_URL_SCHEME(env->isolate());
640+
return std::nullopt;
641+
}
642+
643+
return location;
644+
}
645+
}
646+
}
647+
648+
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
649+
"The \"%s\" argument must be a string, "
650+
"Uint8Array, or URL without null bytes.",
651+
field_name.c_str());
652+
653+
return std::nullopt;
654+
}
655+
588656
void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
589657
Environment* env = Environment::GetCurrent(args);
590-
591658
if (!args.IsConstructCall()) {
592659
THROW_ERR_CONSTRUCT_CALL_REQUIRED(env);
593660
return;
594661
}
595662

596-
if (!args[0]->IsString()) {
597-
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
598-
"The \"path\" argument must be a string.");
663+
std::optional<std::string> location =
664+
ValidateDatabasePath(env, args[0], "path");
665+
if (!location.has_value()) {
599666
return;
600667
}
601668

602-
std::string location =
603-
Utf8Value(env->isolate(), args[0].As<String>()).ToString();
604-
DatabaseOpenConfiguration open_config(std::move(location));
605-
669+
DatabaseOpenConfiguration open_config(std::move(location.value()));
606670
bool open = true;
607671
bool allow_load_extension = false;
608-
609672
if (args.Length() > 1) {
610673
if (!args[1]->IsObject()) {
611674
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
@@ -984,17 +1047,15 @@ void Backup(const FunctionCallbackInfo<Value>& args) {
9841047
DatabaseSync* db;
9851048
ASSIGN_OR_RETURN_UNWRAP(&db, args[0].As<Object>());
9861049
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
987-
if (!args[1]->IsString()) {
988-
THROW_ERR_INVALID_ARG_TYPE(
989-
env->isolate(), "The \"destination\" argument must be a string.");
1050+
std::optional<std::string> dest_path =
1051+
ValidateDatabasePath(env, args[1], "path");
1052+
if (!dest_path.has_value()) {
9901053
return;
9911054
}
9921055

9931056
int rate = 100;
9941057
std::string source_db = "main";
9951058
std::string dest_db = "main";
996-
997-
Utf8Value dest_path(env->isolate(), args[1].As<String>());
9981059
Local<Function> progressFunc = Local<Function>();
9991060

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

10791140
args.GetReturnValue().Set(resolver->GetPromise());
1080-
10811141
BackupJob* job = new BackupJob(env,
10821142
db,
10831143
resolver,
10841144
std::move(source_db),
1085-
*dest_path,
1145+
dest_path.value(),
10861146
std::move(dest_db),
10871147
rate,
10881148
progressFunc);

test/parallel/test-sqlite-backup.mjs

+78-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { join } from 'node:path';
44
import { backup, DatabaseSync } from 'node:sqlite';
55
import { describe, test } from 'node:test';
66
import { writeFileSync } from 'node:fs';
7+
import { pathToFileURL } from 'node:url';
78

89
let cnt = 0;
910

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

16-
function makeSourceDb() {
17-
const database = new DatabaseSync(':memory:');
17+
function makeSourceDb(dbPath = ':memory:') {
18+
const database = new DatabaseSync(dbPath);
1819

1920
database.exec(`
2021
CREATE TABLE data(
@@ -42,21 +43,39 @@ describe('backup()', () => {
4243
});
4344
});
4445

45-
test('throws if path is not a string', (t) => {
46+
test('throws if path is not a string, URL, or Buffer', (t) => {
4647
const database = makeSourceDb();
4748

4849
t.assert.throws(() => {
4950
backup(database);
5051
}, {
5152
code: 'ERR_INVALID_ARG_TYPE',
52-
message: 'The "destination" argument must be a string.'
53+
message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.'
5354
});
5455

5556
t.assert.throws(() => {
5657
backup(database, {});
5758
}, {
5859
code: 'ERR_INVALID_ARG_TYPE',
59-
message: 'The "destination" argument must be a string.'
60+
message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.'
61+
});
62+
});
63+
64+
test('throws if the database path contains null bytes', (t) => {
65+
const database = makeSourceDb();
66+
67+
t.assert.throws(() => {
68+
backup(database, Buffer.from('l\0cation'));
69+
}, {
70+
code: 'ERR_INVALID_ARG_TYPE',
71+
message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.'
72+
});
73+
74+
t.assert.throws(() => {
75+
backup(database, 'l\0cation');
76+
}, {
77+
code: 'ERR_INVALID_ARG_TYPE',
78+
message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.'
6079
});
6180
});
6281

@@ -141,6 +160,46 @@ test('database backup', async (t) => {
141160
});
142161
});
143162

163+
test('backup database using location as URL', async (t) => {
164+
const database = makeSourceDb();
165+
const destDb = pathToFileURL(nextDb());
166+
167+
t.after(() => { database.close(); });
168+
169+
await backup(database, destDb);
170+
171+
const backupDb = new DatabaseSync(destDb);
172+
173+
t.after(() => { backupDb.close(); });
174+
175+
const rows = backupDb.prepare('SELECT * FROM data').all();
176+
177+
t.assert.deepStrictEqual(rows, [
178+
{ __proto__: null, key: 1, value: 'value-1' },
179+
{ __proto__: null, key: 2, value: 'value-2' },
180+
]);
181+
});
182+
183+
test('backup database using location as Buffer', async (t) => {
184+
const database = makeSourceDb();
185+
const destDb = Buffer.from(nextDb());
186+
187+
t.after(() => { database.close(); });
188+
189+
await backup(database, destDb);
190+
191+
const backupDb = new DatabaseSync(destDb);
192+
193+
t.after(() => { backupDb.close(); });
194+
195+
const rows = backupDb.prepare('SELECT * FROM data').all();
196+
197+
t.assert.deepStrictEqual(rows, [
198+
{ __proto__: null, key: 1, value: 'value-1' },
199+
{ __proto__: null, key: 2, value: 'value-2' },
200+
]);
201+
});
202+
144203
test('database backup in a single call', async (t) => {
145204
const progressFn = t.mock.fn();
146205
const database = makeSourceDb();
@@ -179,6 +238,19 @@ test('throws exception when trying to start backup from a closed database', (t)
179238
});
180239
});
181240

241+
test('throws if URL is not file: scheme', (t) => {
242+
const database = new DatabaseSync(':memory:');
243+
244+
t.after(() => { database.close(); });
245+
246+
t.assert.throws(() => {
247+
backup(database, new URL('http://example.com/backup.db'));
248+
}, {
249+
code: 'ERR_INVALID_URL_SCHEME',
250+
message: 'The URL must be of scheme file:',
251+
});
252+
});
253+
182254
test('database backup fails when dest file is not writable', async (t) => {
183255
const readonlyDestDb = nextDb();
184256
writeFileSync(readonlyDestDb, '', { mode: 0o444 });
@@ -225,7 +297,7 @@ test('backup fails when source db is invalid', async (t) => {
225297
});
226298
});
227299

228-
test('backup fails when destination cannot be opened', async (t) => {
300+
test('backup fails when path cannot be opened', async (t) => {
229301
const database = makeSourceDb();
230302

231303
await t.assert.rejects(async () => {

0 commit comments

Comments
 (0)