Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit e80bbb7

Browse filesBrowse 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
Copy full SHA for e80bbb7

File tree

Expand file treeCollapse file tree

6 files changed

+304
-37
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

6 files changed

+304
-37
lines changed
Open diff view settings
Collapse file

‎doc/api/sqlite.md‎

Copy file name to clipboardExpand all lines: doc/api/sqlite.md
+15-7Lines changed: 15 additions & 7 deletions
  • Display the source diff
  • Display the rich diff
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
Collapse file

‎src/env_properties.h‎

Copy file name to clipboardExpand all lines: src/env_properties.h
+1Lines changed: 1 addition & 0 deletions
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") \
Collapse file

‎src/node_sqlite.cc‎

Copy file name to clipboardExpand all lines: src/node_sqlite.cc
+82-22Lines changed: 82 additions & 22 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 "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);
Collapse file

‎test/parallel/test-sqlite-backup.mjs‎

Copy file name to clipboardExpand all lines: test/parallel/test-sqlite-backup.mjs
+78-6Lines changed: 78 additions & 6 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.