Skip to content

Commit b612362

Browse files
Merge pull request #7 from journeyapps/feature/concurrent-transactions
[Feature] Concurrent DB Connections and Transactions
2 parents af0031b + ec1f993 commit b612362

File tree

12 files changed

+230
-170
lines changed

12 files changed

+230
-170
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@journeyapps/powersync-sdk-react-native': patch
3+
'@journeyapps/powersync-sdk-common': patch
4+
---
5+
6+
Updated logic to correspond with React Native Quick SQLite concurrent transactions. Added helper methods on transaction contexts.
7+
8+
API changes include:
9+
- Removal of synchronous DB operations in transactions: `execute`, `commit`, `rollback` are now async functions. `executeAsync`, `commitAsync` and `rollbackAsync` have been removed.
10+
- Transaction contexts now have `get`, `getAll` and `getOptional` helpers.
11+
- Added a default lock timeout of 2 minutes to aide with potential recursive lock/transaction requests.

.changeset/dry-pets-yawn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@journeyapps/powersync-sdk-react-native': patch
3+
---
4+
5+
Update README polyfill command.

.changeset/friendly-shrimps-fry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@journeyapps/powersync-sdk-common': patch
3+
---
4+
5+
Fix update trigger for local only watched tables.

packages/powersync-sdk-common/src/client/AbstractPowerSyncDatabase.ts

Lines changed: 55 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ export const DEFAULT_POWERSYNC_DB_OPTIONS = {
4646
logger: Logger.get('PowerSyncDatabase')
4747
};
4848

49+
/**
50+
* Requesting nested or recursive locks can block the application in some circumstances.
51+
* This default lock timeout will act as a failsafe to throw an error if a lock cannot
52+
* be obtained.
53+
*/
54+
export const DEFAULT_LOCK_TIMEOUT_MS = 120_000; // 2 mins
55+
4956
export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDBListener> {
5057
/**
5158
* Transactions should be queued in the DBAdapter, but we also want to prevent
@@ -70,9 +77,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
7077
this.closed = true;
7178
this.options = { ...DEFAULT_POWERSYNC_DB_OPTIONS, ...options };
7279
this.bucketStorageAdapter = this.generateBucketStorageAdapter();
73-
this.sdkVersion = this.options.database.execute('SELECT powersync_rs_version()').rows?.item(0)[
74-
'powersync_rs_version()'
75-
];
80+
this.sdkVersion = '';
7681
}
7782

7883
get schema() {
@@ -98,7 +103,9 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
98103
this.initialized = (async () => {
99104
await this._init();
100105
await this.bucketStorageAdapter.init();
101-
await this.database.executeAsync('SELECT powersync_replace_schema(?)', [JSON.stringify(this.schema.toJSON())]);
106+
await this.database.execute('SELECT powersync_replace_schema(?)', [JSON.stringify(this.schema.toJSON())]);
107+
const version = await this.options.database.execute('SELECT powersync_rs_version()');
108+
this.sdkVersion = version.rows?.item(0)['powersync_rs_version()'] ?? '';
102109
})();
103110
await this.initialized;
104111
}
@@ -111,7 +118,6 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
111118
await this.disconnect();
112119

113120
await this.initialized;
114-
115121
this.syncStreamImplementation = this.generateSyncStreamImplementation(connector);
116122
this.syncStatusListenerDisposer = this.syncStreamImplementation.registerListener({
117123
statusChanged: (status) => {
@@ -142,20 +148,20 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
142148
await this.disconnect();
143149

144150
// TODO DB name, verify this is necessary with extension
145-
await this.database.transaction(async (tx) => {
146-
await tx.executeAsync('DELETE FROM ps_oplog WHERE 1');
147-
await tx.executeAsync('DELETE FROM ps_crud WHERE 1');
148-
await tx.executeAsync('DELETE FROM ps_buckets WHERE 1');
151+
await this.database.writeTransaction(async (tx) => {
152+
await tx.execute('DELETE FROM ps_oplog WHERE 1');
153+
await tx.execute('DELETE FROM ps_crud WHERE 1');
154+
await tx.execute('DELETE FROM ps_buckets WHERE 1');
149155

150-
const existingTableRows = await tx.executeAsync(
156+
const existingTableRows = await tx.execute(
151157
"SELECT name FROM sqlite_master WHERE type='table' AND name GLOB 'ps_data_*'"
152158
);
153159

154160
if (!existingTableRows.rows.length) {
155161
return;
156162
}
157163
for (const row of existingTableRows.rows._array) {
158-
await tx.executeAsync(`DELETE FROM ${row.name} WHERE 1`);
164+
await tx.execute(`DELETE FROM ${row.name} WHERE 1`);
159165
}
160166
});
161167
}
@@ -181,14 +187,12 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
181187
async getUploadQueueStats(includeSize?: boolean): Promise<UploadQueueStats> {
182188
return this.readTransaction(async (tx) => {
183189
if (includeSize) {
184-
const result = await tx.executeAsync(
185-
'SELECT SUM(cast(data as blob) + 20) as size, count(*) as count FROM ps_crud'
186-
);
190+
const result = await tx.execute('SELECT SUM(cast(data as blob) + 20) as size, count(*) as count FROM ps_crud');
187191

188192
const row = result.rows.item(0);
189193
return new UploadQueueStats(row?.count ?? 0, row?.size ?? 0);
190194
} else {
191-
const result = await tx.executeAsync('SELECT count(*) as count FROM ps_crud');
195+
const result = await tx.execute('SELECT count(*) as count FROM ps_crud');
192196
const row = result.rows.item(0);
193197
return new UploadQueueStats(row?.count ?? 0);
194198
}
@@ -213,7 +217,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
213217
* and a single transaction may be split over multiple batches.
214218
*/
215219
async getCrudBatch(limit: number): Promise<CrudBatch | null> {
216-
const result = await this.database.executeAsync('SELECT id, tx_id, data FROM ps_crud ORDER BY id ASC LIMIT ?', [
220+
const result = await this.database.execute('SELECT id, tx_id, data FROM ps_crud ORDER BY id ASC LIMIT ?', [
217221
limit + 1
218222
]);
219223

@@ -231,11 +235,11 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
231235
const last = all[all.length - 1];
232236
return new CrudBatch(all, haveMore, async (writeCheckpoint?: string) => {
233237
await this.writeTransaction(async (tx) => {
234-
await tx.executeAsync('DELETE FROM ps_crud WHERE id <= ?', [last.clientId]);
235-
if (writeCheckpoint != null && (await tx.executeAsync('SELECT 1 FROM ps_crud LIMIT 1')) == null) {
236-
await tx.executeAsync("UPDATE ps_buckets SET target_op = ? WHERE name='$local'", [writeCheckpoint]);
238+
await tx.execute('DELETE FROM ps_crud WHERE id <= ?', [last.clientId]);
239+
if (writeCheckpoint != null && (await tx.execute('SELECT 1 FROM ps_crud LIMIT 1')) == null) {
240+
await tx.execute("UPDATE ps_buckets SET target_op = ? WHERE name='$local'", [writeCheckpoint]);
237241
} else {
238-
await tx.executeAsync("UPDATE ps_buckets SET target_op = ? WHERE name='$local'", [
242+
await tx.execute("UPDATE ps_buckets SET target_op = ? WHERE name='$local'", [
239243
this.bucketStorageAdapter.getMaxOpId()
240244
]);
241245
}
@@ -258,7 +262,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
258262
*/
259263
async getNextCrudTransaction(): Promise<CrudTransaction> {
260264
return await this.readTransaction(async (tx) => {
261-
const first = await tx.executeAsync('SELECT id, tx_id, data FROM ps_crud ORDER BY id ASC LIMIT 1');
265+
const first = await tx.execute('SELECT id, tx_id, data FROM ps_crud ORDER BY id ASC LIMIT 1');
262266

263267
if (!first.rows.length) {
264268
return null;
@@ -269,9 +273,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
269273
if (!txId) {
270274
all = [CrudEntry.fromRow(first.rows.item(0))];
271275
} else {
272-
const result = await tx.executeAsync('SELECT id, tx_id, data FROM ps_crud WHERE tx_id = ? ORDER BY id ASC', [
273-
txId
274-
]);
276+
const result = await tx.execute('SELECT id, tx_id, data FROM ps_crud WHERE tx_id = ? ORDER BY id ASC', [txId]);
275277
all = result.rows._array.map((row) => CrudEntry.fromRow(row));
276278
}
277279

@@ -281,14 +283,14 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
281283
all,
282284
async (writeCheckpoint?: string) => {
283285
await this.writeTransaction(async (tx) => {
284-
await tx.executeAsync('DELETE FROM ps_crud WHERE id <= ?', [last.clientId]);
286+
await tx.execute('DELETE FROM ps_crud WHERE id <= ?', [last.clientId]);
285287
if (writeCheckpoint) {
286-
const check = await tx.executeAsync('SELECT 1 FROM ps_crud LIMIT 1');
288+
const check = await tx.execute('SELECT 1 FROM ps_crud LIMIT 1');
287289
if (!check.rows?.length) {
288-
await tx.executeAsync("UPDATE ps_buckets SET target_op = ? WHERE name='$local'", [writeCheckpoint]);
290+
await tx.execute("UPDATE ps_buckets SET target_op = ? WHERE name='$local'", [writeCheckpoint]);
289291
}
290292
} else {
291-
await tx.executeAsync("UPDATE ps_buckets SET target_op = ? WHERE name='$local'", [
293+
await tx.execute("UPDATE ps_buckets SET target_op = ? WHERE name='$local'", [
292294
this.bucketStorageAdapter.getMaxOpId()
293295
]);
294296
}
@@ -303,36 +305,32 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
303305
* Execute a statement and optionally return results
304306
*/
305307
async execute(sql: string, parameters?: any[]) {
306-
const res = await this.writeLock((tx) => tx.executeAsync(sql, parameters));
307-
return res;
308+
await this.initialized;
309+
return this.database.execute(sql, parameters);
308310
}
309311

310312
/**
311313
* Execute a read-only query and return results
312314
*/
313315
async getAll<T>(sql: string, parameters?: any[]): Promise<T[]> {
314-
const res = await this.readTransaction((tx) => tx.executeAsync(sql, parameters));
315-
return res.rows?._array ?? [];
316+
await this.initialized;
317+
return this.database.getAll(sql, parameters);
316318
}
317319

318320
/**
319321
* Execute a read-only query and return the first result, or null if the ResultSet is empty.
320322
*/
321323
async getOptional<T>(sql: string, parameters?: any[]): Promise<T | null> {
322-
const res = await this.readTransaction((tx) => tx.executeAsync(sql, parameters));
323-
return res.rows?.item(0) ?? null;
324+
await this.initialized;
325+
return this.database.getOptional(sql, parameters);
324326
}
325327

326328
/**
327329
* Execute a read-only query and return the first result, error if the ResultSet is empty.
328330
*/
329331
async get<T>(sql: string, parameters?: any[]): Promise<T> {
330-
const res = await this.readTransaction((tx) => tx.executeAsync(sql, parameters));
331-
const first = res.rows?.item(0);
332-
if (!first) {
333-
throw new Error('Result set is empty');
334-
}
335-
return first;
332+
await this.initialized;
333+
return this.database.get(sql, parameters);
336334
}
337335

338336
/**
@@ -358,40 +356,43 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
358356
});
359357
}
360358

361-
async readTransaction<T>(callback: (tx: Transaction) => Promise<T>, lockTimeout?: number): Promise<T> {
359+
async readTransaction<T>(
360+
callback: (tx: Transaction) => Promise<T>,
361+
lockTimeout: number = DEFAULT_LOCK_TIMEOUT_MS
362+
): Promise<T> {
362363
await this.initialized;
363-
return this.runLockedTransaction(
364-
AbstractPowerSyncDatabase.transactionMutex,
364+
return this.database.readTransaction(
365365
async (tx) => {
366-
const res = await callback(tx);
367-
await tx.rollbackAsync();
366+
const res = await callback({ ...tx });
367+
await tx.rollback();
368368
return res;
369369
},
370-
lockTimeout
370+
{ timeoutMs: lockTimeout }
371371
);
372372
}
373373

374-
async writeTransaction<T>(callback: (tx: Transaction) => Promise<T>, lockTimeout?: number): Promise<T> {
374+
async writeTransaction<T>(
375+
callback: (tx: Transaction) => Promise<T>,
376+
lockTimeout: number = DEFAULT_LOCK_TIMEOUT_MS
377+
): Promise<T> {
375378
await this.initialized;
376-
return this.runLockedTransaction(
377-
AbstractPowerSyncDatabase.transactionMutex,
379+
return this.database.writeTransaction(
378380
async (tx) => {
379381
const res = await callback(tx);
380-
await tx.commitAsync();
382+
await tx.commit();
381383
_.defer(() => this.syncStreamImplementation?.triggerCrudUpload());
382384
return res;
383385
},
384-
lockTimeout
386+
{ timeoutMs: lockTimeout }
385387
);
386388
}
387389

388-
async *watch(sql: string, parameters: any[], options?: SQLWatchOptions): AsyncIterable<QueryResult> {
390+
async *watch(sql: string, parameters?: any[], options?: SQLWatchOptions): AsyncIterable<QueryResult> {
389391
//Fetch initial data
390392
yield await this.execute(sql, parameters);
391393

392394
const resolvedTables = options?.tables ?? [];
393395
if (!options?.tables) {
394-
// TODO get tables from sql if not specified
395396
const explained = await this.getAll(`EXPLAIN ${sql}`, parameters);
396397
const rootPages = _.chain(explained)
397398
.filter((row) => row['opcode'] == 'OpenRead' && row['p3'] == 0 && _.isNumber(row['p2']))
@@ -401,7 +402,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
401402
`SELECT tbl_name FROM sqlite_master WHERE rootpage IN (SELECT json_each.value FROM json_each(?))`,
402403
[JSON.stringify(rootPages)]
403404
);
404-
tables.forEach((t) => resolvedTables.push(t.tbl_name.replace(/^ps_data__/, '')));
405+
tables.forEach((t) => resolvedTables.push(t.tbl_name.replace(POWERSYNC_TABLE_MATCH, '')));
405406
}
406407
for await (const event of this.onChange({
407408
...(options ?? {}),
@@ -458,27 +459,4 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
458459
return () => dispose();
459460
});
460461
}
461-
462-
private runLockedTransaction<T>(
463-
mutex: Mutex,
464-
callback: (tx: Transaction) => Promise<T>,
465-
lockTimeout?: number
466-
): Promise<T> {
467-
return mutexRunExclusive(
468-
mutex,
469-
() => {
470-
return new Promise<T>(async (resolve, reject) => {
471-
try {
472-
await this.database.transaction(async (tx) => {
473-
const r = await callback(tx);
474-
resolve(r);
475-
});
476-
} catch (ex) {
477-
reject(ex);
478-
}
479-
});
480-
},
481-
{ timeoutMs: lockTimeout }
482-
);
483-
}
484462
}

0 commit comments

Comments
 (0)