@powersync/service-module-mysql 0.15.0 → 0.16.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 +12 -0
- 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/replication/BinLogStream.d.ts +2 -0
- package/dist/replication/BinLogStream.js +42 -14
- package/dist/replication/BinLogStream.js.map +1 -1
- package/dist/replication/zongji/BinLogListener.d.ts +17 -2
- package/dist/replication/zongji/BinLogListener.js +42 -29
- package/dist/replication/zongji/BinLogListener.js.map +1 -1
- package/package.json +4 -4
- 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/replication/BinLogStream.ts +45 -14
- package/src/replication/zongji/BinLogListener.ts +61 -29
- package/test/src/BinLogListener.test.ts +18 -0
- package/test/src/ReplicatedGTID.test.ts +138 -0
- package/test/src/check-source-configuration.test.ts +70 -0
- package/test/src/read-executed-gtid.test.ts +188 -0
- package/test/src/util.ts +25 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { ReplicatedGTID } from '@module/common/ReplicatedGTID.js';
|
|
2
|
+
import {
|
|
3
|
+
getLatestActiveGtid,
|
|
4
|
+
isGtidPositionStillAvailable,
|
|
5
|
+
readExecutedGtid
|
|
6
|
+
} from '@module/common/read-executed-gtid.js';
|
|
7
|
+
import { describe, expect, test } from 'vitest';
|
|
8
|
+
import { createMockMySQLConnection } from './util.js';
|
|
9
|
+
|
|
10
|
+
describe('read-executed-gtid', () => {
|
|
11
|
+
const ACTIVE_SERVER_UUID = 'a7d0ff7b-0c0e-11f0-8b38-566fbaa00004';
|
|
12
|
+
const STALE_SERVER_UUID = '314306f3-ff7b-11ef-a0e0-566fbaa00002';
|
|
13
|
+
|
|
14
|
+
describe('getLatestActiveGtid', () => {
|
|
15
|
+
test('returns the highest transaction id for the active server', async () => {
|
|
16
|
+
const gtid = await getLatestActiveGtid(
|
|
17
|
+
[`${STALE_SERVER_UUID}:1-1000`, `\n${ACTIVE_SERVER_UUID}:1-5:9:12-17`],
|
|
18
|
+
ACTIVE_SERVER_UUID
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(gtid).toEqual(`${ACTIVE_SERVER_UUID}:17`);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('supports a bare transaction id', async () => {
|
|
25
|
+
const gtid = await getLatestActiveGtid([`${ACTIVE_SERVER_UUID}:42`], ACTIVE_SERVER_UUID);
|
|
26
|
+
|
|
27
|
+
expect(gtid).toEqual(`${ACTIVE_SERVER_UUID}:42`);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('returns the active server ZERO GTID when it is absent from the GTID sets', async () => {
|
|
31
|
+
await expect(getLatestActiveGtid([`${STALE_SERVER_UUID}:1-1000`], ACTIVE_SERVER_UUID)).resolves.toEqual(
|
|
32
|
+
`${ACTIVE_SERVER_UUID}:0`
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('readExecutedGtid', () => {
|
|
38
|
+
test('reads binary log status on MySQL 8.4 and selects the active server GTID', async () => {
|
|
39
|
+
const { connection, query } = createConnection({
|
|
40
|
+
version: '8.4.0',
|
|
41
|
+
executedGtidSet: `${STALE_SERVER_UUID}:1-1000,\n${ACTIVE_SERVER_UUID}:1-5:11-18`
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const gtid = await readExecutedGtid(connection);
|
|
45
|
+
|
|
46
|
+
expect(gtid.raw).toEqual(`${ACTIVE_SERVER_UUID}:18`);
|
|
47
|
+
expect(gtid.position).toEqual({ filename: 'binlog.000042', offset: 1234 });
|
|
48
|
+
expect(query).toHaveBeenCalledWith('SHOW BINARY LOG STATUS', []);
|
|
49
|
+
expect(query).not.toHaveBeenCalledWith('SHOW MASTER STATUS', []);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('reads master status on MySQL versions before 8.4', async () => {
|
|
53
|
+
const { connection, query } = createConnection({
|
|
54
|
+
version: '8.0.40',
|
|
55
|
+
executedGtidSet: `${ACTIVE_SERVER_UUID}:1-17`
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const gtid = await readExecutedGtid(connection);
|
|
59
|
+
|
|
60
|
+
expect(gtid.raw).toEqual(`${ACTIVE_SERVER_UUID}:17`);
|
|
61
|
+
expect(query).toHaveBeenCalledWith('SHOW MASTER STATUS', []);
|
|
62
|
+
expect(query).not.toHaveBeenCalledWith('SHOW BINARY LOG STATUS', []);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('returns the active server ZERO GTID when no transactions have executed', async () => {
|
|
66
|
+
const { connection } = createConnection({
|
|
67
|
+
version: '8.4.0',
|
|
68
|
+
executedGtidSet: ' \n\t '
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const gtid = await readExecutedGtid(connection);
|
|
72
|
+
|
|
73
|
+
expect(gtid.raw).toEqual(`${ACTIVE_SERVER_UUID}:0`);
|
|
74
|
+
expect(gtid.position).toEqual({ filename: 'binlog.000042', offset: 1234 });
|
|
75
|
+
expect(gtid.comparable).toEqual(`0000000000000000|${ACTIVE_SERVER_UUID}:0|binlog.000042|1234`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('uses the active server ZERO GTID at the current position when only historical UUIDs exist', async () => {
|
|
79
|
+
const { connection } = createConnection({
|
|
80
|
+
version: '8.4.0',
|
|
81
|
+
executedGtidSet: `${STALE_SERVER_UUID}:1-1000`
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const gtid = await readExecutedGtid(connection);
|
|
85
|
+
|
|
86
|
+
expect(gtid.raw).toEqual(`${ACTIVE_SERVER_UUID}:0`);
|
|
87
|
+
expect(gtid.position).toEqual({ filename: 'binlog.000042', offset: 1234 });
|
|
88
|
+
expect(gtid.comparable).toEqual(`0000000000000000|${ACTIVE_SERVER_UUID}:0|binlog.000042|1234`);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('isGtidPositionStillAvailable', () => {
|
|
93
|
+
const RESUME_GTID = new ReplicatedGTID({
|
|
94
|
+
rawGtid: `${ACTIVE_SERVER_UUID}:17`,
|
|
95
|
+
position: { filename: 'binlog.000042', offset: 1234 }
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('returns true when the GTID is executed and its binlog coordinate is available', async () => {
|
|
99
|
+
const { connection, query } = createResumeCheckConnection({
|
|
100
|
+
isExecuted: 1,
|
|
101
|
+
logFiles: [{ Log_name: 'binlog.000042', File_size: 2000 }]
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await expect(isGtidPositionStillAvailable(connection, RESUME_GTID)).resolves.toBe(true);
|
|
105
|
+
expect(query).toHaveBeenCalledWith('SELECT GTID_SUBSET(?, @@GLOBAL.gtid_executed) AS is_executed', [
|
|
106
|
+
RESUME_GTID.raw
|
|
107
|
+
]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('returns false when the GTID is absent after a source rewind', async () => {
|
|
111
|
+
const { connection } = createResumeCheckConnection({
|
|
112
|
+
isExecuted: 0,
|
|
113
|
+
logFiles: [{ Log_name: 'binlog.000042', File_size: 2000 }]
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await expect(isGtidPositionStillAvailable(connection, RESUME_GTID)).resolves.toBe(false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('validates the synthetic ZERO GTID using only its binlog coordinate', async () => {
|
|
120
|
+
const zeroGtid = new ReplicatedGTID({
|
|
121
|
+
rawGtid: `${ACTIVE_SERVER_UUID}:0`,
|
|
122
|
+
position: { filename: 'binlog.000042', offset: 1234 }
|
|
123
|
+
});
|
|
124
|
+
const { connection, query } = createResumeCheckConnection({
|
|
125
|
+
isExecuted: 0,
|
|
126
|
+
logFiles: [{ Log_name: 'binlog.000042', File_size: 2000 }]
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await expect(isGtidPositionStillAvailable(connection, zeroGtid)).resolves.toBe(true);
|
|
130
|
+
expect(query).not.toHaveBeenCalledWith('SELECT GTID_SUBSET(?, @@GLOBAL.gtid_executed) AS is_executed', [
|
|
131
|
+
zeroGtid.raw
|
|
132
|
+
]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test.each([
|
|
136
|
+
[[], 'the binlog file is absent'],
|
|
137
|
+
[[{ Log_name: 'binlog.000042', File_size: 1000 }], 'the stored offset is past the end of the binlog']
|
|
138
|
+
])('returns false when %s (%s)', async (logFiles) => {
|
|
139
|
+
const { connection, query } = createResumeCheckConnection({ isExecuted: 1, logFiles });
|
|
140
|
+
|
|
141
|
+
await expect(isGtidPositionStillAvailable(connection, RESUME_GTID)).resolves.toBe(false);
|
|
142
|
+
expect(query).not.toHaveBeenCalledWith('SELECT GTID_SUBSET(?, @@GLOBAL.gtid_executed) AS is_executed', [
|
|
143
|
+
RESUME_GTID.raw
|
|
144
|
+
]);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
function createConnection(options: { version: string; executedGtidSet: string }) {
|
|
149
|
+
return createMockMySQLConnection(async (sql) => {
|
|
150
|
+
switch (sql) {
|
|
151
|
+
case 'SELECT VERSION() as version':
|
|
152
|
+
return [[{ version: options.version }], []];
|
|
153
|
+
case 'SHOW BINARY LOG STATUS':
|
|
154
|
+
case 'SHOW MASTER STATUS':
|
|
155
|
+
return [
|
|
156
|
+
[
|
|
157
|
+
{
|
|
158
|
+
File: 'binlog.000042',
|
|
159
|
+
Position: '1234',
|
|
160
|
+
Executed_Gtid_Set: options.executedGtidSet
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
[]
|
|
164
|
+
];
|
|
165
|
+
case 'SELECT @@server_uuid AS server_uuid':
|
|
166
|
+
return [[{ server_uuid: ACTIVE_SERVER_UUID }], []];
|
|
167
|
+
default:
|
|
168
|
+
throw new Error(`Unexpected query: ${sql}`);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function createResumeCheckConnection(options: {
|
|
174
|
+
isExecuted: number;
|
|
175
|
+
logFiles: { Log_name: string; File_size: number }[];
|
|
176
|
+
}) {
|
|
177
|
+
return createMockMySQLConnection(async (sql) => {
|
|
178
|
+
switch (sql) {
|
|
179
|
+
case 'SHOW BINARY LOGS;':
|
|
180
|
+
return [options.logFiles, []];
|
|
181
|
+
case 'SELECT GTID_SUBSET(?, @@GLOBAL.gtid_executed) AS is_executed':
|
|
182
|
+
return [[{ is_executed: options.isExecuted }], []];
|
|
183
|
+
default:
|
|
184
|
+
throw new Error(`Unexpected query: ${sql}`);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
});
|
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,6 +82,13 @@ 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;
|
|
@@ -84,12 +102,15 @@ export async function createBinlogListener(params: CreateBinlogListenerParams):
|
|
|
84
102
|
startGTID = await getFromGTID(connectionManager);
|
|
85
103
|
}
|
|
86
104
|
|
|
105
|
+
const activeServerUuid = await getActiveServerUuid(connectionManager);
|
|
106
|
+
|
|
87
107
|
return new BinLogListener({
|
|
88
108
|
connectionManager: connectionManager,
|
|
89
109
|
eventHandler: eventHandler,
|
|
90
110
|
startGTID: startGTID!,
|
|
91
111
|
sourceTables: sourceTables,
|
|
92
|
-
serverId: createRandomServerId(1)
|
|
112
|
+
serverId: createRandomServerId(1),
|
|
113
|
+
activeServerUuid: activeServerUuid
|
|
93
114
|
});
|
|
94
115
|
}
|
|
95
116
|
|
|
@@ -100,6 +121,7 @@ export class TestBinLogEventHandler implements BinLogEventHandler {
|
|
|
100
121
|
commitCount = 0;
|
|
101
122
|
schemaChanges: SchemaChange[] = [];
|
|
102
123
|
lastKeepAlive: string | undefined;
|
|
124
|
+
lastCommitLsn: string | undefined;
|
|
103
125
|
|
|
104
126
|
unpause: ((value: void | PromiseLike<void>) => void) | undefined;
|
|
105
127
|
private pausedPromise: Promise<void> | undefined;
|
|
@@ -127,6 +149,7 @@ export class TestBinLogEventHandler implements BinLogEventHandler {
|
|
|
127
149
|
|
|
128
150
|
async onCommit(lsn: string) {
|
|
129
151
|
this.commitCount++;
|
|
152
|
+
this.lastCommitLsn = lsn;
|
|
130
153
|
}
|
|
131
154
|
|
|
132
155
|
async onSchemaChange(change: SchemaChange) {
|