@@ -69,7 +69,17 @@ export interface SqlExecutor {
6969 executeBatch : ( query : string , params ?: any [ ] [ ] ) => Promise < QueryResult > ;
7070}
7171
72- export interface LockContext extends SqlExecutor , DBGetUtils { }
72+ export interface LockContext extends SqlExecutor , DBGetUtils {
73+ /**
74+ * How the connection has been opened.
75+ *
76+ * `writer` indicates that the lock context is capable of writing to the database.
77+ * `queryOnly` indicates that the lock context has been opened in a readwrite mode, but a `PRAGMA query_only = TRUE`
78+ * disabled writes.
79+ * `readOnly` indicates that the lock context has been opened by passing `SQLITE_OPEN_READONLY` to `sqlite3_open_v2`.
80+ */
81+ connectionType ?: 'writer' | 'queryOnly' | 'readOnly' ;
82+ }
7383
7484/**
7585 * Implements {@link DBGetUtils} on a {@link SqlRunner}.
@@ -192,11 +202,11 @@ export interface DBAdapter extends ConnectionPool, SqlExecutor, DBGetUtils {
192202export function DBAdapterDefaultMixin < TBase extends new ( ...args : any [ ] ) => ConnectionPool > ( Base : TBase ) {
193203 return class extends Base implements DBAdapter {
194204 readTransaction < T > ( fn : ( tx : Transaction ) => Promise < T > , options ?: DBLockOptions ) : Promise < T > {
195- return this . readLock ( ( ctx ) => TransactionImplementation . runWith ( ctx , fn ) , options ) ;
205+ return this . readLock ( ( ctx ) => TransactionImplementation . runWith ( ctx , false , fn ) , options ) ;
196206 }
197207
198208 writeTransaction < T > ( fn : ( tx : Transaction ) => Promise < T > , options ?: DBLockOptions ) : Promise < T > {
199- return this . writeLock ( ( ctx ) => TransactionImplementation . runWith ( ctx , fn ) , options ) ;
209+ return this . writeLock ( ( ctx ) => TransactionImplementation . runWith ( ctx , true , fn ) , options ) ;
200210 }
201211
202212 getAll < T > ( sql : string , parameters ?: any [ ] ) : Promise < T [ ] > {
@@ -260,11 +270,19 @@ class BaseTransaction implements SqlExecutor {
260270}
261271
262272class TransactionImplementation extends DBGetUtilsDefaultMixin ( BaseTransaction ) {
263- static async runWith < T > ( ctx : LockContext , fn : ( tx : Transaction ) => Promise < T > ) : Promise < T > {
273+ static async runWith < T > ( ctx : LockContext , isWrite : boolean , fn : ( tx : Transaction ) => Promise < T > ) : Promise < T > {
264274 let tx = new TransactionImplementation ( ctx ) ;
265275
276+ // For write transactions, use BEGIN IMMEDIATE to immediately obtain a write lock on the database (instead of doing
277+ // that on the first statement). If we have a genuine read-only connection, we also use BEGIN IMMEDIATE there: In
278+ // WAL mode, that ensures we pin the current state of the database (instead of the state at the first statement in
279+ // the transaction). But if we have a "fake" read-only connection implemented through `pragma query_only = true`, we
280+ // can't use this trick because it would attempt to lock the connection. So there, we use a regular `BEGIN`
281+ // statement.
282+ const useBeginImmediate = isWrite || ctx . connectionType != 'queryOnly' ;
283+
266284 try {
267- await ctx . execute ( 'BEGIN IMMEDIATE' ) ;
285+ await ctx . execute ( useBeginImmediate ? 'BEGIN IMMEDIATE' : 'BEGIN ') ;
268286
269287 const result = await fn ( tx ) ;
270288 await tx . commit ( ) ;
0 commit comments