@powersync/service-module-mysql 0.0.0-dev-20260803144812 → 0.0.0-dev-20260929095716
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 +76 -5
- package/ci/test-connection.yaml +12 -0
- package/dev/config/sync_rules.yaml +8 -4
- package/dist/api/MySQLRouteAPIAdapter.js +3 -3
- package/dist/api/MySQLRouteAPIAdapter.js.map +1 -1
- package/dist/common/ReplicatedGTID.d.ts +16 -9
- package/dist/common/ReplicatedGTID.js +49 -25
- package/dist/common/ReplicatedGTID.js.map +1 -1
- package/dist/common/check-source-configuration.js +11 -0
- package/dist/common/check-source-configuration.js.map +1 -1
- package/dist/common/read-executed-gtid.d.ts +11 -1
- package/dist/common/read-executed-gtid.js +67 -5
- package/dist/common/read-executed-gtid.js.map +1 -1
- package/dist/module/MySQLModule.d.ts +0 -1
- package/dist/module/MySQLModule.js +0 -1
- package/dist/module/MySQLModule.js.map +1 -1
- package/dist/replication/BinLogReplicationJob.js +3 -1
- package/dist/replication/BinLogReplicationJob.js.map +1 -1
- package/dist/replication/BinLogStream.d.ts +8 -0
- package/dist/replication/BinLogStream.js +58 -18
- package/dist/replication/BinLogStream.js.map +1 -1
- package/dist/replication/MySQLConnectionManager.js +6 -0
- package/dist/replication/MySQLConnectionManager.js.map +1 -1
- package/dist/replication/zongji/BinLogListener.d.ts +41 -2
- package/dist/replication/zongji/BinLogListener.js +109 -31
- package/dist/replication/zongji/BinLogListener.js.map +1 -1
- package/dist/utils/mysql-utils.d.ts +6 -0
- package/dist/utils/mysql-utils.js +10 -0
- package/dist/utils/mysql-utils.js.map +1 -1
- package/package.json +10 -10
- package/src/api/MySQLRouteAPIAdapter.ts +4 -4
- package/src/common/ReplicatedGTID.ts +65 -32
- package/src/common/check-source-configuration.ts +15 -0
- package/src/common/read-executed-gtid.ts +76 -6
- package/src/module/MySQLModule.ts +0 -2
- package/src/replication/BinLogReplicationJob.ts +3 -1
- package/src/replication/BinLogStream.ts +63 -18
- package/src/replication/MySQLConnectionManager.ts +6 -0
- package/src/replication/zongji/BinLogListener.ts +136 -31
- package/src/utils/mysql-utils.ts +11 -0
- package/test/src/BinLogListener.test.ts +140 -1
- package/test/src/BinLogListener_queue.test.ts +62 -0
- package/test/src/BinlogStreamUtils.ts +4 -4
- package/test/src/ReplicatedGTID.test.ts +138 -0
- package/test/src/check-source-configuration.test.ts +70 -0
- package/test/src/mysql-utils.test.ts +19 -1
- package/test/src/read-executed-gtid.test.ts +188 -0
- package/test/src/schema-changes.test.ts +54 -36
- package/test/src/util.ts +27 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -319,10 +319,11 @@ function defineTests(config: storage.TestStorageConfig) {
|
|
|
319
319
|
await connectionManager.query(`INSERT INTO test_data(id, description) VALUES('t1','test1')`);
|
|
320
320
|
|
|
321
321
|
await context.replicateSnapshot();
|
|
322
|
-
await context.startStreaming();
|
|
323
322
|
|
|
324
323
|
await connectionManager.query(`ALTER TABLE test_data ADD COLUMN new_column TEXT`);
|
|
325
324
|
await connectionManager.query(`INSERT INTO test_data(id, description) VALUES('t2','test2')`);
|
|
325
|
+
// Replay the DDL after the insert commits so the replacement snapshot includes it.
|
|
326
|
+
await context.startStreaming();
|
|
326
327
|
|
|
327
328
|
const data = await context.getBucketData('global[]');
|
|
328
329
|
|
|
@@ -349,43 +350,54 @@ function defineTests(config: storage.TestStorageConfig) {
|
|
|
349
350
|
]);
|
|
350
351
|
});
|
|
351
352
|
|
|
352
|
-
test(
|
|
353
|
-
|
|
354
|
-
|
|
353
|
+
test.each([true, false])(
|
|
354
|
+
'Change Replication Identity from full to index by adding a unique constraint (insert in snapshot: %s)',
|
|
355
|
+
async (insertInSnapshot) => {
|
|
356
|
+
await using context = await BinlogStreamTestContext.open(factory);
|
|
357
|
+
// Change replica id full by adding a unique index that can serve as the replication id
|
|
355
358
|
|
|
356
|
-
|
|
359
|
+
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
357
360
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
361
|
+
const { connectionManager } = context;
|
|
362
|
+
// No primary key, no unique column, so full replication identity will be used
|
|
363
|
+
await connectionManager.query(`CREATE TABLE test_data (id CHAR(36), description TEXT)`);
|
|
364
|
+
await connectionManager.query(`INSERT INTO test_data(id, description) VALUES('t1','test1')`);
|
|
362
365
|
|
|
363
|
-
|
|
364
|
-
await context.startStreaming();
|
|
366
|
+
await context.replicateSnapshot();
|
|
365
367
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
+
await connectionManager.query(`ALTER TABLE test_data ADD UNIQUE (id)`);
|
|
369
|
+
if (!insertInSnapshot) {
|
|
370
|
+
await context.startStreaming();
|
|
371
|
+
// Wait for the replacement snapshot before inserting the new row.
|
|
372
|
+
await context.getCheckpoint();
|
|
373
|
+
}
|
|
374
|
+
await connectionManager.query(`INSERT INTO test_data(id, description) VALUES('t2','test2')`);
|
|
375
|
+
if (insertInSnapshot) {
|
|
376
|
+
// Commit the insert before replaying the DDL so the snapshot must include it.
|
|
377
|
+
await context.startStreaming();
|
|
378
|
+
}
|
|
368
379
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
380
|
+
const data = await context.getBucketData('global[]');
|
|
381
|
+
const reduced = test_utils.reduceBucket(data).slice(1);
|
|
382
|
+
expect(reduced.sort(compareIds)).toMatchObject([PUT_T1, PUT_T2]);
|
|
372
383
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
384
|
+
expect(data.slice(0, 2)).toMatchObject([
|
|
385
|
+
// Initial inserts
|
|
386
|
+
PUT_T1,
|
|
387
|
+
// Truncate
|
|
388
|
+
REMOVE_T1
|
|
389
|
+
]);
|
|
379
390
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
391
|
+
// Snapshot - order doesn't matter
|
|
392
|
+
expect(data.slice(2)).toMatchObject([
|
|
393
|
+
// Snapshot inserts
|
|
394
|
+
PUT_T1,
|
|
395
|
+
...(insertInSnapshot ? [PUT_T2] : []),
|
|
396
|
+
// Replicated insert
|
|
397
|
+
PUT_T2
|
|
398
|
+
]);
|
|
399
|
+
}
|
|
400
|
+
);
|
|
389
401
|
|
|
390
402
|
test('Change Replication Identity from full to index by adding a unique index', async () => {
|
|
391
403
|
await using context = await BinlogStreamTestContext.open(factory);
|
|
@@ -398,10 +410,11 @@ function defineTests(config: storage.TestStorageConfig) {
|
|
|
398
410
|
await connectionManager.query(`INSERT INTO test_data(id, description) VALUES('t1','test1')`);
|
|
399
411
|
|
|
400
412
|
await context.replicateSnapshot();
|
|
401
|
-
await context.startStreaming();
|
|
402
413
|
|
|
403
414
|
await connectionManager.query(`CREATE UNIQUE INDEX id_idx ON test_data (id)`);
|
|
404
415
|
await connectionManager.query(`INSERT INTO test_data(id, description) VALUES('t2','test2')`);
|
|
416
|
+
// Replay the DDL after the insert commits so the replacement snapshot includes it.
|
|
417
|
+
await context.startStreaming();
|
|
405
418
|
|
|
406
419
|
const data = await context.getBucketData('global[]');
|
|
407
420
|
const reduced = test_utils.reduceBucket(data).slice(1);
|
|
@@ -436,10 +449,11 @@ function defineTests(config: storage.TestStorageConfig) {
|
|
|
436
449
|
await connectionManager.query(`INSERT INTO test_data(id, description) VALUES('t1','test1')`);
|
|
437
450
|
|
|
438
451
|
await context.replicateSnapshot();
|
|
439
|
-
await context.startStreaming();
|
|
440
452
|
|
|
441
453
|
await connectionManager.query(`ALTER TABLE test_data DROP INDEX id`);
|
|
442
454
|
await connectionManager.query(`INSERT INTO test_data(id, description) VALUES('t2','test2')`);
|
|
455
|
+
// Replay the DDL after the insert commits so the replacement snapshot includes it.
|
|
456
|
+
await context.startStreaming();
|
|
443
457
|
|
|
444
458
|
const data = await context.getBucketData('global[]');
|
|
445
459
|
const reduced = test_utils.reduceBucket(data).slice(1);
|
|
@@ -471,10 +485,11 @@ function defineTests(config: storage.TestStorageConfig) {
|
|
|
471
485
|
await connectionManager.query(`INSERT INTO test_data(id, description) VALUES('t1','test1')`);
|
|
472
486
|
|
|
473
487
|
await context.replicateSnapshot();
|
|
474
|
-
await context.startStreaming();
|
|
475
488
|
|
|
476
489
|
await connectionManager.query(`ALTER TABLE test_data MODIFY COLUMN id VARCHAR(36)`);
|
|
477
490
|
await connectionManager.query(`INSERT INTO test_data(id, description) VALUES('t2','test2')`);
|
|
491
|
+
// Replay the DDL after the insert commits so the replacement snapshot includes it.
|
|
492
|
+
await context.startStreaming();
|
|
478
493
|
|
|
479
494
|
const data = await context.getBucketData('global[]');
|
|
480
495
|
const reduced = test_utils.reduceBucket(data).slice(1);
|
|
@@ -513,8 +528,11 @@ function defineTests(config: storage.TestStorageConfig) {
|
|
|
513
528
|
await context.startStreaming();
|
|
514
529
|
|
|
515
530
|
await connectionManager.query(`INSERT INTO test_data(id, description) VALUES('t2','test2')`);
|
|
531
|
+
await context.getCheckpoint();
|
|
516
532
|
|
|
517
533
|
await connectionManager.query(`ALTER TABLE test_data MODIFY COLUMN id VARCHAR(36)`);
|
|
534
|
+
// Finish the replacement snapshot before inserting t3, which is streamed once.
|
|
535
|
+
await context.getCheckpoint();
|
|
518
536
|
await connectionManager.query(`INSERT INTO test_data(id, description) VALUES('t3','test3')`);
|
|
519
537
|
|
|
520
538
|
const data = await context.getBucketData('global[]');
|
|
@@ -535,9 +553,9 @@ function defineTests(config: storage.TestStorageConfig) {
|
|
|
535
553
|
]);
|
|
536
554
|
|
|
537
555
|
// Snapshot - order doesn't matter
|
|
538
|
-
expect(data.slice(4,
|
|
556
|
+
expect(data.slice(4, 6).sort(compareIds)).toMatchObject([PUT_T1, PUT_T2]);
|
|
539
557
|
|
|
540
|
-
expect(data.slice(
|
|
558
|
+
expect(data.slice(6).sort(compareIds)).toMatchObject([
|
|
541
559
|
// Replicated insert
|
|
542
560
|
PUT_T3
|
|
543
561
|
]);
|
package/test/src/util.ts
CHANGED
|
@@ -9,7 +9,7 @@ import * as mongo_storage from '@powersync/service-module-mongodb-storage';
|
|
|
9
9
|
import * as postgres_storage from '@powersync/service-module-postgres-storage';
|
|
10
10
|
import { TablePattern } from '@powersync/service-sync-rules';
|
|
11
11
|
import mysqlPromise from 'mysql2/promise';
|
|
12
|
-
import { describe, TestOptions } from 'vitest';
|
|
12
|
+
import { describe, TestOptions, vi } from 'vitest';
|
|
13
13
|
import { env } from './env.js';
|
|
14
14
|
|
|
15
15
|
export const TEST_URI = env.MYSQL_TEST_URI;
|
|
@@ -28,6 +28,17 @@ export const INITIALIZED_POSTGRES_STORAGE_FACTORY = postgres_storage.test_utils.
|
|
|
28
28
|
url: env.PG_STORAGE_TEST_URL
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
export function createMockMySQLConnection(queryHandler: (sql: string, params?: unknown[]) => Promise<unknown>): {
|
|
32
|
+
connection: mysqlPromise.Connection;
|
|
33
|
+
query: ReturnType<typeof vi.fn>;
|
|
34
|
+
} {
|
|
35
|
+
const query = vi.fn(queryHandler);
|
|
36
|
+
return {
|
|
37
|
+
connection: { query } as unknown as mysqlPromise.Connection,
|
|
38
|
+
query
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
31
42
|
export function describeWithStorage(options: TestOptions, fn: (factory: TestStorageConfig) => void) {
|
|
32
43
|
describe.skipIf(!env.TEST_MONGO_STORAGE)(`mongodb storage`, options, function () {
|
|
33
44
|
fn(INITIALIZED_MONGO_STORAGE_FACTORY);
|
|
@@ -71,11 +82,19 @@ export async function getFromGTID(connectionManager: MySQLConnectionManager) {
|
|
|
71
82
|
return fromGTID;
|
|
72
83
|
}
|
|
73
84
|
|
|
85
|
+
export async function getActiveServerUuid(connectionManager: MySQLConnectionManager) {
|
|
86
|
+
const connection = await connectionManager.getConnection();
|
|
87
|
+
const activeServerUuid = await common.readServerUuid(connection);
|
|
88
|
+
connection.release();
|
|
89
|
+
return activeServerUuid;
|
|
90
|
+
}
|
|
91
|
+
|
|
74
92
|
export interface CreateBinlogListenerParams {
|
|
75
93
|
connectionManager: MySQLConnectionManager;
|
|
76
94
|
eventHandler: BinLogEventHandler;
|
|
77
95
|
sourceTables: TablePattern[];
|
|
78
96
|
startGTID?: common.ReplicatedGTID;
|
|
97
|
+
ctrlConnectionProbeTimeoutMs?: number;
|
|
79
98
|
}
|
|
80
99
|
export async function createBinlogListener(params: CreateBinlogListenerParams): Promise<BinLogListener> {
|
|
81
100
|
let { connectionManager, eventHandler, sourceTables, startGTID } = params;
|
|
@@ -84,12 +103,16 @@ export async function createBinlogListener(params: CreateBinlogListenerParams):
|
|
|
84
103
|
startGTID = await getFromGTID(connectionManager);
|
|
85
104
|
}
|
|
86
105
|
|
|
106
|
+
const activeServerUuid = await getActiveServerUuid(connectionManager);
|
|
107
|
+
|
|
87
108
|
return new BinLogListener({
|
|
88
109
|
connectionManager: connectionManager,
|
|
89
110
|
eventHandler: eventHandler,
|
|
90
111
|
startGTID: startGTID!,
|
|
91
112
|
sourceTables: sourceTables,
|
|
92
|
-
serverId: createRandomServerId(1)
|
|
113
|
+
serverId: createRandomServerId(1),
|
|
114
|
+
activeServerUuid: activeServerUuid,
|
|
115
|
+
ctrlConnectionProbeTimeoutMs: params.ctrlConnectionProbeTimeoutMs
|
|
93
116
|
});
|
|
94
117
|
}
|
|
95
118
|
|
|
@@ -100,6 +123,7 @@ export class TestBinLogEventHandler implements BinLogEventHandler {
|
|
|
100
123
|
commitCount = 0;
|
|
101
124
|
schemaChanges: SchemaChange[] = [];
|
|
102
125
|
lastKeepAlive: string | undefined;
|
|
126
|
+
lastCommitLsn: string | undefined;
|
|
103
127
|
|
|
104
128
|
unpause: ((value: void | PromiseLike<void>) => void) | undefined;
|
|
105
129
|
private pausedPromise: Promise<void> | undefined;
|
|
@@ -127,6 +151,7 @@ export class TestBinLogEventHandler implements BinLogEventHandler {
|
|
|
127
151
|
|
|
128
152
|
async onCommit(lsn: string) {
|
|
129
153
|
this.commitCount++;
|
|
154
|
+
this.lastCommitLsn = lsn;
|
|
130
155
|
}
|
|
131
156
|
|
|
132
157
|
async onSchemaChange(change: SchemaChange) {
|