@powersync/service-module-mysql 0.0.0-dev-20241015210820 → 0.0.0-dev-20241021185145
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 +7 -9
- package/dist/replication/BinLogReplicationJob.js +5 -8
- package/dist/replication/BinLogReplicationJob.js.map +1 -1
- package/dist/replication/BinLogStream.d.ts +3 -0
- package/dist/replication/BinLogStream.js +129 -107
- package/dist/replication/BinLogStream.js.map +1 -1
- package/package.json +9 -9
- package/src/replication/BinLogReplicationJob.ts +6 -9
- package/src/replication/BinLogStream.ts +146 -122
- package/src/replication/zongji/zongji.d.ts +15 -0
- package/test/src/binlog_stream.test.ts +191 -192
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -34,6 +34,12 @@ interface WriteChangePayload {
|
|
|
34
34
|
|
|
35
35
|
export type Data = Record<string, any>;
|
|
36
36
|
|
|
37
|
+
export class BinlogConfigurationError extends Error {
|
|
38
|
+
constructor(message: string) {
|
|
39
|
+
super(message);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
/**
|
|
38
44
|
* MySQL does not have same relation structure. Just returning unique key as string.
|
|
39
45
|
* @param source
|
|
@@ -301,6 +307,7 @@ AND table_type = 'BASE TABLE';`,
|
|
|
301
307
|
// all connections automatically closed, including this one.
|
|
302
308
|
await this.initReplication();
|
|
303
309
|
await this.streamChanges();
|
|
310
|
+
logger.info('BinlogStream has been shut down');
|
|
304
311
|
} catch (e) {
|
|
305
312
|
await this.storage.reportError(e);
|
|
306
313
|
throw e;
|
|
@@ -309,9 +316,13 @@ AND table_type = 'BASE TABLE';`,
|
|
|
309
316
|
|
|
310
317
|
async initReplication() {
|
|
311
318
|
const connection = await this.connections.getConnection();
|
|
312
|
-
await common.checkSourceConfiguration(connection);
|
|
319
|
+
const errors = await common.checkSourceConfiguration(connection);
|
|
313
320
|
connection.release();
|
|
314
321
|
|
|
322
|
+
if (errors.length > 0) {
|
|
323
|
+
throw new BinlogConfigurationError(`Binlog Configuration Errors: ${errors.join(', ')}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
315
326
|
const initialReplicationCompleted = await this.checkInitialReplicated();
|
|
316
327
|
if (!initialReplicationCompleted) {
|
|
317
328
|
await this.startInitialReplication();
|
|
@@ -342,135 +353,148 @@ AND table_type = 'BASE TABLE';`,
|
|
|
342
353
|
const binLogPositionState = fromGTID.position;
|
|
343
354
|
connection.release();
|
|
344
355
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
filename: binLogPositionState.filename,
|
|
363
|
-
offset: evt.nextPosition
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
break;
|
|
367
|
-
case zongji_utils.eventIsRotation(evt):
|
|
368
|
-
// Update the position
|
|
369
|
-
binLogPositionState.filename = evt.binlogName;
|
|
370
|
-
binLogPositionState.offset = evt.position;
|
|
371
|
-
break;
|
|
372
|
-
case zongji_utils.eventIsWriteMutation(evt):
|
|
373
|
-
// TODO, can multiple tables be present?
|
|
374
|
-
const writeTableInfo = evt.tableMap[evt.tableId];
|
|
375
|
-
await this.writeChanges(batch, {
|
|
376
|
-
type: storage.SaveOperationTag.INSERT,
|
|
377
|
-
data: evt.rows,
|
|
378
|
-
database: writeTableInfo.parentSchema,
|
|
379
|
-
table: writeTableInfo.tableName,
|
|
380
|
-
sourceTable: this.getTable(
|
|
381
|
-
getMysqlRelId({
|
|
382
|
-
schema: writeTableInfo.parentSchema,
|
|
383
|
-
name: writeTableInfo.tableName
|
|
384
|
-
})
|
|
385
|
-
)
|
|
386
|
-
});
|
|
387
|
-
break;
|
|
388
|
-
case zongji_utils.eventIsUpdateMutation(evt):
|
|
389
|
-
const updateTableInfo = evt.tableMap[evt.tableId];
|
|
390
|
-
await this.writeChanges(batch, {
|
|
391
|
-
type: storage.SaveOperationTag.UPDATE,
|
|
392
|
-
data: evt.rows.map((row) => row.after),
|
|
393
|
-
previous_data: evt.rows.map((row) => row.before),
|
|
394
|
-
database: updateTableInfo.parentSchema,
|
|
395
|
-
table: updateTableInfo.tableName,
|
|
396
|
-
sourceTable: this.getTable(
|
|
397
|
-
getMysqlRelId({
|
|
398
|
-
schema: updateTableInfo.parentSchema,
|
|
399
|
-
name: updateTableInfo.tableName
|
|
400
|
-
})
|
|
401
|
-
)
|
|
402
|
-
});
|
|
403
|
-
break;
|
|
404
|
-
case zongji_utils.eventIsDeleteMutation(evt):
|
|
405
|
-
// TODO, can multiple tables be present?
|
|
406
|
-
const deleteTableInfo = evt.tableMap[evt.tableId];
|
|
407
|
-
await this.writeChanges(batch, {
|
|
408
|
-
type: storage.SaveOperationTag.DELETE,
|
|
409
|
-
data: evt.rows,
|
|
410
|
-
database: deleteTableInfo.parentSchema,
|
|
411
|
-
table: deleteTableInfo.tableName,
|
|
412
|
-
// TODO cleanup
|
|
413
|
-
sourceTable: this.getTable(
|
|
414
|
-
getMysqlRelId({
|
|
415
|
-
schema: deleteTableInfo.parentSchema,
|
|
416
|
-
name: deleteTableInfo.tableName
|
|
417
|
-
})
|
|
418
|
-
)
|
|
419
|
-
});
|
|
420
|
-
break;
|
|
421
|
-
case zongji_utils.eventIsXid(evt):
|
|
422
|
-
Metrics.getInstance().transactions_replicated_total.add(1);
|
|
423
|
-
// Need to commit with a replicated GTID with updated next position
|
|
424
|
-
await batch.commit(
|
|
425
|
-
new common.ReplicatedGTID({
|
|
426
|
-
raw_gtid: currentGTID!.raw,
|
|
356
|
+
if (!this.stopped) {
|
|
357
|
+
await this.storage.startBatch(
|
|
358
|
+
{ zeroLSN: ReplicatedGTID.ZERO.comparable, defaultSchema: this.defaultSchema },
|
|
359
|
+
async (batch) => {
|
|
360
|
+
const zongji = this.connections.createBinlogListener();
|
|
361
|
+
|
|
362
|
+
let currentGTID: common.ReplicatedGTID | null = null;
|
|
363
|
+
|
|
364
|
+
const queue = async.queue(async (evt: BinLogEvent) => {
|
|
365
|
+
// State machine
|
|
366
|
+
switch (true) {
|
|
367
|
+
case zongji_utils.eventIsGTIDLog(evt):
|
|
368
|
+
currentGTID = common.ReplicatedGTID.fromBinLogEvent({
|
|
369
|
+
raw_gtid: {
|
|
370
|
+
server_id: evt.serverId,
|
|
371
|
+
transaction_range: evt.transactionRange
|
|
372
|
+
},
|
|
427
373
|
position: {
|
|
428
374
|
filename: binLogPositionState.filename,
|
|
429
375
|
offset: evt.nextPosition
|
|
430
376
|
}
|
|
431
|
-
})
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
377
|
+
});
|
|
378
|
+
break;
|
|
379
|
+
case zongji_utils.eventIsRotation(evt):
|
|
380
|
+
// Update the position
|
|
381
|
+
binLogPositionState.filename = evt.binlogName;
|
|
382
|
+
binLogPositionState.offset = evt.position;
|
|
383
|
+
break;
|
|
384
|
+
case zongji_utils.eventIsWriteMutation(evt):
|
|
385
|
+
// TODO, can multiple tables be present?
|
|
386
|
+
const writeTableInfo = evt.tableMap[evt.tableId];
|
|
387
|
+
await this.writeChanges(batch, {
|
|
388
|
+
type: storage.SaveOperationTag.INSERT,
|
|
389
|
+
data: evt.rows,
|
|
390
|
+
database: writeTableInfo.parentSchema,
|
|
391
|
+
table: writeTableInfo.tableName,
|
|
392
|
+
sourceTable: this.getTable(
|
|
393
|
+
getMysqlRelId({
|
|
394
|
+
schema: writeTableInfo.parentSchema,
|
|
395
|
+
name: writeTableInfo.tableName
|
|
396
|
+
})
|
|
397
|
+
)
|
|
398
|
+
});
|
|
399
|
+
break;
|
|
400
|
+
case zongji_utils.eventIsUpdateMutation(evt):
|
|
401
|
+
const updateTableInfo = evt.tableMap[evt.tableId];
|
|
402
|
+
await this.writeChanges(batch, {
|
|
403
|
+
type: storage.SaveOperationTag.UPDATE,
|
|
404
|
+
data: evt.rows.map((row) => row.after),
|
|
405
|
+
previous_data: evt.rows.map((row) => row.before),
|
|
406
|
+
database: updateTableInfo.parentSchema,
|
|
407
|
+
table: updateTableInfo.tableName,
|
|
408
|
+
sourceTable: this.getTable(
|
|
409
|
+
getMysqlRelId({
|
|
410
|
+
schema: updateTableInfo.parentSchema,
|
|
411
|
+
name: updateTableInfo.tableName
|
|
412
|
+
})
|
|
413
|
+
)
|
|
414
|
+
});
|
|
415
|
+
break;
|
|
416
|
+
case zongji_utils.eventIsDeleteMutation(evt):
|
|
417
|
+
// TODO, can multiple tables be present?
|
|
418
|
+
const deleteTableInfo = evt.tableMap[evt.tableId];
|
|
419
|
+
await this.writeChanges(batch, {
|
|
420
|
+
type: storage.SaveOperationTag.DELETE,
|
|
421
|
+
data: evt.rows,
|
|
422
|
+
database: deleteTableInfo.parentSchema,
|
|
423
|
+
table: deleteTableInfo.tableName,
|
|
424
|
+
// TODO cleanup
|
|
425
|
+
sourceTable: this.getTable(
|
|
426
|
+
getMysqlRelId({
|
|
427
|
+
schema: deleteTableInfo.parentSchema,
|
|
428
|
+
name: deleteTableInfo.tableName
|
|
429
|
+
})
|
|
430
|
+
)
|
|
431
|
+
});
|
|
432
|
+
break;
|
|
433
|
+
case zongji_utils.eventIsXid(evt):
|
|
434
|
+
Metrics.getInstance().transactions_replicated_total.add(1);
|
|
435
|
+
// Need to commit with a replicated GTID with updated next position
|
|
436
|
+
await batch.commit(
|
|
437
|
+
new common.ReplicatedGTID({
|
|
438
|
+
raw_gtid: currentGTID!.raw,
|
|
439
|
+
position: {
|
|
440
|
+
filename: binLogPositionState.filename,
|
|
441
|
+
offset: evt.nextPosition
|
|
442
|
+
}
|
|
443
|
+
}).comparable
|
|
444
|
+
);
|
|
445
|
+
currentGTID = null;
|
|
446
|
+
// chunks_replicated_total.add(1);
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
}, 1);
|
|
450
|
+
|
|
451
|
+
zongji.on('binlog', (evt: BinLogEvent) => {
|
|
452
|
+
if (!this.stopped) {
|
|
453
|
+
logger.info(`Pushing Binlog event ${evt.getEventName()}`);
|
|
454
|
+
queue.push(evt);
|
|
455
|
+
} else {
|
|
456
|
+
logger.info(`Replication is busy stopping, ignoring event ${evt.getEventName()}`);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
451
459
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
460
|
+
if (this.stopped) {
|
|
461
|
+
// Powersync is shutting down, don't start replicating
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
// Only listen for changes to tables in the sync rules
|
|
465
|
+
const includedTables = [...this.tableCache.values()].map((table) => table.table);
|
|
466
|
+
logger.info(`Starting replication from ${binLogPositionState.filename}:${binLogPositionState.offset}`);
|
|
467
|
+
zongji.start({
|
|
468
|
+
includeEvents: ['tablemap', 'writerows', 'updaterows', 'deleterows', 'xid', 'rotate', 'gtidlog'],
|
|
469
|
+
excludeEvents: [],
|
|
470
|
+
includeSchema: { [this.defaultSchema]: includedTables },
|
|
471
|
+
filename: binLogPositionState.filename,
|
|
472
|
+
position: binLogPositionState.offset
|
|
458
473
|
});
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
474
|
+
|
|
475
|
+
// Forever young
|
|
476
|
+
await new Promise<void>((resolve, reject) => {
|
|
477
|
+
queue.error((error) => {
|
|
478
|
+
logger.error('Queue error.', error);
|
|
462
479
|
zongji.stop();
|
|
463
480
|
queue.kill();
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
481
|
+
reject(error);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
this.abortSignal.addEventListener(
|
|
485
|
+
'abort',
|
|
486
|
+
() => {
|
|
487
|
+
logger.info('Abort signal received, stopping replication...');
|
|
488
|
+
zongji.stop();
|
|
489
|
+
queue.kill();
|
|
490
|
+
resolve();
|
|
491
|
+
},
|
|
492
|
+
{ once: true }
|
|
493
|
+
);
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
}
|
|
474
498
|
}
|
|
475
499
|
|
|
476
500
|
private async writeChanges(
|
|
@@ -3,11 +3,26 @@ declare module '@powersync/mysql-zongji' {
|
|
|
3
3
|
host: string;
|
|
4
4
|
user: string;
|
|
5
5
|
password: string;
|
|
6
|
+
dateStrings?: boolean;
|
|
6
7
|
};
|
|
7
8
|
|
|
9
|
+
interface DatabaseFilter {
|
|
10
|
+
[databaseName: string]: string[] | true;
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
export type StartOptions = {
|
|
9
14
|
includeEvents?: string[];
|
|
10
15
|
excludeEvents?: string[];
|
|
16
|
+
/**
|
|
17
|
+
* Describe which databases and tables to include (Only for row events). Use database names as the key and pass an array of table names or true (for the entire database).
|
|
18
|
+
* Example: { 'my_database': ['allow_table', 'another_table'], 'another_db': true }
|
|
19
|
+
*/
|
|
20
|
+
includeSchema?: DatabaseFilter;
|
|
21
|
+
/**
|
|
22
|
+
* Object describing which databases and tables to exclude (Same format as includeSchema)
|
|
23
|
+
* Example: { 'other_db': ['disallowed_table'], 'ex_db': true }
|
|
24
|
+
*/
|
|
25
|
+
excludeSchema?: DatabaseFilter;
|
|
11
26
|
/**
|
|
12
27
|
* BinLog position filename to start reading events from
|
|
13
28
|
*/
|