@vlasky/zongji 0.6.1 → 0.7.1
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 +32 -0
- package/README.md +30 -5
- package/example.js +5 -0
- package/index.d.ts +84 -2
- package/index.js +207 -65
- package/lib/binlog_event.js +49 -0
- package/lib/code_map.js +9 -1
- package/lib/common.js +71 -43
- package/lib/json_decode.js +61 -18
- package/lib/packet/binlog.js +7 -0
- package/lib/reader.js +4 -143
- package/lib/sequence/binlog.js +33 -8
- package/package.json +8 -5
- package/.travis.yml +0 -15
- package/docker-compose.yml +0 -33
- package/docker-test.sh +0 -11
- package/eslint.config.js +0 -35
- package/jsconfig.json +0 -15
- package/lib/packet/combinlog.js +0 -27
- package/lib/packet/index.js +0 -66
- package/scripts/start-mysql.sh +0 -28
package/index.js
CHANGED
|
@@ -7,23 +7,18 @@ const ConnectionConfigMap = {
|
|
|
7
7
|
'Pool': obj => obj.config.connectionConfig,
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const TableInfoQuery = `SELECT
|
|
11
11
|
COLUMN_NAME, COLLATION_NAME, CHARACTER_SET_NAME,
|
|
12
12
|
COLUMN_COMMENT, COLUMN_TYPE
|
|
13
|
-
FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA
|
|
13
|
+
FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=? AND TABLE_NAME=?
|
|
14
14
|
ORDER BY ORDINAL_POSITION`;
|
|
15
15
|
|
|
16
|
-
// Simple format function to replace %s placeholders
|
|
17
|
-
function formatSql(template, ...args) {
|
|
18
|
-
let i = 0;
|
|
19
|
-
return template.replace(/%s/g, () => args[i++]);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
16
|
class ZongJi extends EventEmitter {
|
|
23
17
|
constructor(dsn) {
|
|
24
18
|
super();
|
|
25
19
|
|
|
26
20
|
this._pendingErrors = [];
|
|
21
|
+
this._pendingErrorTimer = null;
|
|
27
22
|
this.on('newListener', (event) => {
|
|
28
23
|
if (event !== 'error' || this._pendingErrors.length === 0) {
|
|
29
24
|
return;
|
|
@@ -39,8 +34,12 @@ class ZongJi extends EventEmitter {
|
|
|
39
34
|
this._filters({});
|
|
40
35
|
this.ctrlCallbacks = [];
|
|
41
36
|
this.tableMap = {};
|
|
37
|
+
this._warnedUnsupported = new Set();
|
|
38
|
+
this._currentGtid = undefined;
|
|
42
39
|
this.ready = false;
|
|
43
40
|
this.stopped = false;
|
|
41
|
+
this._starting = false;
|
|
42
|
+
this._startEpoch = 0;
|
|
44
43
|
this.useChecksum = false;
|
|
45
44
|
|
|
46
45
|
this._dsn = dsn;
|
|
@@ -54,7 +53,12 @@ class ZongJi extends EventEmitter {
|
|
|
54
53
|
const createConnection = (options) => {
|
|
55
54
|
const emitError = (err) => {
|
|
56
55
|
if (this.listenerCount('error') === 0) {
|
|
56
|
+
// Buffer errors that fire before the caller attaches a listener
|
|
57
|
+
// (typically within the same tick as construction/start). If no
|
|
58
|
+
// listener ever appears, fall back to EventEmitter's default
|
|
59
|
+
// behaviour (throw) rather than swallowing failures silently.
|
|
57
60
|
this._pendingErrors.push(err);
|
|
61
|
+
this._schedulePendingErrorCheck();
|
|
58
62
|
return;
|
|
59
63
|
}
|
|
60
64
|
this.emit('error', err);
|
|
@@ -95,6 +99,23 @@ class ZongJi extends EventEmitter {
|
|
|
95
99
|
this.connection = createConnection(binlogDsn);
|
|
96
100
|
}
|
|
97
101
|
|
|
102
|
+
_schedulePendingErrorCheck() {
|
|
103
|
+
if (this._pendingErrorTimer) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this._pendingErrorTimer = setImmediate(() => {
|
|
107
|
+
this._pendingErrorTimer = null;
|
|
108
|
+
if (this.listenerCount('error') > 0 || this._pendingErrors.length === 0) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const pending = this._pendingErrors.slice();
|
|
112
|
+
this._pendingErrors.length = 0;
|
|
113
|
+
// No 'error' listener was attached within a macrotask; re-emit so
|
|
114
|
+
// Node's unhandled 'error' semantics apply (throws)
|
|
115
|
+
pending.forEach(err => this.emit('error', err));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
98
119
|
_isChecksumEnabled(next) {
|
|
99
120
|
const SelectChecksumParamSql = 'select @@GLOBAL.binlog_checksum as checksum';
|
|
100
121
|
const SetChecksumSql = 'set @master_binlog_checksum=@@global.binlog_checksum';
|
|
@@ -184,19 +205,28 @@ class ZongJi extends EventEmitter {
|
|
|
184
205
|
}
|
|
185
206
|
|
|
186
207
|
_fetchTableInfo(tableMapEvent, next) {
|
|
187
|
-
const sql = formatSql(TableInfoQueryTemplate,
|
|
188
|
-
tableMapEvent.schemaName, tableMapEvent.tableName);
|
|
189
|
-
|
|
190
208
|
if (!this.ctrlConnection ||
|
|
191
209
|
this.ctrlConnection.state === 'disconnected' ||
|
|
192
210
|
this.ctrlConnection._fatalError ||
|
|
193
211
|
this.ctrlConnection._protocolError ||
|
|
194
212
|
this.ctrlConnection._closing) {
|
|
213
|
+
// The binlog connection stays paused, so processing has halted.
|
|
214
|
+
// During stop() that is expected; otherwise it must not be silent.
|
|
215
|
+
if (!this.stopped) {
|
|
216
|
+
this.emit('error', new Error(
|
|
217
|
+
'Control connection unavailable while fetching column metadata ' +
|
|
218
|
+
'for ' + tableMapEvent.schemaName + '.' + tableMapEvent.tableName +
|
|
219
|
+
'. Binlog processing has halted; call stop() then start() to resume.'));
|
|
220
|
+
}
|
|
195
221
|
return;
|
|
196
222
|
}
|
|
197
223
|
|
|
224
|
+
const params = [tableMapEvent.schemaName, tableMapEvent.tableName];
|
|
198
225
|
try {
|
|
199
|
-
|
|
226
|
+
// execute() uses a server-side prepared statement: parameters are sent
|
|
227
|
+
// out-of-band (never spliced into SQL text) and the statement is cached
|
|
228
|
+
// per connection, so repeated metadata lookups avoid re-parsing.
|
|
229
|
+
this.ctrlConnection.execute(TableInfoQuery, params, (err, rows) => {
|
|
200
230
|
if (err) {
|
|
201
231
|
// Errors should be emitted
|
|
202
232
|
this.emit('error', err);
|
|
@@ -234,12 +264,14 @@ class ZongJi extends EventEmitter {
|
|
|
234
264
|
filename,
|
|
235
265
|
position,
|
|
236
266
|
startAtEnd,
|
|
267
|
+
nonBlock,
|
|
237
268
|
} = {}) {
|
|
238
269
|
this.options = {
|
|
239
270
|
serverId,
|
|
240
271
|
filename,
|
|
241
272
|
position,
|
|
242
273
|
startAtEnd,
|
|
274
|
+
nonBlock,
|
|
243
275
|
};
|
|
244
276
|
}
|
|
245
277
|
|
|
@@ -257,6 +289,28 @@ class ZongJi extends EventEmitter {
|
|
|
257
289
|
includeSchema,
|
|
258
290
|
excludeSchema,
|
|
259
291
|
};
|
|
292
|
+
|
|
293
|
+
// Precompiled lookups so per-event filtering is O(1). Only own keys of
|
|
294
|
+
// the schema objects are considered (no prototype leakage).
|
|
295
|
+
const compileSchemaFilter = (schema) => {
|
|
296
|
+
if (schema === undefined || schema === null) {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
const compiled = new Map();
|
|
300
|
+
for (const database of Object.keys(schema)) {
|
|
301
|
+
const tables = schema[database];
|
|
302
|
+
compiled.set(database, tables === true ?
|
|
303
|
+
true : new Set(Array.isArray(tables) ? tables : []));
|
|
304
|
+
}
|
|
305
|
+
return compiled;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
this._includeEventsSet = includeEvents === undefined ?
|
|
309
|
+
undefined : new Set(Array.isArray(includeEvents) ? includeEvents : []);
|
|
310
|
+
this._excludeEventsSet =
|
|
311
|
+
new Set(Array.isArray(excludeEvents) ? excludeEvents : []);
|
|
312
|
+
this._includeSchemaMap = compileSchemaFilter(includeSchema);
|
|
313
|
+
this._excludeSchemaMap = compileSchemaFilter(excludeSchema) || new Map();
|
|
260
314
|
}
|
|
261
315
|
|
|
262
316
|
get(name) {
|
|
@@ -289,6 +343,27 @@ class ZongJi extends EventEmitter {
|
|
|
289
343
|
return;
|
|
290
344
|
}
|
|
291
345
|
|
|
346
|
+
// A duplicate start() while one is already initialising is ignored -
|
|
347
|
+
// the first call completes - but its filters are applied, exactly as
|
|
348
|
+
// in the already-running branch above: filters are snapshotted, and
|
|
349
|
+
// re-calling start() is the documented way to update them, so updates
|
|
350
|
+
// made during the initialisation window must not be lost. Stream
|
|
351
|
+
// options (filename/position/serverId) still come from the first
|
|
352
|
+
// call. If stop() intervened, this start() instead proceeds as a
|
|
353
|
+
// restart: the epoch below makes the stale initialisation chain
|
|
354
|
+
// abort, so exactly one binlog dump command is ever enqueued.
|
|
355
|
+
if (this._starting && !this.stopped) {
|
|
356
|
+
this._filters(options);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
this._starting = true;
|
|
360
|
+
this._startEpoch += 1;
|
|
361
|
+
const epoch = this._startEpoch;
|
|
362
|
+
|
|
363
|
+
// A resumed stream must not attribute early events to a GTID seen
|
|
364
|
+
// before the restart
|
|
365
|
+
this._currentGtid = undefined;
|
|
366
|
+
|
|
292
367
|
this.stopped = false;
|
|
293
368
|
|
|
294
369
|
if (!this.connection || !this.ctrlConnection) {
|
|
@@ -330,6 +405,16 @@ class ZongJi extends EventEmitter {
|
|
|
330
405
|
});
|
|
331
406
|
};
|
|
332
407
|
|
|
408
|
+
// Attach the current transaction's GTID (tracked at the packet layer,
|
|
409
|
+
// even when 'gtid' events are filtered out). Gtid/AnonymousGtid events
|
|
410
|
+
// keep their own parsed value.
|
|
411
|
+
const attachGtid = (event) => {
|
|
412
|
+
if (!('gtid' in event)) {
|
|
413
|
+
event.gtid = this._currentGtid;
|
|
414
|
+
}
|
|
415
|
+
return event;
|
|
416
|
+
};
|
|
417
|
+
|
|
333
418
|
const binlogHandler = (error, event) => {
|
|
334
419
|
if (error) {
|
|
335
420
|
return this.emit('error', error);
|
|
@@ -347,9 +432,20 @@ class ZongJi extends EventEmitter {
|
|
|
347
432
|
if (!tableMap || tableMap.tableName !== event.tableName || tableMap.columns.length !== event.columnCount) {
|
|
348
433
|
if (!this.connection) return;
|
|
349
434
|
this.connection.pause();
|
|
435
|
+
attachGtid(event);
|
|
350
436
|
this._fetchTableInfo(event, () => {
|
|
351
|
-
|
|
352
|
-
|
|
437
|
+
try {
|
|
438
|
+
// merge the column info with metadata
|
|
439
|
+
event.updateColumnInfo();
|
|
440
|
+
} catch (err) {
|
|
441
|
+
// Schema drift between binlog write and metadata fetch:
|
|
442
|
+
// drop the cached entry (subsequent row events for this
|
|
443
|
+
// table id are skipped rather than misdecoded) and report
|
|
444
|
+
delete this.tableMap[event.tableId];
|
|
445
|
+
this.emit('error', err);
|
|
446
|
+
if (this.connection) this.connection.resume();
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
353
449
|
this.emit('binlog', event);
|
|
354
450
|
if (this.connection) this.connection.resume();
|
|
355
451
|
});
|
|
@@ -358,13 +454,32 @@ class ZongJi extends EventEmitter {
|
|
|
358
454
|
break;
|
|
359
455
|
}
|
|
360
456
|
case 'Rotate':
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
457
|
+
// The payload position is the first event of the NEW file: the
|
|
458
|
+
// only value coherent with binlogName. The header nextPosition
|
|
459
|
+
// refers to the OLD file (0 for the artificial rotate at dump
|
|
460
|
+
// start), so rotates are excluded from the generic update below;
|
|
461
|
+
// persisting (new filename, old-file offset) would corrupt the
|
|
462
|
+
// resume point until the next non-rotate event repaired it.
|
|
463
|
+
this.options.filename = event.binlogName;
|
|
464
|
+
this.options.position = event.position;
|
|
364
465
|
break;
|
|
365
466
|
}
|
|
366
|
-
|
|
367
|
-
|
|
467
|
+
// Never advance the resume position past a TableMap: a consumer
|
|
468
|
+
// persisting options.position could otherwise resume in the gap
|
|
469
|
+
// between a TableMap and its row events, and with no cached table
|
|
470
|
+
// metadata those rows would be silently dropped. Holding position
|
|
471
|
+
// back means the TableMap is replayed before its rows on resume;
|
|
472
|
+
// rows already seen may be re-emitted (at-least-once), which is
|
|
473
|
+
// recoverable where dropping is not. A narrower window remains for
|
|
474
|
+
// multi-table statements (all TableMaps precede all row events, so
|
|
475
|
+
// emitting the first table's rows advances past the later
|
|
476
|
+
// TableMaps); the complete fix is advancing only at transaction
|
|
477
|
+
// boundaries, tracked pre-filter.
|
|
478
|
+
const typeName = event.getTypeName();
|
|
479
|
+
if (typeName !== 'TableMap' && typeName !== 'Rotate') {
|
|
480
|
+
this.options.position = event.nextPosition;
|
|
481
|
+
}
|
|
482
|
+
this.emit('binlog', attachGtid(event));
|
|
368
483
|
};
|
|
369
484
|
|
|
370
485
|
let promises = [new Promise(testChecksum)];
|
|
@@ -375,7 +490,13 @@ class ZongJi extends EventEmitter {
|
|
|
375
490
|
|
|
376
491
|
Promise.all(promises)
|
|
377
492
|
.then(() => {
|
|
378
|
-
//
|
|
493
|
+
// Abort if a newer start() superseded this one (after a stop())
|
|
494
|
+
// while promises were pending
|
|
495
|
+
if (epoch !== this._startEpoch) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
this._starting = false;
|
|
499
|
+
// Abort if stop() was called while promises were pending
|
|
379
500
|
if (this.stopped) {
|
|
380
501
|
return;
|
|
381
502
|
}
|
|
@@ -384,8 +505,9 @@ class ZongJi extends EventEmitter {
|
|
|
384
505
|
this.ready = true;
|
|
385
506
|
this.emit('ready');
|
|
386
507
|
|
|
387
|
-
// Final check right before addCommand - connection may have been
|
|
388
|
-
|
|
508
|
+
// Final check right before addCommand - connection may have been
|
|
509
|
+
// destroyed or this start() superseded by a ready handler
|
|
510
|
+
if (this.stopped || epoch !== this._startEpoch) {
|
|
389
511
|
return;
|
|
390
512
|
}
|
|
391
513
|
|
|
@@ -413,6 +535,10 @@ class ZongJi extends EventEmitter {
|
|
|
413
535
|
this.connection.addCommand(new this.BinlogClass(binlogHandler));
|
|
414
536
|
})
|
|
415
537
|
.catch(err => {
|
|
538
|
+
if (epoch !== this._startEpoch) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
this._starting = false;
|
|
416
542
|
this.emit('error', err);
|
|
417
543
|
});
|
|
418
544
|
|
|
@@ -427,10 +553,25 @@ class ZongJi extends EventEmitter {
|
|
|
427
553
|
return;
|
|
428
554
|
}
|
|
429
555
|
|
|
556
|
+
// Errors emitted by a connection we are deliberately destroying are
|
|
557
|
+
// teardown noise (e.g. in-flight queries failing with
|
|
558
|
+
// ERR_STREAM_WRITE_AFTER_END); do not forward them to the caller
|
|
559
|
+
const silenceErrors = (conn) => {
|
|
560
|
+
conn.removeAllListeners('error');
|
|
561
|
+
conn.on('error', () => {});
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
// Capture the connections this stop() owns: finish() may fire from an
|
|
565
|
+
// async callback after a subsequent start() has already created
|
|
566
|
+
// replacement connections, and must not touch those
|
|
567
|
+
const ctrlConnection = this.ctrlConnection;
|
|
568
|
+
const ctrlToClose = this.ctrlConnectionOwner ? ctrlConnection : null;
|
|
569
|
+
|
|
430
570
|
// Binary log connection does not end with destroy()
|
|
431
571
|
let connectionThreadId = null;
|
|
432
572
|
if (this.connection) {
|
|
433
573
|
connectionThreadId = this.connection.threadId;
|
|
574
|
+
silenceErrors(this.connection);
|
|
434
575
|
this.connection.destroy();
|
|
435
576
|
// @ts-ignore - internal mysql2 API
|
|
436
577
|
if (this.connection.stream && typeof this.connection.stream.unref === 'function') {
|
|
@@ -443,24 +584,29 @@ class ZongJi extends EventEmitter {
|
|
|
443
584
|
const finish = () => {
|
|
444
585
|
if (finished) return;
|
|
445
586
|
finished = true;
|
|
446
|
-
if (
|
|
447
|
-
|
|
587
|
+
if (ctrlToClose) {
|
|
588
|
+
silenceErrors(ctrlToClose);
|
|
589
|
+
ctrlToClose.destroy();
|
|
448
590
|
// @ts-ignore - internal mysql2 API
|
|
449
|
-
if (
|
|
591
|
+
if (ctrlToClose.stream && typeof ctrlToClose.stream.unref === 'function') {
|
|
450
592
|
// @ts-ignore - internal mysql2 API
|
|
451
|
-
|
|
593
|
+
ctrlToClose.stream.unref();
|
|
594
|
+
}
|
|
595
|
+
if (this.ctrlConnection === ctrlToClose) {
|
|
596
|
+
this.ctrlConnection = null;
|
|
452
597
|
}
|
|
453
|
-
this.ctrlConnection = null;
|
|
454
598
|
}
|
|
455
599
|
this.emit('stopped');
|
|
456
600
|
};
|
|
457
601
|
|
|
458
|
-
if (!
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
this.ctrlConnection
|
|
602
|
+
if (!ctrlConnection ||
|
|
603
|
+
ctrlConnection.state === 'disconnected' ||
|
|
604
|
+
ctrlConnection._fatalError ||
|
|
605
|
+
ctrlConnection._protocolError ||
|
|
606
|
+
ctrlConnection._closing) {
|
|
607
|
+
if (this.ctrlConnection === ctrlConnection) {
|
|
608
|
+
this.ctrlConnection = null;
|
|
609
|
+
}
|
|
464
610
|
return finish();
|
|
465
611
|
}
|
|
466
612
|
|
|
@@ -470,7 +616,7 @@ class ZongJi extends EventEmitter {
|
|
|
470
616
|
|
|
471
617
|
const killTimeout = setTimeout(finish, 1000);
|
|
472
618
|
try {
|
|
473
|
-
|
|
619
|
+
ctrlConnection.query(
|
|
474
620
|
'KILL ' + connectionThreadId,
|
|
475
621
|
() => {
|
|
476
622
|
clearTimeout(killTimeout);
|
|
@@ -483,44 +629,40 @@ class ZongJi extends EventEmitter {
|
|
|
483
629
|
}
|
|
484
630
|
}
|
|
485
631
|
|
|
632
|
+
// Emit an error the first time an undecodable event type arrives so that
|
|
633
|
+
// dropped row changes (e.g. compressed transactions) are never silent.
|
|
634
|
+
_warnUnsupportedEvent(EventClass) {
|
|
635
|
+
if (this._warnedUnsupported.has(EventClass.name)) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
this._warnedUnsupported.add(EventClass.name);
|
|
639
|
+
this.emit('error', new Error(EventClass.unsupportedReason));
|
|
640
|
+
}
|
|
641
|
+
|
|
486
642
|
// It includes every events by default.
|
|
487
643
|
_skipEvent(name) {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
let excluded = Array.isArray(excludes) && (excludes.indexOf(name) > -1);
|
|
494
|
-
|
|
495
|
-
return excluded || !included;
|
|
644
|
+
if (this._excludeEventsSet.has(name)) {
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
return this._includeEventsSet !== undefined &&
|
|
648
|
+
!this._includeEventsSet.has(name);
|
|
496
649
|
}
|
|
497
650
|
|
|
498
651
|
// It doesn't skip any schema by default.
|
|
499
652
|
_skipSchema(database, table) {
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
(database in includes) &&
|
|
506
|
-
(
|
|
507
|
-
includes[database] === true ||
|
|
508
|
-
(
|
|
509
|
-
Array.isArray(includes[database]) &&
|
|
510
|
-
includes[database].indexOf(table) > -1
|
|
511
|
-
)
|
|
512
|
-
)
|
|
513
|
-
);
|
|
514
|
-
let excluded = (database in excludes) &&
|
|
515
|
-
(
|
|
516
|
-
excludes[database] === true ||
|
|
517
|
-
(
|
|
518
|
-
Array.isArray(excludes[database]) &&
|
|
519
|
-
excludes[database].indexOf(table) > -1
|
|
520
|
-
)
|
|
521
|
-
);
|
|
653
|
+
const excludeEntry = this._excludeSchemaMap.get(database);
|
|
654
|
+
if (excludeEntry === true ||
|
|
655
|
+
(excludeEntry instanceof Set && excludeEntry.has(table))) {
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
522
658
|
|
|
523
|
-
|
|
659
|
+
if (this._includeSchemaMap === undefined) {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
const includeEntry = this._includeSchemaMap.get(database);
|
|
663
|
+
const included = includeEntry === true ||
|
|
664
|
+
(includeEntry instanceof Set && includeEntry.has(table));
|
|
665
|
+
return !included;
|
|
524
666
|
}
|
|
525
667
|
}
|
|
526
668
|
|
package/lib/binlog_event.js
CHANGED
|
@@ -261,6 +261,16 @@ class TableMap extends BinlogEvent {
|
|
|
261
261
|
const tableMap = this.tableMap[this.tableId];
|
|
262
262
|
|
|
263
263
|
const columnSchemas = tableMap.columnSchemas;
|
|
264
|
+
// Schema drift: the table was altered between this binlog event being
|
|
265
|
+
// written and the metadata fetch. Fail diagnosably rather than with a
|
|
266
|
+
// bare TypeError from indexing missing columns.
|
|
267
|
+
if (!columnSchemas || columnSchemas.length < this.columnCount) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`Table ${this.schemaName}.${this.tableName} schema changed between ` +
|
|
270
|
+
`binlog event and metadata fetch: the event has ${this.columnCount} ` +
|
|
271
|
+
'columns, fetched metadata has ' +
|
|
272
|
+
`${columnSchemas ? columnSchemas.length : 0}`);
|
|
273
|
+
}
|
|
264
274
|
const columns = [];
|
|
265
275
|
for (let j = 0; j < this.columnCount; j++) {
|
|
266
276
|
columns.push({
|
|
@@ -364,6 +374,43 @@ class Unknown extends BinlogEvent {
|
|
|
364
374
|
}
|
|
365
375
|
}
|
|
366
376
|
|
|
377
|
+
/* MySQL 8.0.20+ wraps whole transactions in a single compressed event when
|
|
378
|
+
* binlog_transaction_compression=ON. The embedded row events are
|
|
379
|
+
* zstd-compressed and zongji cannot decode them, so the row changes they
|
|
380
|
+
* carry would be silently lost. The first occurrence emits an error via
|
|
381
|
+
* ZongJi#_warnUnsupportedEvent (see unsupportedReason below).
|
|
382
|
+
*/
|
|
383
|
+
class TransactionPayload extends BinlogEvent {
|
|
384
|
+
constructor(parser, options, zongji) {
|
|
385
|
+
super(parser, options);
|
|
386
|
+
skipEventRemainder(parser, zongji);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
static unsupportedReason =
|
|
390
|
+
'Server sent a TRANSACTION_PAYLOAD_EVENT (binlog_transaction_compression=ON). ' +
|
|
391
|
+
'zongji cannot decode compressed transactions, so their row events are NOT ' +
|
|
392
|
+
'being emitted. Set binlog_transaction_compression=OFF on the server to ' +
|
|
393
|
+
'receive these changes.';
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* MySQL 8.0+ emits partial JSON row updates when
|
|
397
|
+
* binlog_row_value_options=PARTIAL_JSON. The JSON diff format is not
|
|
398
|
+
* decodable by zongji, so these updates would be silently lost. The first
|
|
399
|
+
* occurrence emits an error via ZongJi#_warnUnsupportedEvent.
|
|
400
|
+
*/
|
|
401
|
+
class PartialUpdateRows extends BinlogEvent {
|
|
402
|
+
constructor(parser, options, zongji) {
|
|
403
|
+
super(parser, options);
|
|
404
|
+
skipEventRemainder(parser, zongji);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
static unsupportedReason =
|
|
408
|
+
'Server sent a PARTIAL_UPDATE_ROWS_EVENT (binlog_row_value_options=PARTIAL_JSON). ' +
|
|
409
|
+
'zongji cannot decode partial JSON updates, so these UPDATE row events are ' +
|
|
410
|
+
'NOT being emitted. Clear binlog_row_value_options on the server to receive ' +
|
|
411
|
+
'these changes.';
|
|
412
|
+
}
|
|
413
|
+
|
|
367
414
|
export {
|
|
368
415
|
BinlogEvent,
|
|
369
416
|
Rotate,
|
|
@@ -375,5 +422,7 @@ export {
|
|
|
375
422
|
Gtid,
|
|
376
423
|
AnonymousGtid,
|
|
377
424
|
PreviousGtids,
|
|
425
|
+
TransactionPayload,
|
|
426
|
+
PartialUpdateRows,
|
|
378
427
|
Unknown
|
|
379
428
|
};
|
package/lib/code_map.js
CHANGED
|
@@ -37,7 +37,13 @@ const CodeEvent = [
|
|
|
37
37
|
'DELETE_ROWS_EVENT_V2',
|
|
38
38
|
'GTID_LOG_EVENT',
|
|
39
39
|
'ANONYMOUS_GTID_LOG_EVENT',
|
|
40
|
-
'PREVIOUS_GTIDS_LOG_EVENT'
|
|
40
|
+
'PREVIOUS_GTIDS_LOG_EVENT',
|
|
41
|
+
'TRANSACTION_CONTEXT_EVENT',
|
|
42
|
+
'VIEW_CHANGE_EVENT',
|
|
43
|
+
'XA_PREPARE_LOG_EVENT',
|
|
44
|
+
'PARTIAL_UPDATE_ROWS_EVENT',
|
|
45
|
+
'TRANSACTION_PAYLOAD_EVENT',
|
|
46
|
+
'HEARTBEAT_LOG_EVENT_V2'
|
|
41
47
|
];
|
|
42
48
|
|
|
43
49
|
const EventClass = {
|
|
@@ -50,6 +56,8 @@ const EventClass = {
|
|
|
50
56
|
GTID_LOG_EVENT: events.Gtid,
|
|
51
57
|
ANONYMOUS_GTID_LOG_EVENT: events.AnonymousGtid,
|
|
52
58
|
PREVIOUS_GTIDS_LOG_EVENT: events.PreviousGtids,
|
|
59
|
+
TRANSACTION_PAYLOAD_EVENT: events.TransactionPayload,
|
|
60
|
+
PARTIAL_UPDATE_ROWS_EVENT: events.PartialUpdateRows,
|
|
53
61
|
|
|
54
62
|
TABLE_MAP_EVENT: events.TableMap,
|
|
55
63
|
DELETE_ROWS_EVENT_V1: rowsEvents.DeleteRows,
|