@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.
@@ -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
- let columns: Map<string, ColumnDescriptor>;
295
- return new Promise<void>((resolve, reject) => {
296
- // MAX_EXECUTION_TIME(0) hint disables execution timeout for this query
297
- connection
298
- .query(`SELECT /*+ MAX_EXECUTION_TIME(0) */ * FROM ${table.schema}.${table.table}`)
299
- .on('error', (err) => {
300
- reject(err);
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: common.toSQLiteRow(payload.data, payload.columns),
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
- return this.promisePool.query<RowDataPacket[]>(query, params);
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
  /**
@@ -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
+ }