@vlasky/zongji 0.7.0 → 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 +8 -0
- package/README.md +1 -1
- package/index.js +45 -11
- package/lib/binlog_event.js +10 -0
- package/lib/sequence/binlog.js +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to @vlasky/zongji since forking from nevill/zongji.
|
|
4
4
|
|
|
5
|
+
## [0.7.1] - 2026-07-04
|
|
6
|
+
|
|
7
|
+
- Fix `start()` calls made while a previous `start()` was still initialising silently discarding their filters. Since 0.7.0 filters are snapshotted and re-calling `start()` is the documented way to update them, but updates made between `start()` and the `ready` event were lost; consumers registering tables during boot (e.g. @vlasky/mysql-live-select) missed events for tables added in that window. Filters passed during initialisation now apply, exactly as when already running; stream options (filename/position/serverId) still come from the first call.
|
|
8
|
+
- Fix a resume-position gap that could silently drop row events. `options.position` was advanced past TableMap events on the cached-metadata path, so a consumer persisting `filename`/`position` for reconnect could resume between a TableMap and its row events; the resumed instance had no metadata for the table id and dropped those rows with no error. TableMap events no longer advance the resume position, closing the gap for single-table statements (the common case). A narrower window remains for multi-table statements (multi-table UPDATE, foreign-key cascades), where the server writes all TableMap events before any row events: emitting the first table's rows still advances the position past the later TableMaps. Rows already processed before a crash may be re-delivered after resume (at-least-once), which is recoverable where dropping is not.
|
|
9
|
+
- Fix rotate events corrupting the `filename`/`position` resume pair. A rotate's header position refers to the old binlog file (0 for the artificial rotate at the start of every dump), yet it was written into `options.position` alongside the new file's name; a consumer resuming from that pair after a real rotation could get "position > file size" or a mid-event read, and the artificial rotate silently reset the start position to 0. The rotate's payload position (the start of the new file) is now used, and the filename update is unconditional. Present in every zongji release since the original upstream project.
|
|
10
|
+
- Fix a corrupt GTID event's parse error being swallowed when `gtid` is excluded by `includeEvents`; the whole following transaction was then silently mislabelled as anonymous. The error now reaches the `error` event regardless of filtering.
|
|
11
|
+
- Schema drift between a binlog event being written and the metadata fetch (e.g. a column dropped in between) now emits a descriptive error naming the table and column counts, instead of throwing a bare TypeError from inside event parsing; the affected table's rows are skipped until its next TableMap event refreshes the metadata.
|
|
12
|
+
|
|
5
13
|
## [0.7.0] - 2026-07-04
|
|
6
14
|
|
|
7
15
|
### Breaking changes
|
package/README.md
CHANGED
|
@@ -169,7 +169,7 @@ Event name | Description
|
|
|
169
169
|
`transactionpayload` | Compressed transaction from MySQL 8.0.20+ servers with `binlog_transaction_compression=ON`. ZongJi cannot decode the row events inside it, so it emits an `error` (once per instance) naming the server setting responsible.
|
|
170
170
|
`partialupdaterows` | Partial JSON update from MySQL 8.0+ servers with `binlog_row_value_options=PARTIAL_JSON`. ZongJi cannot decode the JSON diff format, so it emits an `error` (once per instance) naming the server setting responsible.
|
|
171
171
|
|
|
172
|
-
When the server runs with `gtid_mode=ON`, every emitted event carries a `gtid` property (`'uuid:sequence'`) identifying the transaction it belongs to, tracked internally even if `gtid` events are excluded by `includeEvents`. It is `undefined` for anonymous transactions and for events seen before the stream's first GTID event. Use it for checkpointing and deduplication.
|
|
172
|
+
When the server runs with `gtid_mode=ON`, every emitted event carries a `gtid` property (`'uuid:sequence'`) identifying the transaction it belongs to, tracked internally even if `gtid` events are excluded by `includeEvents`. It is `undefined` for anonymous transactions and for events seen before the stream's first GTID event. Note that non-row events arriving between a transaction's commit and the next transaction's GTID event (e.g. a rotate) still carry the previous transaction's `gtid`. Use it for checkpointing and deduplication.
|
|
173
173
|
|
|
174
174
|
**Event Methods**
|
|
175
175
|
|
package/index.js
CHANGED
|
@@ -343,12 +343,17 @@ class ZongJi extends EventEmitter {
|
|
|
343
343
|
return;
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
-
// A duplicate start() while one is already initialising is ignored
|
|
347
|
-
// the first call completes
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
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.
|
|
351
355
|
if (this._starting && !this.stopped) {
|
|
356
|
+
this._filters(options);
|
|
352
357
|
return;
|
|
353
358
|
}
|
|
354
359
|
this._starting = true;
|
|
@@ -429,8 +434,18 @@ class ZongJi extends EventEmitter {
|
|
|
429
434
|
this.connection.pause();
|
|
430
435
|
attachGtid(event);
|
|
431
436
|
this._fetchTableInfo(event, () => {
|
|
432
|
-
|
|
433
|
-
|
|
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
|
+
}
|
|
434
449
|
this.emit('binlog', event);
|
|
435
450
|
if (this.connection) this.connection.resume();
|
|
436
451
|
});
|
|
@@ -439,12 +454,31 @@ class ZongJi extends EventEmitter {
|
|
|
439
454
|
break;
|
|
440
455
|
}
|
|
441
456
|
case 'Rotate':
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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;
|
|
445
465
|
break;
|
|
446
466
|
}
|
|
447
|
-
|
|
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
|
+
}
|
|
448
482
|
this.emit('binlog', attachGtid(event));
|
|
449
483
|
};
|
|
450
484
|
|
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({
|
package/lib/sequence/binlog.js
CHANGED
|
@@ -159,7 +159,10 @@ export default function initBinlogClass(zongji) {
|
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
// A GTID parse failure corrupts transaction attribution for the
|
|
163
|
+
// whole following transaction, so it must surface even when the
|
|
164
|
+
// event type itself is filtered out
|
|
165
|
+
if (zongji._skipEvent(eventName) && !error) {
|
|
163
166
|
return Binlog.prototype.binlogData;
|
|
164
167
|
}
|
|
165
168
|
|