Skip to content

Commit 4f1c22d

Browse files
authored
fix: preserve database state when exporting (#644)
Use SQLite's backup API to fill a single preallocated JS-owned export buffer without closing the source connection or allocating a database-sized WASM snapshot. Preserve callbacks, statements, connection settings and transaction state; reject active write transactions without rolling them back. Add export regression coverage and prepare version 1.15.2. Supersedes #642 and #643. Validated by full CI on all WebAssembly variants.
1 parent b586e36 commit 4f1c22d

6 files changed

Lines changed: 354 additions & 18 deletions

File tree

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sql.js",
3-
"version": "1.15.1",
3+
"version": "1.15.2",
44
"description": "SQLite library with support for opening and writing databases, prepared statements, and more. Compiled to WebAssembly with Emscripten, with JavaScript bindings.",
55
"keywords": [
66
"sql",

‎src/api.js‎

Lines changed: 110 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() {
6060
var NULL = 0;
6161
// SQLite enum
6262
var SQLITE_OK = 0;
63+
var SQLITE_BUSY = 5;
64+
var SQLITE_LOCKED = 6;
6365
var SQLITE_ROW = 100;
6466
var SQLITE_DONE = 101;
6567
var SQLITE_INTEGER = 1;
@@ -75,6 +77,17 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() {
7577
// var - cwrap function
7678
var sqlite3_open = cwrap("sqlite3_open", "number", ["string", "number"]);
7779
var sqlite3_close_v2 = cwrap("sqlite3_close_v2", "number", ["number"]);
80+
var sqlite3_backup_init = cwrap(
81+
"sqlite3_backup_init",
82+
"number",
83+
["number", "string", "number", "string"]
84+
);
85+
var sqlite3_backup_step = cwrap(
86+
"sqlite3_backup_step", "number", ["number", "number"]
87+
);
88+
var sqlite3_backup_finish = cwrap(
89+
"sqlite3_backup_finish", "number", ["number"]
90+
);
7891
var sqlite3_exec = cwrap(
7992
"sqlite3_exec",
8093
"number",
@@ -1103,23 +1116,106 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() {
11031116
return new StatementIterator(sql, this);
11041117
};
11051118

1106-
/** Exports the contents of the database to a binary array. This
1107-
* operation will close and re-open the database which will cause
1108-
* any pragmas to be set back to their default values.
1119+
/** Exports a snapshot of the main database to a binary array without
1120+
* closing the connection. Prepared statements, registered functions,
1121+
* update hooks and connection settings (including pragmas) are preserved.
1122+
* TEMP objects and attached databases remain available on this connection,
1123+
* but are not included in the exported file.
1124+
*
1125+
* Exporting during an active write transaction throws an error. Commit or
1126+
* roll back the transaction before exporting. Read transactions are
1127+
* allowed. Exporting never commits or rolls back a transaction or changes
1128+
* savepoints, even when the export fails.
1129+
*
1130+
* The exported file uses rollback-journal format, even if this connection
1131+
* uses WAL mode.
1132+
*
1133+
* The returned array is an independent copy. It can be used to create a
1134+
* new Database, but JavaScript functions, hooks and connection settings
1135+
* are not stored in the file and must be configured on the new connection.
11091136
@return {Uint8Array} An array of bytes of the SQLite3 database file
11101137
*/
11111138
Database.prototype.export = function exportDatabase() {
1112-
Object.values(this.statements).forEach(function each(stmt) {
1113-
stmt.free();
1114-
});
1115-
this.functions.forEach(removeFunctionPointers);
1116-
this.functions.clear();
1117-
this.handleError(sqlite3_close_v2(this.db));
1118-
var binaryDb = FS.readFile(this.filename, { encoding: "binary" });
1119-
this.handleError(sqlite3_open(this.filename, apiTemp));
1120-
this.db = getValue(apiTemp, "i32");
1121-
registerExtensionFunctions(this.db);
1122-
return binaryDb;
1139+
if (!this.db) {
1140+
throw new Error("Database closed");
1141+
}
1142+
var filename = this.filename + "_export";
1143+
FS.createDataFile("/", filename, [], true, true);
1144+
var destination = NULL;
1145+
var backup = NULL;
1146+
try {
1147+
var file = FS.lookupPath(filename).node;
1148+
var binaryDb = null;
1149+
var write = file.stream_ops.write;
1150+
file.stream_ops = Object.create(file.stream_ops);
1151+
file.stream_ops.write = function writeExport(
1152+
stream, buffer, offset, length, position
1153+
) {
1154+
if (binaryDb === null) {
1155+
return write(stream, buffer, offset, length, position);
1156+
}
1157+
// Write directly into the final JS-owned result buffer.
1158+
// MEMFS would otherwise replace it on the first write.
1159+
file.contents.set(buffer.subarray(offset, offset + length),
1160+
position);
1161+
file.usedBytes = Math.max(file.usedBytes, position + length);
1162+
return length;
1163+
};
1164+
var rc = sqlite3_open(filename, apiTemp);
1165+
destination = getValue(apiTemp, "i32");
1166+
if (rc !== SQLITE_OK) {
1167+
throw new Error(sqlite3_errmsg(destination));
1168+
}
1169+
backup = sqlite3_backup_init(destination, "main", this.db, "main");
1170+
if (backup === NULL) {
1171+
throw new Error(sqlite3_errmsg(destination));
1172+
}
1173+
// Initialize the backup and reject writers before allocating.
1174+
rc = sqlite3_backup_step(backup, 0);
1175+
if (rc === SQLITE_OK) {
1176+
var sizes = this.exec(
1177+
"PRAGMA main.page_count; PRAGMA main.page_size"
1178+
);
1179+
binaryDb = new Uint8Array(
1180+
sizes[0].values[0][0] * sizes[1].values[0][0]
1181+
);
1182+
file.contents = binaryDb;
1183+
rc = sqlite3_backup_step(backup, -1);
1184+
}
1185+
// Do not retry: this synchronous call cannot let a writer progress.
1186+
if (rc === SQLITE_BUSY || rc === SQLITE_LOCKED) {
1187+
throw new Error("Unable to export: SQLite is busy or locked. "
1188+
+ "Finish active write transactions before exporting.");
1189+
}
1190+
if (rc !== SQLITE_DONE) {
1191+
throw new Error(sqlite3_errmsg(destination));
1192+
}
1193+
rc = sqlite3_backup_finish(backup);
1194+
backup = NULL;
1195+
if (rc !== SQLITE_OK) {
1196+
throw new Error(sqlite3_errmsg(destination));
1197+
}
1198+
rc = sqlite3_close_v2(destination);
1199+
if (rc !== SQLITE_OK) {
1200+
throw new Error(sqlite3_errmsg(destination));
1201+
}
1202+
destination = NULL;
1203+
// The private file is closed and unlinked below; hand its buffer
1204+
// to the caller rather than making a full-size FS.readFile copy.
1205+
binaryDb = new Uint8Array(file.contents.buffer,
1206+
file.contents.byteOffset, file.usedBytes);
1207+
if (binaryDb.length > 0) {
1208+
// All pages, including WAL pages, are in the backup.
1209+
// Make the exported copy independent of WAL support.
1210+
binaryDb[18] = 1;
1211+
binaryDb[19] = 1;
1212+
}
1213+
return binaryDb;
1214+
} finally {
1215+
if (backup !== NULL) sqlite3_backup_finish(backup);
1216+
if (destination !== NULL) sqlite3_close_v2(destination);
1217+
FS.unlink("/" + filename);
1218+
}
11231219
};
11241220

11251221
/** Close the database, and all associated prepared statements.

‎src/exported_functions.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
"_malloc",
33
"_free",
44
"_sqlite3_open",
5+
"_sqlite3_backup_init",
6+
"_sqlite3_backup_step",
7+
"_sqlite3_backup_finish",
58
"_sqlite3_exec",
69
"_sqlite3_free",
710
"_sqlite3_errmsg",

0 commit comments

Comments
 (0)