Skip to content

Commit ad155d0

Browse files
Type extracted types. Delete triggers should store previous values.
1 parent 560891d commit ad155d0

File tree

3 files changed

+65
-23
lines changed

3 files changed

+65
-23
lines changed

packages/common/src/client/triggers/TriggerManager.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export interface BaseTriggerDiffRecord {
3131
export interface TriggerDiffUpdateRecord extends BaseTriggerDiffRecord {
3232
operation: DiffTriggerOperation.UPDATE;
3333
value: string;
34-
previous_value?: string;
34+
previous_value: string;
3535
}
3636

3737
/**
@@ -57,6 +57,21 @@ export interface TriggerDiffDeleteRecord extends BaseTriggerDiffRecord {
5757
*/
5858
export type TriggerDiffRecord = TriggerDiffUpdateRecord | TriggerDiffInsertRecord | TriggerDiffDeleteRecord;
5959

60+
/**
61+
* Querying the DIFF table directly with {@link TriggerDiffHandlerContext#withExtractedDiff} will return records
62+
* with the tracked columns extracted from the JSON value.
63+
* This type represents the structure of such records.
64+
* @example
65+
* ```typescript
66+
* const diffs = await context.withExtractedDiff<ExtractedTriggerDiffRecord<{id: string, name: string}>>('SELECT * FROM DIFF');
67+
* ```
68+
*/
69+
export type ExtractedTriggerDiffRecord<T> = T & {
70+
[K in keyof Omit<BaseTriggerDiffRecord, 'id'> as `__${string & K}`]: TriggerDiffRecord[K];
71+
} & {
72+
__previous_value?: string;
73+
};
74+
6075
/**
6176
* Hooks used in the creation of a table diff trigger.
6277
*/
@@ -131,7 +146,7 @@ export interface TriggerDiffHandlerContext extends LockContext {
131146
/**
132147
* The name of the temporary destination table created by the trigger.
133148
*/
134-
destination_table: string;
149+
destinationTable: string;
135150

136151
/**
137152
* Allows querying the database with access to the table containing diff records.
@@ -158,7 +173,7 @@ export interface TriggerDiffHandlerContext extends LockContext {
158173
* WHERE json_extract(DIFF.value, '$.status') = 'active'
159174
* ```
160175
*/
161-
withDiff: <T = any>(query: string, params?: any[]) => Promise<T[]>;
176+
withDiff: <T = any>(query: string, params?: ReadonlyArray<Readonly<any>>) => Promise<T[]>;
162177

163178
/**
164179
* Allows querying the database with access to the table containing diff records.
@@ -189,7 +204,7 @@ export interface TriggerDiffHandlerContext extends LockContext {
189204
* WHERE DIFF.name = 'example'
190205
* ```
191206
*/
192-
withExtractedDiff: <T = any>(query: string, params?: any[]) => Promise<T[]>;
207+
withExtractedDiff: <T = any>(query: string, params?: ReadonlyArray<Readonly<any>>) => Promise<T[]>;
193208
}
194209

195210
/**

packages/common/src/client/triggers/TriggerManagerImpl.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,13 @@ export class TriggerManagerImpl implements TriggerManager {
173173
DiffTriggerOperation.DELETE
174174
]} BEGIN
175175
INSERT INTO
176-
${destination} (id, operation, timestamp)
176+
${destination} (id, operation, timestamp, value)
177177
VALUES
178178
(
179-
NEW.id,
179+
OLD.id,
180180
'DELETE',
181-
strftime ('%Y-%m-%dT%H:%M:%fZ', 'now')
181+
strftime ('%Y-%m-%dT%H:%M:%fZ', 'now'),
182+
OLD.data
182183
);
183184
184185
END;
@@ -231,7 +232,7 @@ export class TriggerManagerImpl implements TriggerManager {
231232
await this.db.writeTransaction(async (tx) => {
232233
const callbackResult = await options.onChange({
233234
...tx,
234-
destination_table: destination,
235+
destinationTable: destination,
235236
withDiff: async <T>(query, params) => {
236237
// Wrap the query to expose the destination table
237238
const wrappedQuery = /* sql */ `
@@ -241,6 +242,8 @@ export class TriggerManagerImpl implements TriggerManager {
241242
*
242243
FROM
243244
${destination}
245+
ORDER BY
246+
timestamp ASC
244247
) ${query}
245248
`;
246249
return tx.getAll<T>(wrappedQuery, params);
@@ -258,6 +261,8 @@ export class TriggerManagerImpl implements TriggerManager {
258261
previous_value as __previous_value
259262
FROM
260263
${destination}
264+
ORDER BY
265+
__timestamp ASC
261266
) ${query}
262267
`;
263268
return tx.getAll<T>(wrappedQuery, params);

packages/node/tests/trigger.test.ts

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DiffTriggerOperation, TriggerDiffRecord } from '@powersync/common';
1+
import { DiffTriggerOperation, ExtractedTriggerDiffRecord, TriggerDiffRecord } from '@powersync/common';
22
// import 'source-map-support/register';
33
import { describe, expect, vi } from 'vitest';
44
import { Database, databaseTest } from './utils';
@@ -15,15 +15,15 @@ describe('Triggers', () => {
1515
source: 'lists',
1616
destination: tempTable,
1717
columns: ['name'],
18-
operations: [DiffTriggerOperation.INSERT, DiffTriggerOperation.UPDATE]
18+
operations: [DiffTriggerOperation.INSERT, DiffTriggerOperation.UPDATE, DiffTriggerOperation.DELETE]
1919
});
2020

2121
const results = [] as TriggerDiffRecord[];
2222

2323
database.onChange(
2424
{
2525
// This callback async processed. Invocations are sequential.
26-
onChange: async (change) => {
26+
onChange: async () => {
2727
await database.writeLock(async (tx) => {
2828
// API exposes a context to run things here.
2929
// using execute seems to be important on Node.js
@@ -48,13 +48,15 @@ describe('Triggers', () => {
4848
// Do some changes to the source table
4949
await database.execute('INSERT INTO lists (id, name) VALUES (uuid(), ?);', ['test list']);
5050
await database.execute(`UPDATE lists SET name = 'wooo'`);
51+
await database.execute('DELETE FROM lists WHERE name = ?', ['wooo']);
5152

5253
// Wait for the changes to be processed and results to be collected
5354
await vi.waitFor(
5455
() => {
55-
expect(results.length).toEqual(2);
56+
expect(results.length).toEqual(3);
5657
expect(results[0].operation).toEqual('INSERT');
5758
expect(results[1].operation).toEqual('UPDATE');
59+
expect(results[2].operation).toEqual('DELETE');
5860
},
5961
{ timeout: 1000 }
6062
);
@@ -63,7 +65,7 @@ describe('Triggers', () => {
6365
/**
6466
* Uses the automatic handlers for triggers to track changes.
6567
*/
66-
databaseTest('Should be able to handle table inserts', async ({ database }) => {
68+
databaseTest('Should be able to track table inserts', async ({ database }) => {
6769
await database.execute(
6870
/* sql */ `
6971
INSERT INTO
@@ -98,8 +100,8 @@ describe('Triggers', () => {
98100
SELECT
99101
todos.*
100102
FROM
101-
diff
102-
JOIN todos ON diff.id = todos.id
103+
DIFF
104+
JOIN todos ON DIFF.id = todos.id
103105
`);
104106

105107
results.push(...newTodos);
@@ -124,7 +126,7 @@ describe('Triggers', () => {
124126
() => {
125127
expect(results.length).toEqual(1);
126128
},
127-
{ timeout: 10000 }
129+
{ timeout: 1000 }
128130
);
129131

130132
// Do further inserts
@@ -144,11 +146,11 @@ describe('Triggers', () => {
144146
() => {
145147
expect(results.length).toEqual(2);
146148
},
147-
{ timeout: 10000 }
149+
{ timeout: 1000 }
148150
);
149151
});
150152

151-
databaseTest('Should be able to handle table updates', async ({ database }) => {
153+
databaseTest('Should be able to track table updates', async ({ database }) => {
152154
const { rows } = await database.execute(
153155
/* sql */ `
154156
INSERT INTO
@@ -161,18 +163,18 @@ describe('Triggers', () => {
161163

162164
const list = rows!.item(0) as Database['lists'];
163165

164-
const changes: Database['lists'][] = [];
166+
const changes: ExtractedTriggerDiffRecord<Database['lists']>[] = [];
165167

166168
/**
167169
* Watch the todos table for changes. Only track the diff for rows belonging to the first list.
168170
*/
169171
await database.triggers.trackTableDiff({
170172
source: 'lists',
171-
when: { [DiffTriggerOperation.INSERT]: `NEW.id = '${list.id}'` },
172-
operations: [DiffTriggerOperation.UPDATE],
173+
when: { [DiffTriggerOperation.UPDATE]: `NEW.id = '${list.id}'` },
174+
operations: [DiffTriggerOperation.UPDATE, DiffTriggerOperation.DELETE],
173175
onChange: async (context) => {
174176
// Fetches the todo records that were inserted during this diff
175-
const diffs = await context.withExtractedDiff<Database['lists']>(/* sql */ `
177+
const diffs = await context.withExtractedDiff<ExtractedTriggerDiffRecord<Database['lists']>>(/* sql */ `
176178
SELECT
177179
*
178180
FROM
@@ -203,7 +205,27 @@ describe('Triggers', () => {
203205
expect(changes.length).toEqual(updateCount);
204206
expect(changes.map((c) => c.name)).toEqual(Array.from({ length: updateCount }, (_, i) => `updated ${i}`));
205207
},
206-
{ timeout: 10000 }
208+
{ timeout: 1000 }
209+
);
210+
211+
// clear the items
212+
await database.execute(
213+
/* sql */ `
214+
DELETE FROM lists
215+
WHERE
216+
id = ?
217+
`,
218+
[list.id]
219+
);
220+
221+
await vi.waitFor(
222+
() => {
223+
expect(changes.length).toEqual(updateCount + 1);
224+
expect(changes[changes.length - 1].__operation).eq(DiffTriggerOperation.DELETE);
225+
// The delete diff should contain the previous value
226+
expect(changes[changes.length - 1].name).eq(`updated ${updateCount - 1}`);
227+
},
228+
{ timeout: 1000 }
207229
);
208230
});
209231

0 commit comments

Comments
 (0)