@powersync/service-module-mysql 0.8.0 → 0.9.0

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.
@@ -1,22 +1,15 @@
1
1
  import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
2
- import {
3
- BinLogEventHandler,
4
- BinLogListener,
5
- Row,
6
- SchemaChange,
7
- SchemaChangeType
8
- } from '@module/replication/zongji/BinLogListener.js';
2
+ import { BinLogListener, SchemaChange, SchemaChangeType } from '@module/replication/zongji/BinLogListener.js';
9
3
  import { MySQLConnectionManager } from '@module/replication/MySQLConnectionManager.js';
10
- import { clearTestDb, createTestDb, TEST_CONNECTION_OPTIONS } from './util.js';
11
- import { v4 as uuid } from 'uuid';
12
- import * as common from '@module/common/common-index.js';
13
4
  import {
14
- createRandomServerId,
15
- getMySQLVersion,
16
- qualifiedMySQLTable,
17
- satisfiesVersion
18
- } from '@module/utils/mysql-utils.js';
19
- import { TableMapEntry } from '@powersync/mysql-zongji';
5
+ clearTestDb,
6
+ createBinlogListener,
7
+ createTestDb,
8
+ TEST_CONNECTION_OPTIONS,
9
+ TestBinLogEventHandler
10
+ } from './util.js';
11
+ import { v4 as uuid } from 'uuid';
12
+ import { getMySQLVersion, qualifiedMySQLTable, satisfiesVersion } from '@module/utils/mysql-utils.js';
20
13
  import crypto from 'crypto';
21
14
  import { TablePattern } from '@powersync/service-sync-rules';
22
15
 
@@ -46,7 +39,11 @@ describe('BinlogListener tests', () => {
46
39
  await connectionManager.query(`CREATE TABLE test_DATA (id CHAR(36) PRIMARY KEY, description MEDIUMTEXT)`);
47
40
  connection.release();
48
41
  eventHandler = new TestBinLogEventHandler();
49
- binLogListener = await createBinlogListener();
42
+ binLogListener = await createBinlogListener({
43
+ connectionManager,
44
+ sourceTables: [new TablePattern(connectionManager.databaseName, 'test_DATA')],
45
+ eventHandler
46
+ });
50
47
  });
51
48
 
52
49
  afterAll(async () => {
@@ -106,6 +103,14 @@ describe('BinlogListener tests', () => {
106
103
  await binLogListener.stop();
107
104
  });
108
105
 
106
+ test('Keepalive event', async () => {
107
+ binLogListener.options.keepAliveInactivitySeconds = 1;
108
+ await binLogListener.start();
109
+ await vi.waitFor(() => expect(eventHandler.lastKeepAlive).toBeDefined(), { timeout: 10000 });
110
+ await binLogListener.stop();
111
+ expect(eventHandler.lastKeepAlive).toEqual(binLogListener.options.startGTID.comparable);
112
+ });
113
+
109
114
  test('Schema change event: Rename table', async () => {
110
115
  await binLogListener.start();
111
116
  await connectionManager.query(`ALTER TABLE test_DATA RENAME test_DATA_new`);
@@ -276,7 +281,11 @@ describe('BinlogListener tests', () => {
276
281
  test('Schema change event: Drop and Add primary key', async () => {
277
282
  await connectionManager.query(`CREATE TABLE test_constraints (id CHAR(36), description VARCHAR(100))`);
278
283
  const sourceTables = [new TablePattern(connectionManager.databaseName, 'test_constraints')];
279
- binLogListener = await createBinlogListener(sourceTables);
284
+ binLogListener = await createBinlogListener({
285
+ connectionManager,
286
+ eventHandler,
287
+ sourceTables
288
+ });
280
289
  await binLogListener.start();
281
290
  await connectionManager.query(`ALTER TABLE test_constraints ADD PRIMARY KEY (id)`);
282
291
  await connectionManager.query(`ALTER TABLE test_constraints DROP PRIMARY KEY`);
@@ -301,7 +310,11 @@ describe('BinlogListener tests', () => {
301
310
  test('Schema change event: Add and drop unique constraint', async () => {
302
311
  await connectionManager.query(`CREATE TABLE test_constraints (id CHAR(36), description VARCHAR(100))`);
303
312
  const sourceTables = [new TablePattern(connectionManager.databaseName, 'test_constraints')];
304
- binLogListener = await createBinlogListener(sourceTables);
313
+ binLogListener = await createBinlogListener({
314
+ connectionManager,
315
+ eventHandler,
316
+ sourceTables
317
+ });
305
318
  await binLogListener.start();
306
319
  await connectionManager.query(`ALTER TABLE test_constraints ADD UNIQUE (description)`);
307
320
  await connectionManager.query(`ALTER TABLE test_constraints DROP INDEX description`);
@@ -326,7 +339,11 @@ describe('BinlogListener tests', () => {
326
339
  test('Schema change event: Add and drop a unique index', async () => {
327
340
  await connectionManager.query(`CREATE TABLE test_constraints (id CHAR(36), description VARCHAR(100))`);
328
341
  const sourceTables = [new TablePattern(connectionManager.databaseName, 'test_constraints')];
329
- binLogListener = await createBinlogListener(sourceTables);
342
+ binLogListener = await createBinlogListener({
343
+ connectionManager,
344
+ eventHandler,
345
+ sourceTables
346
+ });
330
347
  await binLogListener.start();
331
348
  await connectionManager.query(`CREATE UNIQUE INDEX description_idx ON test_constraints (description)`);
332
349
  await connectionManager.query(`DROP INDEX description_idx ON test_constraints`);
@@ -367,7 +384,11 @@ describe('BinlogListener tests', () => {
367
384
  // If there are multiple schema changes in the binlog processing queue, we only restart the binlog listener once
368
385
  // all the schema changes have been processed
369
386
  const sourceTables = [new TablePattern(connectionManager.databaseName, 'test_multiple')];
370
- binLogListener = await createBinlogListener(sourceTables);
387
+ binLogListener = await createBinlogListener({
388
+ connectionManager,
389
+ eventHandler,
390
+ sourceTables
391
+ });
371
392
 
372
393
  await connectionManager.query(`CREATE TABLE test_multiple (id CHAR(36), description VARCHAR(100))`);
373
394
  await connectionManager.query(`ALTER TABLE test_multiple ADD COLUMN new_column VARCHAR(10)`);
@@ -388,7 +409,11 @@ describe('BinlogListener tests', () => {
388
409
  test('Unprocessed binlog event received that does match the current table schema', async () => {
389
410
  // If we process a binlog event for a table which has since had its schema changed, we expect the binlog listener to stop with an error
390
411
  const sourceTables = [new TablePattern(connectionManager.databaseName, 'test_failure')];
391
- binLogListener = await createBinlogListener(sourceTables);
412
+ binLogListener = await createBinlogListener({
413
+ connectionManager,
414
+ eventHandler,
415
+ sourceTables
416
+ });
392
417
 
393
418
  await connectionManager.query(`CREATE TABLE test_failure (id CHAR(36), description VARCHAR(100))`);
394
419
  await connectionManager.query(`INSERT INTO test_failure(id, description) VALUES('${uuid()}','test_failure')`);
@@ -403,7 +428,11 @@ describe('BinlogListener tests', () => {
403
428
 
404
429
  test('Unprocessed binlog event received for a dropped table', async () => {
405
430
  const sourceTables = [new TablePattern(connectionManager.databaseName, 'test_failure')];
406
- binLogListener = await createBinlogListener(sourceTables);
431
+ binLogListener = await createBinlogListener({
432
+ connectionManager,
433
+ eventHandler,
434
+ sourceTables
435
+ });
407
436
 
408
437
  // If we process a binlog event for a table which has since been dropped, we expect the binlog listener to stop with an error
409
438
  await connectionManager.query(`CREATE TABLE test_failure (id CHAR(36), description VARCHAR(100))`);
@@ -424,7 +453,11 @@ describe('BinlogListener tests', () => {
424
453
  new TablePattern(connectionManager.databaseName, 'test_DATA'),
425
454
  new TablePattern('multi_schema', 'test_DATA_multi')
426
455
  ];
427
- binLogListener = await createBinlogListener(sourceTables);
456
+ binLogListener = await createBinlogListener({
457
+ connectionManager,
458
+ eventHandler,
459
+ sourceTables
460
+ });
428
461
  await binLogListener.start();
429
462
 
430
463
  // Default database insert into test_DATA
@@ -439,28 +472,6 @@ describe('BinlogListener tests', () => {
439
472
  assertSchemaChange(eventHandler.schemaChanges[0], SchemaChangeType.DROP_TABLE, 'multi_schema', 'test_DATA_multi');
440
473
  });
441
474
 
442
- async function createBinlogListener(
443
- sourceTables?: TablePattern[],
444
- startPosition?: common.BinLogPosition
445
- ): Promise<BinLogListener> {
446
- if (!sourceTables) {
447
- sourceTables = [new TablePattern(connectionManager.databaseName, 'test_DATA')];
448
- }
449
-
450
- if (!startPosition) {
451
- const fromGTID = await getFromGTID(connectionManager);
452
- startPosition = fromGTID.position;
453
- }
454
-
455
- return new BinLogListener({
456
- connectionManager: connectionManager,
457
- eventHandler: eventHandler,
458
- startPosition: startPosition,
459
- sourceTables: sourceTables,
460
- serverId: createRandomServerId(1)
461
- });
462
- }
463
-
464
475
  function assertSchemaChange(
465
476
  change: SchemaChange,
466
477
  type: SchemaChangeType,
@@ -477,14 +488,6 @@ describe('BinlogListener tests', () => {
477
488
  }
478
489
  });
479
490
 
480
- async function getFromGTID(connectionManager: MySQLConnectionManager) {
481
- const connection = await connectionManager.getConnection();
482
- const fromGTID = await common.readExecutedGtid(connection);
483
- connection.release();
484
-
485
- return fromGTID;
486
- }
487
-
488
491
  async function insertRows(connectionManager: MySQLConnectionManager, count: number) {
489
492
  for (let i = 0; i < count; i++) {
490
493
  await connectionManager.query(
@@ -500,45 +503,3 @@ async function updateRows(connectionManager: MySQLConnectionManager) {
500
503
  async function deleteRows(connectionManager: MySQLConnectionManager) {
501
504
  await connectionManager.query(`DELETE FROM test_DATA`);
502
505
  }
503
-
504
- class TestBinLogEventHandler implements BinLogEventHandler {
505
- rowsWritten = 0;
506
- rowsUpdated = 0;
507
- rowsDeleted = 0;
508
- commitCount = 0;
509
- schemaChanges: SchemaChange[] = [];
510
-
511
- unpause: ((value: void | PromiseLike<void>) => void) | undefined;
512
- private pausedPromise: Promise<void> | undefined;
513
-
514
- pause() {
515
- this.pausedPromise = new Promise((resolve) => {
516
- this.unpause = resolve;
517
- });
518
- }
519
-
520
- async onWrite(rows: Row[], tableMap: TableMapEntry) {
521
- if (this.pausedPromise) {
522
- await this.pausedPromise;
523
- }
524
- this.rowsWritten = this.rowsWritten + rows.length;
525
- }
526
-
527
- async onUpdate(afterRows: Row[], beforeRows: Row[], tableMap: TableMapEntry) {
528
- this.rowsUpdated = this.rowsUpdated + afterRows.length;
529
- }
530
-
531
- async onDelete(rows: Row[], tableMap: TableMapEntry) {
532
- this.rowsDeleted = this.rowsDeleted + rows.length;
533
- }
534
-
535
- async onCommit(lsn: string) {
536
- this.commitCount++;
537
- }
538
-
539
- async onSchemaChange(change: SchemaChange) {
540
- this.schemaChanges.push(change);
541
- }
542
- async onTransactionStart(options: { timestamp: Date }) {}
543
- async onRotate() {}
544
- }
@@ -1,4 +1,4 @@
1
- import { SqliteRow } from '@powersync/service-sync-rules';
1
+ import { SqliteInputRow, SqliteRow } from '@powersync/service-sync-rules';
2
2
  import { afterAll, describe, expect, test } from 'vitest';
3
3
  import { clearTestDb, TEST_CONNECTION_OPTIONS } from './util.js';
4
4
  import { eventIsWriteMutation, eventIsXid } from '@module/replication/zongji/zongji-utils.js';
@@ -298,7 +298,7 @@ INSERT INTO test_data (
298
298
  });
299
299
  });
300
300
 
301
- async function getDatabaseRows(connection: MySQLConnectionManager, tableName: string): Promise<SqliteRow[]> {
301
+ async function getDatabaseRows(connection: MySQLConnectionManager, tableName: string): Promise<SqliteInputRow[]> {
302
302
  const [results, fields] = await connection.query(`SELECT * FROM ${tableName}`);
303
303
  const columns = toColumnDescriptors(fields);
304
304
  return results.map((row) => common.toSQLiteRow(row, columns));
@@ -307,15 +307,15 @@ async function getDatabaseRows(connection: MySQLConnectionManager, tableName: st
307
307
  /**
308
308
  * Return all the inserts from the first transaction in the binlog stream.
309
309
  */
310
- async function getReplicatedRows(expectedTransactionsCount?: number): Promise<SqliteRow[]> {
311
- let transformed: SqliteRow[] = [];
310
+ async function getReplicatedRows(expectedTransactionsCount?: number): Promise<SqliteInputRow[]> {
311
+ let transformed: SqliteInputRow[] = [];
312
312
  const zongji = new ZongJi({
313
313
  host: TEST_CONNECTION_OPTIONS.hostname,
314
314
  user: TEST_CONNECTION_OPTIONS.username,
315
315
  password: TEST_CONNECTION_OPTIONS.password
316
316
  });
317
317
 
318
- const completionPromise = new Promise<SqliteRow[]>((resolve, reject) => {
318
+ const completionPromise = new Promise<SqliteInputRow[]>((resolve, reject) => {
319
319
  zongji.on('binlog', (evt: BinLogEvent) => {
320
320
  try {
321
321
  if (eventIsWriteMutation(evt)) {
@@ -652,12 +652,8 @@ function defineTests(factory: storage.TestStorageFactory) {
652
652
  await connectionManager.query(`INSERT INTO ${testTable}(id, description) VALUES('t3','test3')`);
653
653
  await connectionManager.query(`DROP TABLE ${testTable}`);
654
654
 
655
- // Force a commit on the watched schema to advance the checkpoint
656
- await connectionManager.query(`INSERT INTO test_data(id, description) VALUES('t1','test1')`);
657
-
658
655
  const data = await context.getBucketData('global[]');
659
656
 
660
- // Should only include the entry used to advance the checkpoint
661
- expect(data).toMatchObject([PUT_T1]);
657
+ expect(data).toMatchObject([]);
662
658
  });
663
659
  }
package/test/src/util.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as types from '@module/types/types.js';
2
- import { getMySQLVersion, isVersionAtLeast } from '@module/utils/mysql-utils.js';
2
+ import { createRandomServerId, getMySQLVersion, isVersionAtLeast } from '@module/utils/mysql-utils.js';
3
3
  import * as mongo_storage from '@powersync/service-module-mongodb-storage';
4
4
  import * as postgres_storage from '@powersync/service-module-postgres-storage';
5
5
  import mysqlPromise from 'mysql2/promise';
@@ -7,6 +7,10 @@ import { env } from './env.js';
7
7
  import { describe, TestOptions } from 'vitest';
8
8
  import { TestStorageFactory } from '@powersync/service-core';
9
9
  import { MySQLConnectionManager } from '@module/replication/MySQLConnectionManager.js';
10
+ import { BinLogEventHandler, BinLogListener, Row, SchemaChange } from '@module/replication/zongji/BinLogListener.js';
11
+ import { TableMapEntry } from '@powersync/mysql-zongji';
12
+ import * as common from '@module/common/common-index.js';
13
+ import { TablePattern } from '@powersync/service-sync-rules';
10
14
 
11
15
  export const TEST_URI = env.MYSQL_TEST_URI;
12
16
 
@@ -58,3 +62,79 @@ export async function createTestDb(connectionManager: MySQLConnectionManager, db
58
62
  await connectionManager.query(`DROP DATABASE IF EXISTS ${dbName}`);
59
63
  await connectionManager.query(`CREATE DATABASE IF NOT EXISTS ${dbName}`);
60
64
  }
65
+
66
+ export async function getFromGTID(connectionManager: MySQLConnectionManager) {
67
+ const connection = await connectionManager.getConnection();
68
+ const fromGTID = await common.readExecutedGtid(connection);
69
+ connection.release();
70
+
71
+ return fromGTID;
72
+ }
73
+
74
+ export interface CreateBinlogListenerParams {
75
+ connectionManager: MySQLConnectionManager;
76
+ eventHandler: BinLogEventHandler;
77
+ sourceTables: TablePattern[];
78
+ startGTID?: common.ReplicatedGTID;
79
+ }
80
+ export async function createBinlogListener(params: CreateBinlogListenerParams): Promise<BinLogListener> {
81
+ let { connectionManager, eventHandler, sourceTables, startGTID } = params;
82
+
83
+ if (!startGTID) {
84
+ startGTID = await getFromGTID(connectionManager);
85
+ }
86
+
87
+ return new BinLogListener({
88
+ connectionManager: connectionManager,
89
+ eventHandler: eventHandler,
90
+ startGTID: startGTID!,
91
+ sourceTables: sourceTables,
92
+ serverId: createRandomServerId(1)
93
+ });
94
+ }
95
+
96
+ export class TestBinLogEventHandler implements BinLogEventHandler {
97
+ rowsWritten = 0;
98
+ rowsUpdated = 0;
99
+ rowsDeleted = 0;
100
+ commitCount = 0;
101
+ schemaChanges: SchemaChange[] = [];
102
+ lastKeepAlive: string | undefined;
103
+
104
+ unpause: ((value: void | PromiseLike<void>) => void) | undefined;
105
+ private pausedPromise: Promise<void> | undefined;
106
+
107
+ pause() {
108
+ this.pausedPromise = new Promise((resolve) => {
109
+ this.unpause = resolve;
110
+ });
111
+ }
112
+
113
+ async onWrite(rows: Row[], tableMap: TableMapEntry) {
114
+ if (this.pausedPromise) {
115
+ await this.pausedPromise;
116
+ }
117
+ this.rowsWritten = this.rowsWritten + rows.length;
118
+ }
119
+
120
+ async onUpdate(afterRows: Row[], beforeRows: Row[], tableMap: TableMapEntry) {
121
+ this.rowsUpdated = this.rowsUpdated + afterRows.length;
122
+ }
123
+
124
+ async onDelete(rows: Row[], tableMap: TableMapEntry) {
125
+ this.rowsDeleted = this.rowsDeleted + rows.length;
126
+ }
127
+
128
+ async onCommit(lsn: string) {
129
+ this.commitCount++;
130
+ }
131
+
132
+ async onSchemaChange(change: SchemaChange) {
133
+ this.schemaChanges.push(change);
134
+ }
135
+ async onTransactionStart(options: { timestamp: Date }) {}
136
+ async onRotate() {}
137
+ async onKeepAlive(lsn: string) {
138
+ this.lastKeepAlive = lsn;
139
+ }
140
+ }