Skip to content

Commit a0ba3b0

Browse files
committed
Merge branch 'main' into op-sqlite-tests
2 parents 20031a9 + d3c8d7f commit a0ba3b0

16 files changed

Lines changed: 432 additions & 69 deletions

File tree

.changeset/cuddly-news-carry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/diagnostics-app': patch
3+
---
4+
5+
Fix handling of partial checkpoints

.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 `trackPreviousValues` option on `Table` which sets `CrudEntry.previousValues` to previous values on updates.
6+
- Add `trackMetadata` 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 `ignoreEmptyUpdates` option which skips creating CRUD entries for updates that don't change any values.

.changeset/polite-news-sneeze.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/common': patch
3+
---
4+
5+
Fix applying bucket state around partial syncs.

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 `trackPreviousValues` option has been enabled, this tracks previous values for
70+
* `UPDATE` and `DELETE` statements.
71+
*/
72+
previousValues?: 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 `trackMetadata` 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+
previousValues?: 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.previousValues = previousValues;
122+
this.metadata = metadata;
93123
}
94124

95125
/**

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,9 @@ export class SqliteBucketStorage extends BaseObserver<BucketStorageListener> imp
154154
return { ready: false, checkpointValid: false, checkpointFailures: r.checkpointFailures };
155155
}
156156

157-
const buckets = checkpoint.buckets;
157+
let buckets = checkpoint.buckets;
158158
if (priority !== undefined) {
159-
buckets.filter((b) => hasMatchingPriority(priority, b));
159+
buckets = buckets.filter((b) => hasMatchingPriority(priority, b));
160160
}
161161
const bucketNames = buckets.map((b) => b.bucket);
162162
await this.writeTransaction(async (tx) => {

packages/common/src/db/crud/SyncProgress.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export interface ProgressWithOperations {
5454
* Additionally, the {@link SyncProgress.untilPriority} method can be used to otbain progress towards
5555
* a specific priority (instead of the progress for the entire download).
5656
*
57-
* The reported progress always reflects the status towards th end of a sync iteration (after
57+
* The reported progress always reflects the status towards the end of a sync iteration (after
5858
* which a consistent snapshot of all buckets is available locally).
5959
*
6060
* In rare cases (in particular, when a [compacting](https://docs.powersync.com/usage/lifecycle-maintenance/compacting-buckets)

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: 78 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,34 @@ import { Index } from './Index.js';
1010
import { IndexedColumn } from './IndexedColumn.js';
1111
import { TableV2 } from './TableV2.js';
1212

13-
export interface TableOptions {
13+
interface SharedTableOptions {
14+
localOnly?: boolean;
15+
insertOnly?: boolean;
16+
viewName?: string;
17+
trackPrevious?: boolean | TrackPreviousOptions;
18+
trackMetadata?: boolean;
19+
ignoreEmptyUpdates?: boolean;
20+
}
21+
22+
/** Whether to include previous column values when PowerSync tracks local changes.
23+
*
24+
* Including old values may be helpful for some backend connector implementations, which is
25+
* why it can be enabled on per-table or per-columm basis.
26+
*/
27+
export interface TrackPreviousOptions {
28+
/** When defined, a list of column names for which old values should be tracked. */
29+
columns?: string[];
30+
/** When enabled, only include values that have actually been changed by an update. */
31+
onlyWhenChanged?: boolean;
32+
}
33+
34+
export interface TableOptions extends SharedTableOptions {
1435
/**
1536
* The synced table name, matching sync rules
1637
*/
1738
name: string;
1839
columns: Column[];
1940
indexes?: Index[];
20-
localOnly?: boolean;
21-
insertOnly?: boolean;
22-
viewName?: string;
2341
}
2442

2543
export type RowType<T extends TableV2<any>> = {
@@ -30,17 +48,17 @@ export type RowType<T extends TableV2<any>> = {
3048

3149
export type IndexShorthand = Record<string, string[]>;
3250

33-
export interface TableV2Options {
51+
export interface TableV2Options extends SharedTableOptions {
3452
indexes?: IndexShorthand;
35-
localOnly?: boolean;
36-
insertOnly?: boolean;
37-
viewName?: string;
3853
}
3954

4055
export const DEFAULT_TABLE_OPTIONS = {
4156
indexes: [],
4257
insertOnly: false,
43-
localOnly: false
58+
localOnly: false,
59+
trackPrevious: false,
60+
trackMetadata: false,
61+
ignoreEmptyUpdates: false
4462
};
4563

4664
export const InvalidSQLCharacters = /["'%,.#\s[\]]/;
@@ -137,17 +155,23 @@ export class Table<Columns extends ColumnsType = ColumnsType> {
137155
}
138156
}
139157

158+
copyWithName(name: string): Table {
159+
return new Table({
160+
...this.options,
161+
name
162+
});
163+
}
164+
140165
private isTableV1(arg: TableOptions | Columns): arg is TableOptions {
141166
return 'columns' in arg && Array.isArray(arg.columns);
142167
}
143168

144169
private initTableV1(options: TableOptions) {
145170
this.options = {
146171
...options,
147-
indexes: options.indexes || [],
148-
insertOnly: options.insertOnly ?? DEFAULT_TABLE_OPTIONS.insertOnly,
149-
localOnly: options.localOnly ?? DEFAULT_TABLE_OPTIONS.localOnly
172+
indexes: options.indexes || []
150173
};
174+
this.applyDefaultOptions();
151175
}
152176

153177
private initTableV2(columns: Columns, options?: TableV2Options) {
@@ -173,14 +197,26 @@ export class Table<Columns extends ColumnsType = ColumnsType> {
173197
name: '',
174198
columns: convertedColumns,
175199
indexes: convertedIndexes,
176-
insertOnly: options?.insertOnly ?? DEFAULT_TABLE_OPTIONS.insertOnly,
177-
localOnly: options?.localOnly ?? DEFAULT_TABLE_OPTIONS.localOnly,
178-
viewName: options?.viewName
200+
viewName: options?.viewName,
201+
insertOnly: options?.insertOnly,
202+
localOnly: options?.localOnly,
203+
trackPrevious: options?.trackPrevious,
204+
trackMetadata: options?.trackMetadata,
205+
ignoreEmptyUpdates: options?.ignoreEmptyUpdates
179206
};
207+
this.applyDefaultOptions();
180208

181209
this._mappedColumns = columns;
182210
}
183211

212+
private applyDefaultOptions() {
213+
this.options.insertOnly ??= DEFAULT_TABLE_OPTIONS.insertOnly;
214+
this.options.localOnly ??= DEFAULT_TABLE_OPTIONS.localOnly;
215+
this.options.trackPrevious ??= DEFAULT_TABLE_OPTIONS.trackPrevious;
216+
this.options.trackMetadata ??= DEFAULT_TABLE_OPTIONS.trackMetadata;
217+
this.options.ignoreEmptyUpdates ??= DEFAULT_TABLE_OPTIONS.ignoreEmptyUpdates;
218+
}
219+
184220
get name() {
185221
return this.options.name;
186222
}
@@ -212,11 +248,23 @@ export class Table<Columns extends ColumnsType = ColumnsType> {
212248
}
213249

214250
get localOnly() {
215-
return this.options.localOnly ?? false;
251+
return this.options.localOnly!;
216252
}
217253

218254
get insertOnly() {
219-
return this.options.insertOnly ?? false;
255+
return this.options.insertOnly!;
256+
}
257+
258+
get trackPrevious() {
259+
return this.options.trackPrevious!;
260+
}
261+
262+
get trackMetadata() {
263+
return this.options.trackMetadata!;
264+
}
265+
266+
get ignoreEmptyUpdates() {
267+
return this.options.ignoreEmptyUpdates!;
220268
}
221269

222270
get internalName() {
@@ -250,6 +298,13 @@ export class Table<Columns extends ColumnsType = ColumnsType> {
250298
throw new Error(`Table has too many columns. The maximum number of columns is ${MAX_AMOUNT_OF_COLUMNS}.`);
251299
}
252300

301+
if (this.trackMetadata && this.localOnly) {
302+
throw new Error(`Can't include metadata for local-only tables.`);
303+
}
304+
if (this.trackPrevious != false && this.localOnly) {
305+
throw new Error(`Can't include old values for local-only tables.`);
306+
}
307+
253308
const columnNames = new Set<string>();
254309
columnNames.add('id');
255310
for (const column of this.columns) {
@@ -286,11 +341,17 @@ export class Table<Columns extends ColumnsType = ColumnsType> {
286341
}
287342

288343
toJSON() {
344+
const trackPrevious = this.trackPrevious;
345+
289346
return {
290347
name: this.name,
291348
view_name: this.viewName,
292349
local_only: this.localOnly,
293350
insert_only: this.insertOnly,
351+
include_old: trackPrevious && ((trackPrevious as any).columns ?? true),
352+
include_old_only_when_changed: typeof trackPrevious == 'object' && trackPrevious.onlyWhenChanged == true,
353+
include_metadata: this.trackMetadata,
354+
ignore_empty_update: this.ignoreEmptyUpdates,
294355
columns: this.columns.map((c) => c.toJSON()),
295356
indexes: this.indexes.map((e) => e.toJSON(this))
296357
};

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ describe('Schema', () => {
9090
view_name: 'users',
9191
local_only: false,
9292
insert_only: false,
93+
ignore_empty_update: false,
94+
include_metadata: false,
95+
include_old: false,
96+
include_old_only_when_changed: false,
9397
columns: [
9498
{ name: 'name', type: 'TEXT' },
9599
{ name: 'age', type: 'INTEGER' }
@@ -101,6 +105,10 @@ describe('Schema', () => {
101105
view_name: 'posts',
102106
local_only: false,
103107
insert_only: false,
108+
ignore_empty_update: false,
109+
include_metadata: false,
110+
include_old: false,
111+
include_old_only_when_changed: false,
104112
columns: [
105113
{ name: 'title', type: 'TEXT' },
106114
{ name: 'content', type: 'TEXT' }

0 commit comments

Comments
 (0)