@powersync/service-module-mysql 0.0.0-dev-20241022094219 → 0.0.0-dev-20241023230541
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 +6 -5
- package/dev/docker/mysql/docker-compose.yaml +2 -2
- package/dist/common/mysql-to-sqlite.d.ts +2 -1
- package/dist/common/mysql-to-sqlite.js +10 -2
- package/dist/common/mysql-to-sqlite.js.map +1 -1
- package/dist/common/read-executed-gtid.d.ts +1 -0
- package/dist/common/read-executed-gtid.js +7 -0
- package/dist/common/read-executed-gtid.js.map +1 -1
- package/dist/replication/BinLogStream.js +40 -29
- package/dist/replication/BinLogStream.js.map +1 -1
- package/dist/types/types.d.ts +2 -0
- package/dist/types/types.js +3 -1
- package/dist/types/types.js.map +1 -1
- package/dist/utils/mysql_utils.d.ts +3 -0
- package/dist/utils/mysql_utils.js +6 -1
- package/dist/utils/mysql_utils.js.map +1 -1
- package/package.json +6 -6
- package/src/common/mysql-to-sqlite.ts +10 -2
- package/src/common/read-executed-gtid.ts +12 -0
- package/src/replication/BinLogStream.ts +50 -41
- package/src/replication/zongji/zongji.d.ts +11 -5
- package/src/types/types.ts +5 -1
- package/src/utils/mysql_utils.ts +7 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/test/src/binlog_stream.test.ts +0 -287
- package/test/src/binlog_stream_utils.ts +0 -152
|
@@ -2,15 +2,16 @@ import { logger } from '@powersync/lib-services-framework';
|
|
|
2
2
|
import * as sync_rules from '@powersync/service-sync-rules';
|
|
3
3
|
import async from 'async';
|
|
4
4
|
|
|
5
|
-
import { framework, getUuidReplicaIdentityBson, Metrics, storage } from '@powersync/service-core';
|
|
6
|
-
import mysql from 'mysql2';
|
|
5
|
+
import { ColumnDescriptor, framework, getUuidReplicaIdentityBson, Metrics, storage } from '@powersync/service-core';
|
|
6
|
+
import mysql, { FieldPacket } from 'mysql2';
|
|
7
7
|
|
|
8
|
-
import { BinLogEvent } from '@powersync/mysql-zongji';
|
|
8
|
+
import { BinLogEvent, TableMapEntry } from '@powersync/mysql-zongji';
|
|
9
9
|
import * as common from '../common/common-index.js';
|
|
10
10
|
import * as zongji_utils from './zongji/zongji-utils.js';
|
|
11
11
|
import { MySQLConnectionManager } from './MySQLConnectionManager.js';
|
|
12
|
-
import { ReplicatedGTID } from '../common/common-index.js';
|
|
12
|
+
import { isBinlogStillAvailable, ReplicatedGTID } from '../common/common-index.js';
|
|
13
13
|
import mysqlPromise from 'mysql2/promise';
|
|
14
|
+
import { MySQLTypesMap } from '../utils/mysql_utils.js';
|
|
14
15
|
|
|
15
16
|
export interface BinLogStreamOptions {
|
|
16
17
|
connections: MySQLConnectionManager;
|
|
@@ -30,6 +31,7 @@ interface WriteChangePayload {
|
|
|
30
31
|
database: string;
|
|
31
32
|
table: string;
|
|
32
33
|
sourceTable: storage.SourceTable;
|
|
34
|
+
columns: Map<string, ColumnDescriptor>;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export type Data = Record<string, any>;
|
|
@@ -211,10 +213,23 @@ AND table_type = 'BASE TABLE';`,
|
|
|
211
213
|
*/
|
|
212
214
|
protected async checkInitialReplicated(): Promise<boolean> {
|
|
213
215
|
const status = await this.storage.getStatus();
|
|
216
|
+
const lastKnowGTID = status.checkpoint_lsn ? common.ReplicatedGTID.fromSerialized(status.checkpoint_lsn) : null;
|
|
214
217
|
if (status.snapshot_done && status.checkpoint_lsn) {
|
|
215
|
-
logger.info(`Initial replication already done
|
|
218
|
+
logger.info(`Initial replication already done.`);
|
|
219
|
+
|
|
220
|
+
if (lastKnowGTID) {
|
|
221
|
+
// Check if the binlog is still available. If it isn't we need to snapshot again.
|
|
222
|
+
const connection = await this.connections.getConnection();
|
|
223
|
+
try {
|
|
224
|
+
return await isBinlogStillAvailable(connection, lastKnowGTID.position.filename);
|
|
225
|
+
} finally {
|
|
226
|
+
connection.release();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
216
230
|
return true;
|
|
217
231
|
}
|
|
232
|
+
|
|
218
233
|
return false;
|
|
219
234
|
}
|
|
220
235
|
|
|
@@ -270,17 +285,24 @@ AND table_type = 'BASE TABLE';`,
|
|
|
270
285
|
logger.info(`Replicating ${table.qualifiedName}`);
|
|
271
286
|
// TODO count rows and log progress at certain batch sizes
|
|
272
287
|
|
|
288
|
+
const columns = new Map<string, ColumnDescriptor>();
|
|
273
289
|
return new Promise<void>((resolve, reject) => {
|
|
274
290
|
// MAX_EXECUTION_TIME(0) hint disables execution timeout for this query
|
|
275
291
|
connection
|
|
276
292
|
.query(`SELECT /*+ MAX_EXECUTION_TIME(0) */ * FROM ${table.schema}.${table.table}`)
|
|
277
|
-
.stream()
|
|
278
293
|
.on('error', (err) => {
|
|
279
294
|
reject(err);
|
|
280
295
|
})
|
|
281
|
-
.on('
|
|
296
|
+
.on('fields', (fields: FieldPacket[]) => {
|
|
297
|
+
// Map the columns and their types
|
|
298
|
+
fields.forEach((field) => {
|
|
299
|
+
const columnType = MySQLTypesMap[field.type as number];
|
|
300
|
+
columns.set(field.name, { name: field.name, type: columnType, typeId: field.type });
|
|
301
|
+
});
|
|
302
|
+
})
|
|
303
|
+
.on('result', async (row) => {
|
|
282
304
|
connection.pause();
|
|
283
|
-
const record = common.toSQLiteRow(row);
|
|
305
|
+
const record = common.toSQLiteRow(row, columns);
|
|
284
306
|
|
|
285
307
|
await batch.save({
|
|
286
308
|
tag: storage.SaveOperationTag.INSERT,
|
|
@@ -291,7 +313,6 @@ AND table_type = 'BASE TABLE';`,
|
|
|
291
313
|
afterReplicaId: getUuidReplicaIdentityBson(record, table.replicaIdColumns)
|
|
292
314
|
});
|
|
293
315
|
connection.resume();
|
|
294
|
-
// TODO: These metrics can probably be reported in batches
|
|
295
316
|
Metrics.getInstance().rows_replicated_total.add(1);
|
|
296
317
|
})
|
|
297
318
|
.on('end', async function () {
|
|
@@ -382,19 +403,11 @@ AND table_type = 'BASE TABLE';`,
|
|
|
382
403
|
binLogPositionState.offset = evt.position;
|
|
383
404
|
break;
|
|
384
405
|
case zongji_utils.eventIsWriteMutation(evt):
|
|
385
|
-
// TODO, can multiple tables be present?
|
|
386
406
|
const writeTableInfo = evt.tableMap[evt.tableId];
|
|
387
407
|
await this.writeChanges(batch, {
|
|
388
408
|
type: storage.SaveOperationTag.INSERT,
|
|
389
409
|
data: evt.rows,
|
|
390
|
-
|
|
391
|
-
table: writeTableInfo.tableName,
|
|
392
|
-
sourceTable: this.getTable(
|
|
393
|
-
getMysqlRelId({
|
|
394
|
-
schema: writeTableInfo.parentSchema,
|
|
395
|
-
name: writeTableInfo.tableName
|
|
396
|
-
})
|
|
397
|
-
)
|
|
410
|
+
tableEntry: writeTableInfo
|
|
398
411
|
});
|
|
399
412
|
break;
|
|
400
413
|
case zongji_utils.eventIsUpdateMutation(evt):
|
|
@@ -403,31 +416,15 @@ AND table_type = 'BASE TABLE';`,
|
|
|
403
416
|
type: storage.SaveOperationTag.UPDATE,
|
|
404
417
|
data: evt.rows.map((row) => row.after),
|
|
405
418
|
previous_data: evt.rows.map((row) => row.before),
|
|
406
|
-
|
|
407
|
-
table: updateTableInfo.tableName,
|
|
408
|
-
sourceTable: this.getTable(
|
|
409
|
-
getMysqlRelId({
|
|
410
|
-
schema: updateTableInfo.parentSchema,
|
|
411
|
-
name: updateTableInfo.tableName
|
|
412
|
-
})
|
|
413
|
-
)
|
|
419
|
+
tableEntry: updateTableInfo
|
|
414
420
|
});
|
|
415
421
|
break;
|
|
416
422
|
case zongji_utils.eventIsDeleteMutation(evt):
|
|
417
|
-
// TODO, can multiple tables be present?
|
|
418
423
|
const deleteTableInfo = evt.tableMap[evt.tableId];
|
|
419
424
|
await this.writeChanges(batch, {
|
|
420
425
|
type: storage.SaveOperationTag.DELETE,
|
|
421
426
|
data: evt.rows,
|
|
422
|
-
|
|
423
|
-
table: deleteTableInfo.tableName,
|
|
424
|
-
// TODO cleanup
|
|
425
|
-
sourceTable: this.getTable(
|
|
426
|
-
getMysqlRelId({
|
|
427
|
-
schema: deleteTableInfo.parentSchema,
|
|
428
|
-
name: deleteTableInfo.tableName
|
|
429
|
-
})
|
|
430
|
-
)
|
|
427
|
+
tableEntry: deleteTableInfo
|
|
431
428
|
});
|
|
432
429
|
break;
|
|
433
430
|
case zongji_utils.eventIsXid(evt):
|
|
@@ -503,14 +500,26 @@ AND table_type = 'BASE TABLE';`,
|
|
|
503
500
|
type: storage.SaveOperationTag;
|
|
504
501
|
data: Data[];
|
|
505
502
|
previous_data?: Data[];
|
|
506
|
-
|
|
507
|
-
table: string;
|
|
508
|
-
sourceTable: storage.SourceTable;
|
|
503
|
+
tableEntry: TableMapEntry;
|
|
509
504
|
}
|
|
510
505
|
): Promise<storage.FlushedResult | null> {
|
|
506
|
+
const columns = new Map<string, ColumnDescriptor>();
|
|
507
|
+
msg.tableEntry.columns.forEach((column) => {
|
|
508
|
+
columns.set(column.name, { name: column.name, typeId: column.type });
|
|
509
|
+
});
|
|
510
|
+
|
|
511
511
|
for (const [index, row] of msg.data.entries()) {
|
|
512
512
|
await this.writeChange(batch, {
|
|
513
|
-
|
|
513
|
+
type: msg.type,
|
|
514
|
+
database: msg.tableEntry.parentSchema,
|
|
515
|
+
sourceTable: this.getTable(
|
|
516
|
+
getMysqlRelId({
|
|
517
|
+
schema: msg.tableEntry.parentSchema,
|
|
518
|
+
name: msg.tableEntry.tableName
|
|
519
|
+
})
|
|
520
|
+
),
|
|
521
|
+
table: msg.tableEntry.tableName,
|
|
522
|
+
columns: columns,
|
|
514
523
|
data: row,
|
|
515
524
|
previous_data: msg.previous_data?.[index]
|
|
516
525
|
});
|
|
@@ -525,7 +534,7 @@ AND table_type = 'BASE TABLE';`,
|
|
|
525
534
|
switch (payload.type) {
|
|
526
535
|
case storage.SaveOperationTag.INSERT:
|
|
527
536
|
Metrics.getInstance().rows_replicated_total.add(1);
|
|
528
|
-
const record = common.toSQLiteRow(payload.data);
|
|
537
|
+
const record = common.toSQLiteRow(payload.data, payload.columns);
|
|
529
538
|
return await batch.save({
|
|
530
539
|
tag: storage.SaveOperationTag.INSERT,
|
|
531
540
|
sourceTable: payload.sourceTable,
|
|
@@ -4,6 +4,7 @@ declare module '@powersync/mysql-zongji' {
|
|
|
4
4
|
user: string;
|
|
5
5
|
password: string;
|
|
6
6
|
dateStrings?: boolean;
|
|
7
|
+
timeZone?: string;
|
|
7
8
|
};
|
|
8
9
|
|
|
9
10
|
interface DatabaseFilter {
|
|
@@ -31,6 +32,11 @@ declare module '@powersync/mysql-zongji' {
|
|
|
31
32
|
* BinLog position offset to start reading events from in file specified
|
|
32
33
|
*/
|
|
33
34
|
position?: number;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Unique server ID for this replication client.
|
|
38
|
+
*/
|
|
39
|
+
serverId?: number;
|
|
34
40
|
};
|
|
35
41
|
|
|
36
42
|
export type ColumnSchema = {
|
|
@@ -49,10 +55,10 @@ declare module '@powersync/mysql-zongji' {
|
|
|
49
55
|
};
|
|
50
56
|
|
|
51
57
|
export type TableMapEntry = {
|
|
52
|
-
columnSchemas:
|
|
58
|
+
columnSchemas: ColumnSchema[];
|
|
53
59
|
parentSchema: string;
|
|
54
60
|
tableName: string;
|
|
55
|
-
columns:
|
|
61
|
+
columns: ColumnDefinition[];
|
|
56
62
|
};
|
|
57
63
|
|
|
58
64
|
export type BaseBinLogEvent = {
|
|
@@ -90,14 +96,14 @@ declare module '@powersync/mysql-zongji' {
|
|
|
90
96
|
tableId: number;
|
|
91
97
|
numberOfColumns: number;
|
|
92
98
|
tableMap: Record<string, TableMapEntry>;
|
|
93
|
-
rows:
|
|
99
|
+
rows: Record<string, any>[];
|
|
94
100
|
};
|
|
95
101
|
|
|
96
102
|
export type BinLogUpdateEvent = Omit<BinLogMutationEvent, 'rows'> & {
|
|
97
|
-
rows:
|
|
103
|
+
rows: {
|
|
98
104
|
before: Record<string, any>;
|
|
99
105
|
after: Record<string, any>;
|
|
100
|
-
}
|
|
106
|
+
}[];
|
|
101
107
|
};
|
|
102
108
|
|
|
103
109
|
export type BinLogEvent = BinLogRotationEvent | BinLogGTIDLogEvent | BinLogXidEvent | BinLogMutationEvent;
|
package/src/types/types.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface NormalizedMySQLConnectionConfig {
|
|
|
14
14
|
|
|
15
15
|
username: string;
|
|
16
16
|
password: string;
|
|
17
|
+
server_id: number;
|
|
17
18
|
|
|
18
19
|
cacert?: string;
|
|
19
20
|
client_certificate?: string;
|
|
@@ -29,6 +30,7 @@ export const MySQLConnectionConfig = service_types.configFile.DataSourceConfig.a
|
|
|
29
30
|
username: t.string.optional(),
|
|
30
31
|
password: t.string.optional(),
|
|
31
32
|
database: t.string.optional(),
|
|
33
|
+
server_id: t.number.optional(),
|
|
32
34
|
|
|
33
35
|
cacert: t.string.optional(),
|
|
34
36
|
client_certificate: t.string.optional(),
|
|
@@ -97,6 +99,8 @@ export function normalizeConnectionConfig(options: MySQLConnectionConfig): Norma
|
|
|
97
99
|
database,
|
|
98
100
|
|
|
99
101
|
username,
|
|
100
|
-
password
|
|
102
|
+
password,
|
|
103
|
+
|
|
104
|
+
server_id: options.server_id ?? 1
|
|
101
105
|
};
|
|
102
106
|
}
|
package/src/utils/mysql_utils.ts
CHANGED
|
@@ -3,6 +3,11 @@ import mysql from 'mysql2';
|
|
|
3
3
|
import mysqlPromise from 'mysql2/promise';
|
|
4
4
|
import * as types from '../types/types.js';
|
|
5
5
|
|
|
6
|
+
export const MySQLTypesMap: { [key: number]: string } = {};
|
|
7
|
+
for (const [name, code] of Object.entries(mysql.Types)) {
|
|
8
|
+
MySQLTypesMap[code as number] = name;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
export type RetriedQueryOptions = {
|
|
7
12
|
connection: mysqlPromise.Connection;
|
|
8
13
|
query: string;
|
|
@@ -35,13 +40,14 @@ export function createPool(config: types.NormalizedMySQLConnectionConfig, option
|
|
|
35
40
|
cert: config.client_certificate
|
|
36
41
|
};
|
|
37
42
|
const hasSSLOptions = Object.values(sslOptions).some((v) => !!v);
|
|
38
|
-
// TODO confirm if default options are fine for Powersync use case
|
|
39
43
|
return mysql.createPool({
|
|
40
44
|
host: config.hostname,
|
|
41
45
|
user: config.username,
|
|
42
46
|
password: config.password,
|
|
43
47
|
database: config.database,
|
|
44
48
|
ssl: hasSSLOptions ? sslOptions : undefined,
|
|
49
|
+
supportBigNumbers: true,
|
|
50
|
+
timezone: 'Z', // Ensure no auto timezone manipulation of the dates occur
|
|
45
51
|
...(options || {})
|
|
46
52
|
});
|
|
47
53
|
}
|