All notable changes to @vlasky/zongji since forking from nevill/zongji.
- Export the
MariadbGtidPositionclass (import { MariadbGtidPosition } from '@vlasky/zongji'), the MariaDB counterpart ofGtidSetand the model behindzongji.gtidSeton MariaDB servers: parse a persisted position (azongji.gtidSetcheckpoint or@@gtid_binlog_pos), build one from amariadbgtidlistevent withfromGtidList(), extend it withadd(), and test transaction coverage withcovers(event.gtid, { inclusive })instead of hand-rolling sequence-number comparators (which silently round aboveNumber.MAX_SAFE_INTEGERwhen done with Number). The inclusive form (default) answers "was this transaction's commit at or before the position?" for snapshot barriers; redelivery watermarks must pass{ inclusive: false }, because all events of a transaction share one GTID and an inclusive check would drop every replayed event of the watermark transaction after the first. A server id differing from the position's entry for that domain reports not covered even for a lower sequence number (post-failover sequence numbers can regress under the new server id; for at-least-once consumers an idempotent redelivery is recoverable where a skip is not), as does a domain the position does not list. GtidSetandMariadbGtidPositionnow use BigInt internally, making them exact over MySQL's full GNO range (1 to 2^63-1) and MariaDB's full u64 sequence range. PreviouslyGtidSet.parse/addthrew on GNOs above 2^53, so a snapshot barrier or checkpoint parse failed outright against a server carrying such GTIDs. Public APIs are unchanged: strings in, strings and booleans out.- Fix Previous_gtids decoding silently rounding interval bounds above 2^53: the u64 bounds were combined with Number arithmetic, corrupting
event.gtidSet- the exact text that seedszongji.gtidSetwhen a dump starts at a binlog file boundary. Bounds are now decoded as BigInt, and theevent.sidsintervals follow the Number-or-exact-string convention used for other 64-bit values (typednumber | string; previously typednumberwhile silently wrong beyond 2^53). - Document on the
seqNofields ofmariadbgtidandmariadbgtidlistevents that values aboveNumber.MAX_SAFE_INTEGERarrive as exact strings and must never be compared numerically - useevent.gtidwithMariadbGtidPosition/GtidSetinstead.
- Export the
GtidSetclass (import ZongJi, { GtidSet } from '@vlasky/zongji'): parse a persisted set (e.g. azongji.gtidSetcheckpoint or@@gtid_executed),add()transactions, and test single-transaction membership withcontains(event.gtid)- handy for snapshot drain barriers ("was this event already covered when I took my snapshot?") without maintaining a separate GTID parser. Fully tagged-GTID aware. - Close the remaining resume-position gap for multi-table statements (multi-table
UPDATE, foreign-key cascades). The server writes all of a statement's TableMap events before any row event, so afilename/positionpair persisted while the first table's rows were being processed pointed past the later TableMaps; a consumer resuming there dropped the remaining tables' rows with no error. The resume position now freezes at a transaction's first TableMap and advances to the commit marker's end position, so a persisted position always replays whole transactions. Behaviour change: for row-logged transactions the position advances per transaction rather than per event, so resuming after a mid-transaction crash re-delivers that transaction's earlier row events (at-least-once, as already documented for the single-table case fixed in 0.7.1). Statement-logged content (binlog_format=STATEMENT/MIXED) still advances per event; its replay has no table-metadata dependency, so it never had the dropped-rows hazard. - Tagged GTID support (MySQL 8.3+,
GTID_NEXT='AUTOMATIC:tag'). Tagged transactions arrive as GTID_TAGGED_LOG_EVENT (code 42, a self-describing serialization format unrelated to the classic layout, previouslyunknown) and now decode as ordinarygtidevents withevent.tagset andevent.gtidinuuid:tag:gnoform. GTID sets accept and print the tagged grammar (uuid:1-5:tag_a:1),zongji.gtidSettracks tagged transactions,start({ gtidSet })resumes from a tagged checkpoint (COM_BINLOG_DUMP_GTID's tagged payload encoding, understood by MySQL 8.3+ only), andstartAtEndseeds from agtid_executedcontaining tags. This is also a compatibility fix: once a MySQL 8.3+ server has ever executed a tagged GTID it writes every Previous_gtids event in the tagged set encoding forever (survivingRESET BINARY LOGS AND GTIDS), which previously crashed parsing at the start of every dump, and any tag ingtid_executedbrokestartAtEndseeding. - New
rowsqueryevent (MySQL ROWS_QUERY_LOG_EVENT, previouslyunknown): the SQL statement text behind the row events that follow, MySQL's analogue of MariaDB'sannotaterows. The server sends it to every consumer whilebinlog_rows_query_log_events=ON(defaultOFF); no start option is involved, unlike MariaDB's opt-inrequestAnnotateRows. event.gtidno longer leaks onto events that belong to no transaction: it is now cleared once a commit marker (Xid, XA Prepare, or a COMMIT/ROLLBACK/XA COMMIT/XA ROLLBACK query) has been delivered, so e.g. a rotate arriving between two transactions reportsgtid: undefinedinstead of the previous transaction's GTID. The commit event itself still carries its transaction's GTID. A DDL statement has no commit marker of its own, so its GTID persists until the next transaction begins. Newzongji.lastGtidgetter exposes the same tracked value (the GTID of the transaction currently being delivered); it is only coherent read synchronously inside a'binlog'handler.- Fix text CHAR and VARCHAR columns always decoding as utf8: they now decode via the column's own character set, as TEXT columns already did, so e.g.
latin1andgreekcolumns no longer produce mojibake. In the same pass, charsets whose stored byte order differs from the iconv-lite encoding of the same name are now mapped explicitly: MySQL'slatin1is decoded as cp1252 (its real definition;€,Ÿand friends in 0x80-0x9F previously came back as control characters from TEXT columns), anducs2/utf16/utf32are decoded big-endian (previously byte-swapped garbage from TEXT columns). Behaviour change: consumers that stored the old garbled output will see correct strings after upgrading. - MariaDB support (tested against MariaDB 11.8): ZongJi now detects the server flavour and follows MariaDB binlogs natively, announcing
@mariadb_slave_capability=4so the server sends its real GTID events instead of rewriting them for legacy clients. MariaDB's GTID model (adomain-server-sequencewatermark per replication domain) is fully supported: events carryevent.gtidin MariaDB format,zongji.gtidSetexposes a MariaDB GTID position (seeded fromstart({ gtidSet }), from@@gtid_current_posunderstartAtEnd, or from the GTID list event at the start of a binlog file), andstart({ gtidSet: '0-1-1234' })resumes via@slave_connect_statewith server-side skipping, including across failover. New event types:mariadbgtid,mariadbgtidlist,binlogcheckpoint,startencryption, plusxaprepare(also emitted by MySQL 5.7+, previouslyunknown). MariaDB data types decode to match mysql2 query results:UUID,INET4andINET6as canonical text,VECTORas a raw Buffer,JSONas the LONGTEXT alias text it is,COMPRESSEDcolumns transparently decompressed (with correct charsets, under allbinlog_row_metadatasettings), MariaDB 5.3-era "hires" temporals (mysql56_temporal_format=OFF) decoded correctly instead of desynchronising the row, and compressed binlog events (log_bin_compress=ON) decoded transparently as ordinary query/row events.binlog_row_metadata=FULLworks on MariaDB 10.5+ with the same self-describing decode as MySQL; tables whose classic temporal columns could hide hires encodings automatically useINFORMATION_SCHEMAinstead, andUUID/INET*/VECTORvalues arrive as raw Buffers in FULL mode (the binlog cannot distinguish them fromBINARY). An opt-inrequestAnnotateRowsstart option asks the server forannotaterowsevents carrying the SQL statement text behind each row operation. The test suite runs against MariaDB 11.8 in CI alongside MySQL 5.7/8.0/8.4, and an offline MariaDB fixture pins the captured wire bytes of all of the above in the parser tests. - Fix the resume position (
options.position) going stale when the events carrying real positions are excluded byincludeEvents: filtered events now advance it at the packet layer under the same safety rules as delivered ones (never past a TableMap, never a zero position). On MySQL the position merely lagged; on MariaDB, where events inside a transaction carryend_log_pos=0, common filter sets froze it entirely. - Fix a filtered
rotateevent leaving an incoherent resume pair: the filename never updated while later events advanced the position into the new file. Rotates now update thefilename/positionpair before event filtering, whether delivered or not. - GTID-based resume:
start({ gtidSet })issues COM_BINLOG_DUMP_GTID, letting the server locate the correct binlog file and skip already-processed transactions itself; a persisted checkpoint therefore survives failover to another server in the same replication topology (requiresgtid_mode=ON).zongji.gtidSetexposes the executed set for persisting: seeded exactly from the start set, from the server'sgtid_executedunderstartAtEnd, or from the stream's Previous_gtids event when reading from the start of a binlog file, and extended only as observed transactions commit. Purged-GTID and GTID-mode errors surface through theerrorevent. Heartbeat events (sent in place of server-side-skipped transactions and while idle) are now decoded instead of falling through tounknown, and Previous_gtids string formatting is canonical (single transactions print as8, not8-8) - Support
binlog_row_metadata=FULL(MySQL 8.0+): TableMap events now parse the optional metadata block (column names, signedness, character sets, enum/set value lists, primary key, column visibility), and when it is complete ZongJi decodes rows entirely from the binlog stream with noINFORMATION_SCHEMAqueries and no connection pauses. Each TableMap event rebuilds the table's metadata, soALTER TABLEcan no longer leave stale column definitions behind. Enum/set values containing commas or quotes decode correctly in this mode (theINFORMATION_SCHEMApath cannot represent them). Under the defaultbinlog_row_metadata=MINIMAL, integer signedness now comes from the binlog instead of being inferred from theCOLUMN_TYPEstring. TableMap events exposecolumnNames,signedness,primaryKeyandcolumnVisibilitywhere available, and column schemas gainUNSIGNED,ENUM_VALUESandSET_VALUES.
- Fix
start()calls made while a previousstart()was still initialising silently discarding their filters. Since 0.7.0 filters are snapshotted and re-callingstart()is the documented way to update them, but updates made betweenstart()and thereadyevent 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. - Fix a resume-position gap that could silently drop row events.
options.positionwas advanced past TableMap events on the cached-metadata path, so a consumer persistingfilename/positionfor 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. - Fix rotate events corrupting the
filename/positionresume 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 intooptions.positionalongside 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. - Fix a corrupt GTID event's parse error being swallowed when
gtidis excluded byincludeEvents; the whole following transaction was then silently mislabelled as anonymous. The error now reaches theerrorevent regardless of filtering. - 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.
- DECIMAL columns now emit exact string values (e.g.
'-123.4500') instead of lossy floats, matching mysql2 query results. Migration: setdecimalNumbers: trueon the connection options passed to ZongJi to restore Numbers. - JSON columns now emit parsed JavaScript values instead of JSON strings, matching mysql2 query results. Migration: set
jsonStrings: trueon the connection options passed to ZongJi to restore strings. In string mode, output now uses MySQL's own formatting (spaces after:and,) and 64-bit integers appear as exact raw numerals rather than lossy doubles. - Event and schema filters are snapshotted when
start()is called; mutating the arrays or objects you passed in no longer changes filtering afterwards. Migration: callstart()again with the new filters (the documented way to update them).
- Fix SQL injection in the table metadata query: schema and table names from TableMap events are now bound via a cached prepared statement (
execute()) instead of being spliced into SQL text - Replace the big-integer dependency with native BigInt (one fewer dependency); also fixes silent corruption of 64-bit integers inside JSON columns beyond 2^53, which now follow the same exact Number-or-string rule as BIGINT columns
- Emit an error (once per instance per type) when the server sends undecodable TRANSACTION_PAYLOAD_EVENT (
binlog_transaction_compression=ON) or PARTIAL_UPDATE_ROWS_EVENT (binlog_row_value_options=PARTIAL_JSON) events, instead of silently dropping the row changes; remaining MySQL 8 event codes (TRANSACTION_CONTEXT, VIEW_CHANGE, XA_PREPARE, HEARTBEAT_V2) are now named in the code map - Lifecycle hardening: emit an explicit error instead of hanging silently when the control connection dies during a metadata fetch; a duplicate
start()while one is still initialising is ignored, while stop-then-restart during initialisation now works (exactly one binlog dump command is ever enqueued); errors from connections deliberately destroyed bystop()are no longer forwarded as teardown noise; errors buffered before anerrorlistener attaches are thrown if no listener ever appears, restoring Node's default unhandled'error'behaviour - DECIMAL parsing no longer mutates the shared network packet buffer when flipping the sign bit
- Fix
stop()destroying the control connection of a subsequentstart(): the asynchronous KILL cleanup now only touches the connections that particularstop()owned, so immediate stop-then-restart no longer wedges the new stream on its first metadata fetch - The
nonBlockoption declared in the TypeScript definitions is now actually passed through bystart(); previously it was dropped and the dump command always ran in blocking mode - Remove dead code left over from the mysql.js protocol layer (ComBinlog, EofPacket/ErrorPacket, BufferReader)
- Compile event and schema filters into Sets and Maps for O(1) per-event filtering; only own keys of schema filter objects are considered
- Add a package.json
exportsmap withtypesanddefaultconditions - All emitted events carry an
event.gtidproperty ('uuid:sequence') identifying their transaction when the server runs withgtid_mode=ON, tracked at the packet layer so it works even whengtidevents are excluded byincludeEvents;undefinedfor anonymous transactions - Update mysql2 to ^3.22.5; the internal APIs zongji relies on (addCommand, handlePacket, packet sequence validation) were verified unchanged, and a new regression test covers the binlog stream over a compressed connection
- Continuous integration now tests Node.js 22, 24 and 26 against MySQL 5.7, 8.0 and 8.4. Node.js 18 and 20 are end-of-life: they remain allowed by
engines(nothing in the code requires anything newer) and 0.7.0 passed the full test suite on both at release time, but they are no longer tested and future releases may break on them
- Updated .gitignore and .npmignore to exclude AI tool and build/test files
- Added npm version, downloads, node version, and licence badges to README
- Migrate from @vlasky/mysql to mysql2
- Convert codebase to ES modules
- Add TypeScript definitions
- Add official support for MySQL 8.4
- Fix sequence ID warnings when using compression with binlog streams
- Fix connection cleanup in stop() to prevent reuse of destroyed connections
- Add stopped flag to support dynamic filter updates and safe stop during init
- Fix flaky error test by handling all error events instead of just the first
- Allow BLOB columns with utf8mb3 charset
- Internal version bump
- Fix connection when binlog_checksum is NONE
- Update to @vlasky/mysql 2.18.5 with keepalive probe packet support
- Update to @vlasky/mysql 2.18.4 to support additional charset collations in MySQL 8
- Update to @vlasky/mysql 2.18.3 to support caching_sha2_password authentication plugin (MySQL 8 default)
- Update to @vlasky/mysql 2.18.2 to support new MySQL 8 error codes
- Handle table map events that change table IDs (from YousefED)
- Fix null value in JSON column causing buffer RangeError (from YousefED)
- MySQL 8 compatibility fix for column mapping query order
- Fix IEEE754 conversion error using DataView (from jefbarn)
- Update dependencies
- Initial fork from nevill/zongji
- Add binlog_row_image support