Skip to content

Commit 7fd570f

Browse files
Restore backwards compatibility::: allow limiting/overriding tables which trigger watched queries
1 parent 16e35d4 commit 7fd570f

File tree

6 files changed

+222
-12
lines changed

6 files changed

+222
-12
lines changed

packages/common/src/client/AbstractPowerSyncDatabase.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -971,7 +971,8 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
971971
execute: () => this.executeReadOnly(sql, parameters)
972972
},
973973
reportFetching: false,
974-
throttleMs: options?.throttleMs ?? DEFAULT_WATCH_THROTTLE_MS
974+
throttleMs: options?.throttleMs ?? DEFAULT_WATCH_THROTTLE_MS,
975+
triggerOnTables: options?.tables
975976
}
976977
});
977978

packages/common/src/client/CustomQuery.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ export interface CustomQueryOptions<RowType> {
2222
export class CustomQuery<RowType> implements Query<RowType> {
2323
constructor(protected options: CustomQueryOptions<RowType>) {}
2424

25-
protected resolveOptions(options: WatchedQueryOptions) {
25+
protected resolveOptions(options: WatchedQueryOptions): WatchedQueryOptions {
2626
return {
2727
reportFetching: options?.reportFetching ?? DEFAULT_WATCH_QUERY_OPTIONS.reportFetching,
28-
throttleMs: options?.throttleMs ?? DEFAULT_WATCH_QUERY_OPTIONS.throttleMs
28+
throttleMs: options?.throttleMs ?? DEFAULT_WATCH_QUERY_OPTIONS.throttleMs,
29+
triggerOnTables: options?.triggerOnTables
2930
};
3031
}
3132

packages/common/src/client/watched/WatchedQuery.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ export interface WatchedQueryOptions {
5959
* is not relevant to the consumer.
6060
*/
6161
reportFetching?: boolean;
62+
63+
/**
64+
* By default, watched queries requery the database on any change to any dependant table of the query.
65+
* Supplying an override here can be used to limit the tables which trigger querying the database.
66+
*/
67+
triggerOnTables?: string[];
6268
}
6369

6470
export enum WatchedQueryListenerEvent {

packages/common/src/client/watched/processors/DifferentialQueryProcessor.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,9 @@ export class DifferentialQueryProcessor<RowType>
219219
const { abortSignal } = options;
220220

221221
const compiledQuery = watchOptions.query.compile();
222-
const tables = await db.resolveTables(compiledQuery.sql, compiledQuery.parameters as any[]);
222+
const tables = await db.resolveTables(compiledQuery.sql, compiledQuery.parameters as any[], {
223+
tables: options.settings.triggerOnTables
224+
});
223225

224226
let currentMap: DataHashMap<RowType> = new Map();
225227

packages/common/src/client/watched/processors/OnChangeQueryProcessor.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ export class OnChangeQueryProcessor<Data> extends AbstractQueryProcessor<Data, W
5050
const { abortSignal } = options;
5151

5252
const compiledQuery = watchOptions.query.compile();
53-
const tables = await db.resolveTables(compiledQuery.sql, compiledQuery.parameters as any[]);
53+
const tables = await db.resolveTables(compiledQuery.sql, compiledQuery.parameters as any[], {
54+
tables: options.settings.triggerOnTables
55+
});
5456

5557
db.onChangeWithCallback(
5658
{

packages/web/tests/watch.test.ts

Lines changed: 205 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import {
2-
AbstractPowerSyncDatabase,
3-
ArrayComparator,
4-
GetAllQuery,
5-
QueryResult,
6-
WatchedQueryDifferential,
7-
WatchedQueryState
2+
AbstractPowerSyncDatabase,
3+
ArrayComparator,
4+
GetAllQuery,
5+
QueryResult,
6+
WatchedQueryDifferential,
7+
WatchedQueryState
88
} from '@powersync/common';
99
import { PowerSyncDatabase } from '@powersync/web';
1010
import { v4 as uuid } from 'uuid';
1111
import { afterEach, beforeEach, describe, expect, it, onTestFinished, vi } from 'vitest';
12-
import { testSchema } from './utils/testDb';
12+
import { TestDatabase, testSchema } from './utils/testDb';
1313
vi.useRealTimers();
1414

1515
/**
@@ -270,6 +270,204 @@ describe('Watch Tests', { sequential: true }, () => {
270270
expect(receivedCustomersUpdatesCount).equals(1);
271271
});
272272

273+
it('should allow overriding table dependencies', async () => {
274+
const assetsAbortController = new AbortController();
275+
276+
type CustomerAssetJoin = TestDatabase['assets'] & { customer_name: string; customer_email: string };
277+
const results: CustomerAssetJoin[][] = [];
278+
279+
const onWatchAssets = (resultSet: CustomerAssetJoin[]) => {
280+
results.push(resultSet);
281+
};
282+
283+
const { id: customerId } = await powersync.get<{ id: string }>(`SELECT uuid() as id`);
284+
285+
await powersync.execute(
286+
/* sql */ `
287+
INSERT INTO
288+
customers (id, name, email)
289+
VALUES
290+
(?, ?, ?)
291+
`,
292+
[customerId, 'bob', '[email protected]']
293+
);
294+
295+
await powersync.execute(
296+
/* sql */ `
297+
INSERT into
298+
assets (id, make, model, customer_id)
299+
VALUES
300+
(uuid (), 'sync_engine', 'powersync', ?)
301+
`,
302+
[customerId]
303+
);
304+
305+
powersync.watch(
306+
/* sql */
307+
`
308+
SELECT
309+
assets.make,
310+
assets.model,
311+
assets.serial_number,
312+
customers.name AS customer_name,
313+
customers.email AS customer_email
314+
FROM
315+
assets
316+
LEFT JOIN customers ON assets.customer_id = customers.id;
317+
`,
318+
[],
319+
{ onResult: (r) => onWatchAssets(r.rows?._array ?? []) },
320+
{
321+
signal: assetsAbortController.signal,
322+
// Only trigger on changes to the customers table
323+
tables: ['customers'],
324+
throttleMs: 0
325+
}
326+
);
327+
328+
await vi.waitFor(
329+
() => {
330+
expect(results.length).eq(1);
331+
expect(results[0].length).eq(1);
332+
},
333+
{
334+
timeout: 1000
335+
}
336+
);
337+
338+
// Do an update on the assets table, this should not trigger the watched query
339+
// due to the override
340+
for (let attemptCount = 0; attemptCount < 5; attemptCount++) {
341+
await powersync.execute(
342+
/* sql */ `
343+
INSERT into
344+
assets (id, make, model, customer_id)
345+
VALUES
346+
(uuid (), 'sync_engine', 'powersync_v2', ?)
347+
`,
348+
[customerId]
349+
);
350+
// Give some time for watched queries to fire (if they need to)
351+
await new Promise((r) => setTimeout(r, 100));
352+
}
353+
354+
// now trigger an update on the customers table, this should update the watched query
355+
await powersync.execute(
356+
/* sql */ `
357+
INSERT INTO
358+
customers (id, name, email)
359+
VALUES
360+
(uuid (), ?, ?)
361+
`,
362+
['test', '[email protected]']
363+
);
364+
365+
await vi.waitFor(
366+
() => {
367+
expect(results.length).eq(2);
368+
},
369+
{ timeout: 1000 }
370+
);
371+
});
372+
373+
it('should allow overriding table dependencies (query api)', async () => {
374+
const { id: customerId } = await powersync.get<{ id: string }>(`SELECT uuid() as id`);
375+
376+
await powersync.execute(
377+
/* sql */ `
378+
INSERT INTO
379+
customers (id, name, email)
380+
VALUES
381+
(?, ?, ?)
382+
`,
383+
[customerId, 'bob', '[email protected]']
384+
);
385+
386+
await powersync.execute(
387+
/* sql */ `
388+
INSERT into
389+
assets (id, make, model, customer_id)
390+
VALUES
391+
(uuid (), 'sync_engine', 'powersync', ?)
392+
`,
393+
[customerId]
394+
);
395+
396+
type CustomerAssetJoin = TestDatabase['assets'] & { customer_name: string; customer_email: string };
397+
const results: CustomerAssetJoin[][] = [];
398+
399+
const query = powersync
400+
.query<CustomerAssetJoin>({
401+
sql:
402+
/* sql */
403+
`
404+
SELECT
405+
assets.make,
406+
assets.model,
407+
assets.serial_number,
408+
customers.name AS customer_name,
409+
customers.email AS customer_email
410+
FROM
411+
assets
412+
LEFT JOIN customers ON assets.customer_id = customers.id;
413+
`
414+
})
415+
.watch({
416+
triggerOnTables: ['customers'],
417+
throttleMs: 0
418+
});
419+
420+
query.registerListener({
421+
onData: (data) => {
422+
results.push([...data]);
423+
}
424+
});
425+
426+
await vi.waitFor(
427+
() => {
428+
expect(results.length).eq(1);
429+
expect(results[0].length).eq(1);
430+
},
431+
{
432+
timeout: 1000
433+
}
434+
);
435+
436+
// Do an update on the assets table, this should not trigger the watched query
437+
// due to the override
438+
for (let attemptCount = 0; attemptCount < 5; attemptCount++) {
439+
await powersync.execute(
440+
/* sql */ `
441+
INSERT into
442+
assets (id, make, model, customer_id)
443+
VALUES
444+
(uuid (), 'sync_engine', 'powersync_v2', ?)
445+
`,
446+
[customerId]
447+
);
448+
// Give some time for watched queries to fire (if they need to)
449+
await new Promise((r) => setTimeout(r, 100));
450+
}
451+
452+
// now trigger an update on the customers table, this should update the watched query
453+
await powersync.execute(
454+
/* sql */ `
455+
INSERT INTO
456+
customers (id, name, email)
457+
VALUES
458+
(uuid (), ?, ?)
459+
`,
460+
['test', '[email protected]']
461+
);
462+
463+
await vi.waitFor(
464+
() => {
465+
expect(results.length).eq(2);
466+
},
467+
{ timeout: 1000 }
468+
);
469+
});
470+
273471
it('should handle watch onError callback', async () => {
274472
const abortController = new AbortController();
275473
const onResult = () => {}; // no-op

0 commit comments

Comments
 (0)