@powersync/service-module-postgres-storage 0.0.0-dev-20260601140105 → 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.
- package/CHANGELOG.md +101 -6
- package/dist/migrations/scripts/1782950400000-checkpoint-requested-at.d.ts +3 -0
- package/dist/migrations/scripts/1782950400000-checkpoint-requested-at.js +42 -0
- package/dist/migrations/scripts/1782950400000-checkpoint-requested-at.js.map +1 -0
- package/dist/{@types/storage → storage}/PostgresBucketStorageFactory.d.ts +13 -12
- package/dist/storage/PostgresBucketStorageFactory.js +59 -55
- package/dist/storage/PostgresBucketStorageFactory.js.map +1 -1
- package/dist/{@types/storage → storage}/PostgresCompactor.d.ts +3 -1
- package/dist/storage/PostgresCompactor.js +21 -1
- package/dist/storage/PostgresCompactor.js.map +1 -1
- package/dist/storage/PostgresReportStorage.js.map +1 -1
- package/dist/storage/PostgresStorageProvider.js +2 -1
- package/dist/storage/PostgresStorageProvider.js.map +1 -1
- package/dist/{@types/storage → storage}/PostgresSyncRulesStorage.d.ts +36 -11
- package/dist/storage/PostgresSyncRulesStorage.js +250 -177
- package/dist/storage/PostgresSyncRulesStorage.js.map +1 -1
- package/dist/{@types/storage → storage}/batch/PostgresBucketBatch.d.ts +8 -24
- package/dist/storage/batch/PostgresBucketBatch.js +229 -194
- package/dist/storage/batch/PostgresBucketBatch.js.map +1 -1
- package/dist/{@types/storage → storage}/checkpoints/PostgresWriteCheckpointAPI.d.ts +1 -1
- package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js +161 -24
- package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js.map +1 -1
- package/dist/{@types/storage → storage}/current-data-store.d.ts +8 -2
- package/dist/storage/current-data-store.js +66 -11
- package/dist/storage/current-data-store.js.map +1 -1
- package/dist/{@types/storage → storage}/storage-index.d.ts +1 -0
- package/dist/storage/storage-index.js +1 -0
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/storage/sync-rules/PostgresPersistedSyncConfigContent.d.ts +17 -0
- package/dist/storage/sync-rules/PostgresPersistedSyncConfigContent.js +90 -0
- package/dist/storage/sync-rules/PostgresPersistedSyncConfigContent.js.map +1 -0
- package/dist/{@types/types → types}/models/WriteCheckpoint.d.ts +2 -0
- package/dist/types/models/WriteCheckpoint.js +5 -2
- package/dist/types/models/WriteCheckpoint.js.map +1 -1
- package/dist/utils/checkpoints.d.ts +9 -0
- package/dist/utils/checkpoints.js +26 -0
- package/dist/utils/checkpoints.js.map +1 -0
- package/dist/utils/test-utils.js +2 -2
- package/dist/utils/test-utils.js.map +1 -1
- package/package.json +11 -23
- package/src/migrations/scripts/1782950400000-checkpoint-requested-at.ts +51 -0
- package/src/storage/PostgresBucketStorageFactory.ts +67 -58
- package/src/storage/PostgresCompactor.ts +25 -1
- package/src/storage/PostgresReportStorage.ts +1 -2
- package/src/storage/PostgresStorageProvider.ts +2 -1
- package/src/storage/PostgresSyncRulesStorage.ts +275 -191
- package/src/storage/batch/PostgresBucketBatch.ts +256 -214
- package/src/storage/checkpoints/PostgresWriteCheckpointAPI.ts +174 -24
- package/src/storage/current-data-store.ts +66 -11
- package/src/storage/storage-index.ts +1 -0
- package/src/storage/sync-rules/PostgresPersistedSyncConfigContent.ts +98 -0
- package/src/types/models/WriteCheckpoint.ts +5 -2
- package/src/utils/checkpoints.ts +31 -0
- package/src/utils/test-utils.ts +2 -2
- package/test/src/__snapshots__/storage_sync.test.ts.snap +0 -582
- package/test/src/checkpoint_notifications.test.ts +522 -0
- package/test/src/migrations.test.ts +1 -1
- package/test/src/storage.test.ts +226 -10
- package/test/src/storage_compacting.test.ts +15 -7
- package/test/src/storage_sync.test.ts +11 -8
- package/test/tsconfig.json +1 -3
- package/tsconfig.json +1 -4
- package/tsconfig.tsbuildinfo +1 -0
- package/dist/.tsbuildinfo +0 -1
- package/dist/@types/storage/sync-rules/PostgresPersistedSyncRulesContent.d.ts +0 -9
- package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js +0 -49
- package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js.map +0 -1
- package/src/storage/sync-rules/PostgresPersistedSyncRulesContent.ts +0 -54
- /package/dist/{@types/index.d.ts → index.d.ts} +0 -0
- /package/dist/{@types/migrations → migrations}/PostgresMigrationAgent.d.ts +0 -0
- /package/dist/{@types/migrations → migrations}/PostgresMigrationStore.d.ts +0 -0
- /package/dist/{@types/migrations → migrations}/migration-utils.d.ts +0 -0
- /package/dist/{@types/migrations → migrations}/scripts/1684951997326-init.d.ts +0 -0
- /package/dist/{@types/migrations → migrations}/scripts/1749024804042-snapshot-progress.d.ts +0 -0
- /package/dist/{@types/migrations → migrations}/scripts/1756282360128-connection-reporting.d.ts +0 -0
- /package/dist/{@types/migrations → migrations}/scripts/1771232439485-storage-version.d.ts +0 -0
- /package/dist/{@types/migrations → migrations}/scripts/1771424826685-current-data-pending-deletes.d.ts +0 -0
- /package/dist/{@types/migrations → migrations}/scripts/1771491856000-sync-plan.d.ts +0 -0
- /package/dist/{@types/module → module}/PostgresStorageModule.d.ts +0 -0
- /package/dist/{@types/storage → storage}/PostgresReportStorage.d.ts +0 -0
- /package/dist/{@types/storage → storage}/PostgresStorageProvider.d.ts +0 -0
- /package/dist/{@types/storage → storage}/batch/OperationBatch.d.ts +0 -0
- /package/dist/{@types/storage → storage}/batch/PostgresPersistedBatch.d.ts +0 -0
- /package/dist/{@types/storage → storage}/table-id.d.ts +0 -0
- /package/dist/{@types/types → types}/codecs.d.ts +0 -0
- /package/dist/{@types/types → types}/models/ActiveCheckpoint.d.ts +0 -0
- /package/dist/{@types/types → types}/models/ActiveCheckpointNotification.d.ts +0 -0
- /package/dist/{@types/types → types}/models/BucketData.d.ts +0 -0
- /package/dist/{@types/types → types}/models/BucketParameters.d.ts +0 -0
- /package/dist/{@types/types → types}/models/CurrentData.d.ts +0 -0
- /package/dist/{@types/types → types}/models/Instance.d.ts +0 -0
- /package/dist/{@types/types → types}/models/Migration.d.ts +0 -0
- /package/dist/{@types/types → types}/models/SdkReporting.d.ts +0 -0
- /package/dist/{@types/types → types}/models/SourceTable.d.ts +0 -0
- /package/dist/{@types/types → types}/models/SyncRules.d.ts +0 -0
- /package/dist/{@types/types → types}/models/json.d.ts +0 -0
- /package/dist/{@types/types → types}/models/models-index.d.ts +0 -0
- /package/dist/{@types/types → types}/types.d.ts +0 -0
- /package/dist/{@types/utils → utils}/application-name.d.ts +0 -0
- /package/dist/{@types/utils → utils}/bson.d.ts +0 -0
- /package/dist/{@types/utils → utils}/bucket-data.d.ts +0 -0
- /package/dist/{@types/utils → utils}/db.d.ts +0 -0
- /package/dist/{@types/utils → utils}/test-utils.d.ts +0 -0
- /package/dist/{@types/utils → utils}/ts-codec.d.ts +0 -0
- /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.
|
|
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.
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
460
|
-
OR
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
|
664
|
+
const blockingTables = await db.sql `
|
|
641
665
|
SELECT
|
|
642
|
-
|
|
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({
|
|
650
|
-
.
|
|
651
|
-
if (
|
|
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
|
|
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
|
|
756
|
-
|
|
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
|
-
|
|
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
|
-
|
|
853
|
-
|
|
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
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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 (
|
|
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 (
|
|
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 && !
|
|
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.
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1114
|
-
|
|
1147
|
+
await notifySyncRulesUpdate(db, checkpoint);
|
|
1148
|
+
return 'activated';
|
|
1115
1149
|
}
|
|
1116
1150
|
else if (syncRulesRow?.state != storage.SyncRuleState.PROCESSING) {
|
|
1117
|
-
|
|
1151
|
+
return 'not-processing';
|
|
1118
1152
|
}
|
|
1153
|
+
return 'pending';
|
|
1119
1154
|
});
|
|
1120
|
-
if (
|
|
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
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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
|