| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -77,20 +77,24 @@ console.log(query.all()); | |||
| 77 | 77 | ||
| 78 | 78 | <!-- YAML | |
| 79 | 79 | 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. | ||
| 80 | 84 | --> | |
| 81 | 85 | ||
| 82 | 86 | This class represents a single [connection][] to a SQLite database. All APIs | |
| 83 | 87 | exposed by this class execute synchronously. | |
| 84 | 88 | ||
| 85 | - ### `new DatabaseSync(location[, options])` | ||
| 89 | + ### `new DatabaseSync(path[, options])` | ||
| 86 | 90 | ||
| 87 | 91 | <!-- YAML | |
| 88 | 92 | added: v22.5.0 | |
| 89 | 93 | --> | |
| 90 | 94 | ||
| 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 | ||
| 92 | 96 | 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 | ||
| 94 | 98 | should be the special name `':memory:'`. | |
| 95 | 99 | * `options` {Object} Configuration options for the database connection. The | |
| 96 | 100 | following options are supported: | |
@@ -194,7 +198,7 @@ wrapper around [`sqlite3_create_function_v2()`][]. | |||
| 194 | 198 | added: v22.5.0 | |
| 195 | 199 | --> | |
| 196 | 200 | ||
| 197 | - Opens the database specified in the `location` argument of the `DatabaseSync` | ||
| 201 | + Opens the database specified in the `path` argument of the `DatabaseSync` | ||
| 198 | 202 | constructor. This method should only be used when the database is not opened via | |
| 199 | 203 | the constructor. An exception is thrown if the database is already open. | |
| 200 | 204 | ||
@@ -508,15 +512,19 @@ exception. | |||
| 508 | 512 | | `TEXT` | {string} | | |
| 509 | 513 | | `BLOB` | {TypedArray} or {DataView} | | |
| 510 | 514 | ||
| 511 | - ## `sqlite.backup(sourceDb, destination[, options])` | ||
| 515 | + ## `sqlite.backup(sourceDb, path[, options])` | ||
| 512 | 516 | ||
| 513 | 517 | <!-- YAML | |
| 514 | 518 | 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. | ||
| 515 | 523 | --> | |
| 516 | 524 | ||
| 517 | 525 | * `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. | ||
| 520 | 528 | * `options` {Object} Optional configuration for the backup. The | |
| 521 | 529 | following properties are supported: | |
| 522 | 530 | * `source` {string} Name of the source database. This can be `'main'` (the default primary database) or any other | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -194,6 +194,7 @@ | |||
| 194 | 194 | V(host_string, "host") \ | |
| 195 | 195 | V(hostmaster_string, "hostmaster") \ | |
| 196 | 196 | V(hostname_string, "hostname") \ | |
| 197 | + V(href_string, "href") \ | ||
| 197 | 198 | V(http_1_1_string, "http/1.1") \ | |
| 198 | 199 | V(id_string, "id") \ | |
| 199 | 200 | V(identity_string, "identity") \ | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -7,6 +7,7 @@ | |||
| 7 | 7 | #include "node.h" | |
| 8 | 8 | #include "node_errors.h" | |
| 9 | 9 | #include "node_mem-inl.h" | |
| 10 | + #include "node_url.h" | ||
| 10 | 11 | #include "sqlite3.h" | |
| 11 | 12 | #include "threadpoolwork-inl.h" | |
| 12 | 13 | #include "util-inl.h" | |
@@ -181,10 +182,11 @@ class BackupJob : public ThreadPoolWork { | |||
| 181 | 182 | void ScheduleBackup() { | |
| 182 | 183 | Isolate* isolate = env()->isolate(); | |
| 183 | 184 | 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); | ||
| 188 | 190 | Local<Promise::Resolver> resolver = | |
| 189 | 191 | Local<Promise::Resolver>::New(env()->isolate(), resolver_); | |
| 190 | 192 | if (backup_status_ != SQLITE_OK) { | |
@@ -503,11 +505,14 @@ bool DatabaseSync::Open() { | |||
| 503 | 505 | } | |
| 504 | 506 | ||
| 505 | 507 | // TODO(cjihrig): Support additional flags. | |
| 508 | + int default_flags = SQLITE_OPEN_URI; | ||
| 506 | 509 | int flags = open_config_.get_read_only() | |
| 507 | 510 | ? SQLITE_OPEN_READONLY | |
| 508 | 511 | : 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); | ||
| 511 | 516 | CHECK_ERROR_OR_THROW(env()->isolate(), this, r, SQLITE_OK, false); | |
| 512 | 517 | ||
| 513 | 518 | r = sqlite3_db_config(connection_, | |
@@ -585,27 +590,85 @@ bool DatabaseSync::ShouldIgnoreSQLiteError() { | |||
| 585 | 590 | return ignore_next_sqlite_error_; | |
| 586 | 591 | } | |
| 587 | 592 | ||
| 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 | + | ||
| 588 | 656 | void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) { | |
| 589 | 657 | Environment* env = Environment::GetCurrent(args); | |
| 590 | - | ||
| 591 | 658 | if (!args.IsConstructCall()) { | |
| 592 | 659 | THROW_ERR_CONSTRUCT_CALL_REQUIRED(env); | |
| 593 | 660 | return; | |
| 594 | 661 | } | |
| 595 | 662 | ||
| 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()) { | ||
| 599 | 666 | return; | |
| 600 | 667 | } | |
| 601 | 668 | ||
| 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())); | ||
| 606 | 670 | bool open = true; | |
| 607 | 671 | bool allow_load_extension = false; | |
| 608 | - | ||
| 609 | 672 | if (args.Length() > 1) { | |
| 610 | 673 | if (!args[1]->IsObject()) { | |
| 611 | 674 | THROW_ERR_INVALID_ARG_TYPE(env->isolate(), | |
@@ -984,17 +1047,15 @@ void Backup(const FunctionCallbackInfo<Value>& args) { | |||
| 984 | 1047 | DatabaseSync* db; | |
| 985 | 1048 | ASSIGN_OR_RETURN_UNWRAP(&db, args[0].As<Object>()); | |
| 986 | 1049 | 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()) { | ||
| 990 | 1053 | return; | |
| 991 | 1054 | } | |
| 992 | 1055 | ||
| 993 | 1056 | int rate = 100; | |
| 994 | 1057 | std::string source_db = "main"; | |
| 995 | 1058 | std::string dest_db = "main"; | |
| 996 | - | ||
| 997 | - Utf8Value dest_path(env->isolate(), args[1].As<String>()); | ||
| 998 | 1059 | Local<Function> progressFunc = Local<Function>(); | |
| 999 | 1060 | ||
| 1000 | 1061 | if (args.Length() > 2) { | |
@@ -1077,12 +1138,11 @@ void Backup(const FunctionCallbackInfo<Value>& args) { | |||
| 1077 | 1138 | } | |
| 1078 | 1139 | ||
| 1079 | 1140 | args.GetReturnValue().Set(resolver->GetPromise()); | |
| 1080 | - | ||
| 1081 | 1141 | BackupJob* job = new BackupJob(env, | |
| 1082 | 1142 | db, | |
| 1083 | 1143 | resolver, | |
| 1084 | 1144 | std::move(source_db), | |
| 1085 | - *dest_path, | ||
| 1145 | + dest_path.value(), | ||
| 1086 | 1146 | std::move(dest_db), | |
| 1087 | 1147 | rate, | |
| 1088 | 1148 | progressFunc); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -4,6 +4,7 @@ import { join } from 'node:path'; | |||
| 4 | 4 | import { backup, DatabaseSync } from 'node:sqlite'; | |
| 5 | 5 | import { describe, test } from 'node:test'; | |
| 6 | 6 | import { writeFileSync } from 'node:fs'; | |
| 7 | + import { pathToFileURL } from 'node:url'; | ||
| 7 | 8 | ||
| 8 | 9 | let cnt = 0; | |
| 9 | 10 | ||
@@ -13,8 +14,8 @@ function nextDb() { | |||
| 13 | 14 | return join(tmpdir.path, `database-${cnt++}.db`); | |
| 14 | 15 | } | |
| 15 | 16 | ||
| 16 | - function makeSourceDb() { | ||
| 17 | - const database = new DatabaseSync(':memory:'); | ||
| 17 | + function makeSourceDb(dbPath = ':memory:') { | ||
| 18 | + const database = new DatabaseSync(dbPath); | ||
| 18 | 19 | ||
| 19 | 20 | database.exec(` | |
| 20 | 21 | CREATE TABLE data( | |
@@ -42,21 +43,39 @@ describe('backup()', () => { | |||
| 42 | 43 | }); | |
| 43 | 44 | }); | |
| 44 | 45 | ||
| 45 | - test('throws if path is not a string', (t) => { | ||
| 46 | + test('throws if path is not a string, URL, or Buffer', (t) => { | ||
| 46 | 47 | const database = makeSourceDb(); | |
| 47 | 48 | ||
| 48 | 49 | t.assert.throws(() => { | |
| 49 | 50 | backup(database); | |
| 50 | 51 | }, { | |
| 51 | 52 | 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.' | ||
| 53 | 54 | }); | |
| 54 | 55 | ||
| 55 | 56 | t.assert.throws(() => { | |
| 56 | 57 | backup(database, {}); | |
| 57 | 58 | }, { | |
| 58 | 59 | 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.' | ||
| 60 | 79 | }); | |
| 61 | 80 | }); | |
| 62 | 81 | ||
@@ -141,6 +160,46 @@ test('database backup', async (t) => { | |||
| 141 | 160 | }); | |
| 142 | 161 | }); | |
| 143 | 162 | ||
| 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 | + | ||
| 144 | 203 | test('database backup in a single call', async (t) => { | |
| 145 | 204 | const progressFn = t.mock.fn(); | |
| 146 | 205 | const database = makeSourceDb(); | |
@@ -179,6 +238,19 @@ test('throws exception when trying to start backup from a closed database', (t) | |||
| 179 | 238 | }); | |
| 180 | 239 | }); | |
| 181 | 240 | ||
| 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 | + | ||
| 182 | 254 | test('database backup fails when dest file is not writable', async (t) => { | |
| 183 | 255 | const readonlyDestDb = nextDb(); | |
| 184 | 256 | writeFileSync(readonlyDestDb, '', { mode: 0o444 }); | |
@@ -225,7 +297,7 @@ test('backup fails when source db is invalid', async (t) => { | |||
| 225 | 297 | }); | |
| 226 | 298 | }); | |
| 227 | 299 | ||
| 228 | - test('backup fails when destination cannot be opened', async (t) => { | ||
| 300 | + test('backup fails when path cannot be opened', async (t) => { | ||
| 229 | 301 | const database = makeSourceDb(); | |
| 230 | 302 | ||
| 231 | 303 | await t.assert.rejects(async () => { | |
| Back | FazBrowse Home | New Git URL |
0 commit comments