@vlasky/zongji 0.7.1 → 0.8.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,4 +1,85 @@
1
1
  import * as Common from './common.js';
2
+ import { collationToCharset, charsetMaxLength } from './charset_map.js';
3
+
4
+ // TABLE_MAP_EVENT optional metadata field type codes (MySQL 8.0+).
5
+ // binlog_row_metadata=MINIMAL writes SIGNEDNESS, the charset fields and
6
+ // GEOMETRY_TYPE; FULL adds COLUMN_NAME, SET/ENUM_STR_VALUE, the primary
7
+ // key fields and COLUMN_VISIBILITY.
8
+ const OptionalMetadataTypes = {
9
+ SIGNEDNESS: 1,
10
+ DEFAULT_CHARSET: 2,
11
+ COLUMN_CHARSET: 3,
12
+ COLUMN_NAME: 4,
13
+ SET_STR_VALUE: 5,
14
+ ENUM_STR_VALUE: 6,
15
+ GEOMETRY_TYPE: 7,
16
+ SIMPLE_PRIMARY_KEY: 8,
17
+ PRIMARY_KEY_WITH_PREFIX: 9,
18
+ ENUM_AND_SET_DEFAULT_CHARSET: 10,
19
+ ENUM_AND_SET_COLUMN_CHARSET: 11,
20
+ COLUMN_VISIBILITY: 12,
21
+ };
22
+
23
+ // Column classification used by the optional metadata bitmaps and lists,
24
+ // matching the server's is_numeric_type/is_character_type (sql/log_event.cc).
25
+ // The SIGNEDNESS bitmap covers numeric columns (YEAR included); the charset
26
+ // lists cover string columns (binary ones appear with collation 63), with
27
+ // ENUM and SET columns covered by their own ENUM_AND_SET_* fields.
28
+ const NUMERIC_METADATA_TYPES = new Set([
29
+ Common.MysqlTypes.DECIMAL,
30
+ Common.MysqlTypes.TINY,
31
+ Common.MysqlTypes.SHORT,
32
+ Common.MysqlTypes.INT24,
33
+ Common.MysqlTypes.LONG,
34
+ Common.MysqlTypes.LONGLONG,
35
+ Common.MysqlTypes.NEWDECIMAL,
36
+ Common.MysqlTypes.FLOAT,
37
+ Common.MysqlTypes.DOUBLE,
38
+ Common.MysqlTypes.YEAR,
39
+ ]);
40
+ const CHARACTER_METADATA_TYPES = new Set([
41
+ Common.MysqlTypes.VARCHAR,
42
+ Common.MysqlTypes.VAR_STRING,
43
+ Common.MysqlTypes.STRING,
44
+ Common.MysqlTypes.BLOB,
45
+ Common.MysqlTypes.TINY_BLOB,
46
+ Common.MysqlTypes.MEDIUM_BLOB,
47
+ Common.MysqlTypes.LONG_BLOB,
48
+ // MariaDB COMPRESSED columns count as character columns in the charset
49
+ // lists (verified against a live 11.8 FULL-metadata TableMap)
50
+ Common.MysqlTypes.BLOB_COMPRESSED,
51
+ Common.MysqlTypes.VARCHAR_COMPRESSED,
52
+ ]);
53
+ const BINARY_COLLATION_ID = 63;
54
+
55
+ const withPrecision = function(typeName, decimals) {
56
+ return decimals ? `${typeName}(${decimals})` : typeName;
57
+ };
58
+
59
+ // Field_geom::geometry_type values from the GEOMETRY_TYPE metadata field
60
+ const GEOMETRY_TYPE_NAMES = [
61
+ 'geometry', 'point', 'linestring', 'polygon',
62
+ 'multipoint', 'multilinestring', 'multipolygon', 'geomcollection',
63
+ ];
64
+
65
+ // The BLOB length prefix width distinguishes the four TEXT/BLOB sizes
66
+ const BLOB_TYPE_NAMES = {
67
+ 1: ['tinyblob', 'tinytext'],
68
+ 2: ['blob', 'text'],
69
+ 3: ['mediumblob', 'mediumtext'],
70
+ 4: ['longblob', 'longtext'],
71
+ };
72
+
73
+ // ENUM/SET value strings arrive as raw bytes in the column's own charset
74
+ const decodeEnumSetValues = function(buffers, charsetName) {
75
+ if (!buffers) return [];
76
+ return buffers.map(buffer => {
77
+ const decoded = Common.decodeTextColumn(buffer, charsetName);
78
+ // The value list must contain strings; on an unknown charset (the
79
+ // helper returns the buffer unchanged) fall back to utf8
80
+ return typeof decoded === 'string' ? decoded : buffer.toString('utf8');
81
+ });
82
+ };
2
83
 
3
84
  //TODO get rid parser from binlog event class
4
85
  // probably a factory to create them
@@ -56,16 +137,11 @@ class Format extends BinlogEvent {
56
137
  }
57
138
  }
58
139
 
59
- const formatUuid = function(buffer) {
60
- const hex = buffer.toString('hex');
61
- return [
62
- hex.slice(0, 8),
63
- hex.slice(8, 12),
64
- hex.slice(12, 16),
65
- hex.slice(16, 20),
66
- hex.slice(20)
67
- ].join('-');
68
- };
140
+ const formatUuid = Common.formatUuid;
141
+
142
+ // MariaDB log_bin_compress=ON event code (the rows variants live in
143
+ // rows_event.js)
144
+ const QUERY_COMPRESSED_EVENT = 0xa5;
69
145
 
70
146
  const skipEventRemainder = function(parser, zongji) {
71
147
  const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
@@ -75,15 +151,103 @@ const skipEventRemainder = function(parser, zongji) {
75
151
  }
76
152
  };
77
153
 
154
+ // MySQL 8.3+ GTID_TAGGED_LOG_EVENT; the classic code is 33
155
+ const GTID_TAGGED_LOG_EVENT = 42;
156
+
78
157
  class Gtid extends BinlogEvent {
79
158
  constructor(parser, options, zongji) {
80
159
  super(parser, options);
81
- this.flags = parser.parseUnsignedNumber(1);
82
- this.sid = formatUuid(parser.parseBuffer(16));
83
- this.gno = parser.parseUnsignedNumber(8);
84
- this.gtid = this.sid + ':' + this.gno;
160
+ if (options.eventType === GTID_TAGGED_LOG_EVENT) {
161
+ this._parseTagged(parser, zongji);
162
+ } else {
163
+ this.flags = parser.parseUnsignedNumber(1);
164
+ this.sid = formatUuid(parser.parseBuffer(16));
165
+ // Number within the safe range, exact string beyond it (like BIGINT
166
+ // values), so a colossal GNO degrades to a parse error downstream
167
+ // instead of silently corrupting the executed set
168
+ this.gno = Common.parseUInt64(parser);
169
+ this.gtid = this.sid + ':' + this.gno;
170
+ }
85
171
  skipEventRemainder(parser, zongji);
86
172
  }
173
+
174
+ // Tagged GTID events (written iff the transaction's GTID carries a
175
+ // tag) have no fixed layout: the body is one mysql::serialization
176
+ // message of varint-id-prefixed fields in ascending id order, with
177
+ // absent fields taking defaults (libs/mysql/binlog/event/
178
+ // control_events.h Gtid_event::define_fields, MySQL 8.4). Only the
179
+ // fields the classic event also exposes are kept; the rest
180
+ // (timestamps, logical clock, transaction length, server versions,
181
+ // commit group ticket) are skipped via the message's encoded size.
182
+ _parseTagged(parser, zongji) {
183
+ const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
184
+ const end = parser._packetEnd - checksumBytes;
185
+ const bodyStart = parser._offset;
186
+ const what = 'tagged GTID event';
187
+ const version = Common.parseSerializationVarint(parser, end, what);
188
+ if (version !== 1n) {
189
+ throw new Error(
190
+ `Unsupported tagged GTID serialization version ${version}`);
191
+ }
192
+ const encodedSize =
193
+ Number(Common.parseSerializationVarint(parser, end, what));
194
+ // The encoded size spans the whole message from the version varint;
195
+ // all further reads are bounded by it so a corrupt size cannot make
196
+ // field data leak in from beyond the message
197
+ const messageEnd = Math.min(bodyStart + encodedSize, end);
198
+ const varint = () =>
199
+ Common.parseSerializationVarint(parser, messageEnd, what);
200
+ const lastNonIgnorableId = varint();
201
+ if (lastNonIgnorableId > 12n) {
202
+ throw new Error('Tagged GTID event requires unknown field ' +
203
+ `${lastNonIgnorableId - 1n}`);
204
+ }
205
+ // Field ids up to 11 always encode as one byte (id << 1), so the
206
+ // next field's id can be peeked without consuming it
207
+ const nextFieldIs = (id) =>
208
+ parser._offset < messageEnd &&
209
+ parser._buffer[parser._offset] === id << 1;
210
+
211
+ this.flags = 0;
212
+ if (nextFieldIs(0)) {
213
+ parser.parseUnsignedNumber(1);
214
+ this.flags = Number(varint());
215
+ }
216
+ if (!nextFieldIs(1)) {
217
+ throw new Error('Tagged GTID event without a source UUID');
218
+ }
219
+ parser.parseUnsignedNumber(1);
220
+ // The 16 UUID bytes are serialized as 16 individual varints
221
+ // (1-2 wire bytes each), not as a raw byte string
222
+ const uuid = Buffer.alloc(16);
223
+ for (let i = 0; i < 16; i++) {
224
+ uuid[i] = Number(varint());
225
+ }
226
+ this.sid = formatUuid(uuid);
227
+ if (!nextFieldIs(2)) {
228
+ throw new Error('Tagged GTID event without a GNO');
229
+ }
230
+ parser.parseUnsignedNumber(1);
231
+ const gno =
232
+ Common.parseSerializationVarintSigned(parser, messageEnd, what);
233
+ this.gno = gno >= BigInt(Number.MIN_SAFE_INTEGER) &&
234
+ gno <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(gno) : String(gno);
235
+ this.tag = '';
236
+ if (nextFieldIs(3)) {
237
+ parser.parseUnsignedNumber(1);
238
+ const tagLength = Number(varint());
239
+ if (tagLength > 32 || parser._offset + tagLength > messageEnd) {
240
+ throw new Error('Invalid tagged GTID tag length ' + tagLength);
241
+ }
242
+ this.tag = parser.parseString(tagLength);
243
+ }
244
+ this.gtid = this.tag ?
245
+ `${this.sid}:${this.tag}:${this.gno}` : `${this.sid}:${this.gno}`;
246
+ // Remaining fields (ids 4-11 and any future additions) are skipped
247
+ if (parser._offset < messageEnd) {
248
+ parser._offset = messageEnd;
249
+ }
250
+ }
87
251
  }
88
252
 
89
253
  class AnonymousGtid extends BinlogEvent {
@@ -91,7 +255,7 @@ class AnonymousGtid extends BinlogEvent {
91
255
  super(parser, options);
92
256
  this.flags = parser.parseUnsignedNumber(1);
93
257
  this.sid = formatUuid(parser.parseBuffer(16));
94
- this.gno = parser.parseUnsignedNumber(8);
258
+ this.gno = Common.parseUInt64(parser);
95
259
  this.gtid = this.sid + ':' + this.gno;
96
260
  skipEventRemainder(parser, zongji);
97
261
  }
@@ -100,23 +264,76 @@ class AnonymousGtid extends BinlogEvent {
100
264
  class PreviousGtids extends BinlogEvent {
101
265
  constructor(parser, options, zongji) {
102
266
  super(parser, options);
103
- const sidCount = parser.parseUnsignedNumber(8);
267
+ // The first u64 packs a format code in its top byte (sql/
268
+ // rpl_gtid_set.cc encode_nsids_format, MySQL 8.3+). Format 0 is the
269
+ // classic encoding, where the whole u64 is the sid count. Format 1
270
+ // (written once the server has ever executed a tagged GTID) keeps
271
+ // the sid count in bits 8-55 and adds a tag field to every sid
272
+ // entry; a (uuid, tag) pair is its own entry, repeating the uuid.
273
+ const eventEnd = parser._packetEnd -
274
+ (zongji && zongji.useChecksum ? 4 : 0);
275
+ requireEventBytes(parser, eventEnd, 8, 'PreviousGtids');
276
+ let nSidsField = 0n;
277
+ for (let i = 0; i < 8; i++) {
278
+ nSidsField |= BigInt(parser.parseUnsignedNumber(1)) << BigInt(8 * i);
279
+ }
280
+ const format = nSidsField >> 56n;
281
+ let sidCount;
282
+ if (format === 0n) {
283
+ sidCount = Number(nSidsField);
284
+ } else if (format === 1n) {
285
+ sidCount = Number((nSidsField >> 8n) & 0xffffffffffffn);
286
+ } else {
287
+ throw new Error(`Unknown GTID set encoding format ${format}`);
288
+ }
289
+ const tagged = format === 1n;
104
290
  const sids = [];
105
291
  for (let i = 0; i < sidCount; i++) {
292
+ requireEventBytes(parser, eventEnd, 16, 'PreviousGtids');
106
293
  const sid = formatUuid(parser.parseBuffer(16));
294
+ let tag = '';
295
+ if (tagged) {
296
+ const tagLength = Number(Common.parseSerializationVarint(
297
+ parser, eventEnd, 'PreviousGtids tag'));
298
+ if (tagLength > 32) {
299
+ throw new Error(`Invalid GTID tag length ${tagLength}`);
300
+ }
301
+ requireEventBytes(parser, eventEnd, tagLength, 'PreviousGtids');
302
+ tag = parser.parseString(tagLength);
303
+ }
304
+ requireEventBytes(parser, eventEnd, 8, 'PreviousGtids');
107
305
  const intervalCount = parser.parseUnsignedNumber(8);
306
+ requireEventBytes(parser, eventEnd, intervalCount * 16,
307
+ 'PreviousGtids');
108
308
  const intervals = [];
109
309
  for (let j = 0; j < intervalCount; j++) {
110
310
  const start = parser.parseUnsignedNumber(8);
111
311
  const end = parser.parseUnsignedNumber(8);
112
312
  intervals.push({ start, end });
113
313
  }
114
- sids.push({ sid, intervals });
314
+ sids.push(tagged ? { sid, tag, intervals } : { sid, intervals });
115
315
  }
116
316
  this.sids = sids;
117
- this.gtidSet = sids.map(entry => {
118
- const ranges = entry.intervals.map(interval => `${interval.start}-${interval.end - 1}`);
119
- return `${entry.sid}:${ranges.join(':')}`;
317
+ // Entries sharing a uuid print as one block with the untagged
318
+ // intervals first: 'uuid:1-5:tag_a:1:tag_b:1', matching
319
+ // @@gtid_executed. The server writes entries in that order already;
320
+ // grouping here keeps the text parseable even if a non-canonical
321
+ // producer does not
322
+ const blocks = new Map();
323
+ for (const entry of sids) {
324
+ const group = blocks.get(entry.sid) || [];
325
+ group.push(entry);
326
+ blocks.set(entry.sid, group);
327
+ }
328
+ this.gtidSet = [...blocks.entries()].map(([sid, group]) => {
329
+ group.sort((a, b) => (a.tag || '') < (b.tag || '') ? -1 : 1);
330
+ return sid + group.map(entry => {
331
+ const ranges = entry.intervals.map(interval =>
332
+ interval.start === interval.end - 1 ?
333
+ `${interval.start}` :
334
+ `${interval.start}-${interval.end - 1}`);
335
+ return (entry.tag ? `:${entry.tag}` : '') + `:${ranges.join(':')}`;
336
+ }).join('');
120
337
  }).join(',');
121
338
  skipEventRemainder(parser, zongji);
122
339
  }
@@ -151,7 +368,7 @@ class Xid extends BinlogEvent {
151
368
  */
152
369
 
153
370
  class Query extends BinlogEvent {
154
- constructor(parser, options) {
371
+ constructor(parser, options, zongji) {
155
372
  super(parser, options);
156
373
 
157
374
  this.slaveProxyId = parser.parseUnsignedNumber(4);
@@ -164,8 +381,20 @@ class Query extends BinlogEvent {
164
381
  this.schema = parser.parseString(this.schemaLength);
165
382
  parser.parseUnsignedNumber(1);
166
383
 
167
- // all the left is the query
168
- this.query = parser.parseString(this.size - 13 - this.statusVarsLength - this.schemaLength - 1);
384
+ if (options.eventType === QUERY_COMPRESSED_EVENT) {
385
+ // MariaDB log_bin_compress=ON: identical to a plain Query event up
386
+ // to and including the schema name's NUL; the query text is
387
+ // replaced by a compressed-payload envelope
388
+ const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
389
+ const envelope = parser.parseBuffer(
390
+ parser._packetEnd - checksumBytes - parser._offset);
391
+ this.query =
392
+ Common.uncompressBinlogEventPayload(envelope).toString();
393
+ skipEventRemainder(parser, zongji);
394
+ } else {
395
+ // all the left is the query
396
+ this.query = parser.parseString(this.size - 13 - this.statusVarsLength - this.schemaLength - 1);
397
+ }
169
398
  }
170
399
 
171
400
  dump() {
@@ -246,8 +475,368 @@ class TableMap extends BinlogEvent {
246
475
  // column meta data length
247
476
  parser.parseLengthCodedNumber();
248
477
  this._readColumnMetadata(parser);
249
- // ignore the rest
478
+ try {
479
+ this._readOptionalMetadata(parser, zongji);
480
+ } catch {
481
+ // A malformed or unrecognised optional metadata block must not
482
+ // break the stream: without it we simply keep using the
483
+ // INFORMATION_SCHEMA metadata path, as before MySQL 8.0.
484
+ this.columnNames = undefined;
485
+ this.signedness = undefined;
486
+ this.primaryKey = undefined;
487
+ this.columnVisibility = undefined;
488
+ this.geometryTypes = undefined;
489
+ this._columnCharsetIds = undefined;
490
+ this._enumSetValues = undefined;
491
+ }
492
+ }
493
+ }
494
+
495
+ // The column type byte for ENUM and SET columns is STRING; their real
496
+ // type arrives in the per-column metadata (see _readColumnMetadata).
497
+ _effectiveColumnType(index) {
498
+ const metadata = this.columnsMetadata[index];
499
+ return (metadata && metadata.type) || this.columnTypes[index];
500
+ }
501
+
502
+ // Parses the optional metadata TLV block that MySQL 8.0+ appends to
503
+ // TABLE_MAP_EVENT (binlog_row_metadata=MINIMAL or FULL). Absent on older
504
+ // servers, in which case this reads nothing.
505
+ _readOptionalMetadata(parser, zongji) {
506
+ // One nullability bit per column sits between the per-column metadata
507
+ // and the optional metadata block
508
+ parser.parseBuffer(Math.floor((this.columnCount + 7) / 8));
509
+
510
+ const end = parser._packetEnd - (zongji && zongji.useChecksum ? 4 : 0);
511
+ if (parser._offset >= end) {
512
+ return;
513
+ }
514
+
515
+ // Optional metadata bitmaps and lists cover column subsets classified
516
+ // by real type, in table-definition order
517
+ const numericIndexes = [];
518
+ const characterIndexes = [];
519
+ const enumSetIndexes = [];
520
+ const enumIndexes = [];
521
+ const setIndexes = [];
522
+ const geometryIndexes = [];
523
+ for (let i = 0; i < this.columnCount; i++) {
524
+ const type = this._effectiveColumnType(i);
525
+ if (NUMERIC_METADATA_TYPES.has(type)) {
526
+ numericIndexes.push(i);
527
+ } else if (type === Common.MysqlTypes.ENUM) {
528
+ enumSetIndexes.push(i);
529
+ enumIndexes.push(i);
530
+ } else if (type === Common.MysqlTypes.SET) {
531
+ enumSetIndexes.push(i);
532
+ setIndexes.push(i);
533
+ } else if (CHARACTER_METADATA_TYPES.has(type)) {
534
+ characterIndexes.push(i);
535
+ } else if (type === Common.MysqlTypes.GEOMETRY) {
536
+ geometryIndexes.push(i);
537
+ }
538
+ }
539
+
540
+ // MSB-first bitmap with one bit per entry of `indexes`; distributes the
541
+ // bit values back to full-table column positions
542
+ const readBitmap = (indexes, target) => {
543
+ const bytes = parser.parseBuffer(Math.floor((indexes.length + 7) / 8));
544
+ indexes.forEach((columnIndex, i) => {
545
+ target[columnIndex] = (bytes[i >> 3] & (0x80 >> (i % 8))) !== 0;
546
+ });
547
+ };
548
+
549
+ // DEFAULT_CHARSET layout: default collation id, then pairs of
550
+ // (position among `indexes`, collation id) for columns that differ
551
+ const readDefaultCharset = (fieldEnd, indexes, target) => {
552
+ const defaultCollation = parser.parseLengthCodedNumber();
553
+ indexes.forEach(columnIndex => {
554
+ target[columnIndex] = defaultCollation;
555
+ });
556
+ while (parser._offset < fieldEnd) {
557
+ const position = parser.parseLengthCodedNumber();
558
+ const collation = parser.parseLengthCodedNumber();
559
+ if (position < indexes.length) {
560
+ target[indexes[position]] = collation;
561
+ }
562
+ }
563
+ };
564
+
565
+ // COLUMN_CHARSET layout: one collation id per entry of `indexes`
566
+ const readColumnCharsets = (fieldEnd, indexes, target) => {
567
+ for (let i = 0; parser._offset < fieldEnd; i++) {
568
+ const collation = parser.parseLengthCodedNumber();
569
+ if (i < indexes.length) {
570
+ target[indexes[i]] = collation;
571
+ }
572
+ }
573
+ };
574
+
575
+ // SET_STR_VALUE/ENUM_STR_VALUE layout, per column of that type: value
576
+ // count, then that many length-coded strings (raw bytes in the column's
577
+ // own charset; decoded in buildColumnSchemas once charsets are known)
578
+ const readTypeValues = (fieldEnd, indexes, target) => {
579
+ for (let i = 0; parser._offset < fieldEnd; i++) {
580
+ const count = parser.parseLengthCodedNumber();
581
+ const values = [];
582
+ for (let j = 0; j < count; j++) {
583
+ values.push(parser.parseBuffer(parser.parseLengthCodedNumber()));
584
+ }
585
+ if (i < indexes.length) {
586
+ target[indexes[i]] = values;
587
+ }
588
+ }
589
+ };
590
+
591
+ const charsetIds = new Array(this.columnCount);
592
+ const Types = OptionalMetadataTypes;
593
+
594
+ while (parser._offset < end) {
595
+ const type = parser.parseUnsignedNumber(1);
596
+ const length = parser.parseLengthCodedNumber();
597
+ const fieldEnd = parser._offset + length;
598
+ if (length === null || fieldEnd > end) {
599
+ throw new Error('Malformed TABLE_MAP_EVENT optional metadata');
600
+ }
601
+
602
+ let handled = true;
603
+ switch (type) {
604
+ case Types.SIGNEDNESS:
605
+ this.signedness = new Array(this.columnCount);
606
+ readBitmap(numericIndexes, this.signedness);
607
+ break;
608
+ case Types.DEFAULT_CHARSET:
609
+ readDefaultCharset(fieldEnd, characterIndexes, charsetIds);
610
+ break;
611
+ case Types.COLUMN_CHARSET:
612
+ readColumnCharsets(fieldEnd, characterIndexes, charsetIds);
613
+ break;
614
+ case Types.COLUMN_NAME: {
615
+ const names = [];
616
+ while (parser._offset < fieldEnd) {
617
+ names.push(parser.parseString(parser.parseLengthCodedNumber()));
618
+ }
619
+ this.columnNames = names;
620
+ break;
621
+ }
622
+ case Types.SET_STR_VALUE:
623
+ this._enumSetValues = this._enumSetValues || new Array(this.columnCount);
624
+ readTypeValues(fieldEnd, setIndexes, this._enumSetValues);
625
+ break;
626
+ case Types.ENUM_STR_VALUE:
627
+ this._enumSetValues = this._enumSetValues || new Array(this.columnCount);
628
+ readTypeValues(fieldEnd, enumIndexes, this._enumSetValues);
629
+ break;
630
+ case Types.ENUM_AND_SET_DEFAULT_CHARSET:
631
+ readDefaultCharset(fieldEnd, enumSetIndexes, charsetIds);
632
+ break;
633
+ case Types.ENUM_AND_SET_COLUMN_CHARSET:
634
+ readColumnCharsets(fieldEnd, enumSetIndexes, charsetIds);
635
+ break;
636
+ case Types.SIMPLE_PRIMARY_KEY: {
637
+ const primaryKey = [];
638
+ while (parser._offset < fieldEnd) {
639
+ primaryKey.push(parser.parseLengthCodedNumber());
640
+ }
641
+ this.primaryKey = primaryKey;
642
+ break;
643
+ }
644
+ case Types.PRIMARY_KEY_WITH_PREFIX: {
645
+ const primaryKey = [];
646
+ while (parser._offset < fieldEnd) {
647
+ const columnIndex = parser.parseLengthCodedNumber();
648
+ // Prefix length in characters; 0 means the whole column
649
+ parser.parseLengthCodedNumber();
650
+ primaryKey.push(columnIndex);
651
+ }
652
+ this.primaryKey = primaryKey;
653
+ break;
654
+ }
655
+ case Types.GEOMETRY_TYPE: {
656
+ this.geometryTypes = new Array(this.columnCount);
657
+ for (let i = 0; parser._offset < fieldEnd; i++) {
658
+ const geometryType = parser.parseLengthCodedNumber();
659
+ if (i < geometryIndexes.length) {
660
+ this.geometryTypes[geometryIndexes[i]] = geometryType;
661
+ }
662
+ }
663
+ break;
664
+ }
665
+ case Types.COLUMN_VISIBILITY:
666
+ this.columnVisibility = new Array(this.columnCount).fill(false);
667
+ readBitmap(
668
+ Array.from({ length: this.columnCount }, (value, i) => i),
669
+ this.columnVisibility);
670
+ break;
671
+ default:
672
+ // Unknown future fields (e.g. VECTOR dimensionality in MySQL 9)
673
+ // are skipped via fieldEnd below
674
+ handled = false;
675
+ }
676
+
677
+ // Every known field type consumes its declared length exactly; a
678
+ // mismatch in either direction means the block is corrupt, so treat
679
+ // all optional metadata as unusable rather than trusting partially
680
+ // garbled values (handled by the caller's catch)
681
+ if (handled ? parser._offset !== fieldEnd : parser._offset > fieldEnd) {
682
+ throw new Error('Malformed TABLE_MAP_EVENT optional metadata');
683
+ }
684
+ parser._offset = fieldEnd;
685
+ }
686
+
687
+ this._columnCharsetIds = charsetIds;
688
+ }
689
+
690
+ // True when the event carries binlog_row_metadata=FULL metadata, i.e.
691
+ // enough to decode rows without consulting INFORMATION_SCHEMA
692
+ hasSelfDescribingMetadata() {
693
+ return this.columnNames !== undefined &&
694
+ this.columnNames.length === this.columnCount;
695
+ }
696
+
697
+ // Classic temporal type codes are ambiguous on MariaDB: a 5.3-era
698
+ // "hires" column (fractional seconds, different byte order and width)
699
+ // is binlogged under the same codes with the same (zero) metadata as a
700
+ // classic column, and even FULL optional metadata carries nothing to
701
+ // tell them apart. Only the table definition can, so tables containing
702
+ // these codes must use the INFORMATION_SCHEMA path.
703
+ hasAmbiguousTemporalColumns() {
704
+ return this.columnTypes.some(type =>
705
+ type === Common.MysqlTypes.TIMESTAMP ||
706
+ type === Common.MysqlTypes.TIME ||
707
+ type === Common.MysqlTypes.DATETIME);
708
+ }
709
+
710
+ // Builds INFORMATION_SCHEMA.COLUMNS-shaped rows from the event's own
711
+ // metadata, so the rest of the pipeline works identically with either
712
+ // metadata source. Only meaningful when hasSelfDescribingMetadata().
713
+ buildColumnSchemas() {
714
+ const schemas = [];
715
+ for (let i = 0; i < this.columnCount; i++) {
716
+ const type = this._effectiveColumnType(i);
717
+ const metadata =
718
+ /** @type {Record<string, any>} */ (this.columnsMetadata[i] || {});
719
+ const charsetId = this._columnCharsetIds && this._columnCharsetIds[i];
720
+ const binary = charsetId === BINARY_COLLATION_ID;
721
+ const charsetName = charsetId === undefined || binary ?
722
+ null : collationToCharset(charsetId);
723
+ const unsigned = this.signedness && this.signedness[i];
724
+
725
+ const schema = {
726
+ COLUMN_NAME: this.columnNames[i],
727
+ COLLATION_NAME: null,
728
+ CHARACTER_SET_NAME: charsetName,
729
+ COLUMN_COMMENT: '',
730
+ COLUMN_TYPE: '',
731
+ };
732
+
733
+ const Mysql = Common.MysqlTypes;
734
+ switch (type) {
735
+ case Mysql.TINY: schema.COLUMN_TYPE = 'tinyint'; break;
736
+ case Mysql.SHORT: schema.COLUMN_TYPE = 'smallint'; break;
737
+ case Mysql.INT24: schema.COLUMN_TYPE = 'mediumint'; break;
738
+ case Mysql.LONG: schema.COLUMN_TYPE = 'int'; break;
739
+ case Mysql.LONGLONG: schema.COLUMN_TYPE = 'bigint'; break;
740
+ case Mysql.FLOAT: schema.COLUMN_TYPE = 'float'; break;
741
+ case Mysql.DOUBLE: schema.COLUMN_TYPE = 'double'; break;
742
+ case Mysql.DECIMAL:
743
+ case Mysql.NEWDECIMAL:
744
+ schema.COLUMN_TYPE =
745
+ `decimal(${metadata.precision},${metadata.decimals})`;
746
+ break;
747
+ case Mysql.YEAR: schema.COLUMN_TYPE = 'year'; break;
748
+ case Mysql.STRING:
749
+ case Mysql.VARCHAR:
750
+ case Mysql.VAR_STRING: {
751
+ // Binlog metadata stores byte widths; character widths follow
752
+ // from the charset's maximum bytes per character
753
+ const isChar = type === Mysql.STRING;
754
+ if (binary) {
755
+ schema.COLUMN_TYPE =
756
+ (isChar ? 'binary' : 'varbinary') +
757
+ `(${metadata['max_length']})`;
758
+ } else {
759
+ const chars =
760
+ metadata['max_length'] / charsetMaxLength(charsetName);
761
+ schema.COLUMN_TYPE = (isChar ? 'char' : 'varchar') +
762
+ `(${Math.floor(chars)})`;
763
+ }
764
+ break;
765
+ }
766
+ case Mysql.TINY_BLOB:
767
+ case Mysql.MEDIUM_BLOB:
768
+ case Mysql.LONG_BLOB:
769
+ case Mysql.BLOB:
770
+ schema.COLUMN_TYPE = (BLOB_TYPE_NAMES[metadata['length_size']] ||
771
+ BLOB_TYPE_NAMES[2])[binary ? 0 : 1];
772
+ break;
773
+ case Mysql.BLOB_COMPRESSED:
774
+ // Same I_S form MariaDB itself reports for COMPRESSED columns
775
+ schema.COLUMN_TYPE = (BLOB_TYPE_NAMES[metadata['length_size']] ||
776
+ BLOB_TYPE_NAMES[2])[binary ? 0 : 1] + ' /*M!100301 COMPRESSED*/';
777
+ break;
778
+ case Mysql.VARCHAR_COMPRESSED: {
779
+ // max_length reserves one byte for the compression header
780
+ const byteLength = metadata['max_length'] - 1;
781
+ if (binary) {
782
+ schema.COLUMN_TYPE =
783
+ `varbinary(${byteLength}) /*M!100301 COMPRESSED*/`;
784
+ } else {
785
+ const chars = byteLength / charsetMaxLength(charsetName);
786
+ schema.COLUMN_TYPE =
787
+ `varchar(${Math.floor(chars)}) /*M!100301 COMPRESSED*/`;
788
+ }
789
+ break;
790
+ }
791
+ case Mysql.ENUM:
792
+ case Mysql.SET: {
793
+ const values = decodeEnumSetValues(
794
+ this._enumSetValues && this._enumSetValues[i], charsetName);
795
+ if (type === Mysql.ENUM) {
796
+ schema.ENUM_VALUES = values;
797
+ } else {
798
+ schema.SET_VALUES = values;
799
+ }
800
+ const keyword = type === Mysql.ENUM ? 'enum' : 'set';
801
+ schema.COLUMN_TYPE = keyword + '(' +
802
+ values.map(value => `'${value.replace(/'/g, "''")}'`).join(',') +
803
+ ')';
804
+ break;
805
+ }
806
+ case Mysql.BIT: schema.COLUMN_TYPE = `bit(${metadata.bits})`; break;
807
+ case Mysql.JSON: schema.COLUMN_TYPE = 'json'; break;
808
+ case Mysql.GEOMETRY:
809
+ schema.COLUMN_TYPE = GEOMETRY_TYPE_NAMES[
810
+ this.geometryTypes && this.geometryTypes[i]] || 'geometry';
811
+ break;
812
+ case Mysql.TIMESTAMP:
813
+ case Mysql.TIMESTAMP2:
814
+ schema.COLUMN_TYPE = withPrecision('timestamp', metadata.decimals);
815
+ break;
816
+ case Mysql.DATETIME:
817
+ case Mysql.DATETIME2:
818
+ schema.COLUMN_TYPE = withPrecision('datetime', metadata.decimals);
819
+ break;
820
+ case Mysql.TIME:
821
+ case Mysql.TIME2:
822
+ schema.COLUMN_TYPE = withPrecision('time', metadata.decimals);
823
+ break;
824
+ case Mysql.DATE:
825
+ case Mysql.NEWDATE:
826
+ schema.COLUMN_TYPE = 'date';
827
+ break;
828
+ }
829
+
830
+ if (unsigned !== undefined && NUMERIC_METADATA_TYPES.has(type)) {
831
+ schema.UNSIGNED = unsigned;
832
+ if (unsigned && type !== Mysql.YEAR) {
833
+ schema.COLUMN_TYPE += ' unsigned';
834
+ }
835
+ }
836
+
837
+ schemas.push(schema);
250
838
  }
839
+ return schemas;
251
840
  }
252
841
 
253
842
  updateColumnInfo() {
@@ -271,6 +860,18 @@ class TableMap extends BinlogEvent {
271
860
  'columns, fetched metadata has ' +
272
861
  `${columnSchemas ? columnSchemas.length : 0}`);
273
862
  }
863
+
864
+ // Even under binlog_row_metadata=MINIMAL the binlog carries exact
865
+ // signedness as of the event's write time; it always wins over
866
+ // inference from the INFORMATION_SCHEMA column definition (see
867
+ // parseAnyInt in common.js)
868
+ if (this.signedness && columnSchemas.length === this.columnCount) {
869
+ for (let i = 0; i < this.columnCount; i++) {
870
+ if (this.signedness[i] !== undefined) {
871
+ columnSchemas[i].UNSIGNED = this.signedness[i];
872
+ }
873
+ }
874
+ }
274
875
  const columns = [];
275
876
  for (let j = 0; j < this.columnCount; j++) {
276
877
  columns.push({
@@ -297,6 +898,7 @@ class TableMap extends BinlogEvent {
297
898
  };
298
899
  break;
299
900
  case Common.MysqlTypes.VARCHAR:
901
+ case Common.MysqlTypes.VARCHAR_COMPRESSED:
300
902
  result = {
301
903
  'max_length': parser.parseUnsignedNumber(2)
302
904
  };
@@ -316,6 +918,7 @@ class TableMap extends BinlogEvent {
316
918
  };
317
919
  break;
318
920
  case Common.MysqlTypes.BLOB:
921
+ case Common.MysqlTypes.BLOB_COMPRESSED:
319
922
  case Common.MysqlTypes.GEOMETRY:
320
923
  case Common.MysqlTypes.JSON:
321
924
  result = {
@@ -374,6 +977,250 @@ class Unknown extends BinlogEvent {
374
977
  }
375
978
  }
376
979
 
980
+ /* Sent by the server instead of real events: while the connection idles
981
+ * (when a heartbeat period is configured), and after transactions were
982
+ * skipped server-side during a GTID dump, so the client's position can
983
+ * advance past them. Carries the current binlog filename; nextPosition
984
+ * holds the advanced position.
985
+ */
986
+ class Heartbeat extends BinlogEvent {
987
+ constructor(parser, options, zongji) {
988
+ super(parser, options);
989
+ const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
990
+ // MariaDB: when the heartbeat position exceeds 4 GiB the header
991
+ // log_pos is 0 and a u64 position sub-header precedes the file name
992
+ // (sql/sql_repl.cc send_heartbeat_event, HB_SUB_HEADER_LEN)
993
+ if (zongji && zongji.isMariaDb && options.nextPosition === 0 &&
994
+ parser._packetEnd - checksumBytes - parser._offset >= 8) {
995
+ this.position = Common.parseUInt64(parser);
996
+ }
997
+ this.binlogName = parser.parseString(
998
+ parser._packetEnd - checksumBytes - parser._offset);
999
+ skipEventRemainder(parser, zongji);
1000
+ }
1001
+ }
1002
+
1003
+ /* MariaDB GTID_EVENT (code 162): replaces the BEGIN Query event of a
1004
+ * transaction (FL_STANDALONE clear) or marks a standalone group such as a
1005
+ * DDL statement (FL_STANDALONE set). The GTID itself is
1006
+ * domain_id - server_id - seq_no, where server_id comes from the common
1007
+ * event header. Layout: sql/log_event.cc Gtid_log_event; fields after
1008
+ * flags2 are conditional and the data area is zero-padded to the 19-byte
1009
+ * post-header length when shorter.
1010
+ */
1011
+ const MARIADB_GTID_FLAGS2 = {
1012
+ FL_STANDALONE: 0x01,
1013
+ FL_GROUP_COMMIT_ID: 0x02,
1014
+ FL_TRANSACTIONAL: 0x04,
1015
+ FL_ALLOW_PARALLEL: 0x08,
1016
+ FL_WAITED: 0x10,
1017
+ FL_DDL: 0x20,
1018
+ FL_PREPARED_XA: 0x40,
1019
+ FL_COMPLETED_XA: 0x80,
1020
+ };
1021
+ const MARIADB_GTID_FLAGS_EXTRA = {
1022
+ FL_EXTRA_MULTI_ENGINE: 0x01,
1023
+ FL_START_ALTER: 0x02,
1024
+ FL_COMMIT_ALTER: 0x04,
1025
+ FL_ROLLBACK_ALTER: 0x08,
1026
+ FL_EXTRA_THREAD_ID: 0x10,
1027
+ };
1028
+
1029
+ // The parser's own bounds run to the packet end INCLUDING the trailing
1030
+ // CRC32, so a truncated event body could silently consume checksum bytes
1031
+ // as field data; every fixed or length-prefixed read must be bounded by
1032
+ // the checksum-excluded end instead
1033
+ const requireEventBytes = function(parser, end, bytes, typeName) {
1034
+ if (parser._offset + bytes > end) {
1035
+ throw new Error(
1036
+ `Truncated ${typeName} event: ${bytes} bytes needed, ` +
1037
+ `${end - parser._offset} available`);
1038
+ }
1039
+ };
1040
+
1041
+ class MariadbGtid extends BinlogEvent {
1042
+ constructor(parser, options, zongji) {
1043
+ super(parser, options);
1044
+ const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
1045
+ const end = parser._packetEnd - checksumBytes;
1046
+
1047
+ requireEventBytes(parser, end, 13, 'MariadbGtid');
1048
+ this.seqNo = Common.parseUInt64(parser);
1049
+ this.domainId = parser.parseUnsignedNumber(4);
1050
+ this.serverId = options.serverId;
1051
+ this.flags2 = parser.parseUnsignedNumber(1);
1052
+ this.standalone =
1053
+ (this.flags2 & MARIADB_GTID_FLAGS2.FL_STANDALONE) !== 0;
1054
+ this.transactional =
1055
+ (this.flags2 & MARIADB_GTID_FLAGS2.FL_TRANSACTIONAL) !== 0;
1056
+ this.isDdl = (this.flags2 & MARIADB_GTID_FLAGS2.FL_DDL) !== 0;
1057
+ this.preparedXa =
1058
+ (this.flags2 & MARIADB_GTID_FLAGS2.FL_PREPARED_XA) !== 0;
1059
+ this.completedXa =
1060
+ (this.flags2 & MARIADB_GTID_FLAGS2.FL_COMPLETED_XA) !== 0;
1061
+
1062
+ if (this.flags2 & MARIADB_GTID_FLAGS2.FL_GROUP_COMMIT_ID) {
1063
+ requireEventBytes(parser, end, 8, 'MariadbGtid');
1064
+ this.commitId = Common.parseUInt64(parser);
1065
+ }
1066
+ if (this.preparedXa || this.completedXa) {
1067
+ requireEventBytes(parser, end, 6, 'MariadbGtid');
1068
+ this.xidFormatId = parser.parseUnsignedNumber(4);
1069
+ const gtridLength = parser.parseUnsignedNumber(1);
1070
+ const bqualLength = parser.parseUnsignedNumber(1);
1071
+ requireEventBytes(parser, end, gtridLength + bqualLength,
1072
+ 'MariadbGtid');
1073
+ this.xidGtrid = parser.parseBuffer(gtridLength);
1074
+ this.xidBqual = parser.parseBuffer(bqualLength);
1075
+ }
1076
+ // Remaining bytes (if any) start with flags_extra; a zero pad byte
1077
+ // reads as flags_extra = 0, adding nothing
1078
+ if (parser._offset < end) {
1079
+ this.flagsExtra = parser.parseUnsignedNumber(1);
1080
+ if (this.flagsExtra & MARIADB_GTID_FLAGS_EXTRA.FL_EXTRA_MULTI_ENGINE) {
1081
+ requireEventBytes(parser, end, 1, 'MariadbGtid');
1082
+ this.extraEngines = parser.parseUnsignedNumber(1);
1083
+ }
1084
+ if (this.flagsExtra & (MARIADB_GTID_FLAGS_EXTRA.FL_COMMIT_ALTER |
1085
+ MARIADB_GTID_FLAGS_EXTRA.FL_ROLLBACK_ALTER)) {
1086
+ requireEventBytes(parser, end, 8, 'MariadbGtid');
1087
+ this.saSeqNo = Common.parseUInt64(parser);
1088
+ }
1089
+ if ((this.flagsExtra & MARIADB_GTID_FLAGS_EXTRA.FL_EXTRA_THREAD_ID) &&
1090
+ parser._offset + 4 <= end) {
1091
+ this.threadId = parser.parseUnsignedNumber(4);
1092
+ }
1093
+ }
1094
+
1095
+ this.gtid = `${this.domainId}-${this.serverId}-${this.seqNo}`;
1096
+ skipEventRemainder(parser, zongji);
1097
+ }
1098
+ }
1099
+
1100
+ /* MariaDB GTID_LIST_EVENT (code 163): written at the start of every binlog
1101
+ * file with the last GTID per (domain, server) seen so far - MariaDB's
1102
+ * analogue of MySQL's Previous_gtids. Also sent as an artificial event on
1103
+ * GTID connects that seek mid-file. A domain may have several entries (one
1104
+ * per server_id); the last entry of a domain's run is the most recent.
1105
+ */
1106
+ class MariadbGtidList extends BinlogEvent {
1107
+ constructor(parser, options, zongji) {
1108
+ super(parser, options);
1109
+ const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
1110
+ const end = parser._packetEnd - checksumBytes;
1111
+ requireEventBytes(parser, end, 4, 'MariadbGtidList');
1112
+ const val = parser.parseUnsignedNumber(4);
1113
+ this.count = val & 0x0fffffff;
1114
+ // Bit 28: FLAG_UNTIL_REACHED (fake events only); bit 29: FLAG_IGN_GTIDS
1115
+ this.flags = val >>> 28;
1116
+ this.gtids = [];
1117
+ for (let i = 0; i < this.count; i++) {
1118
+ requireEventBytes(parser, end, 16, 'MariadbGtidList');
1119
+ const domainId = parser.parseUnsignedNumber(4);
1120
+ const serverId = parser.parseUnsignedNumber(4);
1121
+ const seqNo = Common.parseUInt64(parser);
1122
+ this.gtids.push({
1123
+ domainId,
1124
+ serverId,
1125
+ seqNo,
1126
+ gtid: `${domainId}-${serverId}-${seqNo}`,
1127
+ });
1128
+ }
1129
+ skipEventRemainder(parser, zongji);
1130
+ }
1131
+ }
1132
+
1133
+ /* MariaDB BINLOG_CHECKPOINT_EVENT (code 161): an XA-recovery marker naming
1134
+ * the oldest binlog file that may still be needed for crash recovery. Not
1135
+ * used in replication; exposed for observability only.
1136
+ */
1137
+ class BinlogCheckpoint extends BinlogEvent {
1138
+ constructor(parser, options, zongji) {
1139
+ super(parser, options);
1140
+ const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
1141
+ const end = parser._packetEnd - checksumBytes;
1142
+ requireEventBytes(parser, end, 4, 'BinlogCheckpoint');
1143
+ const length = parser.parseUnsignedNumber(4);
1144
+ requireEventBytes(parser, end, length, 'BinlogCheckpoint');
1145
+ this.binlogName = parser.parseString(length);
1146
+ skipEventRemainder(parser, zongji);
1147
+ }
1148
+ }
1149
+
1150
+ /* MariaDB ANNOTATE_ROWS_EVENT (code 160): the SQL statement text for the
1151
+ * row events that follow, MariaDB's analogue of MySQL's
1152
+ * ROWS_QUERY_LOG_EVENT. Only sent when the dump requests it (the
1153
+ * BINLOG_SEND_ANNOTATE_ROWS_EVENT flag).
1154
+ */
1155
+ class AnnotateRows extends BinlogEvent {
1156
+ constructor(parser, options, zongji) {
1157
+ super(parser, options);
1158
+ const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
1159
+ this.statement = parser.parseString(
1160
+ parser._packetEnd - checksumBytes - parser._offset);
1161
+ skipEventRemainder(parser, zongji);
1162
+ }
1163
+ }
1164
+
1165
+ /* MySQL ROWS_QUERY_LOG_EVENT (code 29): the SQL statement text for the
1166
+ * row events that follow, sent to every consumer while the server runs
1167
+ * with binlog_rows_query_log_events=ON. The body starts with one length
1168
+ * byte that only holds the statement length mod 256; readers ignore it
1169
+ * and take the text to the end of the event
1170
+ * (sql/log_event.cc Rows_query_log_event).
1171
+ */
1172
+ class RowsQuery extends BinlogEvent {
1173
+ constructor(parser, options, zongji) {
1174
+ super(parser, options);
1175
+ const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
1176
+ const end = parser._packetEnd - checksumBytes;
1177
+ requireEventBytes(parser, end, 1, 'RowsQuery');
1178
+ parser.parseUnsignedNumber(1);
1179
+ this.statement = parser.parseString(end - parser._offset);
1180
+ skipEventRemainder(parser, zongji);
1181
+ }
1182
+ }
1183
+
1184
+ /* MariaDB START_ENCRYPTION_EVENT (code 164): sent once after the format
1185
+ * description event when the binlog file on disk is encrypted. The dump
1186
+ * thread decrypts server-side, so the stream itself is cleartext and this
1187
+ * event is informational; it arrives flagged LOG_EVENT_IGNORABLE_F.
1188
+ */
1189
+ class StartEncryption extends BinlogEvent {
1190
+ constructor(parser, options, zongji) {
1191
+ super(parser, options);
1192
+ const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
1193
+ requireEventBytes(parser, parser._packetEnd - checksumBytes, 17,
1194
+ 'StartEncryption');
1195
+ this.scheme = parser.parseUnsignedNumber(1);
1196
+ this.keyVersion = parser.parseUnsignedNumber(4);
1197
+ this.nonce = parser.parseBuffer(12);
1198
+ skipEventRemainder(parser, zongji);
1199
+ }
1200
+ }
1201
+
1202
+ /* XA_PREPARE_LOG_EVENT (code 38): terminates an XA-prepared event group
1203
+ * (MySQL 5.7+ and MariaDB share the layout). one_phase distinguishes
1204
+ * XA COMMIT ... ONE PHASE, which commits immediately.
1205
+ */
1206
+ class XaPrepare extends BinlogEvent {
1207
+ constructor(parser, options, zongji) {
1208
+ super(parser, options);
1209
+ const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
1210
+ const end = parser._packetEnd - checksumBytes;
1211
+ requireEventBytes(parser, end, 13, 'XaPrepare');
1212
+ this.onePhase = parser.parseUnsignedNumber(1) !== 0;
1213
+ this.xidFormatId = parser.parseUnsignedNumber(4);
1214
+ const gtridLength = parser.parseUnsignedNumber(4);
1215
+ const bqualLength = parser.parseUnsignedNumber(4);
1216
+ requireEventBytes(parser, end, gtridLength + bqualLength, 'XaPrepare');
1217
+ this.xidGtrid = parser.parseBuffer(gtridLength);
1218
+ this.xidBqual = parser.parseBuffer(bqualLength);
1219
+ skipEventRemainder(parser, zongji);
1220
+ }
1221
+ }
1222
+
1223
+
377
1224
  /* MySQL 8.0.20+ wraps whole transactions in a single compressed event when
378
1225
  * binlog_transaction_compression=ON. The embedded row events are
379
1226
  * zstd-compressed and zongji cannot decode them, so the row changes they
@@ -424,5 +1271,13 @@ export {
424
1271
  PreviousGtids,
425
1272
  TransactionPayload,
426
1273
  PartialUpdateRows,
1274
+ Heartbeat,
1275
+ MariadbGtid,
1276
+ MariadbGtidList,
1277
+ BinlogCheckpoint,
1278
+ AnnotateRows,
1279
+ RowsQuery,
1280
+ StartEncryption,
1281
+ XaPrepare,
427
1282
  Unknown
428
1283
  };