@powersync/service-module-postgres-storage 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/migrations/scripts/1784900000000-source-metadata.d.ts +3 -0
  3. package/dist/migrations/scripts/1784900000000-source-metadata.js +22 -0
  4. package/dist/migrations/scripts/1784900000000-source-metadata.js.map +1 -0
  5. package/dist/migrations/scripts/1788000000000-sync-config-version-label.d.ts +3 -0
  6. package/dist/migrations/scripts/1788000000000-sync-config-version-label.js +12 -0
  7. package/dist/migrations/scripts/1788000000000-sync-config-version-label.js.map +1 -0
  8. package/dist/storage/PostgresBucketStorageFactory.d.ts +1 -0
  9. package/dist/storage/PostgresBucketStorageFactory.js +14 -7
  10. package/dist/storage/PostgresBucketStorageFactory.js.map +1 -1
  11. package/dist/storage/PostgresStorageProvider.js +2 -1
  12. package/dist/storage/PostgresStorageProvider.js.map +1 -1
  13. package/dist/storage/PostgresSyncRulesStorage.d.ts +4 -3
  14. package/dist/storage/PostgresSyncRulesStorage.js +17 -5
  15. package/dist/storage/PostgresSyncRulesStorage.js.map +1 -1
  16. package/dist/storage/batch/PostgresBucketBatch.d.ts +2 -2
  17. package/dist/storage/batch/PostgresBucketBatch.js +63 -94
  18. package/dist/storage/batch/PostgresBucketBatch.js.map +1 -1
  19. package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.d.ts +1 -1
  20. package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js +2 -2
  21. package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js.map +1 -1
  22. package/dist/storage/sync-rules/PostgresPersistedSyncConfigContent.js +5 -1
  23. package/dist/storage/sync-rules/PostgresPersistedSyncConfigContent.js.map +1 -1
  24. package/dist/types/models/ActiveCheckpoint.d.ts +2 -2
  25. package/dist/types/models/ActiveCheckpoint.js +3 -2
  26. package/dist/types/models/ActiveCheckpoint.js.map +1 -1
  27. package/dist/types/models/ActiveCheckpointNotification.d.ts +2 -2
  28. package/dist/types/models/BucketData.d.ts +6 -6
  29. package/dist/types/models/BucketData.js +7 -6
  30. package/dist/types/models/BucketData.js.map +1 -1
  31. package/dist/types/models/CurrentData.d.ts +1 -1
  32. package/dist/types/models/CurrentData.js +2 -1
  33. package/dist/types/models/CurrentData.js.map +1 -1
  34. package/dist/types/models/SdkReporting.d.ts +3 -3
  35. package/dist/types/models/SdkReporting.js +3 -5
  36. package/dist/types/models/SdkReporting.js.map +1 -1
  37. package/dist/types/models/SourceTable.d.ts +13 -8
  38. package/dist/types/models/SourceTable.js +11 -6
  39. package/dist/types/models/SourceTable.js.map +1 -1
  40. package/dist/types/models/SyncRules.d.ts +11 -10
  41. package/dist/types/models/SyncRules.js +12 -11
  42. package/dist/types/models/SyncRules.js.map +1 -1
  43. package/dist/types/models/WriteCheckpoint.d.ts +2 -2
  44. package/dist/types/models/WriteCheckpoint.js +3 -2
  45. package/dist/types/models/WriteCheckpoint.js.map +1 -1
  46. package/dist/types/types.d.ts +2 -0
  47. package/package.json +8 -8
  48. package/src/migrations/scripts/1784900000000-source-metadata.ts +31 -0
  49. package/src/migrations/scripts/1788000000000-sync-config-version-label.ts +18 -0
  50. package/src/storage/PostgresBucketStorageFactory.ts +15 -7
  51. package/src/storage/PostgresStorageProvider.ts +2 -1
  52. package/src/storage/PostgresSyncRulesStorage.ts +21 -6
  53. package/src/storage/batch/PostgresBucketBatch.ts +67 -98
  54. package/src/storage/checkpoints/PostgresWriteCheckpointAPI.ts +2 -2
  55. package/src/storage/sync-rules/PostgresPersistedSyncConfigContent.ts +8 -1
  56. package/src/types/models/ActiveCheckpoint.ts +3 -2
  57. package/src/types/models/BucketData.ts +7 -6
  58. package/src/types/models/CurrentData.ts +2 -1
  59. package/src/types/models/SdkReporting.ts +4 -4
  60. package/src/types/models/SourceTable.ts +12 -6
  61. package/src/types/models/SyncRules.ts +12 -11
  62. package/src/types/models/WriteCheckpoint.ts +3 -2
  63. package/test/src/__snapshots__/storage_sync.test.ts.snap +0 -995
  64. package/test/src/checkpoint_notifications.test.ts +8 -2
  65. package/test/src/storage.test.ts +12 -4
  66. package/test/src/storage_compacting.test.ts +2 -2
  67. package/test/src/storage_sync.test.ts +44 -0
  68. package/test/src/util.ts +1 -1
  69. package/tsconfig.tsbuildinfo +1 -1
@@ -19,6 +19,7 @@ import {
19
19
  utils
20
20
  } from '@powersync/service-core';
21
21
  import * as sync_rules from '@powersync/service-sync-rules';
22
+ import { orNull } from '@powersync/service-types';
22
23
  import * as timers from 'timers/promises';
23
24
  import * as t from 'ts-codec';
24
25
  import * as uuid from 'uuid';
@@ -64,10 +65,10 @@ const StatefulCheckpoint = models.ActiveCheckpoint.and(t.object({ state: t.Enum(
64
65
  const CheckpointWithStatus = StatefulCheckpoint.and(
65
66
  t.object({
66
67
  snapshot_done: t.boolean,
67
- no_checkpoint_before: t.string.or(t.Null),
68
+ no_checkpoint_before: orNull(t.string),
68
69
  can_checkpoint: t.boolean,
69
- keepalive_op: bigint.or(t.Null),
70
- new_last_checkpoint: bigint.or(t.Null),
70
+ keepalive_op: orNull(bigint),
71
+ new_last_checkpoint: orNull(bigint),
71
72
  created_checkpoint: t.boolean
72
73
  })
73
74
  );
@@ -144,6 +145,7 @@ export class PostgresBucketBatch
144
145
 
145
146
  async resolveTables(options: storage.ResolveTablesOptions): Promise<storage.ResolveTablesResult> {
146
147
  const syncRules = options.parsedSyncConfig?.hydratedSyncConfig ?? this.sync_rules;
148
+ const reconcile = options.reconcileSourceTables ?? storage.defaultSourceTableReconciler;
147
149
  const { connection_id, source } = options;
148
150
  const { schema, name: table, objectId, replicaIdColumns, connectionTag, sendsCompleteRows } = source;
149
151
 
@@ -154,9 +156,10 @@ export class PostgresBucketBatch
154
156
  }));
155
157
 
156
158
  return this.db.transaction(async (db) => {
157
- let sourceTableRow: SourceTableDecoded | null;
159
+ // Find records that overlap by name or relation id.
160
+ let candidateRows: SourceTableDecoded[];
158
161
  if (objectId != null) {
159
- sourceTableRow = await db.sql`
162
+ candidateRows = await db.sql`
160
163
  SELECT
161
164
  *
162
165
  FROM
@@ -164,15 +167,18 @@ export class PostgresBucketBatch
164
167
  WHERE
165
168
  group_id = ${{ type: 'int4', value: this.group_id }}
166
169
  AND connection_id = ${{ type: 'int4', value: connection_id }}
167
- AND relation_id = ${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }}
168
- AND schema_name = ${{ type: 'varchar', value: schema }}
169
- AND table_name = ${{ type: 'varchar', value: table }}
170
- AND replica_id_columns = ${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
170
+ AND (
171
+ relation_id = ${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }}
172
+ OR (
173
+ schema_name = ${{ type: 'varchar', value: schema }}
174
+ AND table_name = ${{ type: 'varchar', value: table }}
175
+ )
176
+ )
171
177
  `
172
178
  .decoded(models.SourceTable)
173
- .first();
179
+ .rows();
174
180
  } else {
175
- sourceTableRow = await db.sql`
181
+ candidateRows = await db.sql`
176
182
  SELECT
177
183
  *
178
184
  FROM
@@ -182,15 +188,42 @@ export class PostgresBucketBatch
182
188
  AND connection_id = ${{ type: 'int4', value: connection_id }}
183
189
  AND schema_name = ${{ type: 'varchar', value: schema }}
184
190
  AND table_name = ${{ type: 'varchar', value: table }}
185
- AND replica_id_columns = ${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
186
191
  `
187
192
  .decoded(models.SourceTable)
188
- .first();
193
+ .rows();
189
194
  }
190
195
 
191
- if (sourceTableRow == null) {
196
+ const candidateTables = candidateRows.map((row) => {
197
+ const table = this.sourceTableFromRow(row, connectionTag, syncRules);
198
+ table.storeCurrentData = sendsCompleteRows !== true;
199
+ return table;
200
+ });
201
+ const candidates = candidateTables.map((table) => table.clone());
202
+ const resolution = await reconcile({ source, candidates });
203
+ storage.validateSourceTableCandidateResolution(candidates, resolution);
204
+
205
+ for (const { id, sourceMetadata } of storage.diffSourceTableUpdates(candidateTables, resolution)) {
206
+ await db.sql`
207
+ UPDATE source_tables
208
+ SET
209
+ source_metadata = ${{ type: 'jsonb', value: sourceMetadata ?? null }}
210
+ WHERE
211
+ id = ${{ type: 'varchar', value: id.toString() }}
212
+ `.execute();
213
+ }
214
+
215
+ const materializedResolution = storage.materializeSourceTableResolution(candidateTables, resolution);
216
+ const compatibleTables = materializedResolution.compatibleTables;
217
+
218
+ // Keep one record, preferring one that has already been snapshotted.
219
+ const reuse = compatibleTables.find((candidate) => candidate.snapshotComplete) ?? compatibleTables[0] ?? null;
220
+
221
+ let sourceTable: storage.SourceTable;
222
+ if (reuse != null) {
223
+ sourceTable = reuse;
224
+ } else {
192
225
  const id = options.idGenerator ? postgresTableId(options.idGenerator()) : uuid.v4();
193
- sourceTableRow = await db.sql`
226
+ const insertedRow = await db.sql`
194
227
  INSERT INTO
195
228
  source_tables (
196
229
  id,
@@ -199,7 +232,8 @@ export class PostgresBucketBatch
199
232
  relation_id,
200
233
  schema_name,
201
234
  table_name,
202
- replica_id_columns
235
+ replica_id_columns,
236
+ source_metadata
203
237
  )
204
238
  VALUES
205
239
  (
@@ -209,95 +243,27 @@ export class PostgresBucketBatch
209
243
  ${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }},
210
244
  ${{ type: 'varchar', value: schema }},
211
245
  ${{ type: 'varchar', value: table }},
212
- ${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
246
+ ${{ type: 'jsonb', value: normalizedReplicaIdColumns }},
247
+ ${{ type: 'jsonb', value: resolution.newTableValues.sourceMetadata ?? null }}
213
248
  )
214
249
  RETURNING
215
250
  *
216
251
  `
217
252
  .decoded(models.SourceTable)
218
253
  .first();
254
+ sourceTable = this.sourceTableFromRow(insertedRow!, connectionTag, syncRules);
255
+ sourceTable.storeCurrentData = sendsCompleteRows !== true;
219
256
  }
220
257
 
221
- const sourceTable = new storage.SourceTable({
222
- id: sourceTableRow!.id,
223
- ref: source,
224
- objectId,
225
- replicaIdColumns,
226
- snapshotComplete: sourceTableRow!.snapshot_done ?? true,
227
- ...syncRules.getMatchingSources(source)
228
- });
229
- if (!sourceTable.snapshotComplete) {
230
- sourceTable.snapshotStatus = {
231
- totalEstimatedCount: Number(sourceTableRow!.snapshot_total_estimated_count ?? -1n),
232
- replicatedCount: Number(sourceTableRow!.snapshot_replicated_count ?? 0n),
233
- lastKey: sourceTableRow!.snapshot_last_key
234
- };
235
- }
236
- sourceTable.syncEvent = syncRules.tableTriggersEvent(source);
237
- sourceTable.syncData = sourceTable.bucketDataSources.length > 0;
238
- sourceTable.syncParameters = sourceTable.parameterLookupSources.length > 0;
239
- sourceTable.storeCurrentData = sendsCompleteRows !== true;
240
-
241
- let truncatedTables: SourceTableDecoded[] = [];
242
- if (objectId != null) {
243
- truncatedTables = await db.sql`
244
- SELECT
245
- *
246
- FROM
247
- source_tables
248
- WHERE
249
- group_id = ${{ type: 'int4', value: this.group_id }}
250
- AND connection_id = ${{ type: 'int4', value: connection_id }}
251
- AND id != ${{ type: 'varchar', value: sourceTableRow!.id }}
252
- AND (
253
- relation_id = ${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }}
254
- OR (
255
- schema_name = ${{ type: 'varchar', value: schema }}
256
- AND table_name = ${{ type: 'varchar', value: table }}
257
- )
258
- )
259
- `
260
- .decoded(models.SourceTable)
261
- .rows();
262
- } else {
263
- truncatedTables = await db.sql`
264
- SELECT
265
- *
266
- FROM
267
- source_tables
268
- WHERE
269
- group_id = ${{ type: 'int4', value: this.group_id }}
270
- AND connection_id = ${{ type: 'int4', value: connection_id }}
271
- AND id != ${{ type: 'varchar', value: sourceTableRow!.id }}
272
- AND (
273
- schema_name = ${{ type: 'varchar', value: schema }}
274
- AND table_name = ${{ type: 'varchar', value: table }}
275
- )
276
- `
277
- .decoded(models.SourceTable)
278
- .rows();
279
- }
258
+ const dropTables = [
259
+ ...materializedResolution.incompatibleTables,
260
+ // PostgreSQL storage only keeps one SourceTable per physical table.
261
+ ...compatibleTables.filter((candidate) => !storage.sourceTableIdEquals(candidate.id, sourceTable.id))
262
+ ];
280
263
 
281
264
  return {
282
265
  tables: [sourceTable],
283
- dropTables: truncatedTables.map((doc) => {
284
- const ref = { connectionTag, schema: doc.schema_name, name: doc.table_name };
285
- const dropTable = new storage.SourceTable({
286
- id: doc.id,
287
- ref,
288
- objectId: doc.relation_id?.object_id ?? 0,
289
- replicaIdColumns:
290
- doc.replica_id_columns?.map(
291
- (c) => ({ name: c.name, typeId: c.typeId, type: c.type }) satisfies ColumnDescriptor
292
- ) ?? [],
293
- snapshotComplete: doc.snapshot_done ?? true,
294
- ...syncRules.getMatchingSources(ref)
295
- });
296
- dropTable.syncEvent = syncRules.tableTriggersEvent(ref);
297
- dropTable.syncData = dropTable.bucketDataSources.length > 0;
298
- dropTable.syncParameters = dropTable.parameterLookupSources.length > 0;
299
- return dropTable;
300
- })
266
+ dropTables
301
267
  };
302
268
  });
303
269
  }
@@ -334,9 +300,10 @@ export class PostgresBucketBatch
334
300
  objectId: row.relation_id?.object_id,
335
301
  replicaIdColumns:
336
302
  row.replica_id_columns?.map(
337
- (c) => ({ name: c.name, typeId: c.typeId, type: c.type }) satisfies ColumnDescriptor
303
+ (column) => ({ name: column.name, typeId: column.type_oid, type: column.type }) satisfies ColumnDescriptor
338
304
  ) ?? [],
339
305
  snapshotComplete: row.snapshot_done ?? true,
306
+ sourceMetadata: row.source_metadata,
340
307
  ...syncRules.getMatchingSources(ref)
341
308
  });
342
309
 
@@ -357,7 +324,9 @@ export class PostgresBucketBatch
357
324
  // TODO maybe share with abstract class
358
325
  const { after, before, sourceTable, tag } = record;
359
326
  const storeCurrentData = this.options.store_current_data && sourceTable.storeCurrentData;
360
- // Only the table designated as event carrier by resolveTables may fire events.
327
+ // PostgreSQL storage resolves each physical table to one SourceTable. syncEvent records whether
328
+ // that table matches any event definition; each matching definition is evaluated once here and
329
+ // may produce multiple payloads from its matching queries.
361
330
  if (sourceTable.syncEvent) {
362
331
  for (const event of this.getTableEvents(sourceTable)) {
363
332
  this.iterateListeners((cb) =>
@@ -1368,10 +1337,10 @@ export class PostgresBucketBatch
1368
1337
  }
1369
1338
 
1370
1339
  /**
1371
- * Gets relevant {@link SqlEventDescriptor}s for the given {@link SourceTable}
1340
+ * Gets relevant {@link HydratedEventDescriptor}s for the given {@link SourceTable}
1372
1341
  * TODO maybe share this with an abstract class
1373
1342
  */
1374
- protected getTableEvents(table: storage.SourceTable): sync_rules.SqlEventDescriptor[] {
1343
+ protected getTableEvents(table: storage.SourceTable): sync_rules.HydratedEventDescriptor[] {
1375
1344
  return this.sync_rules.eventDescriptors.filter((evt) =>
1376
1345
  [...evt.getSourceTables()].some((sourceTable) => sourceTable.matches(table.ref))
1377
1346
  );
@@ -21,8 +21,8 @@ export class PostgresWriteCheckpointAPI implements storage.WriteCheckpointAPI {
21
21
  return this._mode;
22
22
  }
23
23
 
24
- setWriteCheckpointMode(mode: storage.WriteCheckpointMode): void {
25
- this._mode = mode;
24
+ setWriteCheckpointMode(config: storage.WriteCheckpointModeConfig): void {
25
+ this._mode = config.mode;
26
26
  }
27
27
 
28
28
  async batchCreateCustomWriteCheckpoints(
@@ -14,7 +14,8 @@ export class PostgresPersistedSyncConfigContent extends storage.PersistedSyncCon
14
14
  compiled_plan: row.sync_plan,
15
15
  replicationStreamName: row.slot_name,
16
16
  storageVersion: row.storage_version ?? storage.LEGACY_STORAGE_VERSION,
17
- syncConfigState: row.state as storage.SyncRuleState
17
+ syncConfigState: row.state as storage.SyncRuleState,
18
+ version_label: row.version_label ?? undefined
18
19
  });
19
20
  }
20
21
 
@@ -61,6 +62,12 @@ export class PostgresPersistedReplicationStream extends storage.PersistedReplica
61
62
  state: row.state as storage.SyncRuleState,
62
63
  storageVersion: row.storage_version ?? storage.LEGACY_STORAGE_VERSION
63
64
  });
65
+ if (this.storageVersion >= storage.STORAGE_VERSION_4) {
66
+ throw new ServiceError(
67
+ ErrorCode.PSYNC_S1005,
68
+ `Unsupported storage version ${this.storageVersion} for PostgreSQL storage`
69
+ );
70
+ }
64
71
  this.syncConfigContent = [new PostgresPersistedSyncConfigContent(this.db, this.row)];
65
72
  }
66
73
 
@@ -1,3 +1,4 @@
1
+ import { orNull } from '@powersync/service-types';
1
2
  import * as t from 'ts-codec';
2
3
  import { bigint, pgwire_number } from '../codecs.js';
3
4
 
@@ -7,8 +8,8 @@ import { bigint, pgwire_number } from '../codecs.js';
7
8
  */
8
9
  export const ActiveCheckpoint = t.object({
9
10
  id: pgwire_number,
10
- last_checkpoint: t.Null.or(bigint),
11
- last_checkpoint_lsn: t.Null.or(t.string)
11
+ last_checkpoint: orNull(bigint),
12
+ last_checkpoint_lsn: orNull(t.string)
12
13
  });
13
14
 
14
15
  export type ActiveCheckpoint = t.Encoded<typeof ActiveCheckpoint>;
@@ -1,3 +1,4 @@
1
+ import { orNull } from '@powersync/service-types';
1
2
  import * as t from 'ts-codec';
2
3
  import { bigint, hexBuffer, pgwire_number } from '../codecs.js';
3
4
 
@@ -13,13 +14,13 @@ export const BucketData = t.object({
13
14
  bucket_name: t.string,
14
15
  op_id: bigint,
15
16
  op: t.Enum(OpType),
16
- source_table: t.Null.or(t.string),
17
- source_key: t.Null.or(hexBuffer),
18
- table_name: t.string.or(t.Null),
19
- row_id: t.string.or(t.Null),
17
+ source_table: orNull(t.string),
18
+ source_key: orNull(hexBuffer),
19
+ table_name: orNull(t.string),
20
+ row_id: orNull(t.string),
20
21
  checksum: bigint,
21
- data: t.Null.or(t.string),
22
- target_op: t.Null.or(bigint)
22
+ data: orNull(t.string),
23
+ target_op: orNull(bigint)
23
24
  });
24
25
 
25
26
  export type BucketData = t.Encoded<typeof BucketData>;
@@ -1,3 +1,4 @@
1
+ import { orNull } from '@powersync/service-types';
1
2
  import * as t from 'ts-codec';
2
3
  import { bigint, hexBuffer, jsonb, pgwire_number } from '../codecs.js';
3
4
 
@@ -26,7 +27,7 @@ export const V3CurrentData = t.object({
26
27
  lookups: t.array(hexBuffer),
27
28
  source_key: hexBuffer,
28
29
  source_table: t.string,
29
- pending_delete: t.Null.or(bigint)
30
+ pending_delete: orNull(bigint)
30
31
  });
31
32
 
32
33
  export type V1CurrentData = t.Encoded<typeof V1CurrentData>;
@@ -1,3 +1,4 @@
1
+ import { orNull } from '@powersync/service-types';
1
2
  import * as t from 'ts-codec';
2
3
  import { bigint, jsonb } from '../codecs.js';
3
4
 
@@ -11,12 +12,11 @@ export type Sdks = t.Encoded<typeof Sdks>;
11
12
 
12
13
  export const SdkReporting = t.object({
13
14
  users: bigint,
14
- sdks: t
15
- .object({
15
+ sdks: orNull(
16
+ t.object({
16
17
  data: jsonb<Sdks[]>(t.array(Sdks))
17
18
  })
18
- .optional()
19
- .or(t.Null)
19
+ ).optional()
20
20
  });
21
21
 
22
22
  export type SdkReporting = t.Encoded<typeof SdkReporting>;
@@ -1,3 +1,5 @@
1
+ import { JsonValue } from '@powersync/service-core';
2
+ import { orNull } from '@powersync/service-types';
1
3
  import * as t from 'ts-codec';
2
4
  import { bigint, hexBuffer, jsonb, jsonb_raw, pgwire_number } from '../codecs.js';
3
5
 
@@ -14,21 +16,25 @@ export const ColumnDescriptor = t.object({
14
16
  /**
15
17
  * Some data sources have a type id that can be used to identify the type of the column
16
18
  */
17
- typeId: t.number.optional()
19
+ type_oid: t.number.optional()
18
20
  });
19
21
 
20
22
  export const SourceTable = t.object({
21
23
  id: t.string,
22
24
  group_id: pgwire_number,
23
25
  connection_id: bigint,
24
- relation_id: t.Null.or(jsonb_raw<StoredRelationId>()),
26
+ relation_id: orNull(jsonb_raw<StoredRelationId>()),
25
27
  schema_name: t.string,
26
28
  table_name: t.string,
27
- replica_id_columns: t.Null.or(jsonb(t.array(ColumnDescriptor))),
29
+ replica_id_columns: orNull(jsonb(t.array(ColumnDescriptor))),
28
30
  snapshot_done: t.boolean,
29
- snapshot_total_estimated_count: t.Null.or(bigint),
30
- snapshot_replicated_count: t.Null.or(bigint),
31
- snapshot_last_key: t.Null.or(hexBuffer)
31
+ snapshot_total_estimated_count: orNull(bigint),
32
+ snapshot_replicated_count: orNull(bigint),
33
+ snapshot_last_key: orNull(hexBuffer),
34
+ /**
35
+ * Source-specific metadata. Null for legacy records.
36
+ */
37
+ source_metadata: orNull(jsonb_raw<JsonValue>())
32
38
  });
33
39
 
34
40
  export type SourceTable = t.Encoded<typeof SourceTable>;
@@ -1,5 +1,5 @@
1
1
  import { framework, storage } from '@powersync/service-core';
2
- import { ReplicationError } from '@powersync/service-types';
2
+ import { orNull, ReplicationError } from '@powersync/service-types';
3
3
  import * as t from 'ts-codec';
4
4
  import { bigint, pgwire_number } from '../codecs.js';
5
5
  import { jsonContainerObject } from './json.js';
@@ -16,42 +16,43 @@ export const SyncRules = t.object({
16
16
  /**
17
17
  * May be set if snapshot_done = false, if the replication stream requires it.
18
18
  */
19
- snapshot_lsn: t.Null.or(t.string),
19
+ snapshot_lsn: orNull(t.string),
20
20
  /**
21
21
  * The last consistent checkpoint.
22
22
  *
23
23
  * There may be higher OpIds used in the database if we're in the middle of replicating a large transaction.
24
24
  */
25
- last_checkpoint: t.Null.or(bigint),
25
+ last_checkpoint: orNull(bigint),
26
26
  /**
27
27
  * The LSN associated with the last consistent checkpoint.
28
28
  */
29
- last_checkpoint_lsn: t.Null.or(t.string),
29
+ last_checkpoint_lsn: orNull(t.string),
30
30
  /**
31
31
  * If set, no new checkpoints may be created < this value.
32
32
  */
33
- no_checkpoint_before: t.Null.or(t.string),
33
+ no_checkpoint_before: orNull(t.string),
34
34
  slot_name: t.string,
35
35
  /**
36
36
  * Last time we persisted a checkpoint.
37
37
  *
38
38
  * This may be old if no data is incoming.
39
39
  */
40
- last_checkpoint_ts: t.Null.or(framework.codecs.date),
40
+ last_checkpoint_ts: orNull(framework.codecs.date),
41
41
  /**
42
42
  * Last time we persisted a checkpoint or keepalive.
43
43
  *
44
44
  * This should stay fairly current while replicating.
45
45
  */
46
- last_keepalive_ts: t.Null.or(framework.codecs.date),
46
+ last_keepalive_ts: orNull(framework.codecs.date),
47
47
  /**
48
48
  * If an error is stopping replication, it will be stored here.
49
49
  */
50
- last_fatal_error: t.Null.or(t.string),
51
- keepalive_op: t.Null.or(bigint),
52
- storage_version: t.Null.or(pgwire_number).optional(),
50
+ last_fatal_error: orNull(t.string),
51
+ keepalive_op: orNull(bigint),
52
+ storage_version: orNull(pgwire_number).optional(),
53
+ version_label: orNull(t.string).optional(),
53
54
  content: t.string,
54
- sync_plan: t.Null.or(
55
+ sync_plan: orNull(
55
56
  jsonContainerObject(
56
57
  t.object({
57
58
  plan: t.any,
@@ -1,4 +1,5 @@
1
1
  import { framework } from '@powersync/service-core';
2
+ import { orNull } from '@powersync/service-types';
2
3
  import * as t from 'ts-codec';
3
4
  import { bigint, jsonb } from '../codecs.js';
4
5
 
@@ -6,7 +7,7 @@ export const WriteCheckpoint = t.object({
6
7
  user_id: t.string,
7
8
  lsns: jsonb(t.record(t.string)),
8
9
  write_checkpoint: bigint,
9
- checkpoint_requested_at: t.Null.or(framework.codecs.date)
10
+ checkpoint_requested_at: orNull(framework.codecs.date)
10
11
  });
11
12
 
12
13
  export type WriteCheckpoint = t.Encoded<typeof WriteCheckpoint>;
@@ -16,7 +17,7 @@ export const CustomWriteCheckpoint = t.object({
16
17
  user_id: t.string,
17
18
  write_checkpoint: bigint,
18
19
  sync_rules_id: bigint,
19
- checkpoint_requested_at: t.Null.or(framework.codecs.date)
20
+ checkpoint_requested_at: orNull(framework.codecs.date)
20
21
  });
21
22
 
22
23
  export type CustomWriteCheckpoint = t.Encoded<typeof CustomWriteCheckpoint>;