Skip to content

Commit 8dc6376

Browse files
geeksilva97targos
authored andcommittedFeb 7, 2025
sqlite, test: expose sqlite online backup api
PR-URL: #56253 Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Vinícius Lourenço Claro Cardoso <[email protected]>
1 parent 3c082d4 commit 8dc6376

File tree

6 files changed

+626
-0
lines changed

6 files changed

+626
-0
lines changed
 

‎doc/api/sqlite.md

+60
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,63 @@ exception.
508508
| `TEXT` | {string} |
509509
| `BLOB` | {TypedArray} or {DataView} |
510510

511+
## `sqlite.backup(sourceDb, destination[, options])`
512+
513+
<!-- YAML
514+
added: REPLACEME
515+
-->
516+
517+
* `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.
520+
* `options` {Object} Optional configuration for the backup. The
521+
following properties are supported:
522+
* `source` {string} Name of the source database. This can be `'main'` (the default primary database) or any other
523+
database that have been added with [`ATTACH DATABASE`][] **Default:** `'main'`.
524+
* `target` {string} Name of the target database. This can be `'main'` (the default primary database) or any other
525+
database that have been added with [`ATTACH DATABASE`][] **Default:** `'main'`.
526+
* `rate` {number} Number of pages to be transmitted in each batch of the backup. **Default:** `100`.
527+
* `progress` {Function} Callback function that will be called with the number of pages copied and the total number of
528+
pages.
529+
* Returns: {Promise} A promise that resolves when the backup is completed and rejects if an error occurs.
530+
531+
This method makes a database backup. This method abstracts the [`sqlite3_backup_init()`][], [`sqlite3_backup_step()`][]
532+
and [`sqlite3_backup_finish()`][] functions.
533+
534+
The backed-up database can be used normally during the backup process. Mutations coming from the same connection - same
535+
{DatabaseSync} - object will be reflected in the backup right away. However, mutations from other connections will cause
536+
the backup process to restart.
537+
538+
```cjs
539+
const { backup, DatabaseSync } = require('node:sqlite');
540+
541+
(async () => {
542+
const sourceDb = new DatabaseSync('source.db');
543+
const totalPagesTransferred = await backup(sourceDb, 'backup.db', {
544+
rate: 1, // Copy one page at a time.
545+
progress: ({ totalPages, remainingPages }) => {
546+
console.log('Backup in progress', { totalPages, remainingPages });
547+
},
548+
});
549+
550+
console.log('Backup completed', totalPagesTransferred);
551+
})();
552+
```
553+
554+
```mjs
555+
import { backup, DatabaseSync } from 'node:sqlite';
556+
557+
const sourceDb = new DatabaseSync('source.db');
558+
const totalPagesTransferred = await backup(sourceDb, 'backup.db', {
559+
rate: 1, // Copy one page at a time.
560+
progress: ({ totalPages, remainingPages }) => {
561+
console.log('Backup in progress', { totalPages, remainingPages });
562+
},
563+
});
564+
565+
console.log('Backup completed', totalPagesTransferred);
566+
```
567+
511568
## `sqlite.constants`
512569

513570
<!-- YAML
@@ -589,6 +646,9 @@ resolution handler passed to [`database.applyChangeset()`][]. See also
589646
[`SQLITE_DIRECTONLY`]: https://www.sqlite.org/c3ref/c_deterministic.html
590647
[`SQLITE_MAX_FUNCTION_ARG`]: https://www.sqlite.org/limits.html#max_function_arg
591648
[`database.applyChangeset()`]: #databaseapplychangesetchangeset-options
649+
[`sqlite3_backup_finish()`]: https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupfinish
650+
[`sqlite3_backup_init()`]: https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupinit
651+
[`sqlite3_backup_step()`]: https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupstep
592652
[`sqlite3_changes64()`]: https://www.sqlite.org/c3ref/changes.html
593653
[`sqlite3_close_v2()`]: https://www.sqlite.org/c3ref/close.html
594654
[`sqlite3_create_function_v2()`]: https://www.sqlite.org/c3ref/create_function.html

‎src/env_properties.h

+4
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
V(asn1curve_string, "asn1Curve") \
7878
V(async_ids_stack_string, "async_ids_stack") \
7979
V(attributes_string, "attributes") \
80+
V(backup_string, "backup") \
8081
V(base_string, "base") \
8182
V(base_url_string, "baseURL") \
8283
V(bits_string, "bits") \
@@ -302,6 +303,7 @@
302303
V(primordials_string, "primordials") \
303304
V(priority_string, "priority") \
304305
V(process_string, "process") \
306+
V(progress_string, "progress") \
305307
V(promise_string, "promise") \
306308
V(protocol_string, "protocol") \
307309
V(prototype_string, "prototype") \
@@ -316,6 +318,7 @@
316318
V(reason_string, "reason") \
317319
V(refresh_string, "refresh") \
318320
V(regexp_string, "regexp") \
321+
V(remaining_pages_string, "remainingPages") \
319322
V(rename_string, "rename") \
320323
V(replacement_string, "replacement") \
321324
V(required_module_facade_url_string, \
@@ -369,6 +372,7 @@
369372
V(time_to_first_byte_sent_string, "timeToFirstByteSent") \
370373
V(time_to_first_header_string, "timeToFirstHeader") \
371374
V(tls_ticket_string, "tlsTicket") \
375+
V(total_pages_string, "totalPages") \
372376
V(transfer_string, "transfer") \
373377
V(transfer_unsupported_type_str, \
374378
"Cannot transfer object of unsupported type.") \

‎src/node_sqlite.cc

+320
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "node_errors.h"
99
#include "node_mem-inl.h"
1010
#include "sqlite3.h"
11+
#include "threadpoolwork-inl.h"
1112
#include "util-inl.h"
1213

1314
#include <cinttypes>
@@ -29,6 +30,7 @@ using v8::FunctionCallback;
2930
using v8::FunctionCallbackInfo;
3031
using v8::FunctionTemplate;
3132
using v8::Global;
33+
using v8::HandleScope;
3234
using v8::Int32;
3335
using v8::Integer;
3436
using v8::Isolate;
@@ -40,6 +42,7 @@ using v8::NewStringType;
4042
using v8::Null;
4143
using v8::Number;
4244
using v8::Object;
45+
using v8::Promise;
4346
using v8::SideEffectType;
4447
using v8::String;
4548
using v8::TryCatch;
@@ -81,6 +84,23 @@ inline MaybeLocal<Object> CreateSQLiteError(Isolate* isolate,
8184
return e;
8285
}
8386

87+
inline MaybeLocal<Object> CreateSQLiteError(Isolate* isolate, int errcode) {
88+
const char* errstr = sqlite3_errstr(errcode);
89+
Local<String> js_errmsg;
90+
Local<Object> e;
91+
Environment* env = Environment::GetCurrent(isolate);
92+
if (!String::NewFromUtf8(isolate, errstr).ToLocal(&js_errmsg) ||
93+
!CreateSQLiteError(isolate, errstr).ToLocal(&e) ||
94+
e->Set(env->context(),
95+
env->errcode_string(),
96+
Integer::New(isolate, errcode))
97+
.IsNothing() ||
98+
e->Set(env->context(), env->errstr_string(), js_errmsg).IsNothing()) {
99+
return MaybeLocal<Object>();
100+
}
101+
return e;
102+
}
103+
84104
inline MaybeLocal<Object> CreateSQLiteError(Isolate* isolate, sqlite3* db) {
85105
int errcode = sqlite3_extended_errcode(db);
86106
const char* errstr = sqlite3_errstr(errcode);
@@ -137,6 +157,169 @@ inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, int errcode) {
137157
}
138158
}
139159

160+
class BackupJob : public ThreadPoolWork {
161+
public:
162+
explicit BackupJob(Environment* env,
163+
DatabaseSync* source,
164+
Local<Promise::Resolver> resolver,
165+
std::string source_db,
166+
std::string destination_name,
167+
std::string dest_db,
168+
int pages,
169+
Local<Function> progressFunc)
170+
: ThreadPoolWork(env, "node_sqlite3.BackupJob"),
171+
env_(env),
172+
source_(source),
173+
pages_(pages),
174+
source_db_(source_db),
175+
destination_name_(destination_name),
176+
dest_db_(dest_db) {
177+
resolver_.Reset(env->isolate(), resolver);
178+
progressFunc_.Reset(env->isolate(), progressFunc);
179+
}
180+
181+
void ScheduleBackup() {
182+
Isolate* isolate = env()->isolate();
183+
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);
188+
Local<Promise::Resolver> resolver =
189+
Local<Promise::Resolver>::New(env()->isolate(), resolver_);
190+
if (backup_status_ != SQLITE_OK) {
191+
HandleBackupError(resolver);
192+
return;
193+
}
194+
195+
backup_ = sqlite3_backup_init(
196+
dest_, dest_db_.c_str(), source_->Connection(), source_db_.c_str());
197+
if (backup_ == nullptr) {
198+
HandleBackupError(resolver);
199+
return;
200+
}
201+
202+
this->ScheduleWork();
203+
}
204+
205+
void DoThreadPoolWork() override {
206+
backup_status_ = sqlite3_backup_step(backup_, pages_);
207+
}
208+
209+
void AfterThreadPoolWork(int status) override {
210+
HandleScope handle_scope(env()->isolate());
211+
Local<Promise::Resolver> resolver =
212+
Local<Promise::Resolver>::New(env()->isolate(), resolver_);
213+
214+
if (!(backup_status_ == SQLITE_OK || backup_status_ == SQLITE_DONE ||
215+
backup_status_ == SQLITE_BUSY || backup_status_ == SQLITE_LOCKED)) {
216+
HandleBackupError(resolver, backup_status_);
217+
return;
218+
}
219+
220+
int total_pages = sqlite3_backup_pagecount(backup_);
221+
int remaining_pages = sqlite3_backup_remaining(backup_);
222+
if (remaining_pages != 0) {
223+
Local<Function> fn =
224+
Local<Function>::New(env()->isolate(), progressFunc_);
225+
if (!fn.IsEmpty()) {
226+
Local<Object> progress_info = Object::New(env()->isolate());
227+
if (progress_info
228+
->Set(env()->context(),
229+
env()->total_pages_string(),
230+
Integer::New(env()->isolate(), total_pages))
231+
.IsNothing() ||
232+
progress_info
233+
->Set(env()->context(),
234+
env()->remaining_pages_string(),
235+
Integer::New(env()->isolate(), remaining_pages))
236+
.IsNothing()) {
237+
return;
238+
}
239+
240+
Local<Value> argv[] = {progress_info};
241+
TryCatch try_catch(env()->isolate());
242+
fn->Call(env()->context(), Null(env()->isolate()), 1, argv)
243+
.FromMaybe(Local<Value>());
244+
if (try_catch.HasCaught()) {
245+
Finalize();
246+
resolver->Reject(env()->context(), try_catch.Exception()).ToChecked();
247+
return;
248+
}
249+
}
250+
251+
// There's still work to do
252+
this->ScheduleWork();
253+
return;
254+
}
255+
256+
if (backup_status_ != SQLITE_DONE) {
257+
HandleBackupError(resolver);
258+
return;
259+
}
260+
261+
Finalize();
262+
resolver
263+
->Resolve(env()->context(), Integer::New(env()->isolate(), total_pages))
264+
.ToChecked();
265+
}
266+
267+
void Finalize() {
268+
Cleanup();
269+
source_->RemoveBackup(this);
270+
}
271+
272+
void Cleanup() {
273+
if (backup_) {
274+
sqlite3_backup_finish(backup_);
275+
backup_ = nullptr;
276+
}
277+
278+
if (dest_) {
279+
backup_status_ = sqlite3_errcode(dest_);
280+
sqlite3_close_v2(dest_);
281+
dest_ = nullptr;
282+
}
283+
}
284+
285+
private:
286+
void HandleBackupError(Local<Promise::Resolver> resolver) {
287+
Local<Object> e;
288+
if (!CreateSQLiteError(env()->isolate(), dest_).ToLocal(&e)) {
289+
Finalize();
290+
return;
291+
}
292+
293+
Finalize();
294+
resolver->Reject(env()->context(), e).ToChecked();
295+
}
296+
297+
void HandleBackupError(Local<Promise::Resolver> resolver, int errcode) {
298+
Local<Object> e;
299+
if (!CreateSQLiteError(env()->isolate(), errcode).ToLocal(&e)) {
300+
Finalize();
301+
return;
302+
}
303+
304+
Finalize();
305+
resolver->Reject(env()->context(), e).ToChecked();
306+
}
307+
308+
Environment* env() const { return env_; }
309+
310+
Environment* env_;
311+
DatabaseSync* source_;
312+
Global<Promise::Resolver> resolver_;
313+
Global<Function> progressFunc_;
314+
sqlite3* dest_ = nullptr;
315+
sqlite3_backup* backup_ = nullptr;
316+
int pages_;
317+
int backup_status_;
318+
std::string source_db_;
319+
std::string destination_name_;
320+
std::string dest_db_;
321+
};
322+
140323
UserDefinedFunction::UserDefinedFunction(Environment* env,
141324
Local<Function> fn,
142325
DatabaseSync* db,
@@ -279,6 +462,14 @@ DatabaseSync::DatabaseSync(Environment* env,
279462
}
280463
}
281464

465+
void DatabaseSync::AddBackup(BackupJob* job) {
466+
backups_.insert(job);
467+
}
468+
469+
void DatabaseSync::RemoveBackup(BackupJob* job) {
470+
backups_.erase(job);
471+
}
472+
282473
void DatabaseSync::DeleteSessions() {
283474
// all attached sessions need to be deleted before the database is closed
284475
// https://www.sqlite.org/session/sqlite3session_create.html
@@ -289,6 +480,8 @@ void DatabaseSync::DeleteSessions() {
289480
}
290481

291482
DatabaseSync::~DatabaseSync() {
483+
FinalizeBackups();
484+
292485
if (IsOpen()) {
293486
FinalizeStatements();
294487
DeleteSessions();
@@ -353,6 +546,14 @@ bool DatabaseSync::Open() {
353546
return true;
354547
}
355548

549+
void DatabaseSync::FinalizeBackups() {
550+
for (auto backup : backups_) {
551+
backup->Cleanup();
552+
}
553+
554+
backups_.clear();
555+
}
556+
356557
void DatabaseSync::FinalizeStatements() {
357558
for (auto stmt : statements_) {
358559
stmt->Finalize();
@@ -772,6 +973,117 @@ void DatabaseSync::CreateSession(const FunctionCallbackInfo<Value>& args) {
772973
args.GetReturnValue().Set(session->object());
773974
}
774975

976+
void Backup(const FunctionCallbackInfo<Value>& args) {
977+
Environment* env = Environment::GetCurrent(args);
978+
if (args.Length() < 1 || !args[0]->IsObject()) {
979+
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
980+
"The \"sourceDb\" argument must be an object.");
981+
return;
982+
}
983+
984+
DatabaseSync* db;
985+
ASSIGN_OR_RETURN_UNWRAP(&db, args[0].As<Object>());
986+
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.");
990+
return;
991+
}
992+
993+
int rate = 100;
994+
std::string source_db = "main";
995+
std::string dest_db = "main";
996+
997+
Utf8Value dest_path(env->isolate(), args[1].As<String>());
998+
Local<Function> progressFunc = Local<Function>();
999+
1000+
if (args.Length() > 2) {
1001+
if (!args[2]->IsObject()) {
1002+
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
1003+
"The \"options\" argument must be an object.");
1004+
return;
1005+
}
1006+
1007+
Local<Object> options = args[2].As<Object>();
1008+
Local<Value> rate_v;
1009+
if (!options->Get(env->context(), env->rate_string()).ToLocal(&rate_v)) {
1010+
return;
1011+
}
1012+
1013+
if (!rate_v->IsUndefined()) {
1014+
if (!rate_v->IsInt32()) {
1015+
THROW_ERR_INVALID_ARG_TYPE(
1016+
env->isolate(),
1017+
"The \"options.rate\" argument must be an integer.");
1018+
return;
1019+
}
1020+
rate = rate_v.As<Int32>()->Value();
1021+
}
1022+
1023+
Local<Value> source_v;
1024+
if (!options->Get(env->context(), env->source_string())
1025+
.ToLocal(&source_v)) {
1026+
return;
1027+
}
1028+
1029+
if (!source_v->IsUndefined()) {
1030+
if (!source_v->IsString()) {
1031+
THROW_ERR_INVALID_ARG_TYPE(
1032+
env->isolate(),
1033+
"The \"options.source\" argument must be a string.");
1034+
return;
1035+
}
1036+
1037+
source_db = Utf8Value(env->isolate(), source_v.As<String>()).ToString();
1038+
}
1039+
1040+
Local<Value> target_v;
1041+
if (!options->Get(env->context(), env->target_string())
1042+
.ToLocal(&target_v)) {
1043+
return;
1044+
}
1045+
1046+
if (!target_v->IsUndefined()) {
1047+
if (!target_v->IsString()) {
1048+
THROW_ERR_INVALID_ARG_TYPE(
1049+
env->isolate(),
1050+
"The \"options.target\" argument must be a string.");
1051+
return;
1052+
}
1053+
1054+
dest_db = Utf8Value(env->isolate(), target_v.As<String>()).ToString();
1055+
}
1056+
1057+
Local<Value> progress_v;
1058+
if (!options->Get(env->context(), env->progress_string())
1059+
.ToLocal(&progress_v)) {
1060+
return;
1061+
}
1062+
1063+
if (!progress_v->IsUndefined()) {
1064+
if (!progress_v->IsFunction()) {
1065+
THROW_ERR_INVALID_ARG_TYPE(
1066+
env->isolate(),
1067+
"The \"options.progress\" argument must be a function.");
1068+
return;
1069+
}
1070+
progressFunc = progress_v.As<Function>();
1071+
}
1072+
}
1073+
1074+
Local<Promise::Resolver> resolver;
1075+
if (!Promise::Resolver::New(env->context()).ToLocal(&resolver)) {
1076+
return;
1077+
}
1078+
1079+
args.GetReturnValue().Set(resolver->GetPromise());
1080+
1081+
BackupJob* job = new BackupJob(
1082+
env, db, resolver, source_db, *dest_path, dest_db, rate, progressFunc);
1083+
db->AddBackup(job);
1084+
job->ScheduleBackup();
1085+
}
1086+
7751087
// the reason for using static functions here is that SQLite needs a
7761088
// function pointer
7771089
static std::function<int(int)> conflictCallback;
@@ -1803,6 +2115,14 @@ static void Initialize(Local<Object> target,
18032115
StatementSync::GetConstructorTemplate(env));
18042116

18052117
target->Set(context, env->constants_string(), constants).Check();
2118+
2119+
Local<Function> backup_function;
2120+
2121+
if (!Function::New(context, Backup).ToLocal(&backup_function)) {
2122+
return;
2123+
}
2124+
2125+
target->Set(context, env->backup_string(), backup_function).Check();
18062126
}
18072127

18082128
} // namespace sqlite

‎src/node_sqlite.h

+5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class DatabaseOpenConfiguration {
4343
};
4444

4545
class StatementSync;
46+
class BackupJob;
4647

4748
class DatabaseSync : public BaseObject {
4849
public:
@@ -64,6 +65,9 @@ class DatabaseSync : public BaseObject {
6465
const v8::FunctionCallbackInfo<v8::Value>& args);
6566
static void LoadExtension(const v8::FunctionCallbackInfo<v8::Value>& args);
6667
void FinalizeStatements();
68+
void RemoveBackup(BackupJob* backup);
69+
void AddBackup(BackupJob* backup);
70+
void FinalizeBackups();
6771
void UntrackStatement(StatementSync* statement);
6872
bool IsOpen();
6973
sqlite3* Connection();
@@ -89,6 +93,7 @@ class DatabaseSync : public BaseObject {
8993
sqlite3* connection_;
9094
bool ignore_next_sqlite_error_;
9195

96+
std::set<BackupJob*> backups_;
9297
std::set<sqlite3_session*> sessions_;
9398
std::unordered_set<StatementSync*> statements_;
9499

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

+236
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import '../common/index.mjs';
2+
import tmpdir from '../common/tmpdir.js';
3+
import { join } from 'node:path';
4+
import { backup, DatabaseSync } from 'node:sqlite';
5+
import { describe, test } from 'node:test';
6+
import { writeFileSync } from 'node:fs';
7+
8+
let cnt = 0;
9+
10+
tmpdir.refresh();
11+
12+
function nextDb() {
13+
return join(tmpdir.path, `database-${cnt++}.db`);
14+
}
15+
16+
function makeSourceDb() {
17+
const database = new DatabaseSync(':memory:');
18+
19+
database.exec(`
20+
CREATE TABLE data(
21+
key INTEGER PRIMARY KEY,
22+
value TEXT
23+
) STRICT
24+
`);
25+
26+
const insert = database.prepare('INSERT INTO data (key, value) VALUES (?, ?)');
27+
28+
for (let i = 1; i <= 2; i++) {
29+
insert.run(i, `value-${i}`);
30+
}
31+
32+
return database;
33+
}
34+
35+
describe('backup()', () => {
36+
test('throws if the source database is not provided', (t) => {
37+
t.assert.throws(() => {
38+
backup();
39+
}, {
40+
code: 'ERR_INVALID_ARG_TYPE',
41+
message: 'The "sourceDb" argument must be an object.'
42+
});
43+
});
44+
45+
test('throws if path is not a string', (t) => {
46+
const database = makeSourceDb();
47+
48+
t.assert.throws(() => {
49+
backup(database);
50+
}, {
51+
code: 'ERR_INVALID_ARG_TYPE',
52+
message: 'The "destination" argument must be a string.'
53+
});
54+
55+
t.assert.throws(() => {
56+
backup(database, {});
57+
}, {
58+
code: 'ERR_INVALID_ARG_TYPE',
59+
message: 'The "destination" argument must be a string.'
60+
});
61+
});
62+
63+
test('throws if options is not an object', (t) => {
64+
const database = makeSourceDb();
65+
66+
t.assert.throws(() => {
67+
backup(database, 'hello.db', 'invalid');
68+
}, {
69+
code: 'ERR_INVALID_ARG_TYPE',
70+
message: 'The "options" argument must be an object.'
71+
});
72+
});
73+
74+
test('throws if any of provided options is invalid', (t) => {
75+
const database = makeSourceDb();
76+
77+
t.assert.throws(() => {
78+
backup(database, 'hello.db', {
79+
source: 42
80+
});
81+
}, {
82+
code: 'ERR_INVALID_ARG_TYPE',
83+
message: 'The "options.source" argument must be a string.'
84+
});
85+
86+
t.assert.throws(() => {
87+
backup(database, 'hello.db', {
88+
target: 42
89+
});
90+
}, {
91+
code: 'ERR_INVALID_ARG_TYPE',
92+
message: 'The "options.target" argument must be a string.'
93+
});
94+
95+
t.assert.throws(() => {
96+
backup(database, 'hello.db', {
97+
rate: 'invalid'
98+
});
99+
}, {
100+
code: 'ERR_INVALID_ARG_TYPE',
101+
message: 'The "options.rate" argument must be an integer.'
102+
});
103+
104+
t.assert.throws(() => {
105+
backup(database, 'hello.db', {
106+
progress: 'invalid'
107+
});
108+
}, {
109+
code: 'ERR_INVALID_ARG_TYPE',
110+
message: 'The "options.progress" argument must be a function.'
111+
});
112+
});
113+
});
114+
115+
test('database backup', async (t) => {
116+
const progressFn = t.mock.fn();
117+
const database = makeSourceDb();
118+
const destDb = nextDb();
119+
120+
await backup(database, destDb, {
121+
rate: 1,
122+
progress: progressFn,
123+
});
124+
125+
const backupDb = new DatabaseSync(destDb);
126+
const rows = backupDb.prepare('SELECT * FROM data').all();
127+
128+
// The source database has two pages - using the default page size -,
129+
// so the progress function should be called once (the last call is not made since
130+
// the promise resolves)
131+
t.assert.strictEqual(progressFn.mock.calls.length, 1);
132+
t.assert.deepStrictEqual(progressFn.mock.calls[0].arguments, [{ totalPages: 2, remainingPages: 1 }]);
133+
t.assert.deepStrictEqual(rows, [
134+
{ __proto__: null, key: 1, value: 'value-1' },
135+
{ __proto__: null, key: 2, value: 'value-2' },
136+
]);
137+
138+
t.after(() => {
139+
database.close();
140+
backupDb.close();
141+
});
142+
});
143+
144+
test('database backup in a single call', async (t) => {
145+
const progressFn = t.mock.fn();
146+
const database = makeSourceDb();
147+
const destDb = nextDb();
148+
149+
// Let rate to be default (100) to backup in a single call
150+
await backup(database, destDb, {
151+
progress: progressFn,
152+
});
153+
154+
const backupDb = new DatabaseSync(destDb);
155+
const rows = backupDb.prepare('SELECT * FROM data').all();
156+
157+
t.assert.strictEqual(progressFn.mock.calls.length, 0);
158+
t.assert.deepStrictEqual(rows, [
159+
{ __proto__: null, key: 1, value: 'value-1' },
160+
{ __proto__: null, key: 2, value: 'value-2' },
161+
]);
162+
163+
t.after(() => {
164+
database.close();
165+
backupDb.close();
166+
});
167+
});
168+
169+
test('throws exception when trying to start backup from a closed database', (t) => {
170+
t.assert.throws(() => {
171+
const database = new DatabaseSync(':memory:');
172+
173+
database.close();
174+
175+
backup(database, 'backup.db');
176+
}, {
177+
code: 'ERR_INVALID_STATE',
178+
message: 'database is not open'
179+
});
180+
});
181+
182+
test('database backup fails when dest file is not writable', async (t) => {
183+
const readonlyDestDb = nextDb();
184+
writeFileSync(readonlyDestDb, '', { mode: 0o444 });
185+
186+
const database = makeSourceDb();
187+
188+
await t.assert.rejects(async () => {
189+
await backup(database, readonlyDestDb);
190+
}, {
191+
code: 'ERR_SQLITE_ERROR',
192+
message: 'attempt to write a readonly database'
193+
});
194+
});
195+
196+
test('backup fails when progress function throws', async (t) => {
197+
const database = makeSourceDb();
198+
const destDb = nextDb();
199+
200+
const progressFn = t.mock.fn(() => {
201+
throw new Error('progress error');
202+
});
203+
204+
await t.assert.rejects(async () => {
205+
await backup(database, destDb, {
206+
rate: 1,
207+
progress: progressFn,
208+
});
209+
}, {
210+
message: 'progress error'
211+
});
212+
});
213+
214+
test('backup fails when source db is invalid', async (t) => {
215+
const database = makeSourceDb();
216+
const destDb = nextDb();
217+
218+
await t.assert.rejects(async () => {
219+
await backup(database, destDb, {
220+
rate: 1,
221+
source: 'invalid',
222+
});
223+
}, {
224+
message: 'unknown database invalid'
225+
});
226+
});
227+
228+
test('backup fails when destination cannot be opened', async (t) => {
229+
const database = makeSourceDb();
230+
231+
await t.assert.rejects(async () => {
232+
await backup(database, `${tmpdir.path}/invalid/backup.db`);
233+
}, {
234+
message: 'unable to open database file'
235+
});
236+
});

‎tools/doc/type-parser.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ const customTypesMap = {
112112
'Channel': 'diagnostics_channel.html#class-channel',
113113
'TracingChannel': 'diagnostics_channel.html#class-tracingchannel',
114114

115+
'DatabaseSync': 'sqlite.html#class-databasesync',
115116
'Domain': 'domain.html#class-domain',
116117

117118
'errors.Error': 'errors.html#class-error',

0 commit comments

Comments
 (0)
Please sign in to comment.