@powersync/service-module-mysql 0.7.4 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/LICENSE +3 -3
- package/dev/docker/mysql/init-scripts/my.cnf +1 -3
- package/dist/api/MySQLRouteAPIAdapter.js +12 -4
- package/dist/api/MySQLRouteAPIAdapter.js.map +1 -1
- package/dist/common/ReplicatedGTID.js +4 -0
- package/dist/common/ReplicatedGTID.js.map +1 -1
- package/dist/common/common-index.d.ts +1 -2
- package/dist/common/common-index.js +1 -2
- package/dist/common/common-index.js.map +1 -1
- package/dist/common/mysql-to-sqlite.d.ts +1 -1
- package/dist/common/mysql-to-sqlite.js +4 -0
- package/dist/common/mysql-to-sqlite.js.map +1 -1
- package/dist/common/schema-utils.d.ts +20 -0
- package/dist/common/{get-replication-columns.js → schema-utils.js} +73 -30
- package/dist/common/schema-utils.js.map +1 -0
- package/dist/replication/BinLogReplicationJob.js +4 -1
- package/dist/replication/BinLogReplicationJob.js.map +1 -1
- package/dist/replication/BinLogStream.d.ts +9 -6
- package/dist/replication/BinLogStream.js +117 -73
- package/dist/replication/BinLogStream.js.map +1 -1
- package/dist/replication/zongji/BinLogListener.d.ts +60 -6
- package/dist/replication/zongji/BinLogListener.js +347 -89
- package/dist/replication/zongji/BinLogListener.js.map +1 -1
- package/dist/replication/zongji/zongji-utils.d.ts +4 -1
- package/dist/replication/zongji/zongji-utils.js +9 -0
- package/dist/replication/zongji/zongji-utils.js.map +1 -1
- package/dist/types/node-sql-parser-extended-types.d.ts +31 -0
- package/dist/types/node-sql-parser-extended-types.js +2 -0
- package/dist/types/node-sql-parser-extended-types.js.map +1 -0
- package/dist/utils/mysql-utils.d.ts +4 -2
- package/dist/utils/mysql-utils.js +15 -3
- package/dist/utils/mysql-utils.js.map +1 -1
- package/dist/utils/parser-utils.d.ts +16 -0
- package/dist/utils/parser-utils.js +58 -0
- package/dist/utils/parser-utils.js.map +1 -0
- package/package.json +12 -11
- package/src/api/MySQLRouteAPIAdapter.ts +15 -4
- package/src/common/ReplicatedGTID.ts +6 -1
- package/src/common/common-index.ts +1 -2
- package/src/common/mysql-to-sqlite.ts +7 -1
- package/src/common/{get-replication-columns.ts → schema-utils.ts} +96 -37
- package/src/replication/BinLogReplicationJob.ts +4 -1
- package/src/replication/BinLogStream.ts +139 -94
- package/src/replication/zongji/BinLogListener.ts +421 -100
- package/src/replication/zongji/zongji-utils.ts +16 -1
- package/src/types/node-sql-parser-extended-types.ts +25 -0
- package/src/utils/mysql-utils.ts +19 -4
- package/src/utils/parser-utils.ts +73 -0
- package/test/src/BinLogListener.test.ts +421 -77
- package/test/src/BinLogStream.test.ts +128 -52
- package/test/src/BinlogStreamUtils.ts +12 -2
- package/test/src/mysql-to-sqlite.test.ts +5 -5
- package/test/src/parser-utils.test.ts +24 -0
- package/test/src/schema-changes.test.ts +659 -0
- package/test/src/util.ts +87 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/common/get-replication-columns.d.ts +0 -12
- package/dist/common/get-replication-columns.js.map +0 -1
- package/dist/common/get-tables-from-pattern.d.ts +0 -7
- package/dist/common/get-tables-from-pattern.js +0 -28
- package/dist/common/get-tables-from-pattern.js.map +0 -1
- package/src/common/get-tables-from-pattern.ts +0 -44
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
ColumnDescriptor,
|
|
11
11
|
framework,
|
|
12
12
|
getUuidReplicaIdentityBson,
|
|
13
|
+
InternalOpId,
|
|
13
14
|
MetricsEngine,
|
|
15
|
+
SourceTable,
|
|
14
16
|
storage
|
|
15
17
|
} from '@powersync/service-core';
|
|
16
18
|
import mysql from 'mysql2';
|
|
@@ -18,10 +20,10 @@ import mysqlPromise from 'mysql2/promise';
|
|
|
18
20
|
|
|
19
21
|
import { TableMapEntry } from '@powersync/mysql-zongji';
|
|
20
22
|
import * as common from '../common/common-index.js';
|
|
21
|
-
import { createRandomServerId,
|
|
23
|
+
import { createRandomServerId, qualifiedMySQLTable } from '../utils/mysql-utils.js';
|
|
22
24
|
import { MySQLConnectionManager } from './MySQLConnectionManager.js';
|
|
23
25
|
import { ReplicationMetric } from '@powersync/service-types';
|
|
24
|
-
import { BinLogEventHandler, BinLogListener, Row } from './zongji/BinLogListener.js';
|
|
26
|
+
import { BinLogEventHandler, BinLogListener, Row, SchemaChange, SchemaChangeType } from './zongji/BinLogListener.js';
|
|
25
27
|
|
|
26
28
|
export interface BinLogStreamOptions {
|
|
27
29
|
connections: MySQLConnectionManager;
|
|
@@ -31,11 +33,6 @@ export interface BinLogStreamOptions {
|
|
|
31
33
|
logger?: Logger;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
interface MysqlRelId {
|
|
35
|
-
schema: string;
|
|
36
|
-
name: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
36
|
interface WriteChangePayload {
|
|
40
37
|
type: storage.SaveOperationTag;
|
|
41
38
|
row: Row;
|
|
@@ -53,11 +50,14 @@ export class BinlogConfigurationError extends Error {
|
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
53
|
+
* Unlike Postgres' relation id, MySQL's tableId is only guaranteed to be unique and stay the same
|
|
54
|
+
* in the context of a single replication session.
|
|
55
|
+
* Instead, we create a unique key by combining the source schema and table name
|
|
56
|
+
* @param schema
|
|
57
|
+
* @param tableName
|
|
58
58
|
*/
|
|
59
|
-
function
|
|
60
|
-
return `${
|
|
59
|
+
function createTableId(schema: string, tableName: string): string {
|
|
60
|
+
return `${schema}.${tableName}`;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
export class BinLogStream {
|
|
@@ -68,11 +68,11 @@ export class BinLogStream {
|
|
|
68
68
|
|
|
69
69
|
private readonly connections: MySQLConnectionManager;
|
|
70
70
|
|
|
71
|
-
private abortSignal: AbortSignal;
|
|
71
|
+
private readonly abortSignal: AbortSignal;
|
|
72
72
|
|
|
73
|
-
private
|
|
73
|
+
private readonly logger: Logger;
|
|
74
74
|
|
|
75
|
-
private
|
|
75
|
+
private tableCache = new Map<string | number, storage.SourceTable>();
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* Time of the oldest uncommitted change, according to the source db.
|
|
@@ -83,7 +83,7 @@ export class BinLogStream {
|
|
|
83
83
|
* Keep track of whether we have done a commit or keepalive yet.
|
|
84
84
|
* We can only compute replication lag if isStartingReplication == false, or oldestUncommittedChange is present.
|
|
85
85
|
*/
|
|
86
|
-
|
|
86
|
+
isStartingReplication = true;
|
|
87
87
|
|
|
88
88
|
constructor(private options: BinLogStreamOptions) {
|
|
89
89
|
this.logger = options.logger ?? defaultLogger;
|
|
@@ -134,15 +134,15 @@ export class BinLogStream {
|
|
|
134
134
|
entity_descriptor: entity,
|
|
135
135
|
sync_rules: this.syncRules
|
|
136
136
|
});
|
|
137
|
-
// objectId is always defined
|
|
137
|
+
// Since we create the objectId ourselves, this is always defined
|
|
138
138
|
this.tableCache.set(entity.objectId!, result.table);
|
|
139
139
|
|
|
140
|
-
// Drop conflicting tables.
|
|
140
|
+
// Drop conflicting tables. In the MySQL case with ObjectIds created from the table name, renames cannot be detected by the storage.
|
|
141
141
|
await batch.drop(result.dropTables);
|
|
142
142
|
|
|
143
143
|
// Snapshot if:
|
|
144
144
|
// 1. Snapshot is requested (false for initial snapshot, since that process handles it elsewhere)
|
|
145
|
-
// 2. Snapshot is not
|
|
145
|
+
// 2. Snapshot is not done yet, AND:
|
|
146
146
|
// 3. The table is used in sync rules.
|
|
147
147
|
const shouldSnapshot = snapshot && !result.table.snapshotComplete && result.table.syncAny;
|
|
148
148
|
|
|
@@ -158,10 +158,10 @@ export class BinLogStream {
|
|
|
158
158
|
const promiseConnection = (connection as mysql.Connection).promise();
|
|
159
159
|
try {
|
|
160
160
|
await promiseConnection.query(`SET time_zone = '+00:00'`);
|
|
161
|
-
await promiseConnection.query('
|
|
161
|
+
await promiseConnection.query('START TRANSACTION');
|
|
162
162
|
try {
|
|
163
163
|
gtid = await common.readExecutedGtid(promiseConnection);
|
|
164
|
-
await this.snapshotTable(connection.
|
|
164
|
+
await this.snapshotTable(connection as mysql.Connection, batch, result.table);
|
|
165
165
|
await promiseConnection.query('COMMIT');
|
|
166
166
|
} catch (e) {
|
|
167
167
|
await this.tryRollback(promiseConnection);
|
|
@@ -185,62 +185,21 @@ export class BinLogStream {
|
|
|
185
185
|
return [];
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
const result = await this.connections.query(
|
|
192
|
-
`SELECT TABLE_NAME
|
|
193
|
-
FROM information_schema.tables
|
|
194
|
-
WHERE TABLE_SCHEMA = ? AND TABLE_NAME LIKE ?;
|
|
195
|
-
`,
|
|
196
|
-
[tablePattern.schema, tablePattern.tablePattern]
|
|
197
|
-
);
|
|
198
|
-
tableRows = result[0];
|
|
199
|
-
} else {
|
|
200
|
-
const result = await this.connections.query(
|
|
201
|
-
`SELECT TABLE_NAME
|
|
202
|
-
FROM information_schema.tables
|
|
203
|
-
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?;
|
|
204
|
-
`,
|
|
205
|
-
[tablePattern.schema, tablePattern.tablePattern]
|
|
206
|
-
);
|
|
207
|
-
tableRows = result[0];
|
|
208
|
-
}
|
|
209
|
-
let tables: storage.SourceTable[] = [];
|
|
210
|
-
|
|
211
|
-
for (let row of tableRows) {
|
|
212
|
-
const name = row['TABLE_NAME'] as string;
|
|
213
|
-
if (prefix && !name.startsWith(prefix)) {
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const result = await this.connections.query(
|
|
218
|
-
`SELECT 1
|
|
219
|
-
FROM information_schema.tables
|
|
220
|
-
WHERE table_schema = ? AND table_name = ?
|
|
221
|
-
AND table_type = 'BASE TABLE';`,
|
|
222
|
-
[tablePattern.schema, tablePattern.name]
|
|
223
|
-
);
|
|
224
|
-
if (result[0].length == 0) {
|
|
225
|
-
this.logger.info(`Skipping ${tablePattern.schema}.${name} - no table exists/is not a base table`);
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
188
|
+
const connection = await this.connections.getConnection();
|
|
189
|
+
const matchedTables: string[] = await common.getTablesFromPattern(connection, tablePattern);
|
|
190
|
+
connection.release();
|
|
228
191
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
schema: tablePattern.schema,
|
|
233
|
-
table_name: tablePattern.name
|
|
234
|
-
});
|
|
235
|
-
connection.release();
|
|
192
|
+
let tables: storage.SourceTable[] = [];
|
|
193
|
+
for (const matchedTable of matchedTables) {
|
|
194
|
+
const replicaIdColumns = await this.getReplicaIdColumns(matchedTable, tablePattern.schema);
|
|
236
195
|
|
|
237
196
|
const table = await this.handleRelation(
|
|
238
197
|
batch,
|
|
239
198
|
{
|
|
240
|
-
name,
|
|
199
|
+
name: matchedTable,
|
|
241
200
|
schema: tablePattern.schema,
|
|
242
|
-
objectId:
|
|
243
|
-
|
|
201
|
+
objectId: createTableId(tablePattern.schema, matchedTable),
|
|
202
|
+
replicaIdColumns: replicaIdColumns
|
|
244
203
|
},
|
|
245
204
|
false
|
|
246
205
|
);
|
|
@@ -251,7 +210,7 @@ AND table_type = 'BASE TABLE';`,
|
|
|
251
210
|
}
|
|
252
211
|
|
|
253
212
|
/**
|
|
254
|
-
* Checks if the initial sync has been completed
|
|
213
|
+
* Checks if the initial sync has already been completed
|
|
255
214
|
*/
|
|
256
215
|
protected async checkInitialReplicated(): Promise<boolean> {
|
|
257
216
|
const status = await this.storage.getStatus();
|
|
@@ -260,7 +219,7 @@ AND table_type = 'BASE TABLE';`,
|
|
|
260
219
|
this.logger.info(`Initial replication already done.`);
|
|
261
220
|
|
|
262
221
|
if (lastKnowGTID) {
|
|
263
|
-
// Check if the binlog is still available. If it isn't we need to snapshot again.
|
|
222
|
+
// Check if the specific binlog file is still available. If it isn't, we need to snapshot again.
|
|
264
223
|
const connection = await this.connections.getConnection();
|
|
265
224
|
try {
|
|
266
225
|
const isAvailable = await common.isBinlogStillAvailable(connection, lastKnowGTID.position.filename);
|
|
@@ -294,6 +253,7 @@ AND table_type = 'BASE TABLE';`,
|
|
|
294
253
|
const promiseConnection = (connection as mysql.Connection).promise();
|
|
295
254
|
const headGTID = await common.readExecutedGtid(promiseConnection);
|
|
296
255
|
this.logger.info(`Using snapshot checkpoint GTID: '${headGTID}'`);
|
|
256
|
+
let lastOp: InternalOpId | null = null;
|
|
297
257
|
try {
|
|
298
258
|
this.logger.info(`Starting initial replication`);
|
|
299
259
|
await promiseConnection.query<mysqlPromise.RowDataPacket[]>(
|
|
@@ -303,7 +263,7 @@ AND table_type = 'BASE TABLE';`,
|
|
|
303
263
|
await promiseConnection.query(`SET time_zone = '+00:00'`);
|
|
304
264
|
|
|
305
265
|
const sourceTables = this.syncRules.getSourceTables();
|
|
306
|
-
await this.storage.startBatch(
|
|
266
|
+
const flushResults = await this.storage.startBatch(
|
|
307
267
|
{
|
|
308
268
|
logger: this.logger,
|
|
309
269
|
zeroLSN: common.ReplicatedGTID.ZERO.comparable,
|
|
@@ -322,6 +282,7 @@ AND table_type = 'BASE TABLE';`,
|
|
|
322
282
|
await batch.commit(headGTID.comparable);
|
|
323
283
|
}
|
|
324
284
|
);
|
|
285
|
+
lastOp = flushResults?.flushed_op ?? null;
|
|
325
286
|
this.logger.info(`Initial replication done`);
|
|
326
287
|
await promiseConnection.query('COMMIT');
|
|
327
288
|
} catch (e) {
|
|
@@ -330,6 +291,15 @@ AND table_type = 'BASE TABLE';`,
|
|
|
330
291
|
} finally {
|
|
331
292
|
connection.release();
|
|
332
293
|
}
|
|
294
|
+
|
|
295
|
+
if (lastOp != null) {
|
|
296
|
+
// Populate the cache _after_ initial replication, but _before_ we switch to this sync rules.
|
|
297
|
+
await this.storage.populatePersistentChecksumCache({
|
|
298
|
+
// No checkpoint yet, but we do have the opId.
|
|
299
|
+
maxOpId: lastOp,
|
|
300
|
+
signal: this.abortSignal
|
|
301
|
+
});
|
|
302
|
+
}
|
|
333
303
|
}
|
|
334
304
|
|
|
335
305
|
private async snapshotTable(
|
|
@@ -337,11 +307,11 @@ AND table_type = 'BASE TABLE';`,
|
|
|
337
307
|
batch: storage.BucketStorageBatch,
|
|
338
308
|
table: storage.SourceTable
|
|
339
309
|
) {
|
|
340
|
-
this.logger.info(`Replicating ${table
|
|
310
|
+
this.logger.info(`Replicating ${qualifiedMySQLTable(table)}`);
|
|
341
311
|
// TODO count rows and log progress at certain batch sizes
|
|
342
312
|
|
|
343
313
|
// MAX_EXECUTION_TIME(0) hint disables execution timeout for this query
|
|
344
|
-
const query = connection.query(`SELECT /*+ MAX_EXECUTION_TIME(0) */ * FROM ${
|
|
314
|
+
const query = connection.query(`SELECT /*+ MAX_EXECUTION_TIME(0) */ * FROM ${qualifiedMySQLTable(table)}`);
|
|
345
315
|
const stream = query.stream();
|
|
346
316
|
|
|
347
317
|
let columns: Map<string, ColumnDescriptor> | undefined = undefined;
|
|
@@ -430,8 +400,6 @@ AND table_type = 'BASE TABLE';`,
|
|
|
430
400
|
}
|
|
431
401
|
|
|
432
402
|
async streamChanges() {
|
|
433
|
-
// Auto-activate as soon as initial replication is done
|
|
434
|
-
await this.storage.autoActivate();
|
|
435
403
|
const serverId = createRandomServerId(this.storage.group_id);
|
|
436
404
|
|
|
437
405
|
const connection = await this.connections.getConnection();
|
|
@@ -442,7 +410,6 @@ AND table_type = 'BASE TABLE';`,
|
|
|
442
410
|
const fromGTID = checkpoint_lsn
|
|
443
411
|
? common.ReplicatedGTID.fromSerialized(checkpoint_lsn)
|
|
444
412
|
: await common.readExecutedGtid(connection);
|
|
445
|
-
const binLogPositionState = fromGTID.position;
|
|
446
413
|
connection.release();
|
|
447
414
|
|
|
448
415
|
if (!this.stopped) {
|
|
@@ -450,12 +417,10 @@ AND table_type = 'BASE TABLE';`,
|
|
|
450
417
|
{ zeroLSN: common.ReplicatedGTID.ZERO.comparable, defaultSchema: this.defaultSchema, storeCurrentData: true },
|
|
451
418
|
async (batch) => {
|
|
452
419
|
const binlogEventHandler = this.createBinlogEventHandler(batch);
|
|
453
|
-
// Only listen for changes to tables in the sync rules
|
|
454
|
-
const includedTables = [...this.tableCache.values()].map((table) => table.table);
|
|
455
420
|
const binlogListener = new BinLogListener({
|
|
456
421
|
logger: this.logger,
|
|
457
|
-
|
|
458
|
-
|
|
422
|
+
sourceTables: this.syncRules.getSourceTables(),
|
|
423
|
+
startGTID: fromGTID,
|
|
459
424
|
connectionManager: this.connections,
|
|
460
425
|
serverId: serverId,
|
|
461
426
|
eventHandler: binlogEventHandler
|
|
@@ -463,15 +428,14 @@ AND table_type = 'BASE TABLE';`,
|
|
|
463
428
|
|
|
464
429
|
this.abortSignal.addEventListener(
|
|
465
430
|
'abort',
|
|
466
|
-
() => {
|
|
467
|
-
|
|
468
|
-
binlogListener.stop();
|
|
431
|
+
async () => {
|
|
432
|
+
await binlogListener.stop();
|
|
469
433
|
},
|
|
470
434
|
{ once: true }
|
|
471
435
|
);
|
|
472
436
|
|
|
473
|
-
// Only returns when the replication is stopped or interrupted by an error
|
|
474
437
|
await binlogListener.start();
|
|
438
|
+
await binlogListener.replicateUntilStopped();
|
|
475
439
|
}
|
|
476
440
|
);
|
|
477
441
|
}
|
|
@@ -502,6 +466,12 @@ AND table_type = 'BASE TABLE';`,
|
|
|
502
466
|
tableEntry: tableMap
|
|
503
467
|
});
|
|
504
468
|
},
|
|
469
|
+
onKeepAlive: async (lsn: string) => {
|
|
470
|
+
const didCommit = await batch.keepalive(lsn);
|
|
471
|
+
if (didCommit) {
|
|
472
|
+
this.oldestUncommittedChange = null;
|
|
473
|
+
}
|
|
474
|
+
},
|
|
505
475
|
onCommit: async (lsn: string) => {
|
|
506
476
|
this.metrics.getCounter(ReplicationMetric.TRANSACTIONS_REPLICATED).add(1);
|
|
507
477
|
const didCommit = await batch.commit(lsn, { oldestUncommittedChange: this.oldestUncommittedChange });
|
|
@@ -517,10 +487,82 @@ AND table_type = 'BASE TABLE';`,
|
|
|
517
487
|
},
|
|
518
488
|
onRotate: async () => {
|
|
519
489
|
this.isStartingReplication = false;
|
|
490
|
+
},
|
|
491
|
+
onSchemaChange: async (change: SchemaChange) => {
|
|
492
|
+
await this.handleSchemaChange(batch, change);
|
|
520
493
|
}
|
|
521
494
|
};
|
|
522
495
|
}
|
|
523
496
|
|
|
497
|
+
private async handleSchemaChange(batch: storage.BucketStorageBatch, change: SchemaChange): Promise<void> {
|
|
498
|
+
if (change.type === SchemaChangeType.RENAME_TABLE) {
|
|
499
|
+
const fromTableId = createTableId(change.schema, change.table);
|
|
500
|
+
|
|
501
|
+
const fromTable = this.tableCache.get(fromTableId);
|
|
502
|
+
// Old table needs to be cleaned up
|
|
503
|
+
if (fromTable) {
|
|
504
|
+
await batch.drop([fromTable]);
|
|
505
|
+
this.tableCache.delete(fromTableId);
|
|
506
|
+
}
|
|
507
|
+
// The new table matched a table in the sync rules
|
|
508
|
+
if (change.newTable) {
|
|
509
|
+
await this.handleCreateOrUpdateTable(batch, change.newTable!, change.schema);
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
const tableId = createTableId(change.schema, change.table);
|
|
513
|
+
|
|
514
|
+
const table = this.getTable(tableId);
|
|
515
|
+
|
|
516
|
+
switch (change.type) {
|
|
517
|
+
case SchemaChangeType.ALTER_TABLE_COLUMN:
|
|
518
|
+
case SchemaChangeType.REPLICATION_IDENTITY:
|
|
519
|
+
// For these changes, we need to update the table if the replication identity columns have changed.
|
|
520
|
+
await this.handleCreateOrUpdateTable(batch, change.table, change.schema);
|
|
521
|
+
break;
|
|
522
|
+
case SchemaChangeType.TRUNCATE_TABLE:
|
|
523
|
+
await batch.truncate([table]);
|
|
524
|
+
break;
|
|
525
|
+
case SchemaChangeType.DROP_TABLE:
|
|
526
|
+
await batch.drop([table]);
|
|
527
|
+
this.tableCache.delete(tableId);
|
|
528
|
+
break;
|
|
529
|
+
default:
|
|
530
|
+
// No action needed for other schema changes
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private async getReplicaIdColumns(tableName: string, schema: string) {
|
|
537
|
+
const connection = await this.connections.getConnection();
|
|
538
|
+
const replicaIdColumns = await common.getReplicationIdentityColumns({
|
|
539
|
+
connection,
|
|
540
|
+
schema,
|
|
541
|
+
tableName
|
|
542
|
+
});
|
|
543
|
+
connection.release();
|
|
544
|
+
|
|
545
|
+
return replicaIdColumns.columns;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private async handleCreateOrUpdateTable(
|
|
549
|
+
batch: storage.BucketStorageBatch,
|
|
550
|
+
tableName: string,
|
|
551
|
+
schema: string
|
|
552
|
+
): Promise<SourceTable> {
|
|
553
|
+
const replicaIdColumns = await this.getReplicaIdColumns(tableName, schema);
|
|
554
|
+
return await this.handleRelation(
|
|
555
|
+
batch,
|
|
556
|
+
{
|
|
557
|
+
name: tableName,
|
|
558
|
+
schema: schema,
|
|
559
|
+
objectId: createTableId(schema, tableName),
|
|
560
|
+
replicaIdColumns: replicaIdColumns
|
|
561
|
+
},
|
|
562
|
+
true
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
524
566
|
private async writeChanges(
|
|
525
567
|
batch: storage.BucketStorageBatch,
|
|
526
568
|
msg: {
|
|
@@ -531,17 +573,20 @@ AND table_type = 'BASE TABLE';`,
|
|
|
531
573
|
}
|
|
532
574
|
): Promise<storage.FlushedResult | null> {
|
|
533
575
|
const columns = common.toColumnDescriptors(msg.tableEntry);
|
|
576
|
+
const tableId = createTableId(msg.tableEntry.parentSchema, msg.tableEntry.tableName);
|
|
577
|
+
|
|
578
|
+
let table = this.tableCache.get(tableId);
|
|
579
|
+
if (table == null) {
|
|
580
|
+
// This is an insert for a new table that matches a table in the sync rules
|
|
581
|
+
// We need to create the table in the storage and cache it.
|
|
582
|
+
table = await this.handleCreateOrUpdateTable(batch, msg.tableEntry.tableName, msg.tableEntry.parentSchema);
|
|
583
|
+
}
|
|
534
584
|
|
|
535
585
|
for (const [index, row] of msg.rows.entries()) {
|
|
536
586
|
await this.writeChange(batch, {
|
|
537
587
|
type: msg.type,
|
|
538
588
|
database: msg.tableEntry.parentSchema,
|
|
539
|
-
sourceTable:
|
|
540
|
-
getMysqlRelId({
|
|
541
|
-
schema: msg.tableEntry.parentSchema,
|
|
542
|
-
name: msg.tableEntry.tableName
|
|
543
|
-
})
|
|
544
|
-
),
|
|
589
|
+
sourceTable: table!,
|
|
545
590
|
table: msg.tableEntry.tableName,
|
|
546
591
|
columns: columns,
|
|
547
592
|
row: row,
|
|
@@ -569,7 +614,7 @@ AND table_type = 'BASE TABLE';`,
|
|
|
569
614
|
});
|
|
570
615
|
case storage.SaveOperationTag.UPDATE:
|
|
571
616
|
this.metrics.getCounter(ReplicationMetric.ROWS_REPLICATED).add(1);
|
|
572
|
-
//
|
|
617
|
+
// The previous row may be null if the replica id columns are unchanged.
|
|
573
618
|
// It's fine to treat that the same as an insert.
|
|
574
619
|
const beforeUpdated = payload.previous_row
|
|
575
620
|
? common.toSQLiteRow(payload.previous_row, payload.columns)
|
|
@@ -610,7 +655,7 @@ AND table_type = 'BASE TABLE';`,
|
|
|
610
655
|
// We don't have anything to compute replication lag with yet.
|
|
611
656
|
return undefined;
|
|
612
657
|
} else {
|
|
613
|
-
// We don't have any uncommitted changes, so replication is up
|
|
658
|
+
// We don't have any uncommitted changes, so replication is up to date.
|
|
614
659
|
return 0;
|
|
615
660
|
}
|
|
616
661
|
}
|