@powersync/service-module-postgres-storage 0.0.0-dev-20260602073133 → 0.0.0-dev-20260909133214
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 +179 -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/migrations/scripts/1784900000000-source-metadata.d.ts +3 -0
- package/dist/migrations/scripts/1784900000000-source-metadata.js +22 -0
- package/dist/migrations/scripts/1784900000000-source-metadata.js.map +1 -0
- package/dist/migrations/scripts/1788000000000-sync-config-version-label.d.ts +3 -0
- package/dist/migrations/scripts/1788000000000-sync-config-version-label.js +12 -0
- package/dist/migrations/scripts/1788000000000-sync-config-version-label.js.map +1 -0
- package/dist/{@types/storage → storage}/PostgresBucketStorageFactory.d.ts +14 -12
- package/dist/storage/PostgresBucketStorageFactory.js +73 -62
- 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 +3 -1
- package/dist/storage/PostgresStorageProvider.js.map +1 -1
- package/dist/{@types/storage → storage}/PostgresSyncRulesStorage.d.ts +39 -13
- package/dist/storage/PostgresSyncRulesStorage.js +266 -181
- package/dist/storage/PostgresSyncRulesStorage.js.map +1 -1
- package/dist/{@types/storage → storage}/batch/PostgresBucketBatch.d.ts +10 -26
- package/dist/storage/batch/PostgresBucketBatch.js +290 -286
- package/dist/storage/batch/PostgresBucketBatch.js.map +1 -1
- package/dist/{@types/storage → storage}/checkpoints/PostgresWriteCheckpointAPI.d.ts +2 -2
- package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js +163 -26
- 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 +94 -0
- package/dist/storage/sync-rules/PostgresPersistedSyncConfigContent.js.map +1 -0
- package/dist/{@types/types → types}/models/ActiveCheckpoint.d.ts +2 -2
- package/dist/types/models/ActiveCheckpoint.js +3 -2
- package/dist/types/models/ActiveCheckpoint.js.map +1 -1
- package/dist/{@types/types → types}/models/ActiveCheckpointNotification.d.ts +2 -2
- package/dist/types/models/BucketData.d.ts +22 -0
- package/dist/types/models/BucketData.js +7 -6
- package/dist/types/models/BucketData.js.map +1 -1
- package/dist/{@types/types → types}/models/CurrentData.d.ts +1 -1
- package/dist/types/models/CurrentData.js +2 -1
- package/dist/types/models/CurrentData.js.map +1 -1
- package/dist/{@types/types → types}/models/SdkReporting.d.ts +3 -3
- package/dist/types/models/SdkReporting.js +3 -5
- package/dist/types/models/SdkReporting.js.map +1 -1
- package/dist/{@types/types → types}/models/SourceTable.d.ts +13 -8
- package/dist/types/models/SourceTable.js +11 -6
- package/dist/types/models/SourceTable.js.map +1 -1
- package/dist/{@types/types → types}/models/SyncRules.d.ts +11 -10
- package/dist/types/models/SyncRules.js +12 -11
- package/dist/types/models/SyncRules.js.map +1 -1
- package/dist/{@types/types → types}/models/WriteCheckpoint.d.ts +2 -0
- package/dist/types/models/WriteCheckpoint.js +6 -2
- package/dist/types/models/WriteCheckpoint.js.map +1 -1
- package/dist/{@types/types → types}/types.d.ts +2 -0
- 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 +10 -22
- package/src/migrations/scripts/1782950400000-checkpoint-requested-at.ts +51 -0
- package/src/migrations/scripts/1784900000000-source-metadata.ts +31 -0
- package/src/migrations/scripts/1788000000000-sync-config-version-label.ts +18 -0
- package/src/storage/PostgresBucketStorageFactory.ts +82 -65
- package/src/storage/PostgresCompactor.ts +25 -1
- package/src/storage/PostgresReportStorage.ts +1 -2
- package/src/storage/PostgresStorageProvider.ts +3 -1
- package/src/storage/PostgresSyncRulesStorage.ts +295 -196
- package/src/storage/batch/PostgresBucketBatch.ts +321 -310
- package/src/storage/checkpoints/PostgresWriteCheckpointAPI.ts +176 -26
- package/src/storage/current-data-store.ts +66 -11
- package/src/storage/storage-index.ts +1 -0
- package/src/storage/sync-rules/PostgresPersistedSyncConfigContent.ts +105 -0
- package/src/types/models/ActiveCheckpoint.ts +3 -2
- package/src/types/models/BucketData.ts +7 -6
- package/src/types/models/CurrentData.ts +2 -1
- package/src/types/models/SdkReporting.ts +4 -4
- package/src/types/models/SourceTable.ts +12 -6
- package/src/types/models/SyncRules.ts +12 -11
- package/src/types/models/WriteCheckpoint.ts +6 -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 +44 -1621
- package/test/src/checkpoint_notifications.test.ts +528 -0
- package/test/src/migrations.test.ts +1 -1
- package/test/src/storage.test.ts +235 -11
- package/test/src/storage_compacting.test.ts +17 -9
- package/test/src/storage_sync.test.ts +55 -8
- package/test/src/util.ts +1 -1
- 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/@types/types/models/BucketData.d.ts +0 -22
- 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/BucketParameters.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/json.d.ts +0 -0
- /package/dist/{@types/types → types}/models/models-index.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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseObserver, container, ErrorCode, errors, ReplicationAssertionError, ServiceAssertionError, ServiceError } from '@powersync/lib-services-framework';
|
|
2
2
|
import { deserializeReplicaId, storage, utils } from '@powersync/service-core';
|
|
3
|
+
import { orNull } from '@powersync/service-types';
|
|
3
4
|
import * as timers from 'timers/promises';
|
|
4
5
|
import * as t from 'ts-codec';
|
|
5
6
|
import * as uuid from 'uuid';
|
|
@@ -19,10 +20,10 @@ import { PostgresPersistedBatch } from './PostgresPersistedBatch.js';
|
|
|
19
20
|
const StatefulCheckpoint = models.ActiveCheckpoint.and(t.object({ state: t.Enum(storage.SyncRuleState) }));
|
|
20
21
|
const CheckpointWithStatus = StatefulCheckpoint.and(t.object({
|
|
21
22
|
snapshot_done: t.boolean,
|
|
22
|
-
no_checkpoint_before: t.string
|
|
23
|
+
no_checkpoint_before: orNull(t.string),
|
|
23
24
|
can_checkpoint: t.boolean,
|
|
24
|
-
keepalive_op: bigint
|
|
25
|
-
new_last_checkpoint: bigint
|
|
25
|
+
keepalive_op: orNull(bigint),
|
|
26
|
+
new_last_checkpoint: orNull(bigint),
|
|
26
27
|
created_checkpoint: t.boolean
|
|
27
28
|
}));
|
|
28
29
|
/**
|
|
@@ -55,7 +56,7 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
55
56
|
this.options = options;
|
|
56
57
|
this.logger = options.logger;
|
|
57
58
|
this.db = options.db;
|
|
58
|
-
this.group_id = options.
|
|
59
|
+
this.group_id = options.replicationStreamId;
|
|
59
60
|
this.last_checkpoint_lsn = options.last_checkpoint_lsn;
|
|
60
61
|
this.resumeFromLsn = options.resumeFromLsn;
|
|
61
62
|
this.skipExistingRows = options.skip_existing_rows;
|
|
@@ -71,9 +72,6 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
71
72
|
this.persisted_op = options.keep_alive_op;
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
|
-
get lastCheckpointLsn() {
|
|
75
|
-
return this.last_checkpoint_lsn;
|
|
76
|
-
}
|
|
77
75
|
async [Symbol.asyncDispose]() {
|
|
78
76
|
if (this.batch != null || this.write_checkpoint_batch.length > 0) {
|
|
79
77
|
// We don't error here, since:
|
|
@@ -87,18 +85,20 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
87
85
|
await this[Symbol.asyncDispose]();
|
|
88
86
|
}
|
|
89
87
|
async resolveTables(options) {
|
|
90
|
-
const syncRules = options.
|
|
88
|
+
const syncRules = options.parsedSyncConfig?.hydratedSyncConfig ?? this.sync_rules;
|
|
89
|
+
const reconcile = options.reconcileSourceTables ?? storage.defaultSourceTableReconciler;
|
|
91
90
|
const { connection_id, source } = options;
|
|
92
|
-
const { schema, name: table, objectId, replicaIdColumns, connectionTag } = source;
|
|
91
|
+
const { schema, name: table, objectId, replicaIdColumns, connectionTag, sendsCompleteRows } = source;
|
|
93
92
|
const normalizedReplicaIdColumns = replicaIdColumns.map((column) => ({
|
|
94
93
|
name: column.name,
|
|
95
94
|
type: column.type,
|
|
96
95
|
type_oid: typeof column.typeId !== 'undefined' ? Number(column.typeId) : column.typeId
|
|
97
96
|
}));
|
|
98
97
|
return this.db.transaction(async (db) => {
|
|
99
|
-
|
|
98
|
+
// Find records that overlap by name or relation id.
|
|
99
|
+
let candidateRows;
|
|
100
100
|
if (objectId != null) {
|
|
101
|
-
|
|
101
|
+
candidateRows = await db.sql `
|
|
102
102
|
SELECT
|
|
103
103
|
*
|
|
104
104
|
FROM
|
|
@@ -106,16 +106,19 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
106
106
|
WHERE
|
|
107
107
|
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
108
108
|
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
109
|
-
AND
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
AND (
|
|
110
|
+
relation_id = ${{ type: 'jsonb', value: { object_id: objectId } }}
|
|
111
|
+
OR (
|
|
112
|
+
schema_name = ${{ type: 'varchar', value: schema }}
|
|
113
|
+
AND table_name = ${{ type: 'varchar', value: table }}
|
|
114
|
+
)
|
|
115
|
+
)
|
|
113
116
|
`
|
|
114
117
|
.decoded(models.SourceTable)
|
|
115
|
-
.
|
|
118
|
+
.rows();
|
|
116
119
|
}
|
|
117
120
|
else {
|
|
118
|
-
|
|
121
|
+
candidateRows = await db.sql `
|
|
119
122
|
SELECT
|
|
120
123
|
*
|
|
121
124
|
FROM
|
|
@@ -125,14 +128,38 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
125
128
|
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
126
129
|
AND schema_name = ${{ type: 'varchar', value: schema }}
|
|
127
130
|
AND table_name = ${{ type: 'varchar', value: table }}
|
|
128
|
-
AND replica_id_columns = ${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
|
|
129
131
|
`
|
|
130
132
|
.decoded(models.SourceTable)
|
|
131
|
-
.
|
|
133
|
+
.rows();
|
|
132
134
|
}
|
|
133
|
-
|
|
135
|
+
const candidateTables = candidateRows.map((row) => {
|
|
136
|
+
const table = this.sourceTableFromRow(row, connectionTag, syncRules);
|
|
137
|
+
table.storeCurrentData = sendsCompleteRows !== true;
|
|
138
|
+
return table;
|
|
139
|
+
});
|
|
140
|
+
const candidates = candidateTables.map((table) => table.clone());
|
|
141
|
+
const resolution = await reconcile({ source, candidates });
|
|
142
|
+
storage.validateSourceTableCandidateResolution(candidates, resolution);
|
|
143
|
+
for (const { id, sourceMetadata } of storage.diffSourceTableUpdates(candidateTables, resolution)) {
|
|
144
|
+
await db.sql `
|
|
145
|
+
UPDATE source_tables
|
|
146
|
+
SET
|
|
147
|
+
source_metadata = ${{ type: 'jsonb', value: sourceMetadata ?? null }}
|
|
148
|
+
WHERE
|
|
149
|
+
id = ${{ type: 'varchar', value: id.toString() }}
|
|
150
|
+
`.execute();
|
|
151
|
+
}
|
|
152
|
+
const materializedResolution = storage.materializeSourceTableResolution(candidateTables, resolution);
|
|
153
|
+
const compatibleTables = materializedResolution.compatibleTables;
|
|
154
|
+
// Keep one record, preferring one that has already been snapshotted.
|
|
155
|
+
const reuse = compatibleTables.find((candidate) => candidate.snapshotComplete) ?? compatibleTables[0] ?? null;
|
|
156
|
+
let sourceTable;
|
|
157
|
+
if (reuse != null) {
|
|
158
|
+
sourceTable = reuse;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
134
161
|
const id = options.idGenerator ? postgresTableId(options.idGenerator()) : uuid.v4();
|
|
135
|
-
|
|
162
|
+
const insertedRow = await db.sql `
|
|
136
163
|
INSERT INTO
|
|
137
164
|
source_tables (
|
|
138
165
|
id,
|
|
@@ -141,7 +168,8 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
141
168
|
relation_id,
|
|
142
169
|
schema_name,
|
|
143
170
|
table_name,
|
|
144
|
-
replica_id_columns
|
|
171
|
+
replica_id_columns,
|
|
172
|
+
source_metadata
|
|
145
173
|
)
|
|
146
174
|
VALUES
|
|
147
175
|
(
|
|
@@ -151,89 +179,25 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
151
179
|
${{ type: 'jsonb', value: { object_id: objectId } }},
|
|
152
180
|
${{ type: 'varchar', value: schema }},
|
|
153
181
|
${{ type: 'varchar', value: table }},
|
|
154
|
-
${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
|
|
182
|
+
${{ type: 'jsonb', value: normalizedReplicaIdColumns }},
|
|
183
|
+
${{ type: 'jsonb', value: resolution.newTableValues.sourceMetadata ?? null }}
|
|
155
184
|
)
|
|
156
185
|
RETURNING
|
|
157
186
|
*
|
|
158
187
|
`
|
|
159
188
|
.decoded(models.SourceTable)
|
|
160
189
|
.first();
|
|
190
|
+
sourceTable = this.sourceTableFromRow(insertedRow, connectionTag, syncRules);
|
|
191
|
+
sourceTable.storeCurrentData = sendsCompleteRows !== true;
|
|
161
192
|
}
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
snapshotComplete: sourceTableRow.snapshot_done ?? true,
|
|
168
|
-
...syncRules.getMatchingSources(source)
|
|
169
|
-
});
|
|
170
|
-
if (!sourceTable.snapshotComplete) {
|
|
171
|
-
sourceTable.snapshotStatus = {
|
|
172
|
-
totalEstimatedCount: Number(sourceTableRow.snapshot_total_estimated_count ?? -1n),
|
|
173
|
-
replicatedCount: Number(sourceTableRow.snapshot_replicated_count ?? 0n),
|
|
174
|
-
lastKey: sourceTableRow.snapshot_last_key
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
sourceTable.syncEvent = syncRules.tableTriggersEvent(source);
|
|
178
|
-
sourceTable.syncData = sourceTable.bucketDataSources.length > 0;
|
|
179
|
-
sourceTable.syncParameters = sourceTable.parameterLookupSources.length > 0;
|
|
180
|
-
let truncatedTables = [];
|
|
181
|
-
if (objectId != null) {
|
|
182
|
-
truncatedTables = await db.sql `
|
|
183
|
-
SELECT
|
|
184
|
-
*
|
|
185
|
-
FROM
|
|
186
|
-
source_tables
|
|
187
|
-
WHERE
|
|
188
|
-
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
189
|
-
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
190
|
-
AND id != ${{ type: 'varchar', value: sourceTableRow.id }}
|
|
191
|
-
AND (
|
|
192
|
-
relation_id = ${{ type: 'jsonb', value: { object_id: objectId } }}
|
|
193
|
-
OR (
|
|
194
|
-
schema_name = ${{ type: 'varchar', value: schema }}
|
|
195
|
-
AND table_name = ${{ type: 'varchar', value: table }}
|
|
196
|
-
)
|
|
197
|
-
)
|
|
198
|
-
`
|
|
199
|
-
.decoded(models.SourceTable)
|
|
200
|
-
.rows();
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
truncatedTables = await db.sql `
|
|
204
|
-
SELECT
|
|
205
|
-
*
|
|
206
|
-
FROM
|
|
207
|
-
source_tables
|
|
208
|
-
WHERE
|
|
209
|
-
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
210
|
-
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
211
|
-
AND id != ${{ type: 'varchar', value: sourceTableRow.id }}
|
|
212
|
-
AND (
|
|
213
|
-
schema_name = ${{ type: 'varchar', value: schema }}
|
|
214
|
-
AND table_name = ${{ type: 'varchar', value: table }}
|
|
215
|
-
)
|
|
216
|
-
`
|
|
217
|
-
.decoded(models.SourceTable)
|
|
218
|
-
.rows();
|
|
219
|
-
}
|
|
193
|
+
const dropTables = [
|
|
194
|
+
...materializedResolution.incompatibleTables,
|
|
195
|
+
// PostgreSQL storage only keeps one SourceTable per physical table.
|
|
196
|
+
...compatibleTables.filter((candidate) => !storage.sourceTableIdEquals(candidate.id, sourceTable.id))
|
|
197
|
+
];
|
|
220
198
|
return {
|
|
221
199
|
tables: [sourceTable],
|
|
222
|
-
dropTables
|
|
223
|
-
const ref = { connectionTag, schema: doc.schema_name, name: doc.table_name };
|
|
224
|
-
const dropTable = new storage.SourceTable({
|
|
225
|
-
id: doc.id,
|
|
226
|
-
ref,
|
|
227
|
-
objectId: doc.relation_id?.object_id ?? 0,
|
|
228
|
-
replicaIdColumns: doc.replica_id_columns?.map((c) => ({ name: c.name, typeId: c.typeId, type: c.type })) ?? [],
|
|
229
|
-
snapshotComplete: doc.snapshot_done ?? true,
|
|
230
|
-
...syncRules.getMatchingSources(ref)
|
|
231
|
-
});
|
|
232
|
-
dropTable.syncEvent = syncRules.tableTriggersEvent(ref);
|
|
233
|
-
dropTable.syncData = dropTable.bucketDataSources.length > 0;
|
|
234
|
-
dropTable.syncParameters = dropTable.parameterLookupSources.length > 0;
|
|
235
|
-
return dropTable;
|
|
236
|
-
})
|
|
200
|
+
dropTables
|
|
237
201
|
};
|
|
238
202
|
});
|
|
239
203
|
}
|
|
@@ -260,8 +224,9 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
260
224
|
id: row.id,
|
|
261
225
|
ref,
|
|
262
226
|
objectId: row.relation_id?.object_id,
|
|
263
|
-
replicaIdColumns: row.replica_id_columns?.map((
|
|
227
|
+
replicaIdColumns: row.replica_id_columns?.map((column) => ({ name: column.name, typeId: column.type_oid, type: column.type })) ?? [],
|
|
264
228
|
snapshotComplete: row.snapshot_done ?? true,
|
|
229
|
+
sourceMetadata: row.source_metadata,
|
|
265
230
|
...syncRules.getMatchingSources(ref)
|
|
266
231
|
});
|
|
267
232
|
if (!sourceTable.snapshotComplete) {
|
|
@@ -279,17 +244,23 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
279
244
|
async save(record) {
|
|
280
245
|
// TODO maybe share with abstract class
|
|
281
246
|
const { after, before, sourceTable, tag } = record;
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
247
|
+
const storeCurrentData = this.options.store_current_data && sourceTable.storeCurrentData;
|
|
248
|
+
// PostgreSQL storage resolves each physical table to one SourceTable. syncEvent records whether
|
|
249
|
+
// that table matches any event definition; each matching definition is evaluated once here and
|
|
250
|
+
// may produce multiple payloads from its matching queries.
|
|
251
|
+
if (sourceTable.syncEvent) {
|
|
252
|
+
for (const event of this.getTableEvents(sourceTable)) {
|
|
253
|
+
this.iterateListeners((cb) => cb.replicationEvent?.({
|
|
254
|
+
batch: this,
|
|
255
|
+
table: sourceTable,
|
|
256
|
+
data: {
|
|
257
|
+
op: tag,
|
|
258
|
+
after: after && utils.isCompleteRow(storeCurrentData, after) ? after : undefined,
|
|
259
|
+
before: before && utils.isCompleteRow(storeCurrentData, before) ? before : undefined
|
|
260
|
+
},
|
|
261
|
+
event
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
293
264
|
}
|
|
294
265
|
/**
|
|
295
266
|
* Return if the table is just an event table
|
|
@@ -332,7 +303,9 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
332
303
|
let processedCount = 0;
|
|
333
304
|
while (lastBatchCount == BATCH_LIMIT) {
|
|
334
305
|
lastBatchCount = 0;
|
|
306
|
+
let clearedError = false;
|
|
335
307
|
await this.withReplicationTransaction(async (db) => {
|
|
308
|
+
clearedError = false;
|
|
336
309
|
const persistedBatch = new PostgresPersistedBatch({
|
|
337
310
|
group_id: this.group_id,
|
|
338
311
|
storageConfig: this.storageConfig,
|
|
@@ -370,10 +343,14 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
370
343
|
}
|
|
371
344
|
}
|
|
372
345
|
const { flushedAny } = await persistedBatch.flush(db);
|
|
373
|
-
|
|
374
|
-
await this.clearError(db);
|
|
375
|
-
}
|
|
346
|
+
clearedError = flushedAny && !this.clearedError;
|
|
376
347
|
});
|
|
348
|
+
if (clearedError) {
|
|
349
|
+
// No need to clear an error more than once per batch, since an error would always result in restarting the batch.
|
|
350
|
+
// Cleared outside the replication transaction - see flushInner.
|
|
351
|
+
await this.clearError();
|
|
352
|
+
this.clearedError = true;
|
|
353
|
+
}
|
|
377
354
|
}
|
|
378
355
|
if (processedCount == 0) {
|
|
379
356
|
// The op sequence should not have progressed
|
|
@@ -419,10 +396,21 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
419
396
|
}
|
|
420
397
|
await this.hooks?.beforeBatchFlush?.(this);
|
|
421
398
|
let resumeBatch = null;
|
|
399
|
+
let clearedError = false;
|
|
422
400
|
const lastOp = await this.withReplicationTransaction(async (db) => {
|
|
423
|
-
|
|
401
|
+
clearedError = false;
|
|
402
|
+
const result = await this.replicateBatch(db, batch);
|
|
403
|
+
resumeBatch = result.resumeBatch;
|
|
404
|
+
clearedError ||= result.clearedError;
|
|
424
405
|
return this.getLastOpIdSequence(db);
|
|
425
406
|
});
|
|
407
|
+
if (clearedError) {
|
|
408
|
+
// Clear the error outside the replication transaction (plain autocommit update,
|
|
409
|
+
// like the keepalive), to avoid serialization conflicts on the sync_rules row
|
|
410
|
+
// when multiple writers flush concurrently.
|
|
411
|
+
await this.clearError();
|
|
412
|
+
this.clearedError = true;
|
|
413
|
+
}
|
|
426
414
|
// null if done, set if we need another flush
|
|
427
415
|
this.batch = resumeBatch;
|
|
428
416
|
if (lastOp == null) {
|
|
@@ -438,136 +426,143 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
438
426
|
await this.flush();
|
|
439
427
|
const now = new Date().toISOString();
|
|
440
428
|
const persisted_op = this.persisted_op ?? null;
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
429
|
+
// Transaction failures restart replication from the last durable checkpoint.
|
|
430
|
+
const result = await this.db.transaction(async (db) => {
|
|
431
|
+
const checkpoint = await db.sql `
|
|
432
|
+
WITH
|
|
433
|
+
selected AS (
|
|
434
|
+
SELECT
|
|
435
|
+
id,
|
|
436
|
+
state,
|
|
437
|
+
last_checkpoint,
|
|
438
|
+
last_checkpoint_lsn,
|
|
439
|
+
snapshot_done,
|
|
440
|
+
no_checkpoint_before,
|
|
441
|
+
keepalive_op,
|
|
442
|
+
(
|
|
443
|
+
snapshot_done = TRUE
|
|
444
|
+
AND (
|
|
445
|
+
last_checkpoint_lsn IS NULL
|
|
446
|
+
OR last_checkpoint_lsn <= ${{ type: 'varchar', value: lsn }}
|
|
447
|
+
)
|
|
448
|
+
AND (
|
|
449
|
+
no_checkpoint_before IS NULL
|
|
450
|
+
OR no_checkpoint_before <= ${{ type: 'varchar', value: lsn }}
|
|
451
|
+
)
|
|
452
|
+
) AS can_checkpoint
|
|
453
|
+
FROM
|
|
454
|
+
sync_rules
|
|
455
|
+
WHERE
|
|
456
|
+
id = ${{ type: 'int4', value: this.group_id }}
|
|
457
|
+
FOR UPDATE
|
|
458
|
+
),
|
|
459
|
+
computed AS (
|
|
460
|
+
SELECT
|
|
461
|
+
selected.*,
|
|
462
|
+
CASE
|
|
463
|
+
WHEN selected.can_checkpoint THEN GREATEST(
|
|
464
|
+
selected.last_checkpoint,
|
|
465
|
+
${{ type: 'int8', value: persisted_op }},
|
|
466
|
+
selected.keepalive_op,
|
|
467
|
+
0
|
|
468
|
+
)
|
|
469
|
+
ELSE selected.last_checkpoint
|
|
470
|
+
END AS new_last_checkpoint,
|
|
471
|
+
CASE
|
|
472
|
+
WHEN selected.can_checkpoint THEN NULL
|
|
473
|
+
ELSE GREATEST(
|
|
474
|
+
selected.keepalive_op,
|
|
475
|
+
${{ type: 'int8', value: persisted_op }},
|
|
476
|
+
0
|
|
477
|
+
)
|
|
478
|
+
END AS new_keepalive_op
|
|
479
|
+
FROM
|
|
480
|
+
selected
|
|
481
|
+
),
|
|
482
|
+
updated AS (
|
|
483
|
+
UPDATE sync_rules AS sr
|
|
484
|
+
SET
|
|
485
|
+
last_checkpoint_lsn = CASE
|
|
486
|
+
WHEN computed.can_checkpoint THEN ${{ type: 'varchar', value: lsn }}
|
|
487
|
+
ELSE sr.last_checkpoint_lsn
|
|
488
|
+
END,
|
|
489
|
+
last_checkpoint_ts = CASE
|
|
490
|
+
WHEN computed.can_checkpoint THEN ${{ type: 1184, value: now }}
|
|
491
|
+
ELSE sr.last_checkpoint_ts
|
|
492
|
+
END,
|
|
493
|
+
last_keepalive_ts = ${{ type: 1184, value: now }},
|
|
494
|
+
last_fatal_error = CASE
|
|
495
|
+
WHEN computed.can_checkpoint THEN NULL
|
|
496
|
+
ELSE sr.last_fatal_error
|
|
497
|
+
END,
|
|
498
|
+
keepalive_op = computed.new_keepalive_op,
|
|
499
|
+
last_checkpoint = computed.new_last_checkpoint,
|
|
500
|
+
snapshot_lsn = CASE
|
|
501
|
+
WHEN computed.can_checkpoint THEN NULL
|
|
502
|
+
ELSE sr.snapshot_lsn
|
|
503
|
+
END
|
|
504
|
+
FROM
|
|
505
|
+
computed
|
|
506
|
+
WHERE
|
|
507
|
+
sr.id = computed.id
|
|
458
508
|
AND (
|
|
459
|
-
|
|
460
|
-
OR
|
|
509
|
+
sr.keepalive_op IS DISTINCT FROM computed.new_keepalive_op
|
|
510
|
+
OR sr.last_checkpoint IS DISTINCT FROM computed.new_last_checkpoint
|
|
511
|
+
OR ${{ type: 'bool', value: createEmptyCheckpoints }}
|
|
461
512
|
)
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
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();
|
|
513
|
+
RETURNING
|
|
514
|
+
sr.id,
|
|
515
|
+
sr.state,
|
|
516
|
+
sr.last_checkpoint,
|
|
517
|
+
sr.last_checkpoint_lsn,
|
|
518
|
+
sr.snapshot_done,
|
|
519
|
+
sr.no_checkpoint_before,
|
|
520
|
+
computed.can_checkpoint,
|
|
521
|
+
computed.keepalive_op,
|
|
522
|
+
computed.new_last_checkpoint
|
|
523
|
+
)
|
|
524
|
+
SELECT
|
|
525
|
+
id,
|
|
526
|
+
state,
|
|
527
|
+
last_checkpoint,
|
|
528
|
+
last_checkpoint_lsn,
|
|
529
|
+
snapshot_done,
|
|
530
|
+
no_checkpoint_before,
|
|
531
|
+
can_checkpoint,
|
|
532
|
+
keepalive_op,
|
|
533
|
+
new_last_checkpoint,
|
|
534
|
+
TRUE AS created_checkpoint
|
|
535
|
+
FROM
|
|
536
|
+
updated
|
|
537
|
+
UNION ALL
|
|
538
|
+
SELECT
|
|
539
|
+
id,
|
|
540
|
+
state,
|
|
541
|
+
new_last_checkpoint AS last_checkpoint,
|
|
542
|
+
last_checkpoint_lsn,
|
|
543
|
+
snapshot_done,
|
|
544
|
+
no_checkpoint_before,
|
|
545
|
+
can_checkpoint,
|
|
546
|
+
keepalive_op,
|
|
547
|
+
new_last_checkpoint,
|
|
548
|
+
FALSE AS created_checkpoint
|
|
549
|
+
FROM
|
|
550
|
+
computed
|
|
551
|
+
WHERE
|
|
552
|
+
NOT EXISTS (
|
|
553
|
+
SELECT
|
|
554
|
+
1
|
|
555
|
+
FROM
|
|
556
|
+
updated
|
|
557
|
+
)
|
|
558
|
+
`
|
|
559
|
+
.decoded(CheckpointWithStatus)
|
|
560
|
+
.first();
|
|
561
|
+
if (checkpoint?.can_checkpoint && checkpoint.state == storage.SyncRuleState.ACTIVE) {
|
|
562
|
+
await notifySyncRulesUpdate(db, checkpoint);
|
|
563
|
+
}
|
|
564
|
+
return checkpoint;
|
|
565
|
+
});
|
|
571
566
|
if (result == null) {
|
|
572
567
|
throw new ReplicationAssertionError('Failed to update sync_rules during checkpoint');
|
|
573
568
|
}
|
|
@@ -591,10 +586,8 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
591
586
|
});
|
|
592
587
|
}
|
|
593
588
|
}
|
|
594
|
-
await this.autoActivate(lsn
|
|
595
|
-
await notifySyncRulesUpdate(this.db, {
|
|
589
|
+
await this.autoActivate(lsn, {
|
|
596
590
|
id: result.id,
|
|
597
|
-
state: result.state,
|
|
598
591
|
last_checkpoint: result.last_checkpoint,
|
|
599
592
|
last_checkpoint_lsn: result.last_checkpoint_lsn
|
|
600
593
|
});
|
|
@@ -637,20 +630,21 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
637
630
|
}
|
|
638
631
|
async markSnapshotDone(no_checkpoint_before_lsn, options) {
|
|
639
632
|
await this.db.transaction(async (db) => {
|
|
640
|
-
const
|
|
633
|
+
const blockingTables = await db.sql `
|
|
641
634
|
SELECT
|
|
642
|
-
|
|
635
|
+
schema_name,
|
|
636
|
+
table_name
|
|
643
637
|
FROM
|
|
644
638
|
source_tables
|
|
645
639
|
WHERE
|
|
646
640
|
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
647
641
|
AND snapshot_done = FALSE
|
|
648
642
|
`
|
|
649
|
-
.decoded(t.object({
|
|
650
|
-
.
|
|
651
|
-
if (
|
|
643
|
+
.decoded(t.object({ schema_name: t.string, table_name: t.string }))
|
|
644
|
+
.rows();
|
|
645
|
+
if (blockingTables.length > 0) {
|
|
652
646
|
if (options?.throwOnConflict ?? true) {
|
|
653
|
-
throw new ReplicationAssertionError(`Cannot mark snapshot done while
|
|
647
|
+
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
648
|
}
|
|
655
649
|
else {
|
|
656
650
|
return;
|
|
@@ -752,14 +746,20 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
752
746
|
}
|
|
753
747
|
async replicateBatch(db, batch) {
|
|
754
748
|
let sizes = undefined;
|
|
755
|
-
if
|
|
756
|
-
|
|
749
|
+
// Check if any table in this batch needs to store current_data
|
|
750
|
+
const anyTableStoresCurrentData = this.options.store_current_data && batch.batch.some((r) => r.record.sourceTable.storeCurrentData);
|
|
751
|
+
if (anyTableStoresCurrentData && !this.options.skip_existing_rows) {
|
|
752
|
+
// We skip this step if no tables store current_data, since the sizes will
|
|
757
753
|
// always be small in that case.
|
|
758
754
|
// With skipExistingRows, we don't load the full documents into memory,
|
|
759
755
|
// so we can also skip the size lookup step.
|
|
760
756
|
// Find sizes of current_data documents, to assist in intelligent batching without
|
|
761
757
|
// exceeding memory limits.
|
|
762
|
-
|
|
758
|
+
// Within this branch the batch stores current_data, so the per-table flag is the
|
|
759
|
+
// effective value - only look up sizes for tables that actually store current_data.
|
|
760
|
+
const sizeLookups = batch.batch
|
|
761
|
+
.filter((r) => r.record.sourceTable.storeCurrentData)
|
|
762
|
+
.map((r) => {
|
|
763
763
|
return {
|
|
764
764
|
source_table: postgresTableId(r.record.sourceTable.id),
|
|
765
765
|
/**
|
|
@@ -849,14 +849,13 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
849
849
|
didFlush ||= flushedAny;
|
|
850
850
|
}
|
|
851
851
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
}
|
|
852
|
+
// The error itself is cleared by the caller, outside the replication transaction - see flushInner.
|
|
853
|
+
const clearedError = didFlush && !this.clearedError;
|
|
855
854
|
// Don't return empty batches
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
855
|
+
return {
|
|
856
|
+
resumeBatch: resumeBatch?.batch.length ? resumeBatch : null,
|
|
857
|
+
clearedError
|
|
858
|
+
};
|
|
860
859
|
}
|
|
861
860
|
async saveOperation(persistedBatch, operation, currentData) {
|
|
862
861
|
const record = operation.record;
|
|
@@ -865,6 +864,9 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
865
864
|
const afterId = operation.afterId;
|
|
866
865
|
let after = record.after;
|
|
867
866
|
const sourceTable = record.sourceTable;
|
|
867
|
+
// Effective per-record flag: store current_data only if both the batch (source-level)
|
|
868
|
+
// and the table (e.g. non-FULL replica identity) require it.
|
|
869
|
+
const storeCurrentData = this.options.store_current_data && sourceTable.storeCurrentData;
|
|
868
870
|
let existingBuckets = [];
|
|
869
871
|
let newBuckets = [];
|
|
870
872
|
let existingLookups = [];
|
|
@@ -890,7 +892,7 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
890
892
|
existingBuckets = [];
|
|
891
893
|
existingLookups = [];
|
|
892
894
|
// Log to help with debugging if there was a consistency issue
|
|
893
|
-
if (
|
|
895
|
+
if (storeCurrentData) {
|
|
894
896
|
if (this.markRecordUnavailable != null) {
|
|
895
897
|
// This will trigger a "resnapshot" of the record.
|
|
896
898
|
// This is not relevant if storeCurrentData is false, since we'll get the full row
|
|
@@ -906,7 +908,7 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
906
908
|
else {
|
|
907
909
|
existingBuckets = result.buckets;
|
|
908
910
|
existingLookups = result.lookups;
|
|
909
|
-
if (
|
|
911
|
+
if (storeCurrentData) {
|
|
910
912
|
const data = storage.deserializeBson(result.data);
|
|
911
913
|
after = storage.mergeToast(after, data);
|
|
912
914
|
}
|
|
@@ -918,7 +920,9 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
918
920
|
// Not an error if we re-apply a transaction
|
|
919
921
|
existingBuckets = [];
|
|
920
922
|
existingLookups = [];
|
|
921
|
-
// Log to help with debugging if there was a consistency issue
|
|
923
|
+
// Log to help with debugging if there was a consistency issue.
|
|
924
|
+
// Gate on the batch-level flag: FULL tables (per-record flag false) still get a
|
|
925
|
+
// current_data entry, so a missing record on DELETE is meaningful for them too.
|
|
922
926
|
if (this.options.store_current_data && this.markRecordUnavailable == null) {
|
|
923
927
|
this.logger.warn(`Cannot find previous record for delete on ${record.sourceTable.qualifiedName}: ${beforeId} / ${record.before?.id}`);
|
|
924
928
|
}
|
|
@@ -929,7 +933,7 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
929
933
|
}
|
|
930
934
|
}
|
|
931
935
|
let afterData;
|
|
932
|
-
if (afterId != null && !
|
|
936
|
+
if (afterId != null && !storeCurrentData) {
|
|
933
937
|
afterData = storage.serializeBson({});
|
|
934
938
|
}
|
|
935
939
|
else if (afterId != null) {
|
|
@@ -948,7 +952,7 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
948
952
|
container.reporter.captureMessage(`Data too big on ${record.sourceTable.qualifiedName}.${record.after?.id}: ${e.message}`, {
|
|
949
953
|
level: errors.ErrorSeverity.WARNING,
|
|
950
954
|
metadata: {
|
|
951
|
-
replication_slot: this.options.
|
|
955
|
+
replication_slot: this.options.replicationStreamName,
|
|
952
956
|
table: record.sourceTable.qualifiedName
|
|
953
957
|
}
|
|
954
958
|
});
|
|
@@ -985,7 +989,7 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
985
989
|
// However, it will be valid by the end of the transaction.
|
|
986
990
|
//
|
|
987
991
|
// In this case, we don't save the op, but we do save the current data.
|
|
988
|
-
if (afterId && after && utils.isCompleteRow(
|
|
992
|
+
if (afterId && after && utils.isCompleteRow(storeCurrentData, after)) {
|
|
989
993
|
// Insert or update
|
|
990
994
|
if (sourceTable.syncData) {
|
|
991
995
|
const { results: evaluated, errors: syncErrors } = this.sync_rules.evaluateRowWithErrors({
|
|
@@ -997,7 +1001,7 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
997
1001
|
container.reporter.captureMessage(`Failed to evaluate data query on ${record.sourceTable.qualifiedName}.${record.after?.id}: ${error.error}`, {
|
|
998
1002
|
level: errors.ErrorSeverity.WARNING,
|
|
999
1003
|
metadata: {
|
|
1000
|
-
replication_slot: this.options.
|
|
1004
|
+
replication_slot: this.options.replicationStreamName,
|
|
1001
1005
|
table: record.sourceTable.qualifiedName
|
|
1002
1006
|
}
|
|
1003
1007
|
});
|
|
@@ -1025,7 +1029,7 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
1025
1029
|
container.reporter.captureMessage(`Failed to evaluate parameter query on ${record.sourceTable.qualifiedName}.${record.after?.id}: ${error.error}`, {
|
|
1026
1030
|
level: errors.ErrorSeverity.WARNING,
|
|
1027
1031
|
metadata: {
|
|
1028
|
-
replication_slot: this.options.
|
|
1032
|
+
replication_slot: this.options.replicationStreamName,
|
|
1029
1033
|
table: record.sourceTable.qualifiedName
|
|
1030
1034
|
}
|
|
1031
1035
|
});
|
|
@@ -1072,13 +1076,12 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
1072
1076
|
*
|
|
1073
1077
|
* Called on new commits.
|
|
1074
1078
|
*/
|
|
1075
|
-
async autoActivate(lsn) {
|
|
1079
|
+
async autoActivate(lsn, checkpoint) {
|
|
1076
1080
|
if (!this.needsActivation) {
|
|
1077
1081
|
// Already activated
|
|
1078
1082
|
return;
|
|
1079
1083
|
}
|
|
1080
|
-
|
|
1081
|
-
await this.db.transaction(async (db) => {
|
|
1084
|
+
const activationResult = await this.db.transaction(async (db) => {
|
|
1082
1085
|
const syncRulesRow = await db.sql `
|
|
1083
1086
|
SELECT
|
|
1084
1087
|
state,
|
|
@@ -1110,19 +1113,23 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
1110
1113
|
)
|
|
1111
1114
|
AND id != ${{ type: 'int4', value: this.group_id }}
|
|
1112
1115
|
`.execute();
|
|
1113
|
-
|
|
1114
|
-
|
|
1116
|
+
await notifySyncRulesUpdate(db, checkpoint);
|
|
1117
|
+
return 'activated';
|
|
1115
1118
|
}
|
|
1116
1119
|
else if (syncRulesRow?.state != storage.SyncRuleState.PROCESSING) {
|
|
1117
|
-
|
|
1120
|
+
return 'not-processing';
|
|
1118
1121
|
}
|
|
1122
|
+
return 'pending';
|
|
1119
1123
|
});
|
|
1120
|
-
if (
|
|
1124
|
+
if (activationResult != 'pending') {
|
|
1125
|
+
this.needsActivation = false;
|
|
1126
|
+
}
|
|
1127
|
+
if (activationResult == 'activated') {
|
|
1121
1128
|
this.logger.info(`Activated new replication stream at ${lsn}`);
|
|
1122
1129
|
}
|
|
1123
1130
|
}
|
|
1124
1131
|
/**
|
|
1125
|
-
* Gets relevant {@link
|
|
1132
|
+
* Gets relevant {@link HydratedEventDescriptor}s for the given {@link SourceTable}
|
|
1126
1133
|
* TODO maybe share this with an abstract class
|
|
1127
1134
|
*/
|
|
1128
1135
|
getTableEvents(table) {
|
|
@@ -1165,10 +1172,6 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
1165
1172
|
}
|
|
1166
1173
|
}
|
|
1167
1174
|
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
1175
|
await db.sql `
|
|
1173
1176
|
UPDATE sync_rules
|
|
1174
1177
|
SET
|
|
@@ -1176,7 +1179,6 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
1176
1179
|
WHERE
|
|
1177
1180
|
id = ${{ type: 'int4', value: this.group_id }}
|
|
1178
1181
|
`.execute();
|
|
1179
|
-
this.clearedError = true;
|
|
1180
1182
|
}
|
|
1181
1183
|
async getLastOpIdSequence(db) {
|
|
1182
1184
|
// When no op_id has been generated, last_value = 1 and nextval() will be 1.
|
|
@@ -1200,11 +1202,13 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
1200
1202
|
* active checkpoint has been updated.
|
|
1201
1203
|
*/
|
|
1202
1204
|
export const notifySyncRulesUpdate = async (db, update) => {
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1205
|
+
const payload = models.ActiveCheckpointNotification.encode({ active_checkpoint: update });
|
|
1206
|
+
await db.sql `
|
|
1207
|
+
SELECT
|
|
1208
|
+
pg_notify (
|
|
1209
|
+
${{ type: 'varchar', value: NOTIFICATION_CHANNEL }},
|
|
1210
|
+
${{ type: 'varchar', value: payload }}
|
|
1211
|
+
)
|
|
1212
|
+
`.execute();
|
|
1209
1213
|
};
|
|
1210
1214
|
//# sourceMappingURL=PostgresBucketBatch.js.map
|