@powersync/service-module-mysql 0.15.0 → 0.16.1
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 +42 -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/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 +104 -30
- 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/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 +131 -30
- package/src/utils/mysql-utils.ts +11 -0
- package/test/src/BinLogListener.test.ts +140 -1
- 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/util.ts +27 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { ReplicatedGTID } from '@module/common/ReplicatedGTID.js';
|
|
2
|
+
import * as uuid from 'uuid';
|
|
3
|
+
import { describe, expect, test } from 'vitest';
|
|
4
|
+
|
|
5
|
+
describe('ReplicatedGTID', () => {
|
|
6
|
+
const SERVER_UUID = 'a7d0ff7b-0c0e-11f0-8b38-566fbaa00004';
|
|
7
|
+
const POSITION = { filename: 'binlog.000042', offset: 1234 };
|
|
8
|
+
|
|
9
|
+
describe('single GTID', () => {
|
|
10
|
+
test('exposes its raw value, server UUID, and binlog position', () => {
|
|
11
|
+
const gtid = new ReplicatedGTID({
|
|
12
|
+
rawGtid: `${SERVER_UUID}:5`,
|
|
13
|
+
position: POSITION
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
expect(gtid.raw).toEqual(`${SERVER_UUID}:5`);
|
|
17
|
+
expect(gtid.serverUuid).toEqual(SERVER_UUID);
|
|
18
|
+
expect(gtid.position).toEqual(POSITION);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('formats a comparable LSN using the transaction id', () => {
|
|
22
|
+
const gtid = new ReplicatedGTID({
|
|
23
|
+
rawGtid: `${SERVER_UUID}:17`,
|
|
24
|
+
position: POSITION
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
expect(gtid.comparable).toEqual(`0000000000000017|${SERVER_UUID}:17|binlog.000042|1234`);
|
|
28
|
+
expect(gtid.toString()).toEqual(gtid.comparable);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('normalizes surrounding whitespace', () => {
|
|
32
|
+
const gtid = new ReplicatedGTID({
|
|
33
|
+
rawGtid: ` \n\t${SERVER_UUID}:17 \r\n`,
|
|
34
|
+
position: POSITION
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(gtid.raw).toEqual(`${SERVER_UUID}:17`);
|
|
38
|
+
expect(gtid.serverUuid).toEqual(SERVER_UUID);
|
|
39
|
+
expect(gtid.comparable).toEqual(`0000000000000017|${SERVER_UUID}:17|binlog.000042|1234`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('keeps the ZERO GTID format stable', () => {
|
|
43
|
+
expect(ReplicatedGTID.ZERO(SERVER_UUID).raw).toEqual(`${SERVER_UUID}:0`);
|
|
44
|
+
expect(ReplicatedGTID.ZERO(SERVER_UUID).comparable).toEqual(`0000000000000000|${SERVER_UUID}:0||0`);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('validation', () => {
|
|
49
|
+
test.each([
|
|
50
|
+
['', 'missing server UUID and transaction id'],
|
|
51
|
+
[SERVER_UUID, 'missing transaction id'],
|
|
52
|
+
[`${SERVER_UUID}:`, 'empty transaction id'],
|
|
53
|
+
[`:${17}`, 'empty server UUID'],
|
|
54
|
+
[`${SERVER_UUID}:1-17`, 'transaction interval'],
|
|
55
|
+
[`${SERVER_UUID}:1:17`, 'multiple transaction components'],
|
|
56
|
+
[`${SERVER_UUID}:abc`, 'non-numeric transaction id'],
|
|
57
|
+
[`${SERVER_UUID}:-1`, 'negative transaction id'],
|
|
58
|
+
[`${SERVER_UUID}:17,another-server:9`, 'comma-separated GTID set'],
|
|
59
|
+
[`${SERVER_UUID}:17,\nanother-server:9`, 'newline-separated GTID set']
|
|
60
|
+
])('rejects %s (%s)', (rawGtid) => {
|
|
61
|
+
expect(() => new ReplicatedGTID({ rawGtid, position: POSITION })).toThrow();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('serialization', () => {
|
|
66
|
+
test('round-trips a single GTID', () => {
|
|
67
|
+
const gtid = new ReplicatedGTID({
|
|
68
|
+
rawGtid: `${SERVER_UUID}:17`,
|
|
69
|
+
position: POSITION
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const deserialized = ReplicatedGTID.fromSerialized(gtid.comparable);
|
|
73
|
+
|
|
74
|
+
expect(deserialized.raw).toEqual(gtid.raw);
|
|
75
|
+
expect(deserialized.serverUuid).toEqual(SERVER_UUID);
|
|
76
|
+
expect(deserialized.position).toEqual(POSITION);
|
|
77
|
+
expect(deserialized.comparable).toEqual(gtid.comparable);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('throws on malformed serialized GTIDs', () => {
|
|
81
|
+
expect(() => ReplicatedGTID.fromSerialized('abc')).toThrow('Invalid serialized GTID');
|
|
82
|
+
expect(() => ReplicatedGTID.fromSerialized(`0000000000000001|${SERVER_UUID}:1|binlog.000001`)).toThrow(
|
|
83
|
+
'Invalid serialized GTID'
|
|
84
|
+
);
|
|
85
|
+
expect(() => ReplicatedGTID.fromSerialized(`0000000000000001|${SERVER_UUID}:1|binlog.000001|notanumber`)).toThrow(
|
|
86
|
+
'Invalid BinLog offset'
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('rejects a serialized GTID set', () => {
|
|
91
|
+
const serialized = `0000000000000017|${SERVER_UUID}:1-17|binlog.000042|1234`;
|
|
92
|
+
|
|
93
|
+
expect(() => ReplicatedGTID.fromSerialized(serialized)).toThrow('Expected a single transaction id');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('binlog events', () => {
|
|
98
|
+
test('creates a single GTID from a binlog event', () => {
|
|
99
|
+
const gtid = ReplicatedGTID.fromBinLogEvent({
|
|
100
|
+
rawGtid: {
|
|
101
|
+
serverUuid: Buffer.from(uuid.parse(SERVER_UUID)),
|
|
102
|
+
transactionId: 17
|
|
103
|
+
},
|
|
104
|
+
position: POSITION
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(gtid.raw).toEqual(`${SERVER_UUID}:17`);
|
|
108
|
+
expect(gtid.position).toEqual(POSITION);
|
|
109
|
+
expect(gtid.comparable).toEqual(`0000000000000017|${SERVER_UUID}:17|binlog.000042|1234`);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('LSN ordering', () => {
|
|
114
|
+
test('orders GTIDs from the same server by transaction id', () => {
|
|
115
|
+
const earlier = new ReplicatedGTID({ rawGtid: `${SERVER_UUID}:9`, position: POSITION });
|
|
116
|
+
const later = new ReplicatedGTID({ rawGtid: `${SERVER_UUID}:18`, position: POSITION });
|
|
117
|
+
|
|
118
|
+
expect(earlier.comparable < later.comparable).toBeTruthy();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('orders LSNs for the same transaction by binlog offset', () => {
|
|
122
|
+
// The binlog offset is not zero-padded, so lexicographic ordering only holds for
|
|
123
|
+
// offsets with the same number of digits. This format cannot change while existing
|
|
124
|
+
// LSNs remain persisted in bucket storage.
|
|
125
|
+
const rawGtid = `${SERVER_UUID}:18`;
|
|
126
|
+
const transactionStart = new ReplicatedGTID({
|
|
127
|
+
rawGtid,
|
|
128
|
+
position: { filename: 'binlog.000042', offset: 157 }
|
|
129
|
+
});
|
|
130
|
+
const transactionEnd = new ReplicatedGTID({
|
|
131
|
+
rawGtid,
|
|
132
|
+
position: { filename: 'binlog.000042', offset: 300 }
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(transactionStart.comparable < transactionEnd.comparable).toBeTruthy();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { checkSourceConfiguration } from '@module/common/check-source-configuration.js';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { createMockMySQLConnection } from './util.js';
|
|
4
|
+
|
|
5
|
+
describe('checkSourceConfiguration', () => {
|
|
6
|
+
test('accepts a primary MySQL server', async () => {
|
|
7
|
+
const { connection, query } = createConnection({ version: '8.4.0', replicaStatuses: [] });
|
|
8
|
+
|
|
9
|
+
await expect(checkSourceConfiguration(connection)).resolves.toEqual([]);
|
|
10
|
+
expect(query).toHaveBeenCalledWith('SHOW REPLICA STATUS', []);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('rejects a replica on MySQL 8.0.22 and later', async () => {
|
|
14
|
+
const { connection, query } = createConnection({
|
|
15
|
+
version: '8.0.22',
|
|
16
|
+
replicaStatuses: [{ Channel_Name: '' }]
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
await expect(checkSourceConfiguration(connection)).resolves.toContain(
|
|
20
|
+
'Connecting PowerSync to a MySQL replica is not supported. Please connect PowerSync directly to the primary server.'
|
|
21
|
+
);
|
|
22
|
+
expect(query).toHaveBeenCalledWith('SHOW REPLICA STATUS', []);
|
|
23
|
+
expect(query).not.toHaveBeenCalledWith('SHOW SLAVE STATUS', []);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('uses legacy replica-status syntax before MySQL 8.0.22', async () => {
|
|
27
|
+
const { connection, query } = createConnection({
|
|
28
|
+
version: '5.7.44',
|
|
29
|
+
replicaStatuses: [{ Channel_Name: '' }]
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await expect(checkSourceConfiguration(connection)).resolves.toContain(
|
|
33
|
+
'Connecting PowerSync to a MySQL replica is not supported. Please connect PowerSync directly to the primary server.'
|
|
34
|
+
);
|
|
35
|
+
expect(query).toHaveBeenCalledWith('SHOW SLAVE STATUS', []);
|
|
36
|
+
expect(query).not.toHaveBeenCalledWith('SHOW REPLICA STATUS', []);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function createConnection(options: { version: string; replicaStatuses: Record<string, unknown>[] }) {
|
|
40
|
+
return createMockMySQLConnection(async (sql) => {
|
|
41
|
+
switch (sql.trim()) {
|
|
42
|
+
case 'SELECT VERSION() as version':
|
|
43
|
+
return [[{ version: options.version }], []];
|
|
44
|
+
case 'SHOW REPLICA STATUS':
|
|
45
|
+
case 'SHOW SLAVE STATUS':
|
|
46
|
+
return [options.replicaStatuses, []];
|
|
47
|
+
case "SHOW VARIABLES LIKE 'binlog_format';":
|
|
48
|
+
return [[{ Value: 'ROW' }], []];
|
|
49
|
+
case "SHOW GLOBAL VARIABLES LIKE 'binlog_row_image';":
|
|
50
|
+
return [[{ Value: 'FULL' }], []];
|
|
51
|
+
default:
|
|
52
|
+
if (sql.includes('@@GLOBAL.gtid_mode AS gtid_mode')) {
|
|
53
|
+
return [
|
|
54
|
+
[
|
|
55
|
+
{
|
|
56
|
+
gtid_mode: 'ON',
|
|
57
|
+
log_bin: 1,
|
|
58
|
+
server_id: 1,
|
|
59
|
+
binlog_file: '/var/lib/mysql/binlog',
|
|
60
|
+
binlog_index_file: '/var/lib/mysql/binlog.index'
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
[]
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`Unexpected query: ${sql}`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as types from '@module/types/types.js';
|
|
2
|
+
import { createPool, isVersionAtLeast, TCP_KEEPALIVE_INITIAL_DELAY } from '@module/utils/mysql-utils.js';
|
|
2
3
|
import { describe, expect, test } from 'vitest';
|
|
3
4
|
|
|
4
5
|
describe('MySQL Utility Tests', () => {
|
|
@@ -14,4 +15,21 @@ describe('MySQL Utility Tests', () => {
|
|
|
14
15
|
expect(isVersionAtLeast(olderVersion, '8.0')).toBeFalsy();
|
|
15
16
|
expect(isVersionAtLeast(improperSemver, '5.7')).toBeTruthy();
|
|
16
17
|
});
|
|
18
|
+
|
|
19
|
+
test('Pool connections are configured with a TCP keepalive initial delay', async () => {
|
|
20
|
+
// mysql2 enables keepalive by default, but without an initial delay the OS default of
|
|
21
|
+
// 7200 seconds applies, which is too late for common 3600 second firewall idle timeouts.
|
|
22
|
+
const config = types.normalizeConnectionConfig({
|
|
23
|
+
type: 'mysql',
|
|
24
|
+
uri: 'mysql://root:password@localhost:3306/mydatabase'
|
|
25
|
+
});
|
|
26
|
+
// The pool is lazy, so no connection is made here.
|
|
27
|
+
const pool = createPool(config);
|
|
28
|
+
const { connectionConfig } = (pool as unknown as { config: { connectionConfig: Record<string, unknown> } }).config;
|
|
29
|
+
|
|
30
|
+
expect(connectionConfig.enableKeepAlive).toBe(true);
|
|
31
|
+
expect(connectionConfig.keepAliveInitialDelay).toBe(TCP_KEEPALIVE_INITIAL_DELAY);
|
|
32
|
+
|
|
33
|
+
await pool.promise().end();
|
|
34
|
+
});
|
|
17
35
|
});
|
|
@@ -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,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) {
|