Skip to content

Commit 2949d58

Browse files
committed
Add tests and changeset entry
1 parent b2885a5 commit 2949d58

File tree

6 files changed

+152
-28
lines changed

6 files changed

+152
-28
lines changed

.changeset/giant-ladybugs-dress.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@powersync/common': minor
3+
---
4+
5+
- Add `includeOld` option on `Table` which sets `CrudEntry.oldData` to previous values on updates.
6+
- Add `includeMetadata` option on `Table` which adds a `_metadata` column that can be used for updates.
7+
The configured metadata is available through `CrudEntry.metadata`.
8+
- Add `ignoreEmptyUpdate` option which skips creating CRUD entries for updates that don't change any values.

packages/common/src/client/sync/bucket/CrudEntry.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ export type CrudEntryJSON = {
2525

2626
type CrudEntryDataJSON = {
2727
data: Record<string, any>;
28+
old?: Record<string, any>;
2829
op: UpdateType;
2930
type: string;
3031
id: string;
32+
metadata?: string;
3133
};
3234

3335
/**
@@ -62,6 +64,13 @@ export class CrudEntry {
6264
* Data associated with the change.
6365
*/
6466
opData?: Record<string, any>;
67+
68+
/**
69+
* For tables where the `includeOld` option has been enabled, this tracks previous values for
70+
* `UPDATE` and `DELETE` statements.
71+
*/
72+
oldData?: Record<string, any>;
73+
6574
/**
6675
* Table that contained the change.
6776
*/
@@ -71,9 +80,26 @@ export class CrudEntry {
7180
*/
7281
transactionId?: number;
7382

83+
/**
84+
* Client-side metadata attached with this write.
85+
*
86+
* This field is only available when the `includeMetadata` option was set to `true` when creating a table
87+
* and the insert or update statement set the `_metadata` column.
88+
*/
89+
metadata?: string;
90+
7491
static fromRow(dbRow: CrudEntryJSON) {
7592
const data: CrudEntryDataJSON = JSON.parse(dbRow.data);
76-
return new CrudEntry(parseInt(dbRow.id), data.op, data.type, data.id, dbRow.tx_id, data.data);
93+
return new CrudEntry(
94+
parseInt(dbRow.id),
95+
data.op,
96+
data.type,
97+
data.id,
98+
dbRow.tx_id,
99+
data.data,
100+
data.old,
101+
data.metadata
102+
);
77103
}
78104

79105
constructor(
@@ -82,14 +108,18 @@ export class CrudEntry {
82108
table: string,
83109
id: string,
84110
transactionId?: number,
85-
opData?: Record<string, any>
111+
opData?: Record<string, any>,
112+
oldData?: Record<string, any>,
113+
metadata?: string
86114
) {
87115
this.clientId = clientId;
88116
this.id = id;
89117
this.op = op;
90118
this.opData = opData;
91119
this.table = table;
92120
this.transactionId = transactionId;
121+
this.oldData = oldData;
122+
this.metadata = metadata;
93123
}
94124

95125
/**

packages/common/src/db/schema/Schema.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,7 @@ export class Schema<S extends SchemaType = SchemaType> {
5353

5454
private convertToClassicTables(props: S) {
5555
return Object.entries(props).map(([name, table]) => {
56-
const convertedTable = new Table({
57-
name,
58-
columns: table.columns,
59-
indexes: table.indexes,
60-
localOnly: table.localOnly,
61-
insertOnly: table.insertOnly,
62-
viewName: table.viewNameOverride || name
63-
});
64-
return convertedTable;
56+
return table.copyWithName(name);
6557
});
6658
}
6759
}

packages/common/src/db/schema/Table.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export const DEFAULT_TABLE_OPTIONS = {
4646
localOnly: false,
4747
includeOld: false,
4848
includeMetadata: false,
49-
ignoreEmptyUpdate: false,
49+
ignoreEmptyUpdate: false
5050
};
5151

5252
export const InvalidSQLCharacters = /["'%,.#\s[\]]/;
@@ -143,6 +143,13 @@ export class Table<Columns extends ColumnsType = ColumnsType> {
143143
}
144144
}
145145

146+
copyWithName(name: string): Table {
147+
return new Table({
148+
...this.options,
149+
name
150+
});
151+
}
152+
146153
private isTableV1(arg: TableOptions | Columns): arg is TableOptions {
147154
return 'columns' in arg && Array.isArray(arg.columns);
148155
}

packages/common/tests/db/schema/Table.test.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -132,18 +132,18 @@ describe('Table', () => {
132132

133133
it('should handle options', () => {
134134
function createTable(options: TableV2Options) {
135-
return new Table({name: column.text}, options);
135+
return new Table({ name: column.text }, options);
136136
}
137137

138138
expect(createTable({}).toJSON().include_metadata).toBe(false);
139-
expect(createTable({includeMetadata: true}).toJSON().include_metadata).toBe(true);
139+
expect(createTable({ includeMetadata: true }).toJSON().include_metadata).toBe(true);
140140

141-
expect(createTable({includeOld: true}).toJSON().include_old).toBe(true);
142-
expect(createTable({includeOld: true}).toJSON().include_old_only_when_changed).toBe(false);
143-
expect(createTable({includeOld: 'when-changed'}).toJSON().include_old).toBe(true);
144-
expect(createTable({includeOld: 'when-changed'}).toJSON().include_old_only_when_changed).toBe(true);
141+
expect(createTable({ includeOld: true }).toJSON().include_old).toBe(true);
142+
expect(createTable({ includeOld: true }).toJSON().include_old_only_when_changed).toBe(false);
143+
expect(createTable({ includeOld: 'when-changed' }).toJSON().include_old).toBe(true);
144+
expect(createTable({ includeOld: 'when-changed' }).toJSON().include_old_only_when_changed).toBe(true);
145145

146-
expect(createTable({ignoreEmptyUpdate: true}).toJSON().ignore_empty_update).toBe(true);
146+
expect(createTable({ ignoreEmptyUpdate: true }).toJSON().ignore_empty_update).toBe(true);
147147
});
148148

149149
describe('validate', () => {
@@ -196,23 +196,32 @@ describe('Table', () => {
196196

197197
it('should throw an error for local-only tables with metadata', () => {
198198
expect(() =>
199-
new Table({
200-
name: column.text,
201-
}, { localOnly: true, includeMetadata: true }).validate()
199+
new Table(
200+
{
201+
name: column.text
202+
},
203+
{ localOnly: true, includeMetadata: true }
204+
).validate()
202205
).toThrowError("Can't include metadata for local-only tables.");
203206
});
204207

205208
it('should throw an error for local-only tables tracking old values', () => {
206209
expect(() =>
207-
new Table({
208-
name: column.text,
209-
}, { localOnly: true, includeOld: true }).validate()
210+
new Table(
211+
{
212+
name: column.text
213+
},
214+
{ localOnly: true, includeOld: true }
215+
).validate()
210216
).toThrowError("Can't include old values for local-only tables.");
211217

212218
expect(() =>
213-
new Table({
214-
name: column.text,
215-
}, { localOnly: true, includeOld: 'when-changed' }).validate()
219+
new Table(
220+
{
221+
name: column.text
222+
},
223+
{ localOnly: true, includeOld: 'when-changed' }
224+
).validate()
216225
).toThrowError("Can't include old values for local-only tables.");
217226
});
218227
});

packages/node/tests/crud.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { expect } from 'vitest';
2+
import { column, Schema, Table } from '@powersync/common';
3+
import { databaseTest } from './utils';
4+
5+
databaseTest('include metadata', async ({ database }) => {
6+
await database.init();
7+
const schema = new Schema({
8+
lists: new Table(
9+
{
10+
name: column.text
11+
},
12+
{ includeMetadata: true }
13+
)
14+
});
15+
await database.updateSchema(schema);
16+
await database.execute('INSERT INTO lists (id, name, _metadata) VALUES (uuid(), ?, ?);', ['entry', 'so meta']);
17+
18+
const batch = await database.getNextCrudTransaction();
19+
expect(batch?.crud[0].metadata).toBe('so meta');
20+
});
21+
22+
databaseTest('include old values', async ({ database }) => {
23+
await database.init();
24+
const schema = new Schema({
25+
lists: new Table(
26+
{
27+
name: column.text
28+
},
29+
{ includeOld: true }
30+
)
31+
});
32+
await database.updateSchema(schema);
33+
await database.execute('INSERT INTO lists (id, name) VALUES (uuid(), ?);', ['entry']);
34+
await database.execute('DELETE FROM ps_crud;');
35+
await database.execute('UPDATE lists SET name = ?', ['new name']);
36+
37+
const batch = await database.getNextCrudTransaction();
38+
expect(batch?.crud[0].oldData).toStrictEqual({name: 'entry'});
39+
});
40+
41+
databaseTest('include old values when changed', async ({ database }) => {
42+
await database.init();
43+
const schema = new Schema({
44+
lists: new Table(
45+
{
46+
name: column.text,
47+
content: column.text
48+
},
49+
{ includeOld: 'when-changed' }
50+
)
51+
});
52+
await database.updateSchema(schema);
53+
await database.execute('INSERT INTO lists (id, name, content) VALUES (uuid(), ?, ?);', ['name', 'content']);
54+
await database.execute('DELETE FROM ps_crud;');
55+
await database.execute('UPDATE lists SET name = ?', ['new name']);
56+
57+
const batch = await database.getNextCrudTransaction();
58+
expect(batch?.crud[0].oldData).toStrictEqual({name: 'name'});
59+
});
60+
61+
databaseTest('ignore empty update', async ({ database }) => {
62+
await database.init();
63+
const schema = new Schema({
64+
lists: new Table(
65+
{
66+
name: column.text
67+
},
68+
{ ignoreEmptyUpdate: true }
69+
)
70+
});
71+
await database.updateSchema(schema);
72+
await database.execute('INSERT INTO lists (id, name) VALUES (uuid(), ?);', ['name']);
73+
await database.execute('DELETE FROM ps_crud;');
74+
await database.execute('UPDATE lists SET name = ?', ['name']);
75+
76+
const batch = await database.getNextCrudTransaction();
77+
expect(batch).toBeNull();
78+
});

0 commit comments

Comments
 (0)