@powersync/service-module-mysql 0.0.0-dev-20241219145106 → 0.0.0-dev-20250102111825
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 +18 -6
- package/dist/common/mysql-to-sqlite.js +39 -9
- package/dist/common/mysql-to-sqlite.js.map +1 -1
- package/dist/replication/BinLogStream.js +36 -32
- package/dist/replication/BinLogStream.js.map +1 -1
- package/dist/replication/MySQLConnectionManager.js +9 -1
- package/dist/replication/MySQLConnectionManager.js.map +1 -1
- package/dist/utils/mysql-utils.d.ts +2 -0
- package/dist/utils/mysql-utils.js +3 -0
- package/dist/utils/mysql-utils.js.map +1 -1
- package/package.json +7 -7
- package/src/common/mysql-to-sqlite.ts +35 -9
- package/src/replication/BinLogStream.ts +43 -33
- package/src/replication/MySQLConnectionManager.ts +8 -1
- package/src/utils/mysql-utils.ts +5 -0
- package/test/src/BinLogStream.test.ts +253 -226
- package/test/src/BinlogStreamUtils.ts +41 -23
- package/test/src/mysql-to-sqlite.test.ts +36 -19
- package/test/src/util.ts +6 -3
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -11,7 +11,7 @@ import * as zongji_utils from './zongji/zongji-utils.js';
|
|
|
11
11
|
import { MySQLConnectionManager } from './MySQLConnectionManager.js';
|
|
12
12
|
import { isBinlogStillAvailable, ReplicatedGTID, toColumnDescriptors } from '../common/common-index.js';
|
|
13
13
|
import mysqlPromise from 'mysql2/promise';
|
|
14
|
-
import { createRandomServerId } from '../utils/mysql-utils.js';
|
|
14
|
+
import { createRandomServerId, escapeMysqlTableName } from '../utils/mysql-utils.js';
|
|
15
15
|
|
|
16
16
|
export interface BinLogStreamOptions {
|
|
17
17
|
connections: MySQLConnectionManager;
|
|
@@ -114,8 +114,10 @@ export class BinLogStream {
|
|
|
114
114
|
// Start the snapshot inside a transaction.
|
|
115
115
|
// We use a dedicated connection for this.
|
|
116
116
|
const connection = await this.connections.getStreamingConnection();
|
|
117
|
+
|
|
117
118
|
const promiseConnection = (connection as mysql.Connection).promise();
|
|
118
119
|
try {
|
|
120
|
+
await promiseConnection.query(`SET time_zone = '+00:00'`);
|
|
119
121
|
await promiseConnection.query('BEGIN');
|
|
120
122
|
try {
|
|
121
123
|
gtid = await common.readExecutedGtid(promiseConnection);
|
|
@@ -258,6 +260,8 @@ AND table_type = 'BASE TABLE';`,
|
|
|
258
260
|
'SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY'
|
|
259
261
|
);
|
|
260
262
|
await promiseConnection.query<mysqlPromise.RowDataPacket[]>('START TRANSACTION');
|
|
263
|
+
await promiseConnection.query(`SET time_zone = '+00:00'`);
|
|
264
|
+
|
|
261
265
|
const sourceTables = this.syncRules.getSourceTables();
|
|
262
266
|
await this.storage.startBatch(
|
|
263
267
|
{ zeroLSN: ReplicatedGTID.ZERO.comparable, defaultSchema: this.defaultSchema, storeCurrentData: true },
|
|
@@ -291,38 +295,32 @@ AND table_type = 'BASE TABLE';`,
|
|
|
291
295
|
logger.info(`Replicating ${table.qualifiedName}`);
|
|
292
296
|
// TODO count rows and log progress at certain batch sizes
|
|
293
297
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
.on('fields', (fields: FieldPacket[]) => {
|
|
303
|
-
// Map the columns and their types
|
|
304
|
-
columns = toColumnDescriptors(fields);
|
|
305
|
-
})
|
|
306
|
-
.on('result', async (row) => {
|
|
307
|
-
connection.pause();
|
|
308
|
-
const record = common.toSQLiteRow(row, columns);
|
|
309
|
-
|
|
310
|
-
await batch.save({
|
|
311
|
-
tag: storage.SaveOperationTag.INSERT,
|
|
312
|
-
sourceTable: table,
|
|
313
|
-
before: undefined,
|
|
314
|
-
beforeReplicaId: undefined,
|
|
315
|
-
after: record,
|
|
316
|
-
afterReplicaId: getUuidReplicaIdentityBson(record, table.replicaIdColumns)
|
|
317
|
-
});
|
|
318
|
-
connection.resume();
|
|
319
|
-
Metrics.getInstance().rows_replicated_total.add(1);
|
|
320
|
-
})
|
|
321
|
-
.on('end', async function () {
|
|
322
|
-
await batch.flush();
|
|
323
|
-
resolve();
|
|
324
|
-
});
|
|
298
|
+
// MAX_EXECUTION_TIME(0) hint disables execution timeout for this query
|
|
299
|
+
const query = connection.query(`SELECT /*+ MAX_EXECUTION_TIME(0) */ * FROM ${escapeMysqlTableName(table)}`);
|
|
300
|
+
const stream = query.stream();
|
|
301
|
+
|
|
302
|
+
let columns: Map<string, ColumnDescriptor> | undefined = undefined;
|
|
303
|
+
stream.on('fields', (fields: FieldPacket[]) => {
|
|
304
|
+
// Map the columns and their types
|
|
305
|
+
columns = toColumnDescriptors(fields);
|
|
325
306
|
});
|
|
307
|
+
|
|
308
|
+
for await (let row of query.stream()) {
|
|
309
|
+
if (columns == null) {
|
|
310
|
+
throw new Error(`No 'fields' event emitted`);
|
|
311
|
+
}
|
|
312
|
+
const record = common.toSQLiteRow(row, columns!);
|
|
313
|
+
|
|
314
|
+
await batch.save({
|
|
315
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
316
|
+
sourceTable: table,
|
|
317
|
+
before: undefined,
|
|
318
|
+
beforeReplicaId: undefined,
|
|
319
|
+
after: record,
|
|
320
|
+
afterReplicaId: getUuidReplicaIdentityBson(record, table.replicaIdColumns)
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
await batch.flush();
|
|
326
324
|
}
|
|
327
325
|
|
|
328
326
|
async replicate() {
|
|
@@ -350,6 +348,18 @@ AND table_type = 'BASE TABLE';`,
|
|
|
350
348
|
const initialReplicationCompleted = await this.checkInitialReplicated();
|
|
351
349
|
if (!initialReplicationCompleted) {
|
|
352
350
|
await this.startInitialReplication();
|
|
351
|
+
} else {
|
|
352
|
+
// We need to find the existing tables, to populate our table cache.
|
|
353
|
+
// This is needed for includeSchema to work correctly.
|
|
354
|
+
const sourceTables = this.syncRules.getSourceTables();
|
|
355
|
+
await this.storage.startBatch(
|
|
356
|
+
{ zeroLSN: ReplicatedGTID.ZERO.comparable, defaultSchema: this.defaultSchema, storeCurrentData: true },
|
|
357
|
+
async (batch) => {
|
|
358
|
+
for (let tablePattern of sourceTables) {
|
|
359
|
+
await this.getQualifiedTableNames(batch, tablePattern);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
);
|
|
353
363
|
}
|
|
354
364
|
}
|
|
355
365
|
|
|
@@ -578,7 +588,7 @@ AND table_type = 'BASE TABLE';`,
|
|
|
578
588
|
beforeReplicaId: beforeUpdated
|
|
579
589
|
? getUuidReplicaIdentityBson(beforeUpdated, payload.sourceTable.replicaIdColumns)
|
|
580
590
|
: undefined,
|
|
581
|
-
after:
|
|
591
|
+
after: after,
|
|
582
592
|
afterReplicaId: getUuidReplicaIdentityBson(after, payload.sourceTable.replicaIdColumns)
|
|
583
593
|
});
|
|
584
594
|
|
|
@@ -62,7 +62,14 @@ export class MySQLConnectionManager {
|
|
|
62
62
|
* @param params
|
|
63
63
|
*/
|
|
64
64
|
async query(query: string, params?: any[]): Promise<[RowDataPacket[], FieldPacket[]]> {
|
|
65
|
-
|
|
65
|
+
let connection: mysqlPromise.PoolConnection | undefined;
|
|
66
|
+
try {
|
|
67
|
+
connection = await this.promisePool.getConnection();
|
|
68
|
+
await connection.query(`SET time_zone = '+00:00'`);
|
|
69
|
+
return connection.query<RowDataPacket[]>(query, params);
|
|
70
|
+
} finally {
|
|
71
|
+
connection?.release();
|
|
72
|
+
}
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
/**
|
package/src/utils/mysql-utils.ts
CHANGED
|
@@ -3,6 +3,7 @@ import mysql from 'mysql2';
|
|
|
3
3
|
import mysqlPromise from 'mysql2/promise';
|
|
4
4
|
import * as types from '../types/types.js';
|
|
5
5
|
import { coerce, gte } from 'semver';
|
|
6
|
+
import { SourceTable } from '@powersync/service-core';
|
|
6
7
|
|
|
7
8
|
export type RetriedQueryOptions = {
|
|
8
9
|
connection: mysqlPromise.Connection;
|
|
@@ -82,3 +83,7 @@ export function isVersionAtLeast(version: string, minimumVersion: string): boole
|
|
|
82
83
|
|
|
83
84
|
return gte(coercedVersion!, coercedMinimumVersion!, { loose: true });
|
|
84
85
|
}
|
|
86
|
+
|
|
87
|
+
export function escapeMysqlTableName(table: SourceTable): string {
|
|
88
|
+
return `\`${table.schema.replaceAll('`', '``')}\`.\`${table.table.replaceAll('`', '``')}\``;
|
|
89
|
+
}
|