@powersync/service-module-mysql 0.0.0-dev-20260225160713 → 0.0.0-dev-20260511080634
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 +104 -5
- package/dist/api/MySQLRouteAPIAdapter.js +1 -1
- package/dist/api/MySQLRouteAPIAdapter.js.map +1 -1
- package/dist/common/check-source-configuration.js +12 -1
- package/dist/common/check-source-configuration.js.map +1 -1
- package/dist/common/common-index.d.ts +1 -1
- package/dist/common/common-index.js +1 -1
- package/dist/common/common-index.js.map +1 -1
- package/dist/common/mysql-to-sqlite.d.ts +2 -2
- package/dist/common/mysql-to-sqlite.js +1 -1
- package/dist/common/mysql-to-sqlite.js.map +1 -1
- package/dist/common/schema-utils.d.ts +1 -1
- package/dist/common/schema-utils.js.map +1 -1
- package/dist/module/MySQLModule.js +3 -3
- package/dist/module/MySQLModule.js.map +1 -1
- package/dist/replication/BinLogReplicationJob.d.ts +1 -1
- package/dist/replication/BinLogReplicationJob.js +3 -3
- package/dist/replication/BinLogReplicationJob.js.map +1 -1
- package/dist/replication/BinLogReplicator.d.ts +0 -1
- package/dist/replication/BinLogReplicator.js +1 -23
- package/dist/replication/BinLogReplicator.js.map +1 -1
- package/dist/replication/BinLogStream.d.ts +3 -11
- package/dist/replication/BinLogStream.js +30 -45
- package/dist/replication/BinLogStream.js.map +1 -1
- package/dist/replication/MySQLConnectionManager.d.ts +3 -3
- package/dist/replication/MySQLConnectionManager.js +1 -1
- package/dist/replication/MySQLConnectionManager.js.map +1 -1
- package/dist/replication/MySQLConnectionManagerFactory.d.ts +1 -1
- package/dist/replication/MySQLConnectionManagerFactory.js.map +1 -1
- package/dist/replication/zongji/BinLogListener.d.ts +4 -4
- package/dist/replication/zongji/BinLogListener.js +7 -7
- package/dist/replication/zongji/BinLogListener.js.map +1 -1
- package/dist/replication/zongji/zongji-utils.d.ts +1 -1
- package/dist/utils/mysql-utils.d.ts +2 -2
- package/dist/utils/mysql-utils.js +1 -1
- package/dist/utils/mysql-utils.js.map +1 -1
- package/dist/utils/parser-utils.d.ts +1 -1
- package/package.json +10 -10
- package/src/api/MySQLRouteAPIAdapter.ts +2 -2
- package/src/common/check-source-configuration.ts +16 -1
- package/src/common/common-index.ts +1 -1
- package/src/common/mysql-to-sqlite.ts +3 -3
- package/src/common/schema-utils.ts +2 -2
- package/src/module/MySQLModule.ts +3 -3
- package/src/replication/BinLogReplicationJob.ts +3 -3
- package/src/replication/BinLogReplicator.ts +1 -26
- package/src/replication/BinLogStream.ts +35 -44
- package/src/replication/MySQLConnectionManager.ts +4 -4
- package/src/replication/MySQLConnectionManagerFactory.ts +1 -1
- package/src/replication/zongji/BinLogListener.ts +10 -10
- package/src/replication/zongji/zongji-utils.ts +6 -6
- package/src/utils/mysql-utils.ts +3 -3
- package/src/utils/parser-utils.ts +1 -1
- package/test/src/BinLogListener.test.ts +37 -26
- package/test/src/BinLogStream.test.ts +4 -2
- package/test/src/BinlogStreamUtils.ts +20 -7
- package/test/src/config.test.ts +1 -1
- package/test/src/env.ts +1 -1
- package/test/src/mysql-to-sqlite.test.ts +5 -6
- package/test/src/mysql-utils.test.ts +1 -1
- package/test/src/parser-utils.test.ts +1 -1
- package/test/src/schema-changes.test.ts +6 -5
- package/test/src/util.ts +9 -9
- package/test/tsconfig.json +0 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { ColumnDefinition, TableMapEntry } from '@powersync/mysql-zongji';
|
|
2
|
+
import { ColumnDescriptor } from '@powersync/service-core';
|
|
3
|
+
import { JSONBig, JsonContainer } from '@powersync/service-jsonbig';
|
|
1
4
|
import * as sync_rules from '@powersync/service-sync-rules';
|
|
2
5
|
import { ExpressionType } from '@powersync/service-sync-rules';
|
|
3
|
-
import { ColumnDescriptor } from '@powersync/service-core';
|
|
4
6
|
import mysql from 'mysql2';
|
|
5
|
-
import { JSONBig, JsonContainer } from '@powersync/service-jsonbig';
|
|
6
|
-
import { ColumnDefinition, TableMapEntry } from '@powersync/mysql-zongji';
|
|
7
7
|
|
|
8
8
|
export enum ADDITIONAL_MYSQL_TYPES {
|
|
9
9
|
DATETIME2 = 18,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import mysqlPromise from 'mysql2/promise';
|
|
2
|
-
import * as mysql_utils from '../utils/mysql-utils.js';
|
|
3
1
|
import { ColumnDescriptor } from '@powersync/service-core';
|
|
4
2
|
import { TablePattern } from '@powersync/service-sync-rules';
|
|
3
|
+
import mysqlPromise from 'mysql2/promise';
|
|
4
|
+
import * as mysql_utils from '../utils/mysql-utils.js';
|
|
5
5
|
|
|
6
6
|
export interface GetColumnsOptions {
|
|
7
7
|
connection: mysqlPromise.Connection;
|
|
@@ -8,13 +8,13 @@ import {
|
|
|
8
8
|
} from '@powersync/service-core';
|
|
9
9
|
|
|
10
10
|
import { MySQLRouteAPIAdapter } from '../api/MySQLRouteAPIAdapter.js';
|
|
11
|
+
import { checkSourceConfiguration } from '../common/check-source-configuration.js';
|
|
11
12
|
import { BinLogReplicator } from '../replication/BinLogReplicator.js';
|
|
13
|
+
import { MySQLConnectionManager } from '../replication/MySQLConnectionManager.js';
|
|
14
|
+
import { MySQLConnectionManagerFactory } from '../replication/MySQLConnectionManagerFactory.js';
|
|
12
15
|
import { MySQLErrorRateLimiter } from '../replication/MySQLErrorRateLimiter.js';
|
|
13
16
|
import * as types from '../types/types.js';
|
|
14
|
-
import { MySQLConnectionManagerFactory } from '../replication/MySQLConnectionManagerFactory.js';
|
|
15
17
|
import { MySQLConnectionConfig } from '../types/types.js';
|
|
16
|
-
import { checkSourceConfiguration } from '../common/check-source-configuration.js';
|
|
17
|
-
import { MySQLConnectionManager } from '../replication/MySQLConnectionManager.js';
|
|
18
18
|
|
|
19
19
|
export class MySQLModule extends replication.ReplicationModule<types.MySQLConnectionConfig> {
|
|
20
20
|
constructor() {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { container
|
|
1
|
+
import { container } from '@powersync/lib-services-framework';
|
|
2
2
|
import { POWERSYNC_VERSION, replication } from '@powersync/service-core';
|
|
3
3
|
import { BinLogStream } from './BinLogStream.js';
|
|
4
4
|
import { MySQLConnectionManagerFactory } from './MySQLConnectionManagerFactory.js';
|
|
@@ -13,7 +13,7 @@ export class BinLogReplicationJob extends replication.AbstractReplicationJob {
|
|
|
13
13
|
|
|
14
14
|
constructor(options: BinLogReplicationJobOptions) {
|
|
15
15
|
super(options);
|
|
16
|
-
this.logger =
|
|
16
|
+
this.logger = options.storage.logger;
|
|
17
17
|
this.connectionFactory = options.connectionFactory;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -88,7 +88,7 @@ export class BinLogReplicationJob extends replication.AbstractReplicationJob {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
getReplicationLagMillis(): number | undefined {
|
|
92
92
|
return this.lastStream?.getReplicationLagMillis();
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { replication, storage } from '@powersync/service-core';
|
|
2
|
+
import { MySQLModule } from '../module/MySQLModule.js';
|
|
2
3
|
import { BinLogReplicationJob } from './BinLogReplicationJob.js';
|
|
3
4
|
import { MySQLConnectionManagerFactory } from './MySQLConnectionManagerFactory.js';
|
|
4
|
-
import { MySQLModule } from '../module/MySQLModule.js';
|
|
5
5
|
|
|
6
6
|
export interface BinLogReplicatorOptions extends replication.AbstractReplicatorOptions {
|
|
7
7
|
connectionFactory: MySQLConnectionManagerFactory;
|
|
@@ -38,29 +38,4 @@ export class BinLogReplicator extends replication.AbstractReplicator<BinLogRepli
|
|
|
38
38
|
async testConnection() {
|
|
39
39
|
return await MySQLModule.testConnection(this.connectionFactory.connectionConfig);
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
async getReplicationLagMillis(): Promise<number | undefined> {
|
|
43
|
-
const lag = await super.getReplicationLagMillis();
|
|
44
|
-
if (lag != null) {
|
|
45
|
-
return lag;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Booting or in an error loop. Check last active replication status.
|
|
49
|
-
// This includes sync rules in an ERROR state.
|
|
50
|
-
const content = await this.storage.getActiveSyncRulesContent();
|
|
51
|
-
if (content == null) {
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
54
|
-
// Measure the lag from the last commit or keepalive timestamp.
|
|
55
|
-
// This is not 100% accurate since it is the commit time in the storage db rather than
|
|
56
|
-
// the source db, but it's the best we currently have for mysql.
|
|
57
|
-
const checkpointTs = content.last_checkpoint_ts?.getTime() ?? 0;
|
|
58
|
-
const keepaliveTs = content.last_keepalive_ts?.getTime() ?? 0;
|
|
59
|
-
const latestTs = Math.max(checkpointTs, keepaliveTs);
|
|
60
|
-
if (latestTs != 0) {
|
|
61
|
-
return Date.now() - latestTs;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return undefined;
|
|
65
|
-
}
|
|
66
41
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Logger,
|
|
3
2
|
logger as defaultLogger,
|
|
3
|
+
Logger,
|
|
4
4
|
ReplicationAbortedError,
|
|
5
5
|
ReplicationAssertionError
|
|
6
6
|
} from '@powersync/lib-services-framework';
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getUuidReplicaIdentityBson,
|
|
13
13
|
InternalOpId,
|
|
14
14
|
MetricsEngine,
|
|
15
|
+
ReplicationLagTracker,
|
|
15
16
|
SourceTable,
|
|
16
17
|
storage
|
|
17
18
|
} from '@powersync/service-core';
|
|
@@ -19,10 +20,10 @@ import mysql from 'mysql2';
|
|
|
19
20
|
import mysqlPromise from 'mysql2/promise';
|
|
20
21
|
|
|
21
22
|
import { TableMapEntry } from '@powersync/mysql-zongji';
|
|
23
|
+
import { ReplicationMetric } from '@powersync/service-types';
|
|
22
24
|
import * as common from '../common/common-index.js';
|
|
23
25
|
import { createRandomServerId, qualifiedMySQLTable } from '../utils/mysql-utils.js';
|
|
24
26
|
import { MySQLConnectionManager } from './MySQLConnectionManager.js';
|
|
25
|
-
import { ReplicationMetric } from '@powersync/service-types';
|
|
26
27
|
import { BinLogEventHandler, BinLogListener, Row, SchemaChange, SchemaChangeType } from './zongji/BinLogListener.js';
|
|
27
28
|
|
|
28
29
|
export interface BinLogStreamOptions {
|
|
@@ -74,16 +75,7 @@ export class BinLogStream {
|
|
|
74
75
|
|
|
75
76
|
private tableCache = new Map<string | number, storage.SourceTable>();
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
* Time of the oldest uncommitted change, according to the source db.
|
|
79
|
-
* This is used to determine the replication lag.
|
|
80
|
-
*/
|
|
81
|
-
private oldestUncommittedChange: Date | null = null;
|
|
82
|
-
/**
|
|
83
|
-
* Keep track of whether we have done a commit or keepalive yet.
|
|
84
|
-
* We can only compute replication lag if isStartingReplication == false, or oldestUncommittedChange is present.
|
|
85
|
-
*/
|
|
86
|
-
isStartingReplication = true;
|
|
78
|
+
private replicationLag = new ReplicationLagTracker();
|
|
87
79
|
|
|
88
80
|
constructor(private options: BinLogStreamOptions) {
|
|
89
81
|
this.logger = options.logger ?? defaultLogger;
|
|
@@ -122,6 +114,10 @@ export class BinLogStream {
|
|
|
122
114
|
return this.abortSignal.aborted;
|
|
123
115
|
}
|
|
124
116
|
|
|
117
|
+
get isStartingReplication() {
|
|
118
|
+
return this.replicationLag.isStartingReplication;
|
|
119
|
+
}
|
|
120
|
+
|
|
125
121
|
get defaultSchema() {
|
|
126
122
|
return this.connections.databaseName;
|
|
127
123
|
}
|
|
@@ -143,7 +139,7 @@ export class BinLogStream {
|
|
|
143
139
|
// Snapshot if:
|
|
144
140
|
// 1. Snapshot is requested (false for initial snapshot, since that process handles it elsewhere)
|
|
145
141
|
// 2. Snapshot is not done yet, AND:
|
|
146
|
-
// 3. The table is used in sync
|
|
142
|
+
// 3. The table is used in sync config.
|
|
147
143
|
const shouldSnapshot = snapshot && !result.table.snapshotComplete && result.table.syncAny;
|
|
148
144
|
|
|
149
145
|
if (shouldSnapshot) {
|
|
@@ -170,7 +166,7 @@ export class BinLogStream {
|
|
|
170
166
|
} finally {
|
|
171
167
|
connection.release();
|
|
172
168
|
}
|
|
173
|
-
const [table] = await batch.
|
|
169
|
+
const [table] = await batch.markTableSnapshotDone([result.table], gtid.comparable);
|
|
174
170
|
return table;
|
|
175
171
|
}
|
|
176
172
|
|
|
@@ -268,17 +264,19 @@ export class BinLogStream {
|
|
|
268
264
|
logger: this.logger,
|
|
269
265
|
zeroLSN: common.ReplicatedGTID.ZERO.comparable,
|
|
270
266
|
defaultSchema: this.defaultSchema,
|
|
271
|
-
storeCurrentData:
|
|
267
|
+
storeCurrentData: false
|
|
272
268
|
},
|
|
273
269
|
async (batch) => {
|
|
274
270
|
for (let tablePattern of sourceTables) {
|
|
275
271
|
const tables = await this.getQualifiedTableNames(batch, tablePattern);
|
|
276
272
|
for (let table of tables) {
|
|
277
273
|
await this.snapshotTable(connection as mysql.Connection, batch, table);
|
|
278
|
-
await batch.
|
|
274
|
+
await batch.markTableSnapshotDone([table], headGTID.comparable);
|
|
279
275
|
await framework.container.probes.touch();
|
|
280
276
|
}
|
|
281
277
|
}
|
|
278
|
+
const snapshotDoneGtid = await common.readExecutedGtid(promiseConnection);
|
|
279
|
+
await batch.markAllSnapshotDone(snapshotDoneGtid.comparable);
|
|
282
280
|
await batch.commit(headGTID.comparable);
|
|
283
281
|
}
|
|
284
282
|
);
|
|
@@ -293,7 +291,7 @@ export class BinLogStream {
|
|
|
293
291
|
}
|
|
294
292
|
|
|
295
293
|
if (lastOp != null) {
|
|
296
|
-
// Populate the cache _after_ initial replication, but _before_ we switch to this
|
|
294
|
+
// Populate the cache _after_ initial replication, but _before_ we switch to this replication stream.
|
|
297
295
|
await this.storage.populatePersistentChecksumCache({
|
|
298
296
|
// No checkpoint yet, but we do have the opId.
|
|
299
297
|
maxOpId: lastOp,
|
|
@@ -322,7 +320,10 @@ export class BinLogStream {
|
|
|
322
320
|
|
|
323
321
|
for await (let row of stream) {
|
|
324
322
|
if (this.stopped) {
|
|
325
|
-
throw new ReplicationAbortedError(
|
|
323
|
+
throw new ReplicationAbortedError(
|
|
324
|
+
'Abort signal received - initial replication interrupted.',
|
|
325
|
+
this.abortSignal.reason
|
|
326
|
+
);
|
|
326
327
|
}
|
|
327
328
|
|
|
328
329
|
if (columns == null) {
|
|
@@ -378,7 +379,7 @@ export class BinLogStream {
|
|
|
378
379
|
logger: this.logger,
|
|
379
380
|
zeroLSN: common.ReplicatedGTID.ZERO.comparable,
|
|
380
381
|
defaultSchema: this.defaultSchema,
|
|
381
|
-
storeCurrentData:
|
|
382
|
+
storeCurrentData: false
|
|
382
383
|
},
|
|
383
384
|
async (batch) => {
|
|
384
385
|
for (let tablePattern of sourceTables) {
|
|
@@ -414,7 +415,7 @@ export class BinLogStream {
|
|
|
414
415
|
|
|
415
416
|
if (!this.stopped) {
|
|
416
417
|
await this.storage.startBatch(
|
|
417
|
-
{ zeroLSN: common.ReplicatedGTID.ZERO.comparable, defaultSchema: this.defaultSchema, storeCurrentData:
|
|
418
|
+
{ zeroLSN: common.ReplicatedGTID.ZERO.comparable, defaultSchema: this.defaultSchema, storeCurrentData: false },
|
|
418
419
|
async (batch) => {
|
|
419
420
|
const binlogEventHandler = this.createBinlogEventHandler(batch);
|
|
420
421
|
const binlogListener = new BinLogListener({
|
|
@@ -467,26 +468,25 @@ export class BinLogStream {
|
|
|
467
468
|
});
|
|
468
469
|
},
|
|
469
470
|
onKeepAlive: async (lsn: string) => {
|
|
470
|
-
const
|
|
471
|
-
if (
|
|
472
|
-
this.
|
|
471
|
+
const { checkpointBlocked } = await batch.keepalive(lsn);
|
|
472
|
+
if (!checkpointBlocked) {
|
|
473
|
+
this.replicationLag.clearUncommittedChange();
|
|
473
474
|
}
|
|
474
475
|
},
|
|
475
476
|
onCommit: async (lsn: string) => {
|
|
476
477
|
this.metrics.getCounter(ReplicationMetric.TRANSACTIONS_REPLICATED).add(1);
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
478
|
+
const { checkpointBlocked } = await batch.commit(lsn, {
|
|
479
|
+
oldestUncommittedChange: this.replicationLag.oldestUncommittedChange
|
|
480
|
+
});
|
|
481
|
+
if (!checkpointBlocked) {
|
|
482
|
+
this.replicationLag.markCommitted();
|
|
481
483
|
}
|
|
482
484
|
},
|
|
483
485
|
onTransactionStart: async (options) => {
|
|
484
|
-
|
|
485
|
-
this.oldestUncommittedChange = options.timestamp;
|
|
486
|
-
}
|
|
486
|
+
this.replicationLag.trackUncommittedChange(options.timestamp);
|
|
487
487
|
},
|
|
488
488
|
onRotate: async () => {
|
|
489
|
-
this.
|
|
489
|
+
this.replicationLag.markStarted();
|
|
490
490
|
},
|
|
491
491
|
onSchemaChange: async (change: SchemaChange) => {
|
|
492
492
|
await this.handleSchemaChange(batch, change);
|
|
@@ -504,7 +504,7 @@ export class BinLogStream {
|
|
|
504
504
|
await batch.drop([fromTable]);
|
|
505
505
|
this.tableCache.delete(fromTableId);
|
|
506
506
|
}
|
|
507
|
-
// The new table matched a table in the sync
|
|
507
|
+
// The new table matched a table in the sync config
|
|
508
508
|
if (change.newTable) {
|
|
509
509
|
await this.handleCreateOrUpdateTable(batch, change.newTable!, change.schema);
|
|
510
510
|
}
|
|
@@ -577,7 +577,7 @@ export class BinLogStream {
|
|
|
577
577
|
|
|
578
578
|
let table = this.tableCache.get(tableId);
|
|
579
579
|
if (table == null) {
|
|
580
|
-
// This is an insert for a new table that matches a table in the sync
|
|
580
|
+
// This is an insert for a new table that matches a table in the sync config
|
|
581
581
|
// We need to create the table in the storage and cache it.
|
|
582
582
|
table = await this.handleCreateOrUpdateTable(batch, msg.tableEntry.tableName, msg.tableEntry.parentSchema);
|
|
583
583
|
}
|
|
@@ -654,17 +654,8 @@ export class BinLogStream {
|
|
|
654
654
|
}
|
|
655
655
|
}
|
|
656
656
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
if (this.isStartingReplication) {
|
|
660
|
-
// We don't have anything to compute replication lag with yet.
|
|
661
|
-
return undefined;
|
|
662
|
-
} else {
|
|
663
|
-
// We don't have any uncommitted changes, so replication is up to date.
|
|
664
|
-
return 0;
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
return Date.now() - this.oldestUncommittedChange.getTime();
|
|
657
|
+
getReplicationLagMillis(): number | undefined {
|
|
658
|
+
return this.replicationLag.getLagMillis();
|
|
668
659
|
}
|
|
669
660
|
|
|
670
661
|
async tryRollback(promiseConnection: mysqlPromise.Connection) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { NormalizedMySQLConnectionConfig } from '../types/types.js';
|
|
2
|
-
import mysqlPromise from 'mysql2/promise';
|
|
3
|
-
import mysql, { FieldPacket, RowDataPacket } from 'mysql2';
|
|
4
|
-
import * as mysql_utils from '../utils/mysql-utils.js';
|
|
5
1
|
import { BaseObserver, logger } from '@powersync/lib-services-framework';
|
|
6
2
|
import { ZongJi } from '@powersync/mysql-zongji';
|
|
3
|
+
import mysql, { FieldPacket, RowDataPacket } from 'mysql2';
|
|
4
|
+
import mysqlPromise from 'mysql2/promise';
|
|
5
|
+
import { NormalizedMySQLConnectionConfig } from '../types/types.js';
|
|
6
|
+
import * as mysql_utils from '../utils/mysql-utils.js';
|
|
7
7
|
|
|
8
8
|
export interface MySQLConnectionManagerListener {
|
|
9
9
|
onEnded(): void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { logger } from '@powersync/lib-services-framework';
|
|
2
2
|
import mysql from 'mysql2/promise';
|
|
3
|
-
import { MySQLConnectionManager } from './MySQLConnectionManager.js';
|
|
4
3
|
import { ResolvedConnectionConfig } from '../types/types.js';
|
|
4
|
+
import { MySQLConnectionManager } from './MySQLConnectionManager.js';
|
|
5
5
|
|
|
6
6
|
export class MySQLConnectionManagerFactory {
|
|
7
7
|
private readonly connectionManagers = new Set<MySQLConnectionManager>();
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import * as common from '../../common/common-index.js';
|
|
2
|
-
import async from 'async';
|
|
3
|
-
import { BinLogEvent, BinLogQueryEvent, StartOptions, TableMapEntry, ZongJi } from '@powersync/mysql-zongji';
|
|
4
|
-
import * as zongji_utils from './zongji-utils.js';
|
|
5
1
|
import { Logger, logger as defaultLogger } from '@powersync/lib-services-framework';
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
2
|
+
import { BinLogEvent, BinLogQueryEvent, StartOptions, TableMapEntry, ZongJi } from '@powersync/mysql-zongji';
|
|
3
|
+
import { TablePattern } from '@powersync/service-sync-rules';
|
|
4
|
+
import async from 'async';
|
|
8
5
|
import pkg, {
|
|
9
6
|
AST,
|
|
10
7
|
BaseFrom,
|
|
@@ -13,6 +10,8 @@ import pkg, {
|
|
|
13
10
|
RenameStatement,
|
|
14
11
|
TruncateStatement
|
|
15
12
|
} from 'node-sql-parser';
|
|
13
|
+
import timers from 'timers/promises';
|
|
14
|
+
import * as common from '../../common/common-index.js';
|
|
16
15
|
import {
|
|
17
16
|
isAlterTable,
|
|
18
17
|
isColumnExpression,
|
|
@@ -25,7 +24,8 @@ import {
|
|
|
25
24
|
isTruncate,
|
|
26
25
|
matchedSchemaChangeQuery
|
|
27
26
|
} from '../../utils/parser-utils.js';
|
|
28
|
-
import {
|
|
27
|
+
import { MySQLConnectionManager } from '../MySQLConnectionManager.js';
|
|
28
|
+
import * as zongji_utils from './zongji-utils.js';
|
|
29
29
|
|
|
30
30
|
const { Parser } = pkg;
|
|
31
31
|
|
|
@@ -445,8 +445,8 @@ export class BinLogListener {
|
|
|
445
445
|
} catch (error) {
|
|
446
446
|
if (matchedSchemaChangeQuery(query, Object.values(this.databaseFilter))) {
|
|
447
447
|
this.logger.warn(
|
|
448
|
-
`Failed to parse query: [${query}].
|
|
449
|
-
|
|
448
|
+
`Failed to parse query: [${query}].
|
|
449
|
+
Please review for the schema changes and manually redeploy the sync config if required.`
|
|
450
450
|
);
|
|
451
451
|
}
|
|
452
452
|
return [];
|
|
@@ -537,7 +537,7 @@ export class BinLogListener {
|
|
|
537
537
|
}
|
|
538
538
|
|
|
539
539
|
private createDatabaseFilter(sourceTables: TablePattern[]): { [schema: string]: (table: string) => boolean } {
|
|
540
|
-
// Group sync
|
|
540
|
+
// Group sync config tables by schema
|
|
541
541
|
const schemaMap = new Map<string, TablePattern[]>();
|
|
542
542
|
for (const table of sourceTables) {
|
|
543
543
|
if (!schemaMap.has(table.schema)) {
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BinLogEvent,
|
|
3
3
|
BinLogGTIDLogEvent,
|
|
4
|
-
|
|
4
|
+
BinLogHeartbeatEvent,
|
|
5
|
+
BinLogHeartbeatEvent_V2,
|
|
6
|
+
BinLogQueryEvent,
|
|
5
7
|
BinLogRotationEvent,
|
|
6
|
-
|
|
8
|
+
BinLogRowEvent,
|
|
7
9
|
BinLogRowUpdateEvent,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
BinLogHeartbeatEvent,
|
|
11
|
-
BinLogHeartbeatEvent_V2
|
|
10
|
+
BinLogTableMapEvent,
|
|
11
|
+
BinLogXidEvent
|
|
12
12
|
} from '@powersync/mysql-zongji';
|
|
13
13
|
|
|
14
14
|
export function eventIsGTIDLog(event: BinLogEvent): event is BinLogGTIDLogEvent {
|
package/src/utils/mysql-utils.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { logger } from '@powersync/lib-services-framework';
|
|
2
|
+
import { SourceEntityDescriptor } from '@powersync/service-core';
|
|
2
3
|
import mysql from 'mysql2';
|
|
3
4
|
import mysqlPromise from 'mysql2/promise';
|
|
4
|
-
import * as types from '../types/types.js';
|
|
5
5
|
import { coerce, gte, satisfies } from 'semver';
|
|
6
|
-
import
|
|
6
|
+
import * as types from '../types/types.js';
|
|
7
7
|
|
|
8
8
|
export type RetriedQueryOptions = {
|
|
9
9
|
connection: mysqlPromise.Connection;
|
|
@@ -63,7 +63,7 @@ export function createPool(config: types.NormalizedMySQLConnectionConfig, option
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
* Return a random server id for a given
|
|
66
|
+
* Return a random server id for a given replication stream id.
|
|
67
67
|
* Expected format is: <syncRuleId>00<random number>
|
|
68
68
|
* The max value for server id in MySQL is 2^32 - 1.
|
|
69
69
|
* We use the GTID format to keep track of our position in the binlog, no state is kept by the MySQL server, therefore
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Alter, AST, Create, Drop,
|
|
1
|
+
import { Alter, AST, Create, Drop, DropIndexStatement, RenameStatement, TruncateStatement } from 'node-sql-parser';
|
|
2
2
|
|
|
3
3
|
// We ignore create table statements, since even in the worst case we will pick up the changes when row events for that
|
|
4
4
|
// table are received.
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
2
|
-
import { BinLogListener, SchemaChange, SchemaChangeType } from '@module/replication/zongji/BinLogListener.js';
|
|
3
1
|
import { MySQLConnectionManager } from '@module/replication/MySQLConnectionManager.js';
|
|
2
|
+
import { BinLogListener, SchemaChange, SchemaChangeType } from '@module/replication/zongji/BinLogListener.js';
|
|
3
|
+
import { getMySQLVersion, qualifiedMySQLTable, satisfiesVersion } from '@module/utils/mysql-utils.js';
|
|
4
|
+
import { TablePattern } from '@powersync/service-sync-rules';
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
import { v4 as uuid } from 'uuid';
|
|
7
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
4
8
|
import {
|
|
5
9
|
clearTestDb,
|
|
6
10
|
createBinlogListener,
|
|
@@ -8,12 +12,8 @@ import {
|
|
|
8
12
|
TEST_CONNECTION_OPTIONS,
|
|
9
13
|
TestBinLogEventHandler
|
|
10
14
|
} from './util.js';
|
|
11
|
-
import { v4 as uuid } from 'uuid';
|
|
12
|
-
import { getMySQLVersion, qualifiedMySQLTable, satisfiesVersion } from '@module/utils/mysql-utils.js';
|
|
13
|
-
import crypto from 'crypto';
|
|
14
|
-
import { TablePattern } from '@powersync/service-sync-rules';
|
|
15
15
|
|
|
16
|
-
describe('BinlogListener tests', () => {
|
|
16
|
+
describe('BinlogListener tests', { timeout: 60_000 }, () => {
|
|
17
17
|
const MAX_QUEUE_CAPACITY_MB = 1;
|
|
18
18
|
const BINLOG_LISTENER_CONNECTION_OPTIONS = {
|
|
19
19
|
...TEST_CONNECTION_OPTIONS,
|
|
@@ -73,14 +73,14 @@ describe('BinlogListener tests', () => {
|
|
|
73
73
|
await binLogListener.start();
|
|
74
74
|
|
|
75
75
|
// Wait for listener to stop due to queue reaching capacity
|
|
76
|
-
await vi.waitFor(() => expect(stopSpy).toHaveBeenCalled(), { timeout:
|
|
76
|
+
await vi.waitFor(() => expect(stopSpy).toHaveBeenCalled(), { timeout: 10_000 });
|
|
77
77
|
|
|
78
78
|
expect(binLogListener.isQueueOverCapacity()).toBeTruthy();
|
|
79
79
|
// Resume event processing
|
|
80
80
|
eventHandler.unpause!();
|
|
81
81
|
const restartSpy = vi.spyOn(binLogListener, 'start');
|
|
82
82
|
|
|
83
|
-
await vi.waitFor(() => expect(eventHandler.rowsWritten).equals(ROW_COUNT), { timeout:
|
|
83
|
+
await vi.waitFor(() => expect(eventHandler.rowsWritten).equals(ROW_COUNT), { timeout: 10_000 });
|
|
84
84
|
await binLogListener.stop();
|
|
85
85
|
// Confirm resume was called after unpausing
|
|
86
86
|
expect(restartSpy).toHaveBeenCalled();
|
|
@@ -106,7 +106,7 @@ describe('BinlogListener tests', () => {
|
|
|
106
106
|
test('Keepalive event', async () => {
|
|
107
107
|
binLogListener.options.keepAliveInactivitySeconds = 1;
|
|
108
108
|
await binLogListener.start();
|
|
109
|
-
await vi.waitFor(() => expect(eventHandler.lastKeepAlive).toBeDefined(), { timeout:
|
|
109
|
+
await vi.waitFor(() => expect(eventHandler.lastKeepAlive).toBeDefined(), { timeout: 10_000 });
|
|
110
110
|
await binLogListener.stop();
|
|
111
111
|
expect(eventHandler.lastKeepAlive).toEqual(binLogListener.options.startGTID.comparable);
|
|
112
112
|
});
|
|
@@ -114,7 +114,7 @@ describe('BinlogListener tests', () => {
|
|
|
114
114
|
test('Schema change event: Rename table', async () => {
|
|
115
115
|
await binLogListener.start();
|
|
116
116
|
await connectionManager.query(`ALTER TABLE test_DATA RENAME test_DATA_new`);
|
|
117
|
-
await
|
|
117
|
+
await waitForSchemaChanges(1);
|
|
118
118
|
await binLogListener.stop();
|
|
119
119
|
assertSchemaChange(
|
|
120
120
|
eventHandler.schemaChanges[0],
|
|
@@ -133,7 +133,7 @@ describe('BinlogListener tests', () => {
|
|
|
133
133
|
test_DATA TO test_DATA_new,
|
|
134
134
|
test_DATA_new TO test_DATA
|
|
135
135
|
`);
|
|
136
|
-
await
|
|
136
|
+
await waitForSchemaChanges(2);
|
|
137
137
|
await binLogListener.stop();
|
|
138
138
|
assertSchemaChange(
|
|
139
139
|
eventHandler.schemaChanges[0],
|
|
@@ -156,7 +156,7 @@ describe('BinlogListener tests', () => {
|
|
|
156
156
|
test('Schema change event: Truncate table', async () => {
|
|
157
157
|
await binLogListener.start();
|
|
158
158
|
await connectionManager.query(`TRUNCATE TABLE test_DATA`);
|
|
159
|
-
await
|
|
159
|
+
await waitForSchemaChanges(1);
|
|
160
160
|
await binLogListener.stop();
|
|
161
161
|
assertSchemaChange(
|
|
162
162
|
eventHandler.schemaChanges[0],
|
|
@@ -169,8 +169,7 @@ describe('BinlogListener tests', () => {
|
|
|
169
169
|
test('Schema change event: Drop table', async () => {
|
|
170
170
|
await binLogListener.start();
|
|
171
171
|
await connectionManager.query(`DROP TABLE test_DATA`);
|
|
172
|
-
await
|
|
173
|
-
await vi.waitFor(() => expect(eventHandler.schemaChanges.length).toBe(1), { timeout: 5000 });
|
|
172
|
+
await waitForSchemaChanges(1);
|
|
174
173
|
await binLogListener.stop();
|
|
175
174
|
assertSchemaChange(
|
|
176
175
|
eventHandler.schemaChanges[0],
|
|
@@ -183,7 +182,7 @@ describe('BinlogListener tests', () => {
|
|
|
183
182
|
test('Schema change event: Drop column', async () => {
|
|
184
183
|
await binLogListener.start();
|
|
185
184
|
await connectionManager.query(`ALTER TABLE test_DATA DROP COLUMN description`);
|
|
186
|
-
await
|
|
185
|
+
await waitForSchemaChanges(1);
|
|
187
186
|
await binLogListener.stop();
|
|
188
187
|
assertSchemaChange(
|
|
189
188
|
eventHandler.schemaChanges[0],
|
|
@@ -196,7 +195,7 @@ describe('BinlogListener tests', () => {
|
|
|
196
195
|
test('Schema change event: Add column', async () => {
|
|
197
196
|
await binLogListener.start();
|
|
198
197
|
await connectionManager.query(`ALTER TABLE test_DATA ADD COLUMN new_column VARCHAR(255)`);
|
|
199
|
-
await
|
|
198
|
+
await waitForSchemaChanges(1);
|
|
200
199
|
await binLogListener.stop();
|
|
201
200
|
assertSchemaChange(
|
|
202
201
|
eventHandler.schemaChanges[0],
|
|
@@ -209,7 +208,7 @@ describe('BinlogListener tests', () => {
|
|
|
209
208
|
test('Schema change event: Modify column', async () => {
|
|
210
209
|
await binLogListener.start();
|
|
211
210
|
await connectionManager.query(`ALTER TABLE test_DATA MODIFY COLUMN description TEXT`);
|
|
212
|
-
await
|
|
211
|
+
await waitForSchemaChanges(1);
|
|
213
212
|
await binLogListener.stop();
|
|
214
213
|
assertSchemaChange(
|
|
215
214
|
eventHandler.schemaChanges[0],
|
|
@@ -222,7 +221,7 @@ describe('BinlogListener tests', () => {
|
|
|
222
221
|
test('Schema change event: Rename column via change statement', async () => {
|
|
223
222
|
await binLogListener.start();
|
|
224
223
|
await connectionManager.query(`ALTER TABLE test_DATA CHANGE COLUMN description description_new MEDIUMTEXT`);
|
|
225
|
-
await
|
|
224
|
+
await waitForSchemaChanges(1);
|
|
226
225
|
await binLogListener.stop();
|
|
227
226
|
assertSchemaChange(
|
|
228
227
|
eventHandler.schemaChanges[0],
|
|
@@ -237,7 +236,7 @@ describe('BinlogListener tests', () => {
|
|
|
237
236
|
if (!isMySQL57) {
|
|
238
237
|
await binLogListener.start();
|
|
239
238
|
await connectionManager.query(`ALTER TABLE test_DATA RENAME COLUMN description TO description_new`);
|
|
240
|
-
await
|
|
239
|
+
await waitForSchemaChanges(1);
|
|
241
240
|
await binLogListener.stop();
|
|
242
241
|
assertSchemaChange(
|
|
243
242
|
eventHandler.schemaChanges[0],
|
|
@@ -254,7 +253,7 @@ describe('BinlogListener tests', () => {
|
|
|
254
253
|
await connectionManager.query(
|
|
255
254
|
`ALTER TABLE test_DATA DROP COLUMN description, ADD COLUMN new_description TEXT, MODIFY COLUMN id VARCHAR(50)`
|
|
256
255
|
);
|
|
257
|
-
await
|
|
256
|
+
await waitForSchemaChanges(3);
|
|
258
257
|
await binLogListener.stop();
|
|
259
258
|
assertSchemaChange(
|
|
260
259
|
eventHandler.schemaChanges[0],
|
|
@@ -289,7 +288,7 @@ describe('BinlogListener tests', () => {
|
|
|
289
288
|
await binLogListener.start();
|
|
290
289
|
await connectionManager.query(`ALTER TABLE test_constraints ADD PRIMARY KEY (id)`);
|
|
291
290
|
await connectionManager.query(`ALTER TABLE test_constraints DROP PRIMARY KEY`);
|
|
292
|
-
await
|
|
291
|
+
await waitForSchemaChanges(2);
|
|
293
292
|
await binLogListener.stop();
|
|
294
293
|
// Event for the add
|
|
295
294
|
assertSchemaChange(
|
|
@@ -318,7 +317,7 @@ describe('BinlogListener tests', () => {
|
|
|
318
317
|
await binLogListener.start();
|
|
319
318
|
await connectionManager.query(`ALTER TABLE test_constraints ADD UNIQUE (description)`);
|
|
320
319
|
await connectionManager.query(`ALTER TABLE test_constraints DROP INDEX description`);
|
|
321
|
-
await
|
|
320
|
+
await waitForSchemaChanges(2);
|
|
322
321
|
await binLogListener.stop();
|
|
323
322
|
// Event for the creation
|
|
324
323
|
assertSchemaChange(
|
|
@@ -347,7 +346,7 @@ describe('BinlogListener tests', () => {
|
|
|
347
346
|
await binLogListener.start();
|
|
348
347
|
await connectionManager.query(`CREATE UNIQUE INDEX description_idx ON test_constraints (description)`);
|
|
349
348
|
await connectionManager.query(`DROP INDEX description_idx ON test_constraints`);
|
|
350
|
-
await
|
|
349
|
+
await waitForSchemaChanges(2);
|
|
351
350
|
await binLogListener.stop();
|
|
352
351
|
// Event for the creation
|
|
353
352
|
assertSchemaChange(
|
|
@@ -398,7 +397,7 @@ describe('BinlogListener tests', () => {
|
|
|
398
397
|
|
|
399
398
|
await binLogListener.start();
|
|
400
399
|
|
|
401
|
-
await
|
|
400
|
+
await waitForSchemaChanges(4);
|
|
402
401
|
await binLogListener.stop();
|
|
403
402
|
expect(eventHandler.schemaChanges[0].type).toBe(SchemaChangeType.ALTER_TABLE_COLUMN);
|
|
404
403
|
expect(eventHandler.schemaChanges[1].type).toBe(SchemaChangeType.REPLICATION_IDENTITY);
|
|
@@ -464,14 +463,26 @@ describe('BinlogListener tests', () => {
|
|
|
464
463
|
await insertRows(connectionManager, 1);
|
|
465
464
|
// multi_schema database insert into test_DATA_multi
|
|
466
465
|
await connectionManager.query(`INSERT INTO ${testTable}(id, description) VALUES('${uuid()}','test')`);
|
|
466
|
+
// Wait for both row entries to appear on the binlog. This avoids a rare race condition in MySQL 5.7
|
|
467
|
+
// where the drop table can hang if run immediately after the table was locked by an insert
|
|
468
|
+
await vi.waitFor(() => expect(eventHandler.rowsWritten).equals(2), { timeout: 5000 });
|
|
469
|
+
|
|
467
470
|
await connectionManager.query(`DROP TABLE ${testTable}`);
|
|
471
|
+
await waitForSchemaChanges(1);
|
|
468
472
|
|
|
469
|
-
await vi.waitFor(() => expect(eventHandler.schemaChanges.length).toBe(1), { timeout: 5000 });
|
|
470
473
|
await binLogListener.stop();
|
|
471
474
|
expect(eventHandler.rowsWritten).toBe(2);
|
|
472
475
|
assertSchemaChange(eventHandler.schemaChanges[0], SchemaChangeType.DROP_TABLE, 'multi_schema', 'test_DATA_multi');
|
|
473
476
|
});
|
|
474
477
|
|
|
478
|
+
async function waitForSchemaChanges(count: number) {
|
|
479
|
+
try {
|
|
480
|
+
await vi.waitFor(() => expect(eventHandler.schemaChanges.length).equals(count), { timeout: 30_000 });
|
|
481
|
+
} catch (error) {
|
|
482
|
+
throw new Error(`Timeout while waiting for [${count}] schema changes.`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
475
486
|
function assertSchemaChange(
|
|
476
487
|
change: SchemaChange,
|
|
477
488
|
type: SchemaChangeType,
|