Skip to content

Commit 1af36d8

Browse files
committed
Support upstream better-sqlite3
1 parent cf0a3b1 commit 1af36d8

14 files changed

+176
-83
lines changed

packages/common/rollup.config.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ export default (commandLineArgs) => {
3838
ReadableStream: ['web-streams-polyfill/ponyfill', 'ReadableStream'],
3939
// Used by can-ndjson-stream
4040
TextDecoder: ['text-encoding', 'TextDecoder']
41-
}),
42-
terser({ sourceMap })
41+
})
42+
//terser({ sourceMap })
4343
],
4444
// This makes life easier
4545
external: [

packages/node/package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,15 @@
5656
},
5757
"homepage": "https://docs.powersync.com/",
5858
"peerDependencies": {
59-
"@powersync/common": "workspace:^1.40.0"
59+
"@powersync/common": "workspace:^1.40.0",
60+
"better-sqlite3": "12.x"
61+
},
62+
"peerDependenciesMeta": {
63+
"better-sqlite3": {
64+
"optional": true
65+
}
6066
},
6167
"dependencies": {
62-
"@powersync/better-sqlite3": "^0.2.0",
6368
"@powersync/common": "workspace:*",
6469
"async-lock": "^1.4.0",
6570
"bson": "^6.6.0",
@@ -70,6 +75,7 @@
7075
"@powersync/drizzle-driver": "workspace:*",
7176
"@types/async-lock": "^1.4.0",
7277
"@types/node": "^24.2.0",
78+
"better-sqlite3": "^12.2.0",
7379
"drizzle-orm": "^0.35.2",
7480
"rollup": "4.14.3",
7581
"typescript": "^5.5.3",

packages/node/rollup.config.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ import dts from 'rollup-plugin-dts';
33
const plugin = () => {
44
return {
55
name: 'mark-as-commonjs',
6+
async resolveId(source, importer, options) {
7+
if (importer && source.indexOf('modules.js')) {
8+
return await this.resolve(source.replace('modules.js', 'modules_commonjs.js'), importer, options);
9+
} else {
10+
return await this.resolve(source, importer, options);
11+
}
12+
},
613
resolveImportMeta: (property) => {
714
if (property == 'isBundlingToCommonJs') {
815
return 'true';

packages/node/src/db/AsyncDatabase.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,4 @@ export interface AsyncDatabase {
2323
executeRaw: (query: string, params: any[]) => Promise<any[][]>;
2424
executeBatch: (query: string, params: any[][]) => Promise<ProxiedQueryResult>;
2525
close: () => Promise<void>;
26-
// Collect table updates made since the last call to collectCommittedUpdates.
27-
// This happens on the worker because we otherwise get race conditions when wrapping
28-
// callbacks to invoke on the main thread (we need a guarantee that collectCommittedUpdates
29-
// contains entries immediately after calling COMMIT).
30-
collectCommittedUpdates: () => Promise<string[]>;
3126
}

packages/node/src/db/BetterSqliteWorker.ts

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,18 @@
1-
import * as Comlink from 'comlink';
2-
import BetterSQLite3Database, { Database } from '@powersync/better-sqlite3';
3-
import { AsyncDatabase, AsyncDatabaseOpener, AsyncDatabaseOpenOptions } from './AsyncDatabase.js';
1+
import type { Database } from 'better-sqlite3';
2+
import { AsyncDatabase, AsyncDatabaseOpenOptions } from './AsyncDatabase.js';
43
import { PowerSyncWorkerOptions } from './SqliteWorker.js';
54
import { threadId } from 'node:worker_threads';
5+
import { dynamicImport } from '../utils/modules.js';
66

77
class BlockingAsyncDatabase implements AsyncDatabase {
88
private readonly db: Database;
99

10-
private readonly uncommittedUpdatedTables = new Set<string>();
11-
private readonly committedUpdatedTables = new Set<string>();
12-
1310
constructor(db: Database) {
1411
this.db = db;
1512

1613
db.function('node_thread_id', () => threadId);
1714
}
1815

19-
collectCommittedUpdates() {
20-
const resolved = Promise.resolve([...this.committedUpdatedTables]);
21-
this.committedUpdatedTables.clear();
22-
return resolved;
23-
}
24-
25-
installUpdateHooks() {
26-
this.db.updateHook((_op: string, _dbName: string, tableName: string, _rowid: bigint) => {
27-
this.uncommittedUpdatedTables.add(tableName);
28-
});
29-
30-
this.db.commitHook(() => {
31-
for (const tableName of this.uncommittedUpdatedTables) {
32-
this.committedUpdatedTables.add(tableName);
33-
}
34-
this.uncommittedUpdatedTables.clear();
35-
return true;
36-
});
37-
38-
this.db.rollbackHook(() => {
39-
this.uncommittedUpdatedTables.clear();
40-
});
41-
}
42-
4316
async close() {
4417
this.db.close();
4518
}
@@ -90,7 +63,8 @@ class BlockingAsyncDatabase implements AsyncDatabase {
9063
}
9164
}
9265

93-
export async function openDatabase(worker: PowerSyncWorkerOptions, options: AsyncDatabaseOpenOptions) {
66+
export async function openDatabase(worker: PowerSyncWorkerOptions, options: AsyncDatabaseOpenOptions, pkg: string) {
67+
const BetterSQLite3Database: typeof Database = (await dynamicImport(pkg)).default;
9468
const baseDB = new BetterSQLite3Database(options.path);
9569
baseDB.pragma('journal_mode = WAL');
9670
baseDB.loadExtension(worker.extensionPath(), 'sqlite3_powersync_init');
@@ -99,6 +73,5 @@ export async function openDatabase(worker: PowerSyncWorkerOptions, options: Asyn
9973
}
10074

10175
const asyncDb = new BlockingAsyncDatabase(baseDB);
102-
asyncDb.installUpdateHooks();
10376
return asyncDb;
10477
}

packages/node/src/db/NodeSqliteWorker.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { threadId } from 'node:worker_threads';
22
import type { DatabaseSync } from 'node:sqlite';
33

4-
import * as Comlink from 'comlink';
5-
import { AsyncDatabase, AsyncDatabaseOpener, AsyncDatabaseOpenOptions } from './AsyncDatabase.js';
4+
import { AsyncDatabase, AsyncDatabaseOpenOptions } from './AsyncDatabase.js';
65
import { PowerSyncWorkerOptions } from './SqliteWorker.js';
6+
import { dynamicImport } from '../utils/modules.js';
77

88
class BlockingNodeDatabase implements AsyncDatabase {
99
private readonly db: DatabaseSync;
@@ -12,16 +12,6 @@ class BlockingNodeDatabase implements AsyncDatabase {
1212
this.db = db;
1313

1414
db.function('node_thread_id', () => threadId);
15-
if (write) {
16-
db.exec("SELECT powersync_update_hooks('install');");
17-
}
18-
}
19-
20-
async collectCommittedUpdates() {
21-
const stmt = this.db.prepare("SELECT powersync_update_hooks('get') AS r;");
22-
const row = stmt.get()!;
23-
24-
return JSON.parse(row['r'] as string) as string[];
2515
}
2616

2717
async close() {
@@ -64,7 +54,7 @@ class BlockingNodeDatabase implements AsyncDatabase {
6454
export async function openDatabase(worker: PowerSyncWorkerOptions, options: AsyncDatabaseOpenOptions) {
6555
// NOTE: We want to import node:sqlite dynamically, to avoid bundlers unconditionally requiring node:sqlite in the
6656
// end, since that would make us incompatible with older Node.JS versions.
67-
const { DatabaseSync } = await import('node:sqlite');
57+
const { DatabaseSync } = await dynamicImport('node:sqlite');
6858

6959
const baseDB = new DatabaseSync(options.path, { allowExtension: true });
7060
baseDB.exec('pragma journal_mode = WAL');

packages/node/src/db/PowerSyncDatabase.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
import { NodeCustomConnectionOptions, NodeRemote } from '../sync/stream/NodeRemote.js';
1818
import { NodeStreamingSyncImplementation } from '../sync/stream/NodeStreamingSyncImplementation.js';
1919

20-
import { BetterSQLite3DBAdapter } from './BetterSQLite3DBAdapter.js';
20+
import { WorkerConnectionPool } from './WorkerConnectionPool.js';
2121
import { NodeSQLOpenOptions } from './options.js';
2222

2323
export type NodePowerSyncDatabaseOptions = PowerSyncDatabaseOptions & {
@@ -54,14 +54,14 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
5454
}
5555

5656
async _initialize(): Promise<void> {
57-
await (this.database as BetterSQLite3DBAdapter).initialize();
57+
await (this.database as WorkerConnectionPool).initialize();
5858
}
5959

6060
/**
6161
* Opens a DBAdapter using better-sqlite3 as the default SQLite open factory.
6262
*/
6363
protected openDBAdapter(options: PowerSyncDatabaseOptionsWithSettings): DBAdapter {
64-
return new BetterSQLite3DBAdapter(options.database);
64+
return new WorkerConnectionPool(options.database);
6565
}
6666

6767
protected generateBucketStorageAdapter(): BucketStorageAdapter {

packages/node/src/db/SqliteWorker.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import url from 'node:url';
66
import { openDatabase as openBetterSqliteDatabase } from './BetterSqliteWorker.js';
77
import { openDatabase as openNodeDatabase } from './NodeSqliteWorker.js';
88
import { AsyncDatabase, AsyncDatabaseOpener, AsyncDatabaseOpenOptions } from './AsyncDatabase.js';
9+
import { isBundledToCommonJs } from '../utils/modules.js';
910

1011
export interface PowerSyncWorkerOptions {
1112
/**
@@ -19,7 +20,7 @@ export interface PowerSyncWorkerOptions {
1920
export function startPowerSyncWorker(options?: Partial<PowerSyncWorkerOptions>) {
2021
const resolvedOptions: PowerSyncWorkerOptions = {
2122
extensionPath() {
22-
const isCommonJsModule = import.meta.isBundlingToCommonJs ?? false;
23+
const isCommonJsModule = isBundledToCommonJs;
2324

2425
const platform = OS.platform();
2526
let extensionPath: string;
@@ -58,11 +59,12 @@ class DatabaseOpenHelper implements AsyncDatabaseOpener {
5859
async open(options: AsyncDatabaseOpenOptions): Promise<AsyncDatabase> {
5960
let database: AsyncDatabase;
6061

61-
switch (options.implementation) {
62+
const implementation = options.implementation;
63+
switch (implementation.type) {
6264
case 'better-sqlite3':
63-
database = await openBetterSqliteDatabase(this.options, options);
65+
database = await openBetterSqliteDatabase(this.options, options, implementation.package ?? 'better-sqlite3');
6466
break;
65-
case 'node':
67+
case 'node:sqlite':
6668
database = await openNodeDatabase(this.options, options);
6769
break;
6870
default:

packages/node/src/db/BetterSQLite3DBAdapter.ts renamed to packages/node/src/db/WorkerConnectionPool.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import { Remote } from 'comlink';
1717
import { AsyncResource } from 'node:async_hooks';
1818
import { AsyncDatabase, AsyncDatabaseOpener } from './AsyncDatabase.js';
1919
import { RemoteConnection } from './RemoteConnection.js';
20-
import { NodeSQLOpenOptions } from './options.js';
20+
import { NodeDatabaseImplementation, NodeSQLOpenOptions } from './options.js';
21+
import { isBundledToCommonJs } from '../utils/modules.js';
2122

2223
export type BetterSQLite3LockContext = LockContext & {
2324
executeBatch(query: string, params?: any[][]): Promise<QueryResult>;
@@ -27,10 +28,15 @@ export type BetterSQLite3Transaction = Transaction & BetterSQLite3LockContext;
2728

2829
const READ_CONNECTIONS = 5;
2930

31+
const defaultDatabaseImplementation: NodeDatabaseImplementation = {
32+
type: 'better-sqlite3',
33+
package: 'better-sqlite3'
34+
};
35+
3036
/**
3137
* Adapter for better-sqlite3
3238
*/
33-
export class BetterSQLite3DBAdapter extends BaseObserver<DBAdapterListener> implements DBAdapter {
39+
export class WorkerConnectionPool extends BaseObserver<DBAdapterListener> implements DBAdapter {
3440
private readonly options: NodeSQLOpenOptions;
3541
public readonly name: string;
3642

@@ -73,7 +79,7 @@ export class BetterSQLite3DBAdapter extends BaseObserver<DBAdapterListener> impl
7379
}
7480

7581
const openWorker = async (isWriter: boolean) => {
76-
const isCommonJsModule = import.meta.isBundlingToCommonJs ?? false;
82+
const isCommonJsModule = isBundledToCommonJs;
7783
let worker: Worker;
7884
const workerName = isWriter ? `write ${dbFilePath}` : `read ${dbFilePath}`;
7985

@@ -120,8 +126,12 @@ export class BetterSQLite3DBAdapter extends BaseObserver<DBAdapterListener> impl
120126
const database = (await comlink.open({
121127
path: dbFilePath,
122128
isWriter,
123-
implementation: this.options.implementation ?? 'better-sqlite3'
129+
implementation: this.options.implementation ?? defaultDatabaseImplementation
124130
})) as Remote<AsyncDatabase>;
131+
if (isWriter) {
132+
await database.execute("SELECT powersync_update_hooks('install');", []);
133+
}
134+
125135
return new RemoteConnection(worker, comlink, database);
126136
};
127137

@@ -192,7 +202,11 @@ export class BetterSQLite3DBAdapter extends BaseObserver<DBAdapterListener> impl
192202
try {
193203
return await fn(this.writeConnection);
194204
} finally {
195-
const updates = await this.writeConnection.database.collectCommittedUpdates();
205+
const serializedUpdates = await this.writeConnection.database.executeRaw(
206+
"SELECT powersync_update_hooks('get');",
207+
[]
208+
);
209+
const updates = JSON.parse(serializedUpdates[0][0] as string) as string[];
196210

197211
if (updates.length > 0) {
198212
const event: BatchedUpdateNotification = {

packages/node/src/db/options.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,30 @@ import { SQLOpenOptions } from '@powersync/common';
33

44
export type WorkerOpener = (...args: ConstructorParameters<typeof Worker>) => InstanceType<typeof Worker>;
55

6-
export type NodeDatabaseImplementation = 'better-sqlite3' | 'node';
6+
/**
7+
* Use the [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) package as a SQLite driver for PowerSync.
8+
*/
9+
export interface BetterSqlite3Options {
10+
type: 'better-sqlite3';
11+
/**
12+
* The package import to resolve for better-sqlite3.
13+
*
14+
* While this defaults to `better-sqlite3`, this allows using forked better-sqlite3 packages, such as those used for
15+
* encryption.
16+
*/
17+
package?: string;
18+
}
19+
20+
/**
21+
* Use the experimental `node:sqlite` interface as a SQLite driver for PowerSync.
22+
*
23+
* Note that this option is not currently tested and highly unstable.
24+
*/
25+
export interface NodeSqliteOptions {
26+
type: 'node:sqlite';
27+
}
28+
29+
export type NodeDatabaseImplementation = BetterSqlite3Options | NodeSqliteOptions;
730

831
/**
932
* The {@link SQLOpenOptions} available across all PowerSync SDKs for JavaScript extended with

0 commit comments

Comments
 (0)