Skip to content

Commit 8ca0956

Browse files
committed
feat(backup): implement standalone backup function with options and tests
1 parent 6aab576 commit 8ca0956

File tree

8 files changed

+459
-16
lines changed

8 files changed

+459
-16
lines changed

doc/api-reference.md

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,67 @@ Complete API documentation for @photostructure/sqlite. This package provides 100
44

55
## Table of Contents
66

7+
- [Module Exports](#module-exports)
78
- [DatabaseSync](#databasesync)
89
- [StatementSync](#statementsync)
910
- [Types and Interfaces](#types-and-interfaces)
1011
- [Constants](#constants)
1112
- [Error Handling](#error-handling)
1213

14+
## Module Exports
15+
16+
The module exports the following items that match `node:sqlite`:
17+
18+
```typescript
19+
import {
20+
DatabaseSync, // Main database class
21+
StatementSync, // Prepared statement class
22+
Session, // Session class for changesets
23+
backup, // Standalone backup function
24+
constants, // SQLite constants
25+
} from "@photostructure/sqlite";
26+
```
27+
28+
### backup()
29+
30+
```typescript
31+
backup(
32+
sourceDb: DatabaseSync,
33+
destination: string | Buffer | URL,
34+
options?: BackupOptions
35+
): Promise<number>
36+
```
37+
38+
Standalone function to create a backup of a database. This function is equivalent to calling `db.backup()` on a database instance, but allows passing the source database as a parameter.
39+
40+
**Parameters:**
41+
42+
- `sourceDb` - The database instance to back up
43+
- `destination` - Path to the backup file (string, Buffer, or file: URL)
44+
- `options` - Optional backup configuration
45+
46+
**Returns:** A Promise that resolves to the total number of pages backed up.
47+
48+
```javascript
49+
import { DatabaseSync, backup } from "@photostructure/sqlite";
50+
51+
const db = new DatabaseSync("source.db");
52+
53+
// Using standalone function
54+
await backup(db, "backup.db");
55+
56+
// Equivalent to:
57+
await db.backup("backup.db");
58+
59+
// With options
60+
await backup(db, "backup.db", {
61+
rate: 10,
62+
progress: ({ totalPages, remainingPages }) => {
63+
console.log(`${totalPages - remainingPages}/${totalPages} pages copied`);
64+
},
65+
});
66+
```
67+
1368
## DatabaseSync
1469

1570
The main database class for synchronous SQLite operations.
@@ -37,7 +92,7 @@ interface DatabaseSyncOptions {
3792
readOnly?: boolean; // Open in read-only mode (default: false)
3893
enableForeignKeyConstraints?: boolean; // Enable foreign keys (default: true)
3994
enableDoubleQuotedStringLiterals?: boolean; // Allow double-quoted strings (default: false)
40-
timeout?: number; // Busy timeout in ms (default: 5000)
95+
timeout?: number; // Busy timeout in ms (default: 0)
4196
allowExtension?: boolean; // Allow loading extensions (default: false)
4297
}
4398
```
@@ -222,6 +277,7 @@ Creates a backup of the database. Returns the total number of pages backed up.
222277
```typescript
223278
interface BackupOptions {
224279
source?: string; // Source database name (default: 'main')
280+
target?: string; // Target database name (default: 'main')
225281
rate?: number; // Pages per iteration (default: 100)
226282
progress?: (info: { totalPages: number; remainingPages: number }) => void;
227283
}

src/binding.cpp

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,54 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
200200

201201
exports.Set("constants", constants);
202202

203-
// TODO: Add backup function
203+
// Add standalone backup() function (Node.js API compatibility)
204+
// Signature: backup(sourceDb, destination, options?) -> Promise
205+
Napi::Function backupFunc = Napi::Function::New(
206+
env,
207+
[](const Napi::CallbackInfo &info) -> Napi::Value {
208+
Napi::Env env = info.Env();
209+
210+
// Validate and unwrap DatabaseSync instance
211+
DatabaseSync *db = nullptr;
212+
if (info.Length() >= 1 && info[0].IsObject()) {
213+
try {
214+
db = DatabaseSync::Unwrap(info[0].As<Napi::Object>());
215+
} catch (...) {
216+
// Fall through to error below
217+
}
218+
}
219+
if (db == nullptr) {
220+
Napi::TypeError::New(
221+
env, "The \"sourceDb\" argument must be a DatabaseSync object")
222+
.ThrowAsJavaScriptException();
223+
return env.Undefined();
224+
}
225+
226+
// Validate destination path is provided
227+
if (info.Length() < 2) {
228+
Napi::TypeError::New(env, "The \"destination\" argument is required")
229+
.ThrowAsJavaScriptException();
230+
return env.Undefined();
231+
}
232+
233+
// Delegate to instance method: db.backup(destination, options?)
234+
std::vector<napi_value> args;
235+
for (size_t i = 1; i < info.Length(); i++) {
236+
args.push_back(info[i]);
237+
}
238+
Napi::Function backupMethod =
239+
db->Value().Get("backup").As<Napi::Function>();
240+
return backupMethod.Call(db->Value(), args);
241+
},
242+
"backup");
243+
244+
// Set function name and length properties (Node.js compatibility)
245+
backupFunc.DefineProperty(Napi::PropertyDescriptor::Value(
246+
"name", Napi::String::New(env, "backup"), napi_enumerable));
247+
backupFunc.DefineProperty(Napi::PropertyDescriptor::Value(
248+
"length", Napi::Number::New(env, 2), napi_enumerable));
249+
250+
exports.Set("backup", backupFunc);
204251

205252
return exports;
206253
}

src/index.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,5 +204,52 @@ export { SQLTagStore };
204204
*/
205205
export const constants: SqliteConstants = binding.constants;
206206

207+
/**
208+
* Options for the backup() function.
209+
*/
210+
export interface BackupOptions {
211+
/** Number of pages to be transmitted in each batch of the backup. @default 100 */
212+
rate?: number;
213+
/** Name of the source database. Can be 'main' or any attached database. @default 'main' */
214+
source?: string;
215+
/** Name of the target database. Can be 'main' or any attached database. @default 'main' */
216+
target?: string;
217+
/** Callback function that will be called with progress information. */
218+
progress?: (info: { totalPages: number; remainingPages: number }) => void;
219+
}
220+
221+
/**
222+
* Standalone function to make a backup of a database.
223+
*
224+
* This function matches the Node.js `node:sqlite` module API which exports
225+
* `backup()` as a standalone function in addition to the `db.backup()` method.
226+
*
227+
* @param sourceDb The database to backup from.
228+
* @param destination The path where the backup will be created.
229+
* @param options Optional configuration for the backup operation.
230+
* @returns A promise that resolves when the backup is completed.
231+
*
232+
* @example
233+
* ```typescript
234+
* import { DatabaseSync, backup } from '@photostructure/sqlite';
235+
*
236+
* const db = new DatabaseSync('./source.db');
237+
* await backup(db, './backup.db');
238+
*
239+
* // With options
240+
* await backup(db, './backup.db', {
241+
* rate: 10,
242+
* progress: ({ totalPages, remainingPages }) => {
243+
* console.log(`Progress: ${totalPages - remainingPages}/${totalPages}`);
244+
* }
245+
* });
246+
* ```
247+
*/
248+
export const backup: (
249+
sourceDb: DatabaseSyncInstance,
250+
destination: string | Buffer | URL,
251+
options?: BackupOptions,
252+
) => Promise<number> = binding.backup;
253+
207254
// Default export for CommonJS compatibility
208255
export default binding as SqliteModule;

src/sqlite_impl.cpp

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2563,17 +2563,17 @@ std::set<BackupJob *> BackupJob::active_job_instances_;
25632563

25642564
// BackupJob Implementation
25652565
BackupJob::BackupJob(Napi::Env env, DatabaseSync *source,
2566-
const std::string &destination_path,
2567-
const std::string &source_db, const std::string &dest_db,
2568-
int pages, Napi::Function progress_func,
2566+
std::string destination_path, std::string source_db,
2567+
std::string dest_db, int pages,
2568+
Napi::Function progress_func,
25692569
Napi::Promise::Deferred deferred)
25702570
: Napi::AsyncProgressWorker<BackupProgress>(
25712571
!progress_func.IsEmpty() && !progress_func.IsUndefined()
25722572
? progress_func
25732573
: Napi::Function::New(env, [](const Napi::CallbackInfo &) {})),
2574-
source_(source), destination_path_(destination_path),
2575-
source_db_(source_db), dest_db_(dest_db), pages_(pages),
2576-
deferred_(deferred) {
2574+
source_(source), destination_path_(std::move(destination_path)),
2575+
source_db_(std::move(source_db)), dest_db_(std::move(dest_db)),
2576+
pages_(pages), deferred_(deferred) {
25772577
if (!progress_func.IsEmpty() && !progress_func.IsUndefined()) {
25782578
progress_func_ = Napi::Reference<Napi::Function>::New(progress_func);
25792579
}
@@ -2822,8 +2822,9 @@ Napi::Value DatabaseSync::Backup(const Napi::CallbackInfo &info) {
28222822
}
28232823

28242824
// Create and schedule backup job
2825-
BackupJob *job = new BackupJob(env, this, destination_path.value(), source_db,
2826-
target_db, rate, progress_func, deferred);
2825+
BackupJob *job = new BackupJob(env, this, std::move(destination_path).value(),
2826+
std::move(source_db), std::move(target_db),
2827+
rate, progress_func, deferred);
28272828

28282829
// Queue the async work - AsyncWorker will delete itself when complete
28292830
job->Queue();

src/sqlite_impl.h

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,10 +351,9 @@ struct BackupProgress {
351351
// Backup job for asynchronous database backup
352352
class BackupJob : public Napi::AsyncProgressWorker<BackupProgress> {
353353
public:
354-
BackupJob(Napi::Env env, DatabaseSync *source,
355-
const std::string &destination_path, const std::string &source_db,
356-
const std::string &dest_db, int pages, Napi::Function progress_func,
357-
Napi::Promise::Deferred deferred);
354+
BackupJob(Napi::Env env, DatabaseSync *source, std::string destination_path,
355+
std::string source_db, std::string dest_db, int pages,
356+
Napi::Function progress_func, Napi::Promise::Deferred deferred);
358357
~BackupJob();
359358

360359
void Execute(const ExecutionProgress &progress) override;

src/types/database-sync-options.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export interface DatabaseSyncOptions {
2424
readonly enableDoubleQuotedStringLiterals?: boolean;
2525
/**
2626
* Sets the busy timeout in milliseconds.
27-
* @default 5000
27+
* @default 0
2828
*/
2929
readonly timeout?: number;
3030
/** If true, enables loading of SQLite extensions. @default false */

test/api-compatibility.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ const _hasDatabaseSync: typeof OurSqlite.DatabaseSync = OurSqlite.DatabaseSync;
4949
const _hasStatementSync: typeof OurSqlite.StatementSync =
5050
OurSqlite.StatementSync;
5151

52+
// Check that standalone backup function is exported
53+
const _hasBackup: typeof OurSqlite.backup = OurSqlite.backup;
54+
5255
// Check that our interfaces correspond to node:sqlite interfaces
5356
// Note: We use different names but should have compatible structure
5457

@@ -309,6 +312,27 @@ function _checkSessionClass() {
309312
}
310313
}
311314

315+
// Check standalone backup function signature
316+
function _checkBackupFunction() {
317+
// The standalone backup function should match node:sqlite's export
318+
const _backup: (
319+
sourceDb: InstanceType<typeof OurSqlite.DatabaseSync>,
320+
destination: string | Buffer | URL,
321+
options?: OurSqlite.BackupOptions,
322+
) => Promise<number> = OurSqlite.backup;
323+
324+
// Verify our BackupOptions type has the right shape
325+
const _opts: OurSqlite.BackupOptions = {
326+
rate: 100,
327+
source: "main",
328+
target: "main",
329+
progress: ({ totalPages, remainingPages }) => {
330+
const _t: number = totalPages;
331+
const _r: number = remainingPages;
332+
},
333+
};
334+
}
335+
312336
// Check constructor signatures
313337
function _checkConstructorSignatures() {
314338
// DatabaseSync constructors
@@ -399,6 +423,27 @@ describe("API Compatibility", () => {
399423
expect(Object.keys(OurSqlite.constants).length).toBe(65);
400424
});
401425
});
426+
427+
describe("standalone backup() function compatibility", () => {
428+
it("exports backup as a function", () => {
429+
expect(typeof OurSqlite.backup).toBe("function");
430+
});
431+
432+
it("backup has correct name property", () => {
433+
expect(OurSqlite.backup.name).toBe("backup");
434+
});
435+
436+
it("backup has correct length property (2 parameters)", () => {
437+
expect(OurSqlite.backup.length).toBe(2);
438+
});
439+
440+
it("backup matches node:sqlite export", () => {
441+
// Verify our backup function matches node:sqlite's backup
442+
expect(typeof NodeSqlite.backup).toBe("function");
443+
expect(OurSqlite.backup.name).toBe(NodeSqlite.backup.name);
444+
expect(OurSqlite.backup.length).toBe(NodeSqlite.backup.length);
445+
});
446+
});
402447
});
403448

404449
export {}; // Make this a module

0 commit comments

Comments
 (0)