@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
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
utils
|
|
20
20
|
} from '@powersync/service-core';
|
|
21
21
|
import * as sync_rules from '@powersync/service-sync-rules';
|
|
22
|
+
import { orNull } from '@powersync/service-types';
|
|
22
23
|
import * as timers from 'timers/promises';
|
|
23
24
|
import * as t from 'ts-codec';
|
|
24
25
|
import * as uuid from 'uuid';
|
|
@@ -38,8 +39,8 @@ export interface PostgresBucketBatchOptions {
|
|
|
38
39
|
logger: Logger;
|
|
39
40
|
db: lib_postgres.DatabaseClient;
|
|
40
41
|
sync_rules: sync_rules.HydratedSyncConfig;
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
replicationStreamId: number;
|
|
43
|
+
replicationStreamName: string;
|
|
43
44
|
last_checkpoint_lsn: string | null;
|
|
44
45
|
store_current_data: boolean;
|
|
45
46
|
keep_alive_op?: InternalOpId | null;
|
|
@@ -60,15 +61,14 @@ export interface PostgresBucketBatchOptions {
|
|
|
60
61
|
* via the Postgres NOTIFY protocol.
|
|
61
62
|
*/
|
|
62
63
|
const StatefulCheckpoint = models.ActiveCheckpoint.and(t.object({ state: t.Enum(storage.SyncRuleState) }));
|
|
63
|
-
type StatefulCheckpointDecoded = t.Decoded<typeof StatefulCheckpoint>;
|
|
64
64
|
|
|
65
65
|
const CheckpointWithStatus = StatefulCheckpoint.and(
|
|
66
66
|
t.object({
|
|
67
67
|
snapshot_done: t.boolean,
|
|
68
|
-
no_checkpoint_before: t.string
|
|
68
|
+
no_checkpoint_before: orNull(t.string),
|
|
69
69
|
can_checkpoint: t.boolean,
|
|
70
|
-
keepalive_op: bigint
|
|
71
|
-
new_last_checkpoint: bigint
|
|
70
|
+
keepalive_op: orNull(bigint),
|
|
71
|
+
new_last_checkpoint: orNull(bigint),
|
|
72
72
|
created_checkpoint: t.boolean
|
|
73
73
|
})
|
|
74
74
|
);
|
|
@@ -112,7 +112,7 @@ export class PostgresBucketBatch
|
|
|
112
112
|
super();
|
|
113
113
|
this.logger = options.logger;
|
|
114
114
|
this.db = options.db;
|
|
115
|
-
this.group_id = options.
|
|
115
|
+
this.group_id = options.replicationStreamId;
|
|
116
116
|
this.last_checkpoint_lsn = options.last_checkpoint_lsn;
|
|
117
117
|
this.resumeFromLsn = options.resumeFromLsn;
|
|
118
118
|
this.skipExistingRows = options.skip_existing_rows;
|
|
@@ -129,10 +129,6 @@ export class PostgresBucketBatch
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
get lastCheckpointLsn() {
|
|
133
|
-
return this.last_checkpoint_lsn;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
132
|
async [Symbol.asyncDispose]() {
|
|
137
133
|
if (this.batch != null || this.write_checkpoint_batch.length > 0) {
|
|
138
134
|
// We don't error here, since:
|
|
@@ -148,19 +144,22 @@ export class PostgresBucketBatch
|
|
|
148
144
|
}
|
|
149
145
|
|
|
150
146
|
async resolveTables(options: storage.ResolveTablesOptions): Promise<storage.ResolveTablesResult> {
|
|
151
|
-
const syncRules = options.
|
|
147
|
+
const syncRules = options.parsedSyncConfig?.hydratedSyncConfig ?? this.sync_rules;
|
|
148
|
+
const reconcile = options.reconcileSourceTables ?? storage.defaultSourceTableReconciler;
|
|
152
149
|
const { connection_id, source } = options;
|
|
153
|
-
const { schema, name: table, objectId, replicaIdColumns, connectionTag } = source;
|
|
150
|
+
const { schema, name: table, objectId, replicaIdColumns, connectionTag, sendsCompleteRows } = source;
|
|
154
151
|
|
|
155
152
|
const normalizedReplicaIdColumns = replicaIdColumns.map((column) => ({
|
|
156
153
|
name: column.name,
|
|
157
154
|
type: column.type,
|
|
158
155
|
type_oid: typeof column.typeId !== 'undefined' ? Number(column.typeId) : column.typeId
|
|
159
156
|
}));
|
|
157
|
+
|
|
160
158
|
return this.db.transaction(async (db) => {
|
|
161
|
-
|
|
159
|
+
// Find records that overlap by name or relation id.
|
|
160
|
+
let candidateRows: SourceTableDecoded[];
|
|
162
161
|
if (objectId != null) {
|
|
163
|
-
|
|
162
|
+
candidateRows = await db.sql`
|
|
164
163
|
SELECT
|
|
165
164
|
*
|
|
166
165
|
FROM
|
|
@@ -168,15 +167,18 @@ export class PostgresBucketBatch
|
|
|
168
167
|
WHERE
|
|
169
168
|
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
170
169
|
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
171
|
-
AND
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
170
|
+
AND (
|
|
171
|
+
relation_id = ${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }}
|
|
172
|
+
OR (
|
|
173
|
+
schema_name = ${{ type: 'varchar', value: schema }}
|
|
174
|
+
AND table_name = ${{ type: 'varchar', value: table }}
|
|
175
|
+
)
|
|
176
|
+
)
|
|
175
177
|
`
|
|
176
178
|
.decoded(models.SourceTable)
|
|
177
|
-
.
|
|
179
|
+
.rows();
|
|
178
180
|
} else {
|
|
179
|
-
|
|
181
|
+
candidateRows = await db.sql`
|
|
180
182
|
SELECT
|
|
181
183
|
*
|
|
182
184
|
FROM
|
|
@@ -186,15 +188,42 @@ export class PostgresBucketBatch
|
|
|
186
188
|
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
187
189
|
AND schema_name = ${{ type: 'varchar', value: schema }}
|
|
188
190
|
AND table_name = ${{ type: 'varchar', value: table }}
|
|
189
|
-
AND replica_id_columns = ${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
|
|
190
191
|
`
|
|
191
192
|
.decoded(models.SourceTable)
|
|
192
|
-
.
|
|
193
|
+
.rows();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const candidateTables = candidateRows.map((row) => {
|
|
197
|
+
const table = this.sourceTableFromRow(row, connectionTag, syncRules);
|
|
198
|
+
table.storeCurrentData = sendsCompleteRows !== true;
|
|
199
|
+
return table;
|
|
200
|
+
});
|
|
201
|
+
const candidates = candidateTables.map((table) => table.clone());
|
|
202
|
+
const resolution = await reconcile({ source, candidates });
|
|
203
|
+
storage.validateSourceTableCandidateResolution(candidates, resolution);
|
|
204
|
+
|
|
205
|
+
for (const { id, sourceMetadata } of storage.diffSourceTableUpdates(candidateTables, resolution)) {
|
|
206
|
+
await db.sql`
|
|
207
|
+
UPDATE source_tables
|
|
208
|
+
SET
|
|
209
|
+
source_metadata = ${{ type: 'jsonb', value: sourceMetadata ?? null }}
|
|
210
|
+
WHERE
|
|
211
|
+
id = ${{ type: 'varchar', value: id.toString() }}
|
|
212
|
+
`.execute();
|
|
193
213
|
}
|
|
194
214
|
|
|
195
|
-
|
|
215
|
+
const materializedResolution = storage.materializeSourceTableResolution(candidateTables, resolution);
|
|
216
|
+
const compatibleTables = materializedResolution.compatibleTables;
|
|
217
|
+
|
|
218
|
+
// Keep one record, preferring one that has already been snapshotted.
|
|
219
|
+
const reuse = compatibleTables.find((candidate) => candidate.snapshotComplete) ?? compatibleTables[0] ?? null;
|
|
220
|
+
|
|
221
|
+
let sourceTable: storage.SourceTable;
|
|
222
|
+
if (reuse != null) {
|
|
223
|
+
sourceTable = reuse;
|
|
224
|
+
} else {
|
|
196
225
|
const id = options.idGenerator ? postgresTableId(options.idGenerator()) : uuid.v4();
|
|
197
|
-
|
|
226
|
+
const insertedRow = await db.sql`
|
|
198
227
|
INSERT INTO
|
|
199
228
|
source_tables (
|
|
200
229
|
id,
|
|
@@ -203,7 +232,8 @@ export class PostgresBucketBatch
|
|
|
203
232
|
relation_id,
|
|
204
233
|
schema_name,
|
|
205
234
|
table_name,
|
|
206
|
-
replica_id_columns
|
|
235
|
+
replica_id_columns,
|
|
236
|
+
source_metadata
|
|
207
237
|
)
|
|
208
238
|
VALUES
|
|
209
239
|
(
|
|
@@ -213,94 +243,27 @@ export class PostgresBucketBatch
|
|
|
213
243
|
${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }},
|
|
214
244
|
${{ type: 'varchar', value: schema }},
|
|
215
245
|
${{ type: 'varchar', value: table }},
|
|
216
|
-
${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
|
|
246
|
+
${{ type: 'jsonb', value: normalizedReplicaIdColumns }},
|
|
247
|
+
${{ type: 'jsonb', value: resolution.newTableValues.sourceMetadata ?? null }}
|
|
217
248
|
)
|
|
218
249
|
RETURNING
|
|
219
250
|
*
|
|
220
251
|
`
|
|
221
252
|
.decoded(models.SourceTable)
|
|
222
253
|
.first();
|
|
254
|
+
sourceTable = this.sourceTableFromRow(insertedRow!, connectionTag, syncRules);
|
|
255
|
+
sourceTable.storeCurrentData = sendsCompleteRows !== true;
|
|
223
256
|
}
|
|
224
257
|
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
snapshotComplete: sourceTableRow!.snapshot_done ?? true,
|
|
231
|
-
...syncRules.getMatchingSources(source)
|
|
232
|
-
});
|
|
233
|
-
if (!sourceTable.snapshotComplete) {
|
|
234
|
-
sourceTable.snapshotStatus = {
|
|
235
|
-
totalEstimatedCount: Number(sourceTableRow!.snapshot_total_estimated_count ?? -1n),
|
|
236
|
-
replicatedCount: Number(sourceTableRow!.snapshot_replicated_count ?? 0n),
|
|
237
|
-
lastKey: sourceTableRow!.snapshot_last_key
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
sourceTable.syncEvent = syncRules.tableTriggersEvent(source);
|
|
241
|
-
sourceTable.syncData = sourceTable.bucketDataSources.length > 0;
|
|
242
|
-
sourceTable.syncParameters = sourceTable.parameterLookupSources.length > 0;
|
|
243
|
-
|
|
244
|
-
let truncatedTables: SourceTableDecoded[] = [];
|
|
245
|
-
if (objectId != null) {
|
|
246
|
-
truncatedTables = await db.sql`
|
|
247
|
-
SELECT
|
|
248
|
-
*
|
|
249
|
-
FROM
|
|
250
|
-
source_tables
|
|
251
|
-
WHERE
|
|
252
|
-
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
253
|
-
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
254
|
-
AND id != ${{ type: 'varchar', value: sourceTableRow!.id }}
|
|
255
|
-
AND (
|
|
256
|
-
relation_id = ${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }}
|
|
257
|
-
OR (
|
|
258
|
-
schema_name = ${{ type: 'varchar', value: schema }}
|
|
259
|
-
AND table_name = ${{ type: 'varchar', value: table }}
|
|
260
|
-
)
|
|
261
|
-
)
|
|
262
|
-
`
|
|
263
|
-
.decoded(models.SourceTable)
|
|
264
|
-
.rows();
|
|
265
|
-
} else {
|
|
266
|
-
truncatedTables = await db.sql`
|
|
267
|
-
SELECT
|
|
268
|
-
*
|
|
269
|
-
FROM
|
|
270
|
-
source_tables
|
|
271
|
-
WHERE
|
|
272
|
-
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
273
|
-
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
274
|
-
AND id != ${{ type: 'varchar', value: sourceTableRow!.id }}
|
|
275
|
-
AND (
|
|
276
|
-
schema_name = ${{ type: 'varchar', value: schema }}
|
|
277
|
-
AND table_name = ${{ type: 'varchar', value: table }}
|
|
278
|
-
)
|
|
279
|
-
`
|
|
280
|
-
.decoded(models.SourceTable)
|
|
281
|
-
.rows();
|
|
282
|
-
}
|
|
258
|
+
const dropTables = [
|
|
259
|
+
...materializedResolution.incompatibleTables,
|
|
260
|
+
// PostgreSQL storage only keeps one SourceTable per physical table.
|
|
261
|
+
...compatibleTables.filter((candidate) => !storage.sourceTableIdEquals(candidate.id, sourceTable.id))
|
|
262
|
+
];
|
|
283
263
|
|
|
284
264
|
return {
|
|
285
265
|
tables: [sourceTable],
|
|
286
|
-
dropTables
|
|
287
|
-
const ref = { connectionTag, schema: doc.schema_name, name: doc.table_name };
|
|
288
|
-
const dropTable = new storage.SourceTable({
|
|
289
|
-
id: doc.id,
|
|
290
|
-
ref,
|
|
291
|
-
objectId: doc.relation_id?.object_id ?? 0,
|
|
292
|
-
replicaIdColumns:
|
|
293
|
-
doc.replica_id_columns?.map(
|
|
294
|
-
(c) => ({ name: c.name, typeId: c.typeId, type: c.type }) satisfies ColumnDescriptor
|
|
295
|
-
) ?? [],
|
|
296
|
-
snapshotComplete: doc.snapshot_done ?? true,
|
|
297
|
-
...syncRules.getMatchingSources(ref)
|
|
298
|
-
});
|
|
299
|
-
dropTable.syncEvent = syncRules.tableTriggersEvent(ref);
|
|
300
|
-
dropTable.syncData = dropTable.bucketDataSources.length > 0;
|
|
301
|
-
dropTable.syncParameters = dropTable.parameterLookupSources.length > 0;
|
|
302
|
-
return dropTable;
|
|
303
|
-
})
|
|
266
|
+
dropTables
|
|
304
267
|
};
|
|
305
268
|
});
|
|
306
269
|
}
|
|
@@ -337,9 +300,10 @@ export class PostgresBucketBatch
|
|
|
337
300
|
objectId: row.relation_id?.object_id,
|
|
338
301
|
replicaIdColumns:
|
|
339
302
|
row.replica_id_columns?.map(
|
|
340
|
-
(
|
|
303
|
+
(column) => ({ name: column.name, typeId: column.type_oid, type: column.type }) satisfies ColumnDescriptor
|
|
341
304
|
) ?? [],
|
|
342
305
|
snapshotComplete: row.snapshot_done ?? true,
|
|
306
|
+
sourceMetadata: row.source_metadata,
|
|
343
307
|
...syncRules.getMatchingSources(ref)
|
|
344
308
|
});
|
|
345
309
|
|
|
@@ -359,19 +323,25 @@ export class PostgresBucketBatch
|
|
|
359
323
|
async save(record: storage.SaveOptions): Promise<storage.FlushedResult | null> {
|
|
360
324
|
// TODO maybe share with abstract class
|
|
361
325
|
const { after, before, sourceTable, tag } = record;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
326
|
+
const storeCurrentData = this.options.store_current_data && sourceTable.storeCurrentData;
|
|
327
|
+
// PostgreSQL storage resolves each physical table to one SourceTable. syncEvent records whether
|
|
328
|
+
// that table matches any event definition; each matching definition is evaluated once here and
|
|
329
|
+
// may produce multiple payloads from its matching queries.
|
|
330
|
+
if (sourceTable.syncEvent) {
|
|
331
|
+
for (const event of this.getTableEvents(sourceTable)) {
|
|
332
|
+
this.iterateListeners((cb) =>
|
|
333
|
+
cb.replicationEvent?.({
|
|
334
|
+
batch: this,
|
|
335
|
+
table: sourceTable,
|
|
336
|
+
data: {
|
|
337
|
+
op: tag,
|
|
338
|
+
after: after && utils.isCompleteRow(storeCurrentData, after) ? after : undefined,
|
|
339
|
+
before: before && utils.isCompleteRow(storeCurrentData, before) ? before : undefined
|
|
340
|
+
},
|
|
341
|
+
event
|
|
342
|
+
})
|
|
343
|
+
);
|
|
344
|
+
}
|
|
375
345
|
}
|
|
376
346
|
/**
|
|
377
347
|
* Return if the table is just an event table
|
|
@@ -420,7 +390,9 @@ export class PostgresBucketBatch
|
|
|
420
390
|
let processedCount = 0;
|
|
421
391
|
while (lastBatchCount == BATCH_LIMIT) {
|
|
422
392
|
lastBatchCount = 0;
|
|
393
|
+
let clearedError = false;
|
|
423
394
|
await this.withReplicationTransaction(async (db) => {
|
|
395
|
+
clearedError = false;
|
|
424
396
|
const persistedBatch = new PostgresPersistedBatch({
|
|
425
397
|
group_id: this.group_id,
|
|
426
398
|
storageConfig: this.storageConfig,
|
|
@@ -460,10 +432,14 @@ export class PostgresBucketBatch
|
|
|
460
432
|
}
|
|
461
433
|
}
|
|
462
434
|
const { flushedAny } = await persistedBatch.flush(db);
|
|
463
|
-
|
|
464
|
-
await this.clearError(db);
|
|
465
|
-
}
|
|
435
|
+
clearedError = flushedAny && !this.clearedError;
|
|
466
436
|
});
|
|
437
|
+
if (clearedError) {
|
|
438
|
+
// No need to clear an error more than once per batch, since an error would always result in restarting the batch.
|
|
439
|
+
// Cleared outside the replication transaction - see flushInner.
|
|
440
|
+
await this.clearError();
|
|
441
|
+
this.clearedError = true;
|
|
442
|
+
}
|
|
467
443
|
}
|
|
468
444
|
if (processedCount == 0) {
|
|
469
445
|
// The op sequence should not have progressed
|
|
@@ -516,13 +492,25 @@ export class PostgresBucketBatch
|
|
|
516
492
|
await this.hooks?.beforeBatchFlush?.(this);
|
|
517
493
|
|
|
518
494
|
let resumeBatch: OperationBatch | null = null;
|
|
495
|
+
let clearedError = false;
|
|
519
496
|
|
|
520
497
|
const lastOp = await this.withReplicationTransaction(async (db) => {
|
|
521
|
-
|
|
498
|
+
clearedError = false;
|
|
499
|
+
const result = await this.replicateBatch(db, batch);
|
|
500
|
+
resumeBatch = result.resumeBatch;
|
|
501
|
+
clearedError ||= result.clearedError;
|
|
522
502
|
|
|
523
503
|
return this.getLastOpIdSequence(db);
|
|
524
504
|
});
|
|
525
505
|
|
|
506
|
+
if (clearedError) {
|
|
507
|
+
// Clear the error outside the replication transaction (plain autocommit update,
|
|
508
|
+
// like the keepalive), to avoid serialization conflicts on the sync_rules row
|
|
509
|
+
// when multiple writers flush concurrently.
|
|
510
|
+
await this.clearError();
|
|
511
|
+
this.clearedError = true;
|
|
512
|
+
}
|
|
513
|
+
|
|
526
514
|
// null if done, set if we need another flush
|
|
527
515
|
this.batch = resumeBatch;
|
|
528
516
|
|
|
@@ -545,136 +533,144 @@ export class PostgresBucketBatch
|
|
|
545
533
|
|
|
546
534
|
const persisted_op = this.persisted_op ?? null;
|
|
547
535
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
536
|
+
// Transaction failures restart replication from the last durable checkpoint.
|
|
537
|
+
const result = await this.db.transaction(async (db) => {
|
|
538
|
+
const checkpoint = await db.sql`
|
|
539
|
+
WITH
|
|
540
|
+
selected AS (
|
|
541
|
+
SELECT
|
|
542
|
+
id,
|
|
543
|
+
state,
|
|
544
|
+
last_checkpoint,
|
|
545
|
+
last_checkpoint_lsn,
|
|
546
|
+
snapshot_done,
|
|
547
|
+
no_checkpoint_before,
|
|
548
|
+
keepalive_op,
|
|
549
|
+
(
|
|
550
|
+
snapshot_done = TRUE
|
|
551
|
+
AND (
|
|
552
|
+
last_checkpoint_lsn IS NULL
|
|
553
|
+
OR last_checkpoint_lsn <= ${{ type: 'varchar', value: lsn }}
|
|
554
|
+
)
|
|
555
|
+
AND (
|
|
556
|
+
no_checkpoint_before IS NULL
|
|
557
|
+
OR no_checkpoint_before <= ${{ type: 'varchar', value: lsn }}
|
|
558
|
+
)
|
|
559
|
+
) AS can_checkpoint
|
|
560
|
+
FROM
|
|
561
|
+
sync_rules
|
|
562
|
+
WHERE
|
|
563
|
+
id = ${{ type: 'int4', value: this.group_id }}
|
|
564
|
+
FOR UPDATE
|
|
565
|
+
),
|
|
566
|
+
computed AS (
|
|
567
|
+
SELECT
|
|
568
|
+
selected.*,
|
|
569
|
+
CASE
|
|
570
|
+
WHEN selected.can_checkpoint THEN GREATEST(
|
|
571
|
+
selected.last_checkpoint,
|
|
572
|
+
${{ type: 'int8', value: persisted_op }},
|
|
573
|
+
selected.keepalive_op,
|
|
574
|
+
0
|
|
575
|
+
)
|
|
576
|
+
ELSE selected.last_checkpoint
|
|
577
|
+
END AS new_last_checkpoint,
|
|
578
|
+
CASE
|
|
579
|
+
WHEN selected.can_checkpoint THEN NULL
|
|
580
|
+
ELSE GREATEST(
|
|
581
|
+
selected.keepalive_op,
|
|
582
|
+
${{ type: 'int8', value: persisted_op }},
|
|
583
|
+
0
|
|
584
|
+
)
|
|
585
|
+
END AS new_keepalive_op
|
|
586
|
+
FROM
|
|
587
|
+
selected
|
|
588
|
+
),
|
|
589
|
+
updated AS (
|
|
590
|
+
UPDATE sync_rules AS sr
|
|
591
|
+
SET
|
|
592
|
+
last_checkpoint_lsn = CASE
|
|
593
|
+
WHEN computed.can_checkpoint THEN ${{ type: 'varchar', value: lsn }}
|
|
594
|
+
ELSE sr.last_checkpoint_lsn
|
|
595
|
+
END,
|
|
596
|
+
last_checkpoint_ts = CASE
|
|
597
|
+
WHEN computed.can_checkpoint THEN ${{ type: 1184, value: now }}
|
|
598
|
+
ELSE sr.last_checkpoint_ts
|
|
599
|
+
END,
|
|
600
|
+
last_keepalive_ts = ${{ type: 1184, value: now }},
|
|
601
|
+
last_fatal_error = CASE
|
|
602
|
+
WHEN computed.can_checkpoint THEN NULL
|
|
603
|
+
ELSE sr.last_fatal_error
|
|
604
|
+
END,
|
|
605
|
+
keepalive_op = computed.new_keepalive_op,
|
|
606
|
+
last_checkpoint = computed.new_last_checkpoint,
|
|
607
|
+
snapshot_lsn = CASE
|
|
608
|
+
WHEN computed.can_checkpoint THEN NULL
|
|
609
|
+
ELSE sr.snapshot_lsn
|
|
610
|
+
END
|
|
611
|
+
FROM
|
|
612
|
+
computed
|
|
613
|
+
WHERE
|
|
614
|
+
sr.id = computed.id
|
|
565
615
|
AND (
|
|
566
|
-
|
|
567
|
-
OR
|
|
616
|
+
sr.keepalive_op IS DISTINCT FROM computed.new_keepalive_op
|
|
617
|
+
OR sr.last_checkpoint IS DISTINCT FROM computed.new_last_checkpoint
|
|
618
|
+
OR ${{ type: 'bool', value: createEmptyCheckpoints }}
|
|
568
619
|
)
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
WHERE
|
|
624
|
-
sr.id = computed.id
|
|
625
|
-
AND (
|
|
626
|
-
sr.keepalive_op IS DISTINCT FROM computed.new_keepalive_op
|
|
627
|
-
OR sr.last_checkpoint IS DISTINCT FROM computed.new_last_checkpoint
|
|
628
|
-
OR ${{ type: 'bool', value: createEmptyCheckpoints }}
|
|
629
|
-
)
|
|
630
|
-
RETURNING
|
|
631
|
-
sr.id,
|
|
632
|
-
sr.state,
|
|
633
|
-
sr.last_checkpoint,
|
|
634
|
-
sr.last_checkpoint_lsn,
|
|
635
|
-
sr.snapshot_done,
|
|
636
|
-
sr.no_checkpoint_before,
|
|
637
|
-
computed.can_checkpoint,
|
|
638
|
-
computed.keepalive_op,
|
|
639
|
-
computed.new_last_checkpoint
|
|
640
|
-
)
|
|
641
|
-
SELECT
|
|
642
|
-
id,
|
|
643
|
-
state,
|
|
644
|
-
last_checkpoint,
|
|
645
|
-
last_checkpoint_lsn,
|
|
646
|
-
snapshot_done,
|
|
647
|
-
no_checkpoint_before,
|
|
648
|
-
can_checkpoint,
|
|
649
|
-
keepalive_op,
|
|
650
|
-
new_last_checkpoint,
|
|
651
|
-
TRUE AS created_checkpoint
|
|
652
|
-
FROM
|
|
653
|
-
updated
|
|
654
|
-
UNION ALL
|
|
655
|
-
SELECT
|
|
656
|
-
id,
|
|
657
|
-
state,
|
|
658
|
-
new_last_checkpoint AS last_checkpoint,
|
|
659
|
-
last_checkpoint_lsn,
|
|
660
|
-
snapshot_done,
|
|
661
|
-
no_checkpoint_before,
|
|
662
|
-
can_checkpoint,
|
|
663
|
-
keepalive_op,
|
|
664
|
-
new_last_checkpoint,
|
|
665
|
-
FALSE AS created_checkpoint
|
|
666
|
-
FROM
|
|
667
|
-
computed
|
|
668
|
-
WHERE
|
|
669
|
-
NOT EXISTS (
|
|
670
|
-
SELECT
|
|
671
|
-
1
|
|
672
|
-
FROM
|
|
673
|
-
updated
|
|
674
|
-
)
|
|
675
|
-
`
|
|
676
|
-
.decoded(CheckpointWithStatus)
|
|
677
|
-
.first();
|
|
620
|
+
RETURNING
|
|
621
|
+
sr.id,
|
|
622
|
+
sr.state,
|
|
623
|
+
sr.last_checkpoint,
|
|
624
|
+
sr.last_checkpoint_lsn,
|
|
625
|
+
sr.snapshot_done,
|
|
626
|
+
sr.no_checkpoint_before,
|
|
627
|
+
computed.can_checkpoint,
|
|
628
|
+
computed.keepalive_op,
|
|
629
|
+
computed.new_last_checkpoint
|
|
630
|
+
)
|
|
631
|
+
SELECT
|
|
632
|
+
id,
|
|
633
|
+
state,
|
|
634
|
+
last_checkpoint,
|
|
635
|
+
last_checkpoint_lsn,
|
|
636
|
+
snapshot_done,
|
|
637
|
+
no_checkpoint_before,
|
|
638
|
+
can_checkpoint,
|
|
639
|
+
keepalive_op,
|
|
640
|
+
new_last_checkpoint,
|
|
641
|
+
TRUE AS created_checkpoint
|
|
642
|
+
FROM
|
|
643
|
+
updated
|
|
644
|
+
UNION ALL
|
|
645
|
+
SELECT
|
|
646
|
+
id,
|
|
647
|
+
state,
|
|
648
|
+
new_last_checkpoint AS last_checkpoint,
|
|
649
|
+
last_checkpoint_lsn,
|
|
650
|
+
snapshot_done,
|
|
651
|
+
no_checkpoint_before,
|
|
652
|
+
can_checkpoint,
|
|
653
|
+
keepalive_op,
|
|
654
|
+
new_last_checkpoint,
|
|
655
|
+
FALSE AS created_checkpoint
|
|
656
|
+
FROM
|
|
657
|
+
computed
|
|
658
|
+
WHERE
|
|
659
|
+
NOT EXISTS (
|
|
660
|
+
SELECT
|
|
661
|
+
1
|
|
662
|
+
FROM
|
|
663
|
+
updated
|
|
664
|
+
)
|
|
665
|
+
`
|
|
666
|
+
.decoded(CheckpointWithStatus)
|
|
667
|
+
.first();
|
|
668
|
+
|
|
669
|
+
if (checkpoint?.can_checkpoint && checkpoint.state == storage.SyncRuleState.ACTIVE) {
|
|
670
|
+
await notifySyncRulesUpdate(db, checkpoint);
|
|
671
|
+
}
|
|
672
|
+
return checkpoint;
|
|
673
|
+
});
|
|
678
674
|
|
|
679
675
|
if (result == null) {
|
|
680
676
|
throw new ReplicationAssertionError('Failed to update sync_rules during checkpoint');
|
|
@@ -706,10 +702,8 @@ export class PostgresBucketBatch
|
|
|
706
702
|
});
|
|
707
703
|
}
|
|
708
704
|
}
|
|
709
|
-
await this.autoActivate(lsn
|
|
710
|
-
await notifySyncRulesUpdate(this.db, {
|
|
705
|
+
await this.autoActivate(lsn, {
|
|
711
706
|
id: result.id,
|
|
712
|
-
state: result.state,
|
|
713
707
|
last_checkpoint: result.last_checkpoint,
|
|
714
708
|
last_checkpoint_lsn: result.last_checkpoint_lsn
|
|
715
709
|
});
|
|
@@ -758,23 +752,22 @@ export class PostgresBucketBatch
|
|
|
758
752
|
|
|
759
753
|
async markSnapshotDone(no_checkpoint_before_lsn: string, options?: { throwOnConflict?: boolean }): Promise<void> {
|
|
760
754
|
await this.db.transaction(async (db) => {
|
|
761
|
-
const
|
|
755
|
+
const blockingTables = await db.sql`
|
|
762
756
|
SELECT
|
|
763
|
-
|
|
757
|
+
schema_name,
|
|
758
|
+
table_name
|
|
764
759
|
FROM
|
|
765
760
|
source_tables
|
|
766
761
|
WHERE
|
|
767
762
|
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
768
763
|
AND snapshot_done = FALSE
|
|
769
764
|
`
|
|
770
|
-
.decoded(t.object({
|
|
771
|
-
.
|
|
772
|
-
if (
|
|
765
|
+
.decoded(t.object({ schema_name: t.string, table_name: t.string }))
|
|
766
|
+
.rows();
|
|
767
|
+
if (blockingTables.length > 0) {
|
|
773
768
|
if (options?.throwOnConflict ?? true) {
|
|
774
769
|
throw new ReplicationAssertionError(
|
|
775
|
-
`Cannot mark snapshot done while ${
|
|
776
|
-
snapshotRequiredCount?.count == 1n ? '' : 's'
|
|
777
|
-
} still require snapshotting`
|
|
770
|
+
`Cannot mark snapshot done while source tables still require snapshotting. Tables: ${blockingTables.map((t) => `${t.schema_name}.${t.table_name}`).join(', ')}`
|
|
778
771
|
);
|
|
779
772
|
} else {
|
|
780
773
|
return;
|
|
@@ -890,10 +883,17 @@ export class PostgresBucketBatch
|
|
|
890
883
|
});
|
|
891
884
|
}
|
|
892
885
|
|
|
893
|
-
protected async replicateBatch(
|
|
886
|
+
protected async replicateBatch(
|
|
887
|
+
db: lib_postgres.WrappedConnection,
|
|
888
|
+
batch: OperationBatch
|
|
889
|
+
): Promise<{ resumeBatch: OperationBatch | null; clearedError: boolean }> {
|
|
894
890
|
let sizes: Map<string, number> | undefined = undefined;
|
|
895
|
-
if
|
|
896
|
-
|
|
891
|
+
// Check if any table in this batch needs to store current_data
|
|
892
|
+
const anyTableStoresCurrentData =
|
|
893
|
+
this.options.store_current_data && batch.batch.some((r) => r.record.sourceTable.storeCurrentData);
|
|
894
|
+
|
|
895
|
+
if (anyTableStoresCurrentData && !this.options.skip_existing_rows) {
|
|
896
|
+
// We skip this step if no tables store current_data, since the sizes will
|
|
897
897
|
// always be small in that case.
|
|
898
898
|
|
|
899
899
|
// With skipExistingRows, we don't load the full documents into memory,
|
|
@@ -901,15 +901,19 @@ export class PostgresBucketBatch
|
|
|
901
901
|
|
|
902
902
|
// Find sizes of current_data documents, to assist in intelligent batching without
|
|
903
903
|
// exceeding memory limits.
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
904
|
+
// Within this branch the batch stores current_data, so the per-table flag is the
|
|
905
|
+
// effective value - only look up sizes for tables that actually store current_data.
|
|
906
|
+
const sizeLookups = batch.batch
|
|
907
|
+
.filter((r) => r.record.sourceTable.storeCurrentData)
|
|
908
|
+
.map((r) => {
|
|
909
|
+
return {
|
|
910
|
+
source_table: postgresTableId(r.record.sourceTable.id),
|
|
911
|
+
/**
|
|
912
|
+
* Encode to hex in order to pass a jsonb
|
|
913
|
+
*/
|
|
914
|
+
source_key: storage.serializeReplicaId(r.beforeId).toString('hex')
|
|
915
|
+
};
|
|
916
|
+
});
|
|
913
917
|
|
|
914
918
|
sizes = new Map<string, number>();
|
|
915
919
|
|
|
@@ -1006,15 +1010,14 @@ export class PostgresBucketBatch
|
|
|
1006
1010
|
}
|
|
1007
1011
|
}
|
|
1008
1012
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
}
|
|
1013
|
+
// The error itself is cleared by the caller, outside the replication transaction - see flushInner.
|
|
1014
|
+
const clearedError = didFlush && !this.clearedError;
|
|
1012
1015
|
|
|
1013
1016
|
// Don't return empty batches
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1017
|
+
return {
|
|
1018
|
+
resumeBatch: resumeBatch?.batch.length ? resumeBatch : null,
|
|
1019
|
+
clearedError
|
|
1020
|
+
};
|
|
1018
1021
|
}
|
|
1019
1022
|
|
|
1020
1023
|
protected async saveOperation(
|
|
@@ -1028,6 +1031,9 @@ export class PostgresBucketBatch
|
|
|
1028
1031
|
const afterId = operation.afterId;
|
|
1029
1032
|
let after = record.after;
|
|
1030
1033
|
const sourceTable = record.sourceTable;
|
|
1034
|
+
// Effective per-record flag: store current_data only if both the batch (source-level)
|
|
1035
|
+
// and the table (e.g. non-FULL replica identity) require it.
|
|
1036
|
+
const storeCurrentData = this.options.store_current_data && sourceTable.storeCurrentData;
|
|
1031
1037
|
|
|
1032
1038
|
let existingBuckets: CurrentBucket[] = [];
|
|
1033
1039
|
let newBuckets: CurrentBucket[] = [];
|
|
@@ -1056,7 +1062,7 @@ export class PostgresBucketBatch
|
|
|
1056
1062
|
existingLookups = [];
|
|
1057
1063
|
// Log to help with debugging if there was a consistency issue
|
|
1058
1064
|
|
|
1059
|
-
if (
|
|
1065
|
+
if (storeCurrentData) {
|
|
1060
1066
|
if (this.markRecordUnavailable != null) {
|
|
1061
1067
|
// This will trigger a "resnapshot" of the record.
|
|
1062
1068
|
// This is not relevant if storeCurrentData is false, since we'll get the full row
|
|
@@ -1072,7 +1078,7 @@ export class PostgresBucketBatch
|
|
|
1072
1078
|
} else {
|
|
1073
1079
|
existingBuckets = result.buckets;
|
|
1074
1080
|
existingLookups = result.lookups;
|
|
1075
|
-
if (
|
|
1081
|
+
if (storeCurrentData) {
|
|
1076
1082
|
const data = storage.deserializeBson(result.data) as sync_rules.SqliteRow;
|
|
1077
1083
|
after = storage.mergeToast(after!, data);
|
|
1078
1084
|
}
|
|
@@ -1083,7 +1089,9 @@ export class PostgresBucketBatch
|
|
|
1083
1089
|
// Not an error if we re-apply a transaction
|
|
1084
1090
|
existingBuckets = [];
|
|
1085
1091
|
existingLookups = [];
|
|
1086
|
-
// Log to help with debugging if there was a consistency issue
|
|
1092
|
+
// Log to help with debugging if there was a consistency issue.
|
|
1093
|
+
// Gate on the batch-level flag: FULL tables (per-record flag false) still get a
|
|
1094
|
+
// current_data entry, so a missing record on DELETE is meaningful for them too.
|
|
1087
1095
|
if (this.options.store_current_data && this.markRecordUnavailable == null) {
|
|
1088
1096
|
this.logger.warn(
|
|
1089
1097
|
`Cannot find previous record for delete on ${record.sourceTable.qualifiedName}: ${beforeId} / ${record.before?.id}`
|
|
@@ -1096,7 +1104,7 @@ export class PostgresBucketBatch
|
|
|
1096
1104
|
}
|
|
1097
1105
|
|
|
1098
1106
|
let afterData: Buffer<ArrayBuffer> | undefined;
|
|
1099
|
-
if (afterId != null && !
|
|
1107
|
+
if (afterId != null && !storeCurrentData) {
|
|
1100
1108
|
afterData = storage.serializeBson({});
|
|
1101
1109
|
} else if (afterId != null) {
|
|
1102
1110
|
try {
|
|
@@ -1118,7 +1126,7 @@ export class PostgresBucketBatch
|
|
|
1118
1126
|
{
|
|
1119
1127
|
level: errors.ErrorSeverity.WARNING,
|
|
1120
1128
|
metadata: {
|
|
1121
|
-
replication_slot: this.options.
|
|
1129
|
+
replication_slot: this.options.replicationStreamName,
|
|
1122
1130
|
table: record.sourceTable.qualifiedName
|
|
1123
1131
|
}
|
|
1124
1132
|
}
|
|
@@ -1159,7 +1167,7 @@ export class PostgresBucketBatch
|
|
|
1159
1167
|
// However, it will be valid by the end of the transaction.
|
|
1160
1168
|
//
|
|
1161
1169
|
// In this case, we don't save the op, but we do save the current data.
|
|
1162
|
-
if (afterId && after && utils.isCompleteRow(
|
|
1170
|
+
if (afterId && after && utils.isCompleteRow(storeCurrentData, after)) {
|
|
1163
1171
|
// Insert or update
|
|
1164
1172
|
if (sourceTable.syncData) {
|
|
1165
1173
|
const { results: evaluated, errors: syncErrors } = this.sync_rules.evaluateRowWithErrors({
|
|
@@ -1174,7 +1182,7 @@ export class PostgresBucketBatch
|
|
|
1174
1182
|
{
|
|
1175
1183
|
level: errors.ErrorSeverity.WARNING,
|
|
1176
1184
|
metadata: {
|
|
1177
|
-
replication_slot: this.options.
|
|
1185
|
+
replication_slot: this.options.replicationStreamName,
|
|
1178
1186
|
table: record.sourceTable.qualifiedName
|
|
1179
1187
|
}
|
|
1180
1188
|
}
|
|
@@ -1215,7 +1223,7 @@ export class PostgresBucketBatch
|
|
|
1215
1223
|
{
|
|
1216
1224
|
level: errors.ErrorSeverity.WARNING,
|
|
1217
1225
|
metadata: {
|
|
1218
|
-
replication_slot: this.options.
|
|
1226
|
+
replication_slot: this.options.replicationStreamName,
|
|
1219
1227
|
table: record.sourceTable.qualifiedName
|
|
1220
1228
|
}
|
|
1221
1229
|
}
|
|
@@ -1272,14 +1280,13 @@ export class PostgresBucketBatch
|
|
|
1272
1280
|
*
|
|
1273
1281
|
* Called on new commits.
|
|
1274
1282
|
*/
|
|
1275
|
-
private async autoActivate(lsn: string): Promise<void> {
|
|
1283
|
+
private async autoActivate(lsn: string, checkpoint: models.ActiveCheckpointDecoded): Promise<void> {
|
|
1276
1284
|
if (!this.needsActivation) {
|
|
1277
1285
|
// Already activated
|
|
1278
1286
|
return;
|
|
1279
1287
|
}
|
|
1280
1288
|
|
|
1281
|
-
|
|
1282
|
-
await this.db.transaction(async (db) => {
|
|
1289
|
+
const activationResult = await this.db.transaction(async (db) => {
|
|
1283
1290
|
const syncRulesRow = await db.sql`
|
|
1284
1291
|
SELECT
|
|
1285
1292
|
state,
|
|
@@ -1313,22 +1320,27 @@ export class PostgresBucketBatch
|
|
|
1313
1320
|
)
|
|
1314
1321
|
AND id != ${{ type: 'int4', value: this.group_id }}
|
|
1315
1322
|
`.execute();
|
|
1316
|
-
|
|
1317
|
-
|
|
1323
|
+
await notifySyncRulesUpdate(db, checkpoint);
|
|
1324
|
+
return 'activated';
|
|
1318
1325
|
} else if (syncRulesRow?.state != storage.SyncRuleState.PROCESSING) {
|
|
1319
|
-
|
|
1326
|
+
return 'not-processing';
|
|
1320
1327
|
}
|
|
1328
|
+
return 'pending';
|
|
1321
1329
|
});
|
|
1322
|
-
|
|
1330
|
+
|
|
1331
|
+
if (activationResult != 'pending') {
|
|
1332
|
+
this.needsActivation = false;
|
|
1333
|
+
}
|
|
1334
|
+
if (activationResult == 'activated') {
|
|
1323
1335
|
this.logger.info(`Activated new replication stream at ${lsn}`);
|
|
1324
1336
|
}
|
|
1325
1337
|
}
|
|
1326
1338
|
|
|
1327
1339
|
/**
|
|
1328
|
-
* Gets relevant {@link
|
|
1340
|
+
* Gets relevant {@link HydratedEventDescriptor}s for the given {@link SourceTable}
|
|
1329
1341
|
* TODO maybe share this with an abstract class
|
|
1330
1342
|
*/
|
|
1331
|
-
protected getTableEvents(table: storage.SourceTable): sync_rules.
|
|
1343
|
+
protected getTableEvents(table: storage.SourceTable): sync_rules.HydratedEventDescriptor[] {
|
|
1332
1344
|
return this.sync_rules.eventDescriptors.filter((evt) =>
|
|
1333
1345
|
[...evt.getSourceTables()].some((sourceTable) => sourceTable.matches(table.ref))
|
|
1334
1346
|
);
|
|
@@ -1374,11 +1386,6 @@ export class PostgresBucketBatch
|
|
|
1374
1386
|
protected async clearError(
|
|
1375
1387
|
db: lib_postgres.AbstractPostgresConnection | lib_postgres.DatabaseClient = this.db
|
|
1376
1388
|
): Promise<void> {
|
|
1377
|
-
// No need to clear an error more than once per batch, since an error would always result in restarting the batch.
|
|
1378
|
-
if (this.clearedError) {
|
|
1379
|
-
return;
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
1389
|
await db.sql`
|
|
1383
1390
|
UPDATE sync_rules
|
|
1384
1391
|
SET
|
|
@@ -1386,7 +1393,6 @@ export class PostgresBucketBatch
|
|
|
1386
1393
|
WHERE
|
|
1387
1394
|
id = ${{ type: 'int4', value: this.group_id }}
|
|
1388
1395
|
`.execute();
|
|
1389
|
-
this.clearedError = true;
|
|
1390
1396
|
}
|
|
1391
1397
|
|
|
1392
1398
|
private async getLastOpIdSequence(db: lib_postgres.AbstractPostgresConnection) {
|
|
@@ -1411,12 +1417,17 @@ export class PostgresBucketBatch
|
|
|
1411
1417
|
* Uses Postgres' NOTIFY functionality to update different processes when the
|
|
1412
1418
|
* active checkpoint has been updated.
|
|
1413
1419
|
*/
|
|
1414
|
-
export const notifySyncRulesUpdate = async (
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1420
|
+
export const notifySyncRulesUpdate = async (
|
|
1421
|
+
db: lib_postgres.AbstractPostgresConnection,
|
|
1422
|
+
update: models.ActiveCheckpointDecoded
|
|
1423
|
+
) => {
|
|
1424
|
+
const payload = models.ActiveCheckpointNotification.encode({ active_checkpoint: update });
|
|
1425
|
+
|
|
1426
|
+
await db.sql`
|
|
1427
|
+
SELECT
|
|
1428
|
+
pg_notify (
|
|
1429
|
+
${{ type: 'varchar', value: NOTIFICATION_CHANNEL }},
|
|
1430
|
+
${{ type: 'varchar', value: payload }}
|
|
1431
|
+
)
|
|
1432
|
+
`.execute();
|
|
1422
1433
|
};
|