@powersync/service-module-mysql 0.0.0-dev-20260225160713 → 0.0.0-dev-20260511080634

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.
Files changed (65) hide show
  1. package/CHANGELOG.md +104 -5
  2. package/dist/api/MySQLRouteAPIAdapter.js +1 -1
  3. package/dist/api/MySQLRouteAPIAdapter.js.map +1 -1
  4. package/dist/common/check-source-configuration.js +12 -1
  5. package/dist/common/check-source-configuration.js.map +1 -1
  6. package/dist/common/common-index.d.ts +1 -1
  7. package/dist/common/common-index.js +1 -1
  8. package/dist/common/common-index.js.map +1 -1
  9. package/dist/common/mysql-to-sqlite.d.ts +2 -2
  10. package/dist/common/mysql-to-sqlite.js +1 -1
  11. package/dist/common/mysql-to-sqlite.js.map +1 -1
  12. package/dist/common/schema-utils.d.ts +1 -1
  13. package/dist/common/schema-utils.js.map +1 -1
  14. package/dist/module/MySQLModule.js +3 -3
  15. package/dist/module/MySQLModule.js.map +1 -1
  16. package/dist/replication/BinLogReplicationJob.d.ts +1 -1
  17. package/dist/replication/BinLogReplicationJob.js +3 -3
  18. package/dist/replication/BinLogReplicationJob.js.map +1 -1
  19. package/dist/replication/BinLogReplicator.d.ts +0 -1
  20. package/dist/replication/BinLogReplicator.js +1 -23
  21. package/dist/replication/BinLogReplicator.js.map +1 -1
  22. package/dist/replication/BinLogStream.d.ts +3 -11
  23. package/dist/replication/BinLogStream.js +30 -45
  24. package/dist/replication/BinLogStream.js.map +1 -1
  25. package/dist/replication/MySQLConnectionManager.d.ts +3 -3
  26. package/dist/replication/MySQLConnectionManager.js +1 -1
  27. package/dist/replication/MySQLConnectionManager.js.map +1 -1
  28. package/dist/replication/MySQLConnectionManagerFactory.d.ts +1 -1
  29. package/dist/replication/MySQLConnectionManagerFactory.js.map +1 -1
  30. package/dist/replication/zongji/BinLogListener.d.ts +4 -4
  31. package/dist/replication/zongji/BinLogListener.js +7 -7
  32. package/dist/replication/zongji/BinLogListener.js.map +1 -1
  33. package/dist/replication/zongji/zongji-utils.d.ts +1 -1
  34. package/dist/utils/mysql-utils.d.ts +2 -2
  35. package/dist/utils/mysql-utils.js +1 -1
  36. package/dist/utils/mysql-utils.js.map +1 -1
  37. package/dist/utils/parser-utils.d.ts +1 -1
  38. package/package.json +10 -10
  39. package/src/api/MySQLRouteAPIAdapter.ts +2 -2
  40. package/src/common/check-source-configuration.ts +16 -1
  41. package/src/common/common-index.ts +1 -1
  42. package/src/common/mysql-to-sqlite.ts +3 -3
  43. package/src/common/schema-utils.ts +2 -2
  44. package/src/module/MySQLModule.ts +3 -3
  45. package/src/replication/BinLogReplicationJob.ts +3 -3
  46. package/src/replication/BinLogReplicator.ts +1 -26
  47. package/src/replication/BinLogStream.ts +35 -44
  48. package/src/replication/MySQLConnectionManager.ts +4 -4
  49. package/src/replication/MySQLConnectionManagerFactory.ts +1 -1
  50. package/src/replication/zongji/BinLogListener.ts +10 -10
  51. package/src/replication/zongji/zongji-utils.ts +6 -6
  52. package/src/utils/mysql-utils.ts +3 -3
  53. package/src/utils/parser-utils.ts +1 -1
  54. package/test/src/BinLogListener.test.ts +37 -26
  55. package/test/src/BinLogStream.test.ts +4 -2
  56. package/test/src/BinlogStreamUtils.ts +20 -7
  57. package/test/src/config.test.ts +1 -1
  58. package/test/src/env.ts +1 -1
  59. package/test/src/mysql-to-sqlite.test.ts +5 -6
  60. package/test/src/mysql-utils.test.ts +1 -1
  61. package/test/src/parser-utils.test.ts +1 -1
  62. package/test/src/schema-changes.test.ts +6 -5
  63. package/test/src/util.ts +9 -9
  64. package/test/tsconfig.json +0 -1
  65. package/tsconfig.tsbuildinfo +1 -1
@@ -1,9 +1,9 @@
1
+ import { ColumnDefinition, TableMapEntry } from '@powersync/mysql-zongji';
2
+ import { ColumnDescriptor } from '@powersync/service-core';
3
+ import { JSONBig, JsonContainer } from '@powersync/service-jsonbig';
1
4
  import * as sync_rules from '@powersync/service-sync-rules';
2
5
  import { ExpressionType } from '@powersync/service-sync-rules';
3
- import { ColumnDescriptor } from '@powersync/service-core';
4
6
  import mysql from 'mysql2';
5
- import { JSONBig, JsonContainer } from '@powersync/service-jsonbig';
6
- import { ColumnDefinition, TableMapEntry } from '@powersync/mysql-zongji';
7
7
 
8
8
  export enum ADDITIONAL_MYSQL_TYPES {
9
9
  DATETIME2 = 18,
@@ -1,7 +1,7 @@
1
- import mysqlPromise from 'mysql2/promise';
2
- import * as mysql_utils from '../utils/mysql-utils.js';
3
1
  import { ColumnDescriptor } from '@powersync/service-core';
4
2
  import { TablePattern } from '@powersync/service-sync-rules';
3
+ import mysqlPromise from 'mysql2/promise';
4
+ import * as mysql_utils from '../utils/mysql-utils.js';
5
5
 
6
6
  export interface GetColumnsOptions {
7
7
  connection: mysqlPromise.Connection;
@@ -8,13 +8,13 @@ import {
8
8
  } from '@powersync/service-core';
9
9
 
10
10
  import { MySQLRouteAPIAdapter } from '../api/MySQLRouteAPIAdapter.js';
11
+ import { checkSourceConfiguration } from '../common/check-source-configuration.js';
11
12
  import { BinLogReplicator } from '../replication/BinLogReplicator.js';
13
+ import { MySQLConnectionManager } from '../replication/MySQLConnectionManager.js';
14
+ import { MySQLConnectionManagerFactory } from '../replication/MySQLConnectionManagerFactory.js';
12
15
  import { MySQLErrorRateLimiter } from '../replication/MySQLErrorRateLimiter.js';
13
16
  import * as types from '../types/types.js';
14
- import { MySQLConnectionManagerFactory } from '../replication/MySQLConnectionManagerFactory.js';
15
17
  import { MySQLConnectionConfig } from '../types/types.js';
16
- import { checkSourceConfiguration } from '../common/check-source-configuration.js';
17
- import { MySQLConnectionManager } from '../replication/MySQLConnectionManager.js';
18
18
 
19
19
  export class MySQLModule extends replication.ReplicationModule<types.MySQLConnectionConfig> {
20
20
  constructor() {
@@ -1,4 +1,4 @@
1
- import { container, logger as defaultLogger } from '@powersync/lib-services-framework';
1
+ import { container } from '@powersync/lib-services-framework';
2
2
  import { POWERSYNC_VERSION, replication } from '@powersync/service-core';
3
3
  import { BinLogStream } from './BinLogStream.js';
4
4
  import { MySQLConnectionManagerFactory } from './MySQLConnectionManagerFactory.js';
@@ -13,7 +13,7 @@ export class BinLogReplicationJob extends replication.AbstractReplicationJob {
13
13
 
14
14
  constructor(options: BinLogReplicationJobOptions) {
15
15
  super(options);
16
- this.logger = defaultLogger.child({ prefix: `[powersync_${this.options.storage.group_id}] ` });
16
+ this.logger = options.storage.logger;
17
17
  this.connectionFactory = options.connectionFactory;
18
18
  }
19
19
 
@@ -88,7 +88,7 @@ export class BinLogReplicationJob extends replication.AbstractReplicationJob {
88
88
  }
89
89
  }
90
90
 
91
- async getReplicationLagMillis(): Promise<number | undefined> {
91
+ getReplicationLagMillis(): number | undefined {
92
92
  return this.lastStream?.getReplicationLagMillis();
93
93
  }
94
94
  }
@@ -1,7 +1,7 @@
1
1
  import { replication, storage } from '@powersync/service-core';
2
+ import { MySQLModule } from '../module/MySQLModule.js';
2
3
  import { BinLogReplicationJob } from './BinLogReplicationJob.js';
3
4
  import { MySQLConnectionManagerFactory } from './MySQLConnectionManagerFactory.js';
4
- import { MySQLModule } from '../module/MySQLModule.js';
5
5
 
6
6
  export interface BinLogReplicatorOptions extends replication.AbstractReplicatorOptions {
7
7
  connectionFactory: MySQLConnectionManagerFactory;
@@ -38,29 +38,4 @@ export class BinLogReplicator extends replication.AbstractReplicator<BinLogRepli
38
38
  async testConnection() {
39
39
  return await MySQLModule.testConnection(this.connectionFactory.connectionConfig);
40
40
  }
41
-
42
- async getReplicationLagMillis(): Promise<number | undefined> {
43
- const lag = await super.getReplicationLagMillis();
44
- if (lag != null) {
45
- return lag;
46
- }
47
-
48
- // Booting or in an error loop. Check last active replication status.
49
- // This includes sync rules in an ERROR state.
50
- const content = await this.storage.getActiveSyncRulesContent();
51
- if (content == null) {
52
- return undefined;
53
- }
54
- // Measure the lag from the last commit or keepalive timestamp.
55
- // This is not 100% accurate since it is the commit time in the storage db rather than
56
- // the source db, but it's the best we currently have for mysql.
57
- const checkpointTs = content.last_checkpoint_ts?.getTime() ?? 0;
58
- const keepaliveTs = content.last_keepalive_ts?.getTime() ?? 0;
59
- const latestTs = Math.max(checkpointTs, keepaliveTs);
60
- if (latestTs != 0) {
61
- return Date.now() - latestTs;
62
- }
63
-
64
- return undefined;
65
- }
66
41
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
- Logger,
3
2
  logger as defaultLogger,
3
+ Logger,
4
4
  ReplicationAbortedError,
5
5
  ReplicationAssertionError
6
6
  } from '@powersync/lib-services-framework';
@@ -12,6 +12,7 @@ import {
12
12
  getUuidReplicaIdentityBson,
13
13
  InternalOpId,
14
14
  MetricsEngine,
15
+ ReplicationLagTracker,
15
16
  SourceTable,
16
17
  storage
17
18
  } from '@powersync/service-core';
@@ -19,10 +20,10 @@ import mysql from 'mysql2';
19
20
  import mysqlPromise from 'mysql2/promise';
20
21
 
21
22
  import { TableMapEntry } from '@powersync/mysql-zongji';
23
+ import { ReplicationMetric } from '@powersync/service-types';
22
24
  import * as common from '../common/common-index.js';
23
25
  import { createRandomServerId, qualifiedMySQLTable } from '../utils/mysql-utils.js';
24
26
  import { MySQLConnectionManager } from './MySQLConnectionManager.js';
25
- import { ReplicationMetric } from '@powersync/service-types';
26
27
  import { BinLogEventHandler, BinLogListener, Row, SchemaChange, SchemaChangeType } from './zongji/BinLogListener.js';
27
28
 
28
29
  export interface BinLogStreamOptions {
@@ -74,16 +75,7 @@ export class BinLogStream {
74
75
 
75
76
  private tableCache = new Map<string | number, storage.SourceTable>();
76
77
 
77
- /**
78
- * Time of the oldest uncommitted change, according to the source db.
79
- * This is used to determine the replication lag.
80
- */
81
- private oldestUncommittedChange: Date | null = null;
82
- /**
83
- * Keep track of whether we have done a commit or keepalive yet.
84
- * We can only compute replication lag if isStartingReplication == false, or oldestUncommittedChange is present.
85
- */
86
- isStartingReplication = true;
78
+ private replicationLag = new ReplicationLagTracker();
87
79
 
88
80
  constructor(private options: BinLogStreamOptions) {
89
81
  this.logger = options.logger ?? defaultLogger;
@@ -122,6 +114,10 @@ export class BinLogStream {
122
114
  return this.abortSignal.aborted;
123
115
  }
124
116
 
117
+ get isStartingReplication() {
118
+ return this.replicationLag.isStartingReplication;
119
+ }
120
+
125
121
  get defaultSchema() {
126
122
  return this.connections.databaseName;
127
123
  }
@@ -143,7 +139,7 @@ export class BinLogStream {
143
139
  // Snapshot if:
144
140
  // 1. Snapshot is requested (false for initial snapshot, since that process handles it elsewhere)
145
141
  // 2. Snapshot is not done yet, AND:
146
- // 3. The table is used in sync rules.
142
+ // 3. The table is used in sync config.
147
143
  const shouldSnapshot = snapshot && !result.table.snapshotComplete && result.table.syncAny;
148
144
 
149
145
  if (shouldSnapshot) {
@@ -170,7 +166,7 @@ export class BinLogStream {
170
166
  } finally {
171
167
  connection.release();
172
168
  }
173
- const [table] = await batch.markSnapshotDone([result.table], gtid.comparable);
169
+ const [table] = await batch.markTableSnapshotDone([result.table], gtid.comparable);
174
170
  return table;
175
171
  }
176
172
 
@@ -268,17 +264,19 @@ export class BinLogStream {
268
264
  logger: this.logger,
269
265
  zeroLSN: common.ReplicatedGTID.ZERO.comparable,
270
266
  defaultSchema: this.defaultSchema,
271
- storeCurrentData: true
267
+ storeCurrentData: false
272
268
  },
273
269
  async (batch) => {
274
270
  for (let tablePattern of sourceTables) {
275
271
  const tables = await this.getQualifiedTableNames(batch, tablePattern);
276
272
  for (let table of tables) {
277
273
  await this.snapshotTable(connection as mysql.Connection, batch, table);
278
- await batch.markSnapshotDone([table], headGTID.comparable);
274
+ await batch.markTableSnapshotDone([table], headGTID.comparable);
279
275
  await framework.container.probes.touch();
280
276
  }
281
277
  }
278
+ const snapshotDoneGtid = await common.readExecutedGtid(promiseConnection);
279
+ await batch.markAllSnapshotDone(snapshotDoneGtid.comparable);
282
280
  await batch.commit(headGTID.comparable);
283
281
  }
284
282
  );
@@ -293,7 +291,7 @@ export class BinLogStream {
293
291
  }
294
292
 
295
293
  if (lastOp != null) {
296
- // Populate the cache _after_ initial replication, but _before_ we switch to this sync rules.
294
+ // Populate the cache _after_ initial replication, but _before_ we switch to this replication stream.
297
295
  await this.storage.populatePersistentChecksumCache({
298
296
  // No checkpoint yet, but we do have the opId.
299
297
  maxOpId: lastOp,
@@ -322,7 +320,10 @@ export class BinLogStream {
322
320
 
323
321
  for await (let row of stream) {
324
322
  if (this.stopped) {
325
- throw new ReplicationAbortedError('Abort signal received - initial replication interrupted.');
323
+ throw new ReplicationAbortedError(
324
+ 'Abort signal received - initial replication interrupted.',
325
+ this.abortSignal.reason
326
+ );
326
327
  }
327
328
 
328
329
  if (columns == null) {
@@ -378,7 +379,7 @@ export class BinLogStream {
378
379
  logger: this.logger,
379
380
  zeroLSN: common.ReplicatedGTID.ZERO.comparable,
380
381
  defaultSchema: this.defaultSchema,
381
- storeCurrentData: true
382
+ storeCurrentData: false
382
383
  },
383
384
  async (batch) => {
384
385
  for (let tablePattern of sourceTables) {
@@ -414,7 +415,7 @@ export class BinLogStream {
414
415
 
415
416
  if (!this.stopped) {
416
417
  await this.storage.startBatch(
417
- { zeroLSN: common.ReplicatedGTID.ZERO.comparable, defaultSchema: this.defaultSchema, storeCurrentData: true },
418
+ { zeroLSN: common.ReplicatedGTID.ZERO.comparable, defaultSchema: this.defaultSchema, storeCurrentData: false },
418
419
  async (batch) => {
419
420
  const binlogEventHandler = this.createBinlogEventHandler(batch);
420
421
  const binlogListener = new BinLogListener({
@@ -467,26 +468,25 @@ export class BinLogStream {
467
468
  });
468
469
  },
469
470
  onKeepAlive: async (lsn: string) => {
470
- const didCommit = await batch.keepalive(lsn);
471
- if (didCommit) {
472
- this.oldestUncommittedChange = null;
471
+ const { checkpointBlocked } = await batch.keepalive(lsn);
472
+ if (!checkpointBlocked) {
473
+ this.replicationLag.clearUncommittedChange();
473
474
  }
474
475
  },
475
476
  onCommit: async (lsn: string) => {
476
477
  this.metrics.getCounter(ReplicationMetric.TRANSACTIONS_REPLICATED).add(1);
477
- const didCommit = await batch.commit(lsn, { oldestUncommittedChange: this.oldestUncommittedChange });
478
- if (didCommit) {
479
- this.oldestUncommittedChange = null;
480
- this.isStartingReplication = false;
478
+ const { checkpointBlocked } = await batch.commit(lsn, {
479
+ oldestUncommittedChange: this.replicationLag.oldestUncommittedChange
480
+ });
481
+ if (!checkpointBlocked) {
482
+ this.replicationLag.markCommitted();
481
483
  }
482
484
  },
483
485
  onTransactionStart: async (options) => {
484
- if (this.oldestUncommittedChange == null) {
485
- this.oldestUncommittedChange = options.timestamp;
486
- }
486
+ this.replicationLag.trackUncommittedChange(options.timestamp);
487
487
  },
488
488
  onRotate: async () => {
489
- this.isStartingReplication = false;
489
+ this.replicationLag.markStarted();
490
490
  },
491
491
  onSchemaChange: async (change: SchemaChange) => {
492
492
  await this.handleSchemaChange(batch, change);
@@ -504,7 +504,7 @@ export class BinLogStream {
504
504
  await batch.drop([fromTable]);
505
505
  this.tableCache.delete(fromTableId);
506
506
  }
507
- // The new table matched a table in the sync rules
507
+ // The new table matched a table in the sync config
508
508
  if (change.newTable) {
509
509
  await this.handleCreateOrUpdateTable(batch, change.newTable!, change.schema);
510
510
  }
@@ -577,7 +577,7 @@ export class BinLogStream {
577
577
 
578
578
  let table = this.tableCache.get(tableId);
579
579
  if (table == null) {
580
- // This is an insert for a new table that matches a table in the sync rules
580
+ // This is an insert for a new table that matches a table in the sync config
581
581
  // We need to create the table in the storage and cache it.
582
582
  table = await this.handleCreateOrUpdateTable(batch, msg.tableEntry.tableName, msg.tableEntry.parentSchema);
583
583
  }
@@ -654,17 +654,8 @@ export class BinLogStream {
654
654
  }
655
655
  }
656
656
 
657
- async getReplicationLagMillis(): Promise<number | undefined> {
658
- if (this.oldestUncommittedChange == null) {
659
- if (this.isStartingReplication) {
660
- // We don't have anything to compute replication lag with yet.
661
- return undefined;
662
- } else {
663
- // We don't have any uncommitted changes, so replication is up to date.
664
- return 0;
665
- }
666
- }
667
- return Date.now() - this.oldestUncommittedChange.getTime();
657
+ getReplicationLagMillis(): number | undefined {
658
+ return this.replicationLag.getLagMillis();
668
659
  }
669
660
 
670
661
  async tryRollback(promiseConnection: mysqlPromise.Connection) {
@@ -1,9 +1,9 @@
1
- import { NormalizedMySQLConnectionConfig } from '../types/types.js';
2
- import mysqlPromise from 'mysql2/promise';
3
- import mysql, { FieldPacket, RowDataPacket } from 'mysql2';
4
- import * as mysql_utils from '../utils/mysql-utils.js';
5
1
  import { BaseObserver, logger } from '@powersync/lib-services-framework';
6
2
  import { ZongJi } from '@powersync/mysql-zongji';
3
+ import mysql, { FieldPacket, RowDataPacket } from 'mysql2';
4
+ import mysqlPromise from 'mysql2/promise';
5
+ import { NormalizedMySQLConnectionConfig } from '../types/types.js';
6
+ import * as mysql_utils from '../utils/mysql-utils.js';
7
7
 
8
8
  export interface MySQLConnectionManagerListener {
9
9
  onEnded(): void;
@@ -1,7 +1,7 @@
1
1
  import { logger } from '@powersync/lib-services-framework';
2
2
  import mysql from 'mysql2/promise';
3
- import { MySQLConnectionManager } from './MySQLConnectionManager.js';
4
3
  import { ResolvedConnectionConfig } from '../types/types.js';
4
+ import { MySQLConnectionManager } from './MySQLConnectionManager.js';
5
5
 
6
6
  export class MySQLConnectionManagerFactory {
7
7
  private readonly connectionManagers = new Set<MySQLConnectionManager>();
@@ -1,10 +1,7 @@
1
- import * as common from '../../common/common-index.js';
2
- import async from 'async';
3
- import { BinLogEvent, BinLogQueryEvent, StartOptions, TableMapEntry, ZongJi } from '@powersync/mysql-zongji';
4
- import * as zongji_utils from './zongji-utils.js';
5
1
  import { Logger, logger as defaultLogger } from '@powersync/lib-services-framework';
6
- import { MySQLConnectionManager } from '../MySQLConnectionManager.js';
7
- import timers from 'timers/promises';
2
+ import { BinLogEvent, BinLogQueryEvent, StartOptions, TableMapEntry, ZongJi } from '@powersync/mysql-zongji';
3
+ import { TablePattern } from '@powersync/service-sync-rules';
4
+ import async from 'async';
8
5
  import pkg, {
9
6
  AST,
10
7
  BaseFrom,
@@ -13,6 +10,8 @@ import pkg, {
13
10
  RenameStatement,
14
11
  TruncateStatement
15
12
  } from 'node-sql-parser';
13
+ import timers from 'timers/promises';
14
+ import * as common from '../../common/common-index.js';
16
15
  import {
17
16
  isAlterTable,
18
17
  isColumnExpression,
@@ -25,7 +24,8 @@ import {
25
24
  isTruncate,
26
25
  matchedSchemaChangeQuery
27
26
  } from '../../utils/parser-utils.js';
28
- import { TablePattern } from '@powersync/service-sync-rules';
27
+ import { MySQLConnectionManager } from '../MySQLConnectionManager.js';
28
+ import * as zongji_utils from './zongji-utils.js';
29
29
 
30
30
  const { Parser } = pkg;
31
31
 
@@ -445,8 +445,8 @@ export class BinLogListener {
445
445
  } catch (error) {
446
446
  if (matchedSchemaChangeQuery(query, Object.values(this.databaseFilter))) {
447
447
  this.logger.warn(
448
- `Failed to parse query: [${query}].
449
- Please review for the schema changes and manually redeploy the sync rules if required.`
448
+ `Failed to parse query: [${query}].
449
+ Please review for the schema changes and manually redeploy the sync config if required.`
450
450
  );
451
451
  }
452
452
  return [];
@@ -537,7 +537,7 @@ export class BinLogListener {
537
537
  }
538
538
 
539
539
  private createDatabaseFilter(sourceTables: TablePattern[]): { [schema: string]: (table: string) => boolean } {
540
- // Group sync rule tables by schema
540
+ // Group sync config tables by schema
541
541
  const schemaMap = new Map<string, TablePattern[]>();
542
542
  for (const table of sourceTables) {
543
543
  if (!schemaMap.has(table.schema)) {
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  BinLogEvent,
3
3
  BinLogGTIDLogEvent,
4
- BinLogRowEvent,
4
+ BinLogHeartbeatEvent,
5
+ BinLogHeartbeatEvent_V2,
6
+ BinLogQueryEvent,
5
7
  BinLogRotationEvent,
6
- BinLogTableMapEvent,
8
+ BinLogRowEvent,
7
9
  BinLogRowUpdateEvent,
8
- BinLogXidEvent,
9
- BinLogQueryEvent,
10
- BinLogHeartbeatEvent,
11
- BinLogHeartbeatEvent_V2
10
+ BinLogTableMapEvent,
11
+ BinLogXidEvent
12
12
  } from '@powersync/mysql-zongji';
13
13
 
14
14
  export function eventIsGTIDLog(event: BinLogEvent): event is BinLogGTIDLogEvent {
@@ -1,9 +1,9 @@
1
1
  import { logger } from '@powersync/lib-services-framework';
2
+ import { SourceEntityDescriptor } from '@powersync/service-core';
2
3
  import mysql from 'mysql2';
3
4
  import mysqlPromise from 'mysql2/promise';
4
- import * as types from '../types/types.js';
5
5
  import { coerce, gte, satisfies } from 'semver';
6
- import { SourceEntityDescriptor } from '@powersync/service-core';
6
+ import * as types from '../types/types.js';
7
7
 
8
8
  export type RetriedQueryOptions = {
9
9
  connection: mysqlPromise.Connection;
@@ -63,7 +63,7 @@ export function createPool(config: types.NormalizedMySQLConnectionConfig, option
63
63
  }
64
64
 
65
65
  /**
66
- * Return a random server id for a given sync rule id.
66
+ * Return a random server id for a given replication stream id.
67
67
  * Expected format is: <syncRuleId>00<random number>
68
68
  * The max value for server id in MySQL is 2^32 - 1.
69
69
  * We use the GTID format to keep track of our position in the binlog, no state is kept by the MySQL server, therefore
@@ -1,4 +1,4 @@
1
- import { Alter, AST, Create, Drop, TruncateStatement, RenameStatement, DropIndexStatement } from 'node-sql-parser';
1
+ import { Alter, AST, Create, Drop, DropIndexStatement, RenameStatement, TruncateStatement } from 'node-sql-parser';
2
2
 
3
3
  // We ignore create table statements, since even in the worst case we will pick up the changes when row events for that
4
4
  // table are received.
@@ -1,6 +1,10 @@
1
- import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
2
- import { BinLogListener, SchemaChange, SchemaChangeType } from '@module/replication/zongji/BinLogListener.js';
3
1
  import { MySQLConnectionManager } from '@module/replication/MySQLConnectionManager.js';
2
+ import { BinLogListener, SchemaChange, SchemaChangeType } from '@module/replication/zongji/BinLogListener.js';
3
+ import { getMySQLVersion, qualifiedMySQLTable, satisfiesVersion } from '@module/utils/mysql-utils.js';
4
+ import { TablePattern } from '@powersync/service-sync-rules';
5
+ import crypto from 'crypto';
6
+ import { v4 as uuid } from 'uuid';
7
+ import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
4
8
  import {
5
9
  clearTestDb,
6
10
  createBinlogListener,
@@ -8,12 +12,8 @@ import {
8
12
  TEST_CONNECTION_OPTIONS,
9
13
  TestBinLogEventHandler
10
14
  } from './util.js';
11
- import { v4 as uuid } from 'uuid';
12
- import { getMySQLVersion, qualifiedMySQLTable, satisfiesVersion } from '@module/utils/mysql-utils.js';
13
- import crypto from 'crypto';
14
- import { TablePattern } from '@powersync/service-sync-rules';
15
15
 
16
- describe('BinlogListener tests', () => {
16
+ describe('BinlogListener tests', { timeout: 60_000 }, () => {
17
17
  const MAX_QUEUE_CAPACITY_MB = 1;
18
18
  const BINLOG_LISTENER_CONNECTION_OPTIONS = {
19
19
  ...TEST_CONNECTION_OPTIONS,
@@ -73,14 +73,14 @@ describe('BinlogListener tests', () => {
73
73
  await binLogListener.start();
74
74
 
75
75
  // Wait for listener to stop due to queue reaching capacity
76
- await vi.waitFor(() => expect(stopSpy).toHaveBeenCalled(), { timeout: 5000 });
76
+ await vi.waitFor(() => expect(stopSpy).toHaveBeenCalled(), { timeout: 10_000 });
77
77
 
78
78
  expect(binLogListener.isQueueOverCapacity()).toBeTruthy();
79
79
  // Resume event processing
80
80
  eventHandler.unpause!();
81
81
  const restartSpy = vi.spyOn(binLogListener, 'start');
82
82
 
83
- await vi.waitFor(() => expect(eventHandler.rowsWritten).equals(ROW_COUNT), { timeout: 5000 });
83
+ await vi.waitFor(() => expect(eventHandler.rowsWritten).equals(ROW_COUNT), { timeout: 10_000 });
84
84
  await binLogListener.stop();
85
85
  // Confirm resume was called after unpausing
86
86
  expect(restartSpy).toHaveBeenCalled();
@@ -106,7 +106,7 @@ describe('BinlogListener tests', () => {
106
106
  test('Keepalive event', async () => {
107
107
  binLogListener.options.keepAliveInactivitySeconds = 1;
108
108
  await binLogListener.start();
109
- await vi.waitFor(() => expect(eventHandler.lastKeepAlive).toBeDefined(), { timeout: 10000 });
109
+ await vi.waitFor(() => expect(eventHandler.lastKeepAlive).toBeDefined(), { timeout: 10_000 });
110
110
  await binLogListener.stop();
111
111
  expect(eventHandler.lastKeepAlive).toEqual(binLogListener.options.startGTID.comparable);
112
112
  });
@@ -114,7 +114,7 @@ describe('BinlogListener tests', () => {
114
114
  test('Schema change event: Rename table', async () => {
115
115
  await binLogListener.start();
116
116
  await connectionManager.query(`ALTER TABLE test_DATA RENAME test_DATA_new`);
117
- await vi.waitFor(() => expect(eventHandler.schemaChanges.length).toBe(1), { timeout: 5000 });
117
+ await waitForSchemaChanges(1);
118
118
  await binLogListener.stop();
119
119
  assertSchemaChange(
120
120
  eventHandler.schemaChanges[0],
@@ -133,7 +133,7 @@ describe('BinlogListener tests', () => {
133
133
  test_DATA TO test_DATA_new,
134
134
  test_DATA_new TO test_DATA
135
135
  `);
136
- await vi.waitFor(() => expect(eventHandler.schemaChanges.length).toBe(2), { timeout: 5000 });
136
+ await waitForSchemaChanges(2);
137
137
  await binLogListener.stop();
138
138
  assertSchemaChange(
139
139
  eventHandler.schemaChanges[0],
@@ -156,7 +156,7 @@ describe('BinlogListener tests', () => {
156
156
  test('Schema change event: Truncate table', async () => {
157
157
  await binLogListener.start();
158
158
  await connectionManager.query(`TRUNCATE TABLE test_DATA`);
159
- await vi.waitFor(() => expect(eventHandler.schemaChanges.length).toBe(1), { timeout: 5000 });
159
+ await waitForSchemaChanges(1);
160
160
  await binLogListener.stop();
161
161
  assertSchemaChange(
162
162
  eventHandler.schemaChanges[0],
@@ -169,8 +169,7 @@ describe('BinlogListener tests', () => {
169
169
  test('Schema change event: Drop table', async () => {
170
170
  await binLogListener.start();
171
171
  await connectionManager.query(`DROP TABLE test_DATA`);
172
- await connectionManager.query(`CREATE TABLE test_DATA (id CHAR(36) PRIMARY KEY, description MEDIUMTEXT)`);
173
- await vi.waitFor(() => expect(eventHandler.schemaChanges.length).toBe(1), { timeout: 5000 });
172
+ await waitForSchemaChanges(1);
174
173
  await binLogListener.stop();
175
174
  assertSchemaChange(
176
175
  eventHandler.schemaChanges[0],
@@ -183,7 +182,7 @@ describe('BinlogListener tests', () => {
183
182
  test('Schema change event: Drop column', async () => {
184
183
  await binLogListener.start();
185
184
  await connectionManager.query(`ALTER TABLE test_DATA DROP COLUMN description`);
186
- await vi.waitFor(() => expect(eventHandler.schemaChanges.length).toBe(1), { timeout: 5000 });
185
+ await waitForSchemaChanges(1);
187
186
  await binLogListener.stop();
188
187
  assertSchemaChange(
189
188
  eventHandler.schemaChanges[0],
@@ -196,7 +195,7 @@ describe('BinlogListener tests', () => {
196
195
  test('Schema change event: Add column', async () => {
197
196
  await binLogListener.start();
198
197
  await connectionManager.query(`ALTER TABLE test_DATA ADD COLUMN new_column VARCHAR(255)`);
199
- await vi.waitFor(() => expect(eventHandler.schemaChanges.length).toBe(1), { timeout: 5000 });
198
+ await waitForSchemaChanges(1);
200
199
  await binLogListener.stop();
201
200
  assertSchemaChange(
202
201
  eventHandler.schemaChanges[0],
@@ -209,7 +208,7 @@ describe('BinlogListener tests', () => {
209
208
  test('Schema change event: Modify column', async () => {
210
209
  await binLogListener.start();
211
210
  await connectionManager.query(`ALTER TABLE test_DATA MODIFY COLUMN description TEXT`);
212
- await vi.waitFor(() => expect(eventHandler.schemaChanges.length).toBe(1), { timeout: 5000 });
211
+ await waitForSchemaChanges(1);
213
212
  await binLogListener.stop();
214
213
  assertSchemaChange(
215
214
  eventHandler.schemaChanges[0],
@@ -222,7 +221,7 @@ describe('BinlogListener tests', () => {
222
221
  test('Schema change event: Rename column via change statement', async () => {
223
222
  await binLogListener.start();
224
223
  await connectionManager.query(`ALTER TABLE test_DATA CHANGE COLUMN description description_new MEDIUMTEXT`);
225
- await vi.waitFor(() => expect(eventHandler.schemaChanges.length).toBe(1), { timeout: 5000 });
224
+ await waitForSchemaChanges(1);
226
225
  await binLogListener.stop();
227
226
  assertSchemaChange(
228
227
  eventHandler.schemaChanges[0],
@@ -237,7 +236,7 @@ describe('BinlogListener tests', () => {
237
236
  if (!isMySQL57) {
238
237
  await binLogListener.start();
239
238
  await connectionManager.query(`ALTER TABLE test_DATA RENAME COLUMN description TO description_new`);
240
- await vi.waitFor(() => expect(eventHandler.schemaChanges.length).toBe(1), { timeout: 5000 });
239
+ await waitForSchemaChanges(1);
241
240
  await binLogListener.stop();
242
241
  assertSchemaChange(
243
242
  eventHandler.schemaChanges[0],
@@ -254,7 +253,7 @@ describe('BinlogListener tests', () => {
254
253
  await connectionManager.query(
255
254
  `ALTER TABLE test_DATA DROP COLUMN description, ADD COLUMN new_description TEXT, MODIFY COLUMN id VARCHAR(50)`
256
255
  );
257
- await vi.waitFor(() => expect(eventHandler.schemaChanges.length).toBe(3), { timeout: 5000 });
256
+ await waitForSchemaChanges(3);
258
257
  await binLogListener.stop();
259
258
  assertSchemaChange(
260
259
  eventHandler.schemaChanges[0],
@@ -289,7 +288,7 @@ describe('BinlogListener tests', () => {
289
288
  await binLogListener.start();
290
289
  await connectionManager.query(`ALTER TABLE test_constraints ADD PRIMARY KEY (id)`);
291
290
  await connectionManager.query(`ALTER TABLE test_constraints DROP PRIMARY KEY`);
292
- await vi.waitFor(() => expect(eventHandler.schemaChanges.length).toBe(2), { timeout: 5000 });
291
+ await waitForSchemaChanges(2);
293
292
  await binLogListener.stop();
294
293
  // Event for the add
295
294
  assertSchemaChange(
@@ -318,7 +317,7 @@ describe('BinlogListener tests', () => {
318
317
  await binLogListener.start();
319
318
  await connectionManager.query(`ALTER TABLE test_constraints ADD UNIQUE (description)`);
320
319
  await connectionManager.query(`ALTER TABLE test_constraints DROP INDEX description`);
321
- await vi.waitFor(() => expect(eventHandler.schemaChanges.length).toBe(2), { timeout: 5000 });
320
+ await waitForSchemaChanges(2);
322
321
  await binLogListener.stop();
323
322
  // Event for the creation
324
323
  assertSchemaChange(
@@ -347,7 +346,7 @@ describe('BinlogListener tests', () => {
347
346
  await binLogListener.start();
348
347
  await connectionManager.query(`CREATE UNIQUE INDEX description_idx ON test_constraints (description)`);
349
348
  await connectionManager.query(`DROP INDEX description_idx ON test_constraints`);
350
- await vi.waitFor(() => expect(eventHandler.schemaChanges.length).toBe(2), { timeout: 5000 });
349
+ await waitForSchemaChanges(2);
351
350
  await binLogListener.stop();
352
351
  // Event for the creation
353
352
  assertSchemaChange(
@@ -398,7 +397,7 @@ describe('BinlogListener tests', () => {
398
397
 
399
398
  await binLogListener.start();
400
399
 
401
- await vi.waitFor(() => expect(eventHandler.schemaChanges.length).toBe(4), { timeout: 5000 });
400
+ await waitForSchemaChanges(4);
402
401
  await binLogListener.stop();
403
402
  expect(eventHandler.schemaChanges[0].type).toBe(SchemaChangeType.ALTER_TABLE_COLUMN);
404
403
  expect(eventHandler.schemaChanges[1].type).toBe(SchemaChangeType.REPLICATION_IDENTITY);
@@ -464,14 +463,26 @@ describe('BinlogListener tests', () => {
464
463
  await insertRows(connectionManager, 1);
465
464
  // multi_schema database insert into test_DATA_multi
466
465
  await connectionManager.query(`INSERT INTO ${testTable}(id, description) VALUES('${uuid()}','test')`);
466
+ // Wait for both row entries to appear on the binlog. This avoids a rare race condition in MySQL 5.7
467
+ // where the drop table can hang if run immediately after the table was locked by an insert
468
+ await vi.waitFor(() => expect(eventHandler.rowsWritten).equals(2), { timeout: 5000 });
469
+
467
470
  await connectionManager.query(`DROP TABLE ${testTable}`);
471
+ await waitForSchemaChanges(1);
468
472
 
469
- await vi.waitFor(() => expect(eventHandler.schemaChanges.length).toBe(1), { timeout: 5000 });
470
473
  await binLogListener.stop();
471
474
  expect(eventHandler.rowsWritten).toBe(2);
472
475
  assertSchemaChange(eventHandler.schemaChanges[0], SchemaChangeType.DROP_TABLE, 'multi_schema', 'test_DATA_multi');
473
476
  });
474
477
 
478
+ async function waitForSchemaChanges(count: number) {
479
+ try {
480
+ await vi.waitFor(() => expect(eventHandler.schemaChanges.length).equals(count), { timeout: 30_000 });
481
+ } catch (error) {
482
+ throw new Error(`Timeout while waiting for [${count}] schema changes.`);
483
+ }
484
+ }
485
+
475
486
  function assertSchemaChange(
476
487
  change: SchemaChange,
477
488
  type: SchemaChangeType,