@powersync/service-module-postgres-storage 0.0.0-dev-20260602073133 → 0.0.0-dev-20260803144812

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 (105) hide show
  1. package/CHANGELOG.md +101 -6
  2. package/dist/migrations/scripts/1782950400000-checkpoint-requested-at.d.ts +3 -0
  3. package/dist/migrations/scripts/1782950400000-checkpoint-requested-at.js +42 -0
  4. package/dist/migrations/scripts/1782950400000-checkpoint-requested-at.js.map +1 -0
  5. package/dist/{@types/storage → storage}/PostgresBucketStorageFactory.d.ts +13 -12
  6. package/dist/storage/PostgresBucketStorageFactory.js +59 -55
  7. package/dist/storage/PostgresBucketStorageFactory.js.map +1 -1
  8. package/dist/{@types/storage → storage}/PostgresCompactor.d.ts +3 -1
  9. package/dist/storage/PostgresCompactor.js +21 -1
  10. package/dist/storage/PostgresCompactor.js.map +1 -1
  11. package/dist/storage/PostgresReportStorage.js.map +1 -1
  12. package/dist/storage/PostgresStorageProvider.js +2 -1
  13. package/dist/storage/PostgresStorageProvider.js.map +1 -1
  14. package/dist/{@types/storage → storage}/PostgresSyncRulesStorage.d.ts +36 -11
  15. package/dist/storage/PostgresSyncRulesStorage.js +250 -177
  16. package/dist/storage/PostgresSyncRulesStorage.js.map +1 -1
  17. package/dist/{@types/storage → storage}/batch/PostgresBucketBatch.d.ts +8 -24
  18. package/dist/storage/batch/PostgresBucketBatch.js +229 -194
  19. package/dist/storage/batch/PostgresBucketBatch.js.map +1 -1
  20. package/dist/{@types/storage → storage}/checkpoints/PostgresWriteCheckpointAPI.d.ts +1 -1
  21. package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js +161 -24
  22. package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js.map +1 -1
  23. package/dist/{@types/storage → storage}/current-data-store.d.ts +8 -2
  24. package/dist/storage/current-data-store.js +66 -11
  25. package/dist/storage/current-data-store.js.map +1 -1
  26. package/dist/{@types/storage → storage}/storage-index.d.ts +1 -0
  27. package/dist/storage/storage-index.js +1 -0
  28. package/dist/storage/storage-index.js.map +1 -1
  29. package/dist/storage/sync-rules/PostgresPersistedSyncConfigContent.d.ts +17 -0
  30. package/dist/storage/sync-rules/PostgresPersistedSyncConfigContent.js +90 -0
  31. package/dist/storage/sync-rules/PostgresPersistedSyncConfigContent.js.map +1 -0
  32. package/dist/{@types/types → types}/models/WriteCheckpoint.d.ts +2 -0
  33. package/dist/types/models/WriteCheckpoint.js +5 -2
  34. package/dist/types/models/WriteCheckpoint.js.map +1 -1
  35. package/dist/utils/checkpoints.d.ts +9 -0
  36. package/dist/utils/checkpoints.js +26 -0
  37. package/dist/utils/checkpoints.js.map +1 -0
  38. package/dist/utils/test-utils.js +2 -2
  39. package/dist/utils/test-utils.js.map +1 -1
  40. package/package.json +10 -22
  41. package/src/migrations/scripts/1782950400000-checkpoint-requested-at.ts +51 -0
  42. package/src/storage/PostgresBucketStorageFactory.ts +67 -58
  43. package/src/storage/PostgresCompactor.ts +25 -1
  44. package/src/storage/PostgresReportStorage.ts +1 -2
  45. package/src/storage/PostgresStorageProvider.ts +2 -1
  46. package/src/storage/PostgresSyncRulesStorage.ts +275 -191
  47. package/src/storage/batch/PostgresBucketBatch.ts +256 -214
  48. package/src/storage/checkpoints/PostgresWriteCheckpointAPI.ts +174 -24
  49. package/src/storage/current-data-store.ts +66 -11
  50. package/src/storage/storage-index.ts +1 -0
  51. package/src/storage/sync-rules/PostgresPersistedSyncConfigContent.ts +98 -0
  52. package/src/types/models/WriteCheckpoint.ts +5 -2
  53. package/src/utils/checkpoints.ts +31 -0
  54. package/src/utils/test-utils.ts +2 -2
  55. package/test/src/__snapshots__/storage_sync.test.ts.snap +0 -582
  56. package/test/src/checkpoint_notifications.test.ts +522 -0
  57. package/test/src/migrations.test.ts +1 -1
  58. package/test/src/storage.test.ts +226 -10
  59. package/test/src/storage_compacting.test.ts +15 -7
  60. package/test/src/storage_sync.test.ts +11 -8
  61. package/test/tsconfig.json +1 -3
  62. package/tsconfig.json +1 -4
  63. package/tsconfig.tsbuildinfo +1 -0
  64. package/dist/.tsbuildinfo +0 -1
  65. package/dist/@types/storage/sync-rules/PostgresPersistedSyncRulesContent.d.ts +0 -9
  66. package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js +0 -49
  67. package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js.map +0 -1
  68. package/src/storage/sync-rules/PostgresPersistedSyncRulesContent.ts +0 -54
  69. /package/dist/{@types/index.d.ts → index.d.ts} +0 -0
  70. /package/dist/{@types/migrations → migrations}/PostgresMigrationAgent.d.ts +0 -0
  71. /package/dist/{@types/migrations → migrations}/PostgresMigrationStore.d.ts +0 -0
  72. /package/dist/{@types/migrations → migrations}/migration-utils.d.ts +0 -0
  73. /package/dist/{@types/migrations → migrations}/scripts/1684951997326-init.d.ts +0 -0
  74. /package/dist/{@types/migrations → migrations}/scripts/1749024804042-snapshot-progress.d.ts +0 -0
  75. /package/dist/{@types/migrations → migrations}/scripts/1756282360128-connection-reporting.d.ts +0 -0
  76. /package/dist/{@types/migrations → migrations}/scripts/1771232439485-storage-version.d.ts +0 -0
  77. /package/dist/{@types/migrations → migrations}/scripts/1771424826685-current-data-pending-deletes.d.ts +0 -0
  78. /package/dist/{@types/migrations → migrations}/scripts/1771491856000-sync-plan.d.ts +0 -0
  79. /package/dist/{@types/module → module}/PostgresStorageModule.d.ts +0 -0
  80. /package/dist/{@types/storage → storage}/PostgresReportStorage.d.ts +0 -0
  81. /package/dist/{@types/storage → storage}/PostgresStorageProvider.d.ts +0 -0
  82. /package/dist/{@types/storage → storage}/batch/OperationBatch.d.ts +0 -0
  83. /package/dist/{@types/storage → storage}/batch/PostgresPersistedBatch.d.ts +0 -0
  84. /package/dist/{@types/storage → storage}/table-id.d.ts +0 -0
  85. /package/dist/{@types/types → types}/codecs.d.ts +0 -0
  86. /package/dist/{@types/types → types}/models/ActiveCheckpoint.d.ts +0 -0
  87. /package/dist/{@types/types → types}/models/ActiveCheckpointNotification.d.ts +0 -0
  88. /package/dist/{@types/types → types}/models/BucketData.d.ts +0 -0
  89. /package/dist/{@types/types → types}/models/BucketParameters.d.ts +0 -0
  90. /package/dist/{@types/types → types}/models/CurrentData.d.ts +0 -0
  91. /package/dist/{@types/types → types}/models/Instance.d.ts +0 -0
  92. /package/dist/{@types/types → types}/models/Migration.d.ts +0 -0
  93. /package/dist/{@types/types → types}/models/SdkReporting.d.ts +0 -0
  94. /package/dist/{@types/types → types}/models/SourceTable.d.ts +0 -0
  95. /package/dist/{@types/types → types}/models/SyncRules.d.ts +0 -0
  96. /package/dist/{@types/types → types}/models/json.d.ts +0 -0
  97. /package/dist/{@types/types → types}/models/models-index.d.ts +0 -0
  98. /package/dist/{@types/types → types}/types.d.ts +0 -0
  99. /package/dist/{@types/utils → utils}/application-name.d.ts +0 -0
  100. /package/dist/{@types/utils → utils}/bson.d.ts +0 -0
  101. /package/dist/{@types/utils → utils}/bucket-data.d.ts +0 -0
  102. /package/dist/{@types/utils → utils}/db.d.ts +0 -0
  103. /package/dist/{@types/utils → utils}/test-utils.d.ts +0 -0
  104. /package/dist/{@types/utils → utils}/ts-codec.d.ts +0 -0
  105. /package/dist/{@types/utils → utils}/utils-index.d.ts +0 -0
@@ -55,7 +55,7 @@ export class PostgresBucketBatch extends BaseObserver {
55
55
  this.options = options;
56
56
  this.logger = options.logger;
57
57
  this.db = options.db;
58
- this.group_id = options.group_id;
58
+ this.group_id = options.replicationStreamId;
59
59
  this.last_checkpoint_lsn = options.last_checkpoint_lsn;
60
60
  this.resumeFromLsn = options.resumeFromLsn;
61
61
  this.skipExistingRows = options.skip_existing_rows;
@@ -71,9 +71,6 @@ export class PostgresBucketBatch extends BaseObserver {
71
71
  this.persisted_op = options.keep_alive_op;
72
72
  }
73
73
  }
74
- get lastCheckpointLsn() {
75
- return this.last_checkpoint_lsn;
76
- }
77
74
  async [Symbol.asyncDispose]() {
78
75
  if (this.batch != null || this.write_checkpoint_batch.length > 0) {
79
76
  // We don't error here, since:
@@ -87,9 +84,9 @@ export class PostgresBucketBatch extends BaseObserver {
87
84
  await this[Symbol.asyncDispose]();
88
85
  }
89
86
  async resolveTables(options) {
90
- const syncRules = options.syncRules ?? this.sync_rules;
87
+ const syncRules = options.parsedSyncConfig?.hydratedSyncConfig ?? this.sync_rules;
91
88
  const { connection_id, source } = options;
92
- const { schema, name: table, objectId, replicaIdColumns, connectionTag } = source;
89
+ const { schema, name: table, objectId, replicaIdColumns, connectionTag, sendsCompleteRows } = source;
93
90
  const normalizedReplicaIdColumns = replicaIdColumns.map((column) => ({
94
91
  name: column.name,
95
92
  type: column.type,
@@ -177,6 +174,7 @@ export class PostgresBucketBatch extends BaseObserver {
177
174
  sourceTable.syncEvent = syncRules.tableTriggersEvent(source);
178
175
  sourceTable.syncData = sourceTable.bucketDataSources.length > 0;
179
176
  sourceTable.syncParameters = sourceTable.parameterLookupSources.length > 0;
177
+ sourceTable.storeCurrentData = sendsCompleteRows !== true;
180
178
  let truncatedTables = [];
181
179
  if (objectId != null) {
182
180
  truncatedTables = await db.sql `
@@ -279,17 +277,21 @@ export class PostgresBucketBatch extends BaseObserver {
279
277
  async save(record) {
280
278
  // TODO maybe share with abstract class
281
279
  const { after, before, sourceTable, tag } = record;
282
- for (const event of this.getTableEvents(sourceTable)) {
283
- this.iterateListeners((cb) => cb.replicationEvent?.({
284
- batch: this,
285
- table: sourceTable,
286
- data: {
287
- op: tag,
288
- after: after && utils.isCompleteRow(this.options.store_current_data, after) ? after : undefined,
289
- before: before && utils.isCompleteRow(this.options.store_current_data, before) ? before : undefined
290
- },
291
- event
292
- }));
280
+ const storeCurrentData = this.options.store_current_data && sourceTable.storeCurrentData;
281
+ // Only the table designated as event carrier by resolveTables may fire events.
282
+ if (sourceTable.syncEvent) {
283
+ for (const event of this.getTableEvents(sourceTable)) {
284
+ this.iterateListeners((cb) => cb.replicationEvent?.({
285
+ batch: this,
286
+ table: sourceTable,
287
+ data: {
288
+ op: tag,
289
+ after: after && utils.isCompleteRow(storeCurrentData, after) ? after : undefined,
290
+ before: before && utils.isCompleteRow(storeCurrentData, before) ? before : undefined
291
+ },
292
+ event
293
+ }));
294
+ }
293
295
  }
294
296
  /**
295
297
  * Return if the table is just an event table
@@ -332,7 +334,9 @@ export class PostgresBucketBatch extends BaseObserver {
332
334
  let processedCount = 0;
333
335
  while (lastBatchCount == BATCH_LIMIT) {
334
336
  lastBatchCount = 0;
337
+ let clearedError = false;
335
338
  await this.withReplicationTransaction(async (db) => {
339
+ clearedError = false;
336
340
  const persistedBatch = new PostgresPersistedBatch({
337
341
  group_id: this.group_id,
338
342
  storageConfig: this.storageConfig,
@@ -370,10 +374,14 @@ export class PostgresBucketBatch extends BaseObserver {
370
374
  }
371
375
  }
372
376
  const { flushedAny } = await persistedBatch.flush(db);
373
- if (flushedAny) {
374
- await this.clearError(db);
375
- }
377
+ clearedError = flushedAny && !this.clearedError;
376
378
  });
379
+ if (clearedError) {
380
+ // No need to clear an error more than once per batch, since an error would always result in restarting the batch.
381
+ // Cleared outside the replication transaction - see flushInner.
382
+ await this.clearError();
383
+ this.clearedError = true;
384
+ }
377
385
  }
378
386
  if (processedCount == 0) {
379
387
  // The op sequence should not have progressed
@@ -419,10 +427,21 @@ export class PostgresBucketBatch extends BaseObserver {
419
427
  }
420
428
  await this.hooks?.beforeBatchFlush?.(this);
421
429
  let resumeBatch = null;
430
+ let clearedError = false;
422
431
  const lastOp = await this.withReplicationTransaction(async (db) => {
423
- resumeBatch = await this.replicateBatch(db, batch);
432
+ clearedError = false;
433
+ const result = await this.replicateBatch(db, batch);
434
+ resumeBatch = result.resumeBatch;
435
+ clearedError ||= result.clearedError;
424
436
  return this.getLastOpIdSequence(db);
425
437
  });
438
+ if (clearedError) {
439
+ // Clear the error outside the replication transaction (plain autocommit update,
440
+ // like the keepalive), to avoid serialization conflicts on the sync_rules row
441
+ // when multiple writers flush concurrently.
442
+ await this.clearError();
443
+ this.clearedError = true;
444
+ }
426
445
  // null if done, set if we need another flush
427
446
  this.batch = resumeBatch;
428
447
  if (lastOp == null) {
@@ -438,136 +457,143 @@ export class PostgresBucketBatch extends BaseObserver {
438
457
  await this.flush();
439
458
  const now = new Date().toISOString();
440
459
  const persisted_op = this.persisted_op ?? null;
441
- const result = await this.db.sql `
442
- WITH
443
- selected AS (
444
- SELECT
445
- id,
446
- state,
447
- last_checkpoint,
448
- last_checkpoint_lsn,
449
- snapshot_done,
450
- no_checkpoint_before,
451
- keepalive_op,
452
- (
453
- snapshot_done = TRUE
454
- AND (
455
- last_checkpoint_lsn IS NULL
456
- OR last_checkpoint_lsn <= ${{ type: 'varchar', value: lsn }}
457
- )
460
+ // Transaction failures restart replication from the last durable checkpoint.
461
+ const result = await this.db.transaction(async (db) => {
462
+ const checkpoint = await db.sql `
463
+ WITH
464
+ selected AS (
465
+ SELECT
466
+ id,
467
+ state,
468
+ last_checkpoint,
469
+ last_checkpoint_lsn,
470
+ snapshot_done,
471
+ no_checkpoint_before,
472
+ keepalive_op,
473
+ (
474
+ snapshot_done = TRUE
475
+ AND (
476
+ last_checkpoint_lsn IS NULL
477
+ OR last_checkpoint_lsn <= ${{ type: 'varchar', value: lsn }}
478
+ )
479
+ AND (
480
+ no_checkpoint_before IS NULL
481
+ OR no_checkpoint_before <= ${{ type: 'varchar', value: lsn }}
482
+ )
483
+ ) AS can_checkpoint
484
+ FROM
485
+ sync_rules
486
+ WHERE
487
+ id = ${{ type: 'int4', value: this.group_id }}
488
+ FOR UPDATE
489
+ ),
490
+ computed AS (
491
+ SELECT
492
+ selected.*,
493
+ CASE
494
+ WHEN selected.can_checkpoint THEN GREATEST(
495
+ selected.last_checkpoint,
496
+ ${{ type: 'int8', value: persisted_op }},
497
+ selected.keepalive_op,
498
+ 0
499
+ )
500
+ ELSE selected.last_checkpoint
501
+ END AS new_last_checkpoint,
502
+ CASE
503
+ WHEN selected.can_checkpoint THEN NULL
504
+ ELSE GREATEST(
505
+ selected.keepalive_op,
506
+ ${{ type: 'int8', value: persisted_op }},
507
+ 0
508
+ )
509
+ END AS new_keepalive_op
510
+ FROM
511
+ selected
512
+ ),
513
+ updated AS (
514
+ UPDATE sync_rules AS sr
515
+ SET
516
+ last_checkpoint_lsn = CASE
517
+ WHEN computed.can_checkpoint THEN ${{ type: 'varchar', value: lsn }}
518
+ ELSE sr.last_checkpoint_lsn
519
+ END,
520
+ last_checkpoint_ts = CASE
521
+ WHEN computed.can_checkpoint THEN ${{ type: 1184, value: now }}
522
+ ELSE sr.last_checkpoint_ts
523
+ END,
524
+ last_keepalive_ts = ${{ type: 1184, value: now }},
525
+ last_fatal_error = CASE
526
+ WHEN computed.can_checkpoint THEN NULL
527
+ ELSE sr.last_fatal_error
528
+ END,
529
+ keepalive_op = computed.new_keepalive_op,
530
+ last_checkpoint = computed.new_last_checkpoint,
531
+ snapshot_lsn = CASE
532
+ WHEN computed.can_checkpoint THEN NULL
533
+ ELSE sr.snapshot_lsn
534
+ END
535
+ FROM
536
+ computed
537
+ WHERE
538
+ sr.id = computed.id
458
539
  AND (
459
- no_checkpoint_before IS NULL
460
- OR no_checkpoint_before <= ${{ type: 'varchar', value: lsn }}
461
- )
462
- ) AS can_checkpoint
463
- FROM
464
- sync_rules
465
- WHERE
466
- id = ${{ type: 'int4', value: this.group_id }}
467
- FOR UPDATE
468
- ),
469
- computed AS (
470
- SELECT
471
- selected.*,
472
- CASE
473
- WHEN selected.can_checkpoint THEN GREATEST(
474
- selected.last_checkpoint,
475
- ${{ type: 'int8', value: persisted_op }},
476
- selected.keepalive_op,
477
- 0
540
+ sr.keepalive_op IS DISTINCT FROM computed.new_keepalive_op
541
+ OR sr.last_checkpoint IS DISTINCT FROM computed.new_last_checkpoint
542
+ OR ${{ type: 'bool', value: createEmptyCheckpoints }}
478
543
  )
479
- ELSE selected.last_checkpoint
480
- END AS new_last_checkpoint,
481
- CASE
482
- WHEN selected.can_checkpoint THEN NULL
483
- ELSE GREATEST(
484
- selected.keepalive_op,
485
- ${{ type: 'int8', value: persisted_op }},
486
- 0
487
- )
488
- END AS new_keepalive_op
489
- FROM
490
- selected
491
- ),
492
- updated AS (
493
- UPDATE sync_rules AS sr
494
- SET
495
- last_checkpoint_lsn = CASE
496
- WHEN computed.can_checkpoint THEN ${{ type: 'varchar', value: lsn }}
497
- ELSE sr.last_checkpoint_lsn
498
- END,
499
- last_checkpoint_ts = CASE
500
- WHEN computed.can_checkpoint THEN ${{ type: 1184, value: now }}
501
- ELSE sr.last_checkpoint_ts
502
- END,
503
- last_keepalive_ts = ${{ type: 1184, value: now }},
504
- last_fatal_error = CASE
505
- WHEN computed.can_checkpoint THEN NULL
506
- ELSE sr.last_fatal_error
507
- END,
508
- keepalive_op = computed.new_keepalive_op,
509
- last_checkpoint = computed.new_last_checkpoint,
510
- snapshot_lsn = CASE
511
- WHEN computed.can_checkpoint THEN NULL
512
- ELSE sr.snapshot_lsn
513
- END
514
- FROM
515
- computed
516
- WHERE
517
- sr.id = computed.id
518
- AND (
519
- sr.keepalive_op IS DISTINCT FROM computed.new_keepalive_op
520
- OR sr.last_checkpoint IS DISTINCT FROM computed.new_last_checkpoint
521
- OR ${{ type: 'bool', value: createEmptyCheckpoints }}
522
- )
523
- RETURNING
524
- sr.id,
525
- sr.state,
526
- sr.last_checkpoint,
527
- sr.last_checkpoint_lsn,
528
- sr.snapshot_done,
529
- sr.no_checkpoint_before,
530
- computed.can_checkpoint,
531
- computed.keepalive_op,
532
- computed.new_last_checkpoint
533
- )
534
- SELECT
535
- id,
536
- state,
537
- last_checkpoint,
538
- last_checkpoint_lsn,
539
- snapshot_done,
540
- no_checkpoint_before,
541
- can_checkpoint,
542
- keepalive_op,
543
- new_last_checkpoint,
544
- TRUE AS created_checkpoint
545
- FROM
546
- updated
547
- UNION ALL
548
- SELECT
549
- id,
550
- state,
551
- new_last_checkpoint AS last_checkpoint,
552
- last_checkpoint_lsn,
553
- snapshot_done,
554
- no_checkpoint_before,
555
- can_checkpoint,
556
- keepalive_op,
557
- new_last_checkpoint,
558
- FALSE AS created_checkpoint
559
- FROM
560
- computed
561
- WHERE
562
- NOT EXISTS (
563
- SELECT
564
- 1
565
- FROM
566
- updated
567
- )
568
- `
569
- .decoded(CheckpointWithStatus)
570
- .first();
544
+ RETURNING
545
+ sr.id,
546
+ sr.state,
547
+ sr.last_checkpoint,
548
+ sr.last_checkpoint_lsn,
549
+ sr.snapshot_done,
550
+ sr.no_checkpoint_before,
551
+ computed.can_checkpoint,
552
+ computed.keepalive_op,
553
+ computed.new_last_checkpoint
554
+ )
555
+ SELECT
556
+ id,
557
+ state,
558
+ last_checkpoint,
559
+ last_checkpoint_lsn,
560
+ snapshot_done,
561
+ no_checkpoint_before,
562
+ can_checkpoint,
563
+ keepalive_op,
564
+ new_last_checkpoint,
565
+ TRUE AS created_checkpoint
566
+ FROM
567
+ updated
568
+ UNION ALL
569
+ SELECT
570
+ id,
571
+ state,
572
+ new_last_checkpoint AS last_checkpoint,
573
+ last_checkpoint_lsn,
574
+ snapshot_done,
575
+ no_checkpoint_before,
576
+ can_checkpoint,
577
+ keepalive_op,
578
+ new_last_checkpoint,
579
+ FALSE AS created_checkpoint
580
+ FROM
581
+ computed
582
+ WHERE
583
+ NOT EXISTS (
584
+ SELECT
585
+ 1
586
+ FROM
587
+ updated
588
+ )
589
+ `
590
+ .decoded(CheckpointWithStatus)
591
+ .first();
592
+ if (checkpoint?.can_checkpoint && checkpoint.state == storage.SyncRuleState.ACTIVE) {
593
+ await notifySyncRulesUpdate(db, checkpoint);
594
+ }
595
+ return checkpoint;
596
+ });
571
597
  if (result == null) {
572
598
  throw new ReplicationAssertionError('Failed to update sync_rules during checkpoint');
573
599
  }
@@ -591,10 +617,8 @@ export class PostgresBucketBatch extends BaseObserver {
591
617
  });
592
618
  }
593
619
  }
594
- await this.autoActivate(lsn);
595
- await notifySyncRulesUpdate(this.db, {
620
+ await this.autoActivate(lsn, {
596
621
  id: result.id,
597
- state: result.state,
598
622
  last_checkpoint: result.last_checkpoint,
599
623
  last_checkpoint_lsn: result.last_checkpoint_lsn
600
624
  });
@@ -637,20 +661,21 @@ export class PostgresBucketBatch extends BaseObserver {
637
661
  }
638
662
  async markSnapshotDone(no_checkpoint_before_lsn, options) {
639
663
  await this.db.transaction(async (db) => {
640
- const snapshotRequiredCount = await db.sql `
664
+ const blockingTables = await db.sql `
641
665
  SELECT
642
- COUNT(*) AS count
666
+ schema_name,
667
+ table_name
643
668
  FROM
644
669
  source_tables
645
670
  WHERE
646
671
  group_id = ${{ type: 'int4', value: this.group_id }}
647
672
  AND snapshot_done = FALSE
648
673
  `
649
- .decoded(t.object({ count: bigint }))
650
- .first();
651
- if ((snapshotRequiredCount?.count ?? 0n) > 0n) {
674
+ .decoded(t.object({ schema_name: t.string, table_name: t.string }))
675
+ .rows();
676
+ if (blockingTables.length > 0) {
652
677
  if (options?.throwOnConflict ?? true) {
653
- throw new ReplicationAssertionError(`Cannot mark snapshot done while ${snapshotRequiredCount?.count} source table${snapshotRequiredCount?.count == 1n ? '' : 's'} still require snapshotting`);
678
+ throw new ReplicationAssertionError(`Cannot mark snapshot done while source tables still require snapshotting. Tables: ${blockingTables.map((t) => `${t.schema_name}.${t.table_name}`).join(', ')}`);
654
679
  }
655
680
  else {
656
681
  return;
@@ -752,14 +777,20 @@ export class PostgresBucketBatch extends BaseObserver {
752
777
  }
753
778
  async replicateBatch(db, batch) {
754
779
  let sizes = undefined;
755
- if (this.options.store_current_data && !this.options.skip_existing_rows) {
756
- // We skip this step if we don't store current_data, since the sizes will
780
+ // Check if any table in this batch needs to store current_data
781
+ const anyTableStoresCurrentData = this.options.store_current_data && batch.batch.some((r) => r.record.sourceTable.storeCurrentData);
782
+ if (anyTableStoresCurrentData && !this.options.skip_existing_rows) {
783
+ // We skip this step if no tables store current_data, since the sizes will
757
784
  // always be small in that case.
758
785
  // With skipExistingRows, we don't load the full documents into memory,
759
786
  // so we can also skip the size lookup step.
760
787
  // Find sizes of current_data documents, to assist in intelligent batching without
761
788
  // exceeding memory limits.
762
- const sizeLookups = batch.batch.map((r) => {
789
+ // Within this branch the batch stores current_data, so the per-table flag is the
790
+ // effective value - only look up sizes for tables that actually store current_data.
791
+ const sizeLookups = batch.batch
792
+ .filter((r) => r.record.sourceTable.storeCurrentData)
793
+ .map((r) => {
763
794
  return {
764
795
  source_table: postgresTableId(r.record.sourceTable.id),
765
796
  /**
@@ -849,14 +880,13 @@ export class PostgresBucketBatch extends BaseObserver {
849
880
  didFlush ||= flushedAny;
850
881
  }
851
882
  }
852
- if (didFlush) {
853
- await this.clearError(db);
854
- }
883
+ // The error itself is cleared by the caller, outside the replication transaction - see flushInner.
884
+ const clearedError = didFlush && !this.clearedError;
855
885
  // Don't return empty batches
856
- if (resumeBatch?.batch.length) {
857
- return resumeBatch;
858
- }
859
- return null;
886
+ return {
887
+ resumeBatch: resumeBatch?.batch.length ? resumeBatch : null,
888
+ clearedError
889
+ };
860
890
  }
861
891
  async saveOperation(persistedBatch, operation, currentData) {
862
892
  const record = operation.record;
@@ -865,6 +895,9 @@ export class PostgresBucketBatch extends BaseObserver {
865
895
  const afterId = operation.afterId;
866
896
  let after = record.after;
867
897
  const sourceTable = record.sourceTable;
898
+ // Effective per-record flag: store current_data only if both the batch (source-level)
899
+ // and the table (e.g. non-FULL replica identity) require it.
900
+ const storeCurrentData = this.options.store_current_data && sourceTable.storeCurrentData;
868
901
  let existingBuckets = [];
869
902
  let newBuckets = [];
870
903
  let existingLookups = [];
@@ -890,7 +923,7 @@ export class PostgresBucketBatch extends BaseObserver {
890
923
  existingBuckets = [];
891
924
  existingLookups = [];
892
925
  // Log to help with debugging if there was a consistency issue
893
- if (this.options.store_current_data) {
926
+ if (storeCurrentData) {
894
927
  if (this.markRecordUnavailable != null) {
895
928
  // This will trigger a "resnapshot" of the record.
896
929
  // This is not relevant if storeCurrentData is false, since we'll get the full row
@@ -906,7 +939,7 @@ export class PostgresBucketBatch extends BaseObserver {
906
939
  else {
907
940
  existingBuckets = result.buckets;
908
941
  existingLookups = result.lookups;
909
- if (this.options.store_current_data) {
942
+ if (storeCurrentData) {
910
943
  const data = storage.deserializeBson(result.data);
911
944
  after = storage.mergeToast(after, data);
912
945
  }
@@ -918,7 +951,9 @@ export class PostgresBucketBatch extends BaseObserver {
918
951
  // Not an error if we re-apply a transaction
919
952
  existingBuckets = [];
920
953
  existingLookups = [];
921
- // Log to help with debugging if there was a consistency issue
954
+ // Log to help with debugging if there was a consistency issue.
955
+ // Gate on the batch-level flag: FULL tables (per-record flag false) still get a
956
+ // current_data entry, so a missing record on DELETE is meaningful for them too.
922
957
  if (this.options.store_current_data && this.markRecordUnavailable == null) {
923
958
  this.logger.warn(`Cannot find previous record for delete on ${record.sourceTable.qualifiedName}: ${beforeId} / ${record.before?.id}`);
924
959
  }
@@ -929,7 +964,7 @@ export class PostgresBucketBatch extends BaseObserver {
929
964
  }
930
965
  }
931
966
  let afterData;
932
- if (afterId != null && !this.options.store_current_data) {
967
+ if (afterId != null && !storeCurrentData) {
933
968
  afterData = storage.serializeBson({});
934
969
  }
935
970
  else if (afterId != null) {
@@ -948,7 +983,7 @@ export class PostgresBucketBatch extends BaseObserver {
948
983
  container.reporter.captureMessage(`Data too big on ${record.sourceTable.qualifiedName}.${record.after?.id}: ${e.message}`, {
949
984
  level: errors.ErrorSeverity.WARNING,
950
985
  metadata: {
951
- replication_slot: this.options.slot_name,
986
+ replication_slot: this.options.replicationStreamName,
952
987
  table: record.sourceTable.qualifiedName
953
988
  }
954
989
  });
@@ -985,7 +1020,7 @@ export class PostgresBucketBatch extends BaseObserver {
985
1020
  // However, it will be valid by the end of the transaction.
986
1021
  //
987
1022
  // In this case, we don't save the op, but we do save the current data.
988
- if (afterId && after && utils.isCompleteRow(this.options.store_current_data, after)) {
1023
+ if (afterId && after && utils.isCompleteRow(storeCurrentData, after)) {
989
1024
  // Insert or update
990
1025
  if (sourceTable.syncData) {
991
1026
  const { results: evaluated, errors: syncErrors } = this.sync_rules.evaluateRowWithErrors({
@@ -997,7 +1032,7 @@ export class PostgresBucketBatch extends BaseObserver {
997
1032
  container.reporter.captureMessage(`Failed to evaluate data query on ${record.sourceTable.qualifiedName}.${record.after?.id}: ${error.error}`, {
998
1033
  level: errors.ErrorSeverity.WARNING,
999
1034
  metadata: {
1000
- replication_slot: this.options.slot_name,
1035
+ replication_slot: this.options.replicationStreamName,
1001
1036
  table: record.sourceTable.qualifiedName
1002
1037
  }
1003
1038
  });
@@ -1025,7 +1060,7 @@ export class PostgresBucketBatch extends BaseObserver {
1025
1060
  container.reporter.captureMessage(`Failed to evaluate parameter query on ${record.sourceTable.qualifiedName}.${record.after?.id}: ${error.error}`, {
1026
1061
  level: errors.ErrorSeverity.WARNING,
1027
1062
  metadata: {
1028
- replication_slot: this.options.slot_name,
1063
+ replication_slot: this.options.replicationStreamName,
1029
1064
  table: record.sourceTable.qualifiedName
1030
1065
  }
1031
1066
  });
@@ -1072,13 +1107,12 @@ export class PostgresBucketBatch extends BaseObserver {
1072
1107
  *
1073
1108
  * Called on new commits.
1074
1109
  */
1075
- async autoActivate(lsn) {
1110
+ async autoActivate(lsn, checkpoint) {
1076
1111
  if (!this.needsActivation) {
1077
1112
  // Already activated
1078
1113
  return;
1079
1114
  }
1080
- let didActivate = false;
1081
- await this.db.transaction(async (db) => {
1115
+ const activationResult = await this.db.transaction(async (db) => {
1082
1116
  const syncRulesRow = await db.sql `
1083
1117
  SELECT
1084
1118
  state,
@@ -1110,14 +1144,18 @@ export class PostgresBucketBatch extends BaseObserver {
1110
1144
  )
1111
1145
  AND id != ${{ type: 'int4', value: this.group_id }}
1112
1146
  `.execute();
1113
- didActivate = true;
1114
- this.needsActivation = false;
1147
+ await notifySyncRulesUpdate(db, checkpoint);
1148
+ return 'activated';
1115
1149
  }
1116
1150
  else if (syncRulesRow?.state != storage.SyncRuleState.PROCESSING) {
1117
- this.needsActivation = false;
1151
+ return 'not-processing';
1118
1152
  }
1153
+ return 'pending';
1119
1154
  });
1120
- if (didActivate) {
1155
+ if (activationResult != 'pending') {
1156
+ this.needsActivation = false;
1157
+ }
1158
+ if (activationResult == 'activated') {
1121
1159
  this.logger.info(`Activated new replication stream at ${lsn}`);
1122
1160
  }
1123
1161
  }
@@ -1165,10 +1203,6 @@ export class PostgresBucketBatch extends BaseObserver {
1165
1203
  }
1166
1204
  }
1167
1205
  async clearError(db = this.db) {
1168
- // No need to clear an error more than once per batch, since an error would always result in restarting the batch.
1169
- if (this.clearedError) {
1170
- return;
1171
- }
1172
1206
  await db.sql `
1173
1207
  UPDATE sync_rules
1174
1208
  SET
@@ -1176,7 +1210,6 @@ export class PostgresBucketBatch extends BaseObserver {
1176
1210
  WHERE
1177
1211
  id = ${{ type: 'int4', value: this.group_id }}
1178
1212
  `.execute();
1179
- this.clearedError = true;
1180
1213
  }
1181
1214
  async getLastOpIdSequence(db) {
1182
1215
  // When no op_id has been generated, last_value = 1 and nextval() will be 1.
@@ -1200,11 +1233,13 @@ export class PostgresBucketBatch extends BaseObserver {
1200
1233
  * active checkpoint has been updated.
1201
1234
  */
1202
1235
  export const notifySyncRulesUpdate = async (db, update) => {
1203
- if (update.state != storage.SyncRuleState.ACTIVE) {
1204
- return;
1205
- }
1206
- await db.query({
1207
- statement: `NOTIFY ${NOTIFICATION_CHANNEL}, '${models.ActiveCheckpointNotification.encode({ active_checkpoint: update })}'`
1208
- });
1236
+ const payload = models.ActiveCheckpointNotification.encode({ active_checkpoint: update });
1237
+ await db.sql `
1238
+ SELECT
1239
+ pg_notify (
1240
+ ${{ type: 'varchar', value: NOTIFICATION_CHANNEL }},
1241
+ ${{ type: 'varchar', value: payload }}
1242
+ )
1243
+ `.execute();
1209
1244
  };
1210
1245
  //# sourceMappingURL=PostgresBucketBatch.js.map