From f091a94c565ad98c2d0f074ebbf27180d34cac25 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Wed, 29 Oct 2025 16:57:25 +0200 Subject: [PATCH 1/3] prevent database is locked errors --- .changeset/friendly-queens-invite.md | 5 ++++ packages/node/src/db/WorkerConnectionPool.ts | 16 +++++++----- packages/node/tests/PowerSyncDatabase.test.ts | 26 ++++++++++++++++--- pnpm-workspace.yaml | 23 ++++++++++++++++ 4 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 .changeset/friendly-queens-invite.md diff --git a/.changeset/friendly-queens-invite.md b/.changeset/friendly-queens-invite.md new file mode 100644 index 000000000..26c52535c --- /dev/null +++ b/.changeset/friendly-queens-invite.md @@ -0,0 +1,5 @@ +--- +'@powersync/node': patch +--- + +[`node:sqlite`] Prevent `database is locked` errors when instantiating the database. diff --git a/packages/node/src/db/WorkerConnectionPool.ts b/packages/node/src/db/WorkerConnectionPool.ts index 068baebc3..d05c21725 100644 --- a/packages/node/src/db/WorkerConnectionPool.ts +++ b/packages/node/src/db/WorkerConnectionPool.ts @@ -1,24 +1,24 @@ +import * as Comlink from 'comlink'; import fs from 'node:fs/promises'; import * as path from 'node:path'; import { Worker } from 'node:worker_threads'; -import * as Comlink from 'comlink'; import { BaseObserver, BatchedUpdateNotification, DBAdapter, DBAdapterListener, - LockContext, - Transaction, DBLockOptions, - QueryResult + LockContext, + QueryResult, + Transaction } from '@powersync/common'; import { Remote } from 'comlink'; import { AsyncResource } from 'node:async_hooks'; +import { isBundledToCommonJs } from '../utils/modules.js'; import { AsyncDatabase, AsyncDatabaseOpener } from './AsyncDatabase.js'; import { RemoteConnection } from './RemoteConnection.js'; import { NodeDatabaseImplementation, NodeSQLOpenOptions } from './options.js'; -import { isBundledToCommonJs } from '../utils/modules.js'; export type BetterSQLite3LockContext = LockContext & { executeBatch(query: string, params?: any[][]): Promise; @@ -135,10 +135,12 @@ export class WorkerConnectionPool extends BaseObserver implem if (this.options.initializeConnection) { await this.options.initializeConnection(connection, isWriter); } - - await connection.execute('pragma journal_mode = WAL'); if (!isWriter) { await connection.execute('pragma query_only = true'); + } else { + // We only need to enable this on the writer connection. + // We can get `database is locked` errors if we enable this on concurrently opening read connections. + await connection.execute('pragma journal_mode = WAL'); } return connection; diff --git a/packages/node/tests/PowerSyncDatabase.test.ts b/packages/node/tests/PowerSyncDatabase.test.ts index 4e0a9a731..69115c5d9 100644 --- a/packages/node/tests/PowerSyncDatabase.test.ts +++ b/packages/node/tests/PowerSyncDatabase.test.ts @@ -1,11 +1,12 @@ import * as path from 'node:path'; import { Worker } from 'node:worker_threads'; -import { vi, expect, test } from 'vitest'; -import { AppSchema, databaseTest, tempDirectoryTest } from './utils'; +import { LockContext } from '@powersync/common'; +import { randomUUID } from 'node:crypto'; +import { expect, test, vi } from 'vitest'; import { CrudEntry, CrudTransaction, PowerSyncDatabase } from '../lib'; import { WorkerOpener } from '../lib/db/options'; -import { LockContext } from '@powersync/common'; +import { AppSchema, databaseTest, tempDirectoryTest } from './utils'; test('validates options', async () => { await expect(async () => { @@ -203,3 +204,22 @@ databaseTest('getCrudTransactions', async ({ database }) => { const remainingTransaction = await database.getNextCrudTransaction(); expect(remainingTransaction?.crud).toHaveLength(15); }); + +tempDirectoryTest('should not present database is locked errors on startup', async ({ tmpdir }) => { + for (let i = 0; i < 10; i++) { + const database = new PowerSyncDatabase({ + schema: AppSchema, + database: { + dbFilename: `${randomUUID()}.sqlite`, + dbLocation: tmpdir, + implementation: { + type: 'node:sqlite' + } + } + }); + + // This should not throw + await database.waitForReady(); + await database.close(); + } +}); diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e3b97df79..f1521b96d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,3 +3,26 @@ packages: - packages/* - tools/* - docs/ + +ignoredBuiltDependencies: + - '@journeyapps/wa-sqlite' + - '@parcel/watcher' + - '@swc/core' + - core-js + - core-js-pure + - detox + - dtrace-provider + - electron + - electron-winstaller + - esbuild + - lmdb + - lzma-native + - msgpackr-extract + - react-native-elements + - supabase + - unrs-resolver + - vue-demi + +onlyBuiltDependencies: + - better-sqlite3 + - better-sqlite3-multiple-ciphers From 0951a42963547822c61515a70c8e526b1b1a3334 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Thu, 30 Oct 2025 09:20:09 +0200 Subject: [PATCH 2/3] Skip node:sqlite test on older node. Manually cleanup pnpm approves --- packages/node/tests/PowerSyncDatabase.test.ts | 37 +++++++++++-------- pnpm-workspace.yaml | 19 ---------- 2 files changed, 22 insertions(+), 34 deletions(-) diff --git a/packages/node/tests/PowerSyncDatabase.test.ts b/packages/node/tests/PowerSyncDatabase.test.ts index 69115c5d9..9bd7fd282 100644 --- a/packages/node/tests/PowerSyncDatabase.test.ts +++ b/packages/node/tests/PowerSyncDatabase.test.ts @@ -205,21 +205,28 @@ databaseTest('getCrudTransactions', async ({ database }) => { expect(remainingTransaction?.crud).toHaveLength(15); }); -tempDirectoryTest('should not present database is locked errors on startup', async ({ tmpdir }) => { - for (let i = 0; i < 10; i++) { - const database = new PowerSyncDatabase({ - schema: AppSchema, - database: { - dbFilename: `${randomUUID()}.sqlite`, - dbLocation: tmpdir, - implementation: { - type: 'node:sqlite' +tempDirectoryTest( + 'should not present database is locked errors on startup', + { + // This is not a SemVer check, but is basic enough to skip this test on older versions of Node.js + skip: process.versions.node < '22.5.0' + }, + async ({ tmpdir }) => { + for (let i = 0; i < 10; i++) { + const database = new PowerSyncDatabase({ + schema: AppSchema, + database: { + dbFilename: `${randomUUID()}.sqlite`, + dbLocation: tmpdir, + implementation: { + type: 'node:sqlite' + } } - } - }); + }); - // This should not throw - await database.waitForReady(); - await database.close(); + // This should not throw + await database.waitForReady(); + await database.close(); + } } -}); +); diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f1521b96d..a3ee417db 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,25 +4,6 @@ packages: - tools/* - docs/ -ignoredBuiltDependencies: - - '@journeyapps/wa-sqlite' - - '@parcel/watcher' - - '@swc/core' - - core-js - - core-js-pure - - detox - - dtrace-provider - - electron - - electron-winstaller - - esbuild - - lmdb - - lzma-native - - msgpackr-extract - - react-native-elements - - supabase - - unrs-resolver - - vue-demi - onlyBuiltDependencies: - better-sqlite3 - better-sqlite3-multiple-ciphers From a75a11396ec0702e0aad1d6fe5aa717e5b9ecfb4 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Thu, 30 Oct 2025 10:47:14 +0200 Subject: [PATCH 3/3] Fix skipif --- packages/node/tests/PowerSyncDatabase.test.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/node/tests/PowerSyncDatabase.test.ts b/packages/node/tests/PowerSyncDatabase.test.ts index 9bd7fd282..e3660841a 100644 --- a/packages/node/tests/PowerSyncDatabase.test.ts +++ b/packages/node/tests/PowerSyncDatabase.test.ts @@ -205,12 +205,9 @@ databaseTest('getCrudTransactions', async ({ database }) => { expect(remainingTransaction?.crud).toHaveLength(15); }); -tempDirectoryTest( +// This is not a SemVer check, but is basic enough to skip this test on older versions of Node.js +tempDirectoryTest.skipIf(process.versions.node < '22.5.0')( 'should not present database is locked errors on startup', - { - // This is not a SemVer check, but is basic enough to skip this test on older versions of Node.js - skip: process.versions.node < '22.5.0' - }, async ({ tmpdir }) => { for (let i = 0; i < 10; i++) { const database = new PowerSyncDatabase({