Skip to content

Commit aa7e46d

Browse files
committed
Add tests and changeset entry
1 parent 598fd96 commit aa7e46d

File tree

11 files changed

+173
-51
lines changed

11 files changed

+173
-51
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/SQLOpenFactory.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export interface SQLOpenOptions {
77
dbFilename: string;
88
/**
99
* Directory where the database file is located.
10-
*
10+
*
1111
* When set, the directory must exist when the database is opened, it will
1212
* not be created automatically.
1313
*/

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/client/sync/stream/AbstractStreamingSyncImplementation.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -497,10 +497,7 @@ The next upload iteration will be delayed.`);
497497
return [req, localDescriptions];
498498
}
499499

500-
protected async streamingSyncIteration(
501-
signal: AbortSignal,
502-
options?: PowerSyncConnectionOptions
503-
): Promise<void> {
500+
protected async streamingSyncIteration(signal: AbortSignal, options?: PowerSyncConnectionOptions): Promise<void> {
504501
await this.obtainLock({
505502
type: LockType.SYNC,
506503
signal,
@@ -673,7 +670,7 @@ The next upload iteration will be delayed.`);
673670
* (uses the same one), this should have some delay.
674671
*/
675672
await this.delayRetry();
676-
return ;
673+
return;
677674
}
678675
this.triggerCrudUpload();
679676
} else {

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

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export class SyncStatus {
3434

3535
/**
3636
* Indicates if the client is currently connected to the PowerSync service.
37-
*
37+
*
3838
* @returns {boolean} True if connected, false otherwise. Defaults to false if not specified.
3939
*/
4040
get connected() {
@@ -43,7 +43,7 @@ export class SyncStatus {
4343

4444
/**
4545
* Indicates if the client is in the process of establishing a connection to the PowerSync service.
46-
*
46+
*
4747
* @returns {boolean} True if connecting, false otherwise. Defaults to false if not specified.
4848
*/
4949
get connecting() {
@@ -53,7 +53,7 @@ export class SyncStatus {
5353
/**
5454
* Time that a last sync has fully completed, if any.
5555
* This timestamp is reset to null after a restart of the PowerSync service.
56-
*
56+
*
5757
* @returns {Date | undefined} The timestamp of the last successful sync, or undefined if no sync has completed.
5858
*/
5959
get lastSyncedAt() {
@@ -62,7 +62,7 @@ export class SyncStatus {
6262

6363
/**
6464
* Indicates whether there has been at least one full sync completed since initialization.
65-
*
65+
*
6666
* @returns {boolean | undefined} True if at least one sync has completed, false if no sync has completed,
6767
* or undefined when the state is still being loaded from the database.
6868
*/
@@ -72,7 +72,7 @@ export class SyncStatus {
7272

7373
/**
7474
* Provides the current data flow status regarding uploads and downloads.
75-
*
75+
*
7676
* @returns {SyncDataFlowStatus} An object containing:
7777
* - downloading: True if actively downloading changes (only when connected is also true)
7878
* - uploading: True if actively uploading changes
@@ -96,7 +96,7 @@ export class SyncStatus {
9696

9797
/**
9898
* Provides sync status information for all bucket priorities, sorted by priority (highest first).
99-
*
99+
*
100100
* @returns {SyncPriorityStatus[]} An array of status entries for different sync priority levels,
101101
* sorted with highest priorities (lower numbers) first.
102102
*/
@@ -105,18 +105,18 @@ export class SyncStatus {
105105
}
106106

107107
/**
108-
* Reports the sync status (a pair of {@link SyncStatus#hasSynced} and {@link SyncStatus#lastSyncedAt} fields)
108+
* Reports the sync status (a pair of {@link SyncStatus#hasSynced} and {@link SyncStatus#lastSyncedAt} fields)
109109
* for a specific bucket priority level.
110-
*
110+
*
111111
* When buckets with different priorities are declared, PowerSync may choose to synchronize higher-priority
112112
* buckets first. When a consistent view over all buckets for all priorities up until the given priority is
113113
* reached, PowerSync makes data from those buckets available before lower-priority buckets have finished
114114
* syncing.
115-
*
116-
* This method returns the status for the requested priority or the next higher priority level that has
117-
* status information available. This is because when PowerSync makes data for a given priority available,
115+
*
116+
* This method returns the status for the requested priority or the next higher priority level that has
117+
* status information available. This is because when PowerSync makes data for a given priority available,
118118
* all buckets in higher-priorities are guaranteed to be consistent with that checkpoint.
119-
*
119+
*
120120
* For example, if PowerSync just finished synchronizing buckets in priority level 3, calling this method
121121
* with a priority of 1 may return information for priority level 3.
122122
*
@@ -143,7 +143,7 @@ export class SyncStatus {
143143
/**
144144
* Compares this SyncStatus instance with another to determine if they are equal.
145145
* Equality is determined by comparing the serialized JSON representation of both instances.
146-
*
146+
*
147147
* @param {SyncStatus} status The SyncStatus instance to compare against
148148
* @returns {boolean} True if the instances are considered equal, false otherwise
149149
*/
@@ -154,7 +154,7 @@ export class SyncStatus {
154154
/**
155155
* Creates a human-readable string representation of the current sync status.
156156
* Includes information about connection state, sync completion, and data flow.
157-
*
157+
*
158158
* @returns {string} A string representation of the sync status
159159
*/
160160
getMessage() {
@@ -164,7 +164,7 @@ export class SyncStatus {
164164

165165
/**
166166
* Serializes the SyncStatus instance to a plain object.
167-
*
167+
*
168168
* @returns {SyncStatusOptions} A plain object representation of the sync status
169169
*/
170170
toJSON(): SyncStatusOptions {

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/src/db/BetterSQLite3DBAdapter.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ export class BetterSQLite3DBAdapter extends BaseObserver<DBAdapterListener> impl
6464
}
6565

6666
if (!directoryExists) {
67-
throw new Error(`The dbLocation directory at "${this.options.dbLocation}" does not exist. Please create it before opening the PowerSync database!`);
67+
throw new Error(
68+
`The dbLocation directory at "${this.options.dbLocation}" does not exist. Please create it before opening the PowerSync database!`
69+
);
6870
}
6971

7072
dbFilePath = path.join(this.options.dbLocation, dbFilePath);

packages/node/tests/PowerSyncDatabase.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as path from 'node:path';
2-
import * as fs from 'node:fs/promises';
32
import { Worker } from 'node:worker_threads';
43

54
import { vi, expect, test } from 'vitest';

0 commit comments

Comments
 (0)