@vlasky/zongji 0.7.1 → 0.9.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,102 @@ 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 = Common.bigintToNumberOrString(gno);
234
+ this.tag = '';
235
+ if (nextFieldIs(3)) {
236
+ parser.parseUnsignedNumber(1);
237
+ const tagLength = Number(varint());
238
+ if (tagLength > 32 || parser._offset + tagLength > messageEnd) {
239
+ throw new Error('Invalid tagged GTID tag length ' + tagLength);
240
+ }
241
+ this.tag = parser.parseString(tagLength);
242
+ }
243
+ this.gtid = this.tag ?
244
+ `${this.sid}:${this.tag}:${this.gno}` : `${this.sid}:${this.gno}`;
245
+ // Remaining fields (ids 4-11 and any future additions) are skipped
246
+ if (parser._offset < messageEnd) {
247
+ parser._offset = messageEnd;
248
+ }
249
+ }
87
250
  }
88
251
 
89
252
  class AnonymousGtid extends BinlogEvent {
@@ -91,7 +254,7 @@ class AnonymousGtid extends BinlogEvent {
91
254
  super(parser, options);
92
255
  this.flags = parser.parseUnsignedNumber(1);
93
256
  this.sid = formatUuid(parser.parseBuffer(16));
94
- this.gno = parser.parseUnsignedNumber(8);
257
+ this.gno = Common.parseUInt64(parser);
95
258
  this.gtid = this.sid + ':' + this.gno;
96
259
  skipEventRemainder(parser, zongji);
97
260
  }
@@ -100,23 +263,87 @@ class AnonymousGtid extends BinlogEvent {
100
263
  class PreviousGtids extends BinlogEvent {
101
264
  constructor(parser, options, zongji) {
102
265
  super(parser, options);
103
- const sidCount = parser.parseUnsignedNumber(8);
266
+ // The first u64 packs a format code in its top byte (sql/
267
+ // rpl_gtid_set.cc encode_nsids_format, MySQL 8.3+). Format 0 is the
268
+ // classic encoding, where the whole u64 is the sid count. Format 1
269
+ // (written once the server has ever executed a tagged GTID) keeps
270
+ // the sid count in bits 8-55 and adds a tag field to every sid
271
+ // entry; a (uuid, tag) pair is its own entry, repeating the uuid.
272
+ const eventEnd = parser._packetEnd -
273
+ (zongji && zongji.useChecksum ? 4 : 0);
274
+ requireEventBytes(parser, eventEnd, 8, 'PreviousGtids');
275
+ let nSidsField = 0n;
276
+ for (let i = 0; i < 8; i++) {
277
+ nSidsField |= BigInt(parser.parseUnsignedNumber(1)) << BigInt(8 * i);
278
+ }
279
+ const format = nSidsField >> 56n;
280
+ let sidCount;
281
+ if (format === 0n) {
282
+ sidCount = Number(nSidsField);
283
+ } else if (format === 1n) {
284
+ sidCount = Number((nSidsField >> 8n) & 0xffffffffffffn);
285
+ } else {
286
+ throw new Error(`Unknown GTID set encoding format ${format}`);
287
+ }
288
+ const tagged = format === 1n;
104
289
  const sids = [];
105
290
  for (let i = 0; i < sidCount; i++) {
291
+ requireEventBytes(parser, eventEnd, 16, 'PreviousGtids');
106
292
  const sid = formatUuid(parser.parseBuffer(16));
293
+ let tag = '';
294
+ if (tagged) {
295
+ const tagLength = Number(Common.parseSerializationVarint(
296
+ parser, eventEnd, 'PreviousGtids tag'));
297
+ if (tagLength > 32) {
298
+ throw new Error(`Invalid GTID tag length ${tagLength}`);
299
+ }
300
+ requireEventBytes(parser, eventEnd, tagLength, 'PreviousGtids');
301
+ tag = parser.parseString(tagLength);
302
+ }
303
+ requireEventBytes(parser, eventEnd, 8, 'PreviousGtids');
107
304
  const intervalCount = parser.parseUnsignedNumber(8);
305
+ requireEventBytes(parser, eventEnd, intervalCount * 16,
306
+ 'PreviousGtids');
108
307
  const intervals = [];
109
308
  for (let j = 0; j < intervalCount; j++) {
110
- const start = parser.parseUnsignedNumber(8);
111
- const end = parser.parseUnsignedNumber(8);
309
+ // u64 bounds as BigInt: parseUnsignedNumber(8) would silently
310
+ // round beyond 2^53, corrupting the gtidSet text this event seeds
311
+ // the executed-set tracker with
312
+ const start = parser.parseBuffer(8).readBigUInt64LE(0);
313
+ const end = parser.parseBuffer(8).readBigUInt64LE(0);
112
314
  intervals.push({ start, end });
113
315
  }
114
- sids.push({ sid, intervals });
316
+ sids.push(tagged ? { sid, tag, intervals } : { sid, intervals });
317
+ }
318
+ // The public sids follow the Number-or-exact-string convention for
319
+ // 64-bit values (events must stay JSON-serialisable, so no BigInt)
320
+ this.sids = sids.map(entry => ({
321
+ ...entry,
322
+ intervals: entry.intervals.map(({ start, end }) => ({
323
+ start: Common.bigintToNumberOrString(start),
324
+ end: Common.bigintToNumberOrString(end),
325
+ })),
326
+ }));
327
+ // Entries sharing a uuid print as one block with the untagged
328
+ // intervals first: 'uuid:1-5:tag_a:1:tag_b:1', matching
329
+ // @@gtid_executed. The server writes entries in that order already;
330
+ // grouping here keeps the text parseable even if a non-canonical
331
+ // producer does not
332
+ const blocks = new Map();
333
+ for (const entry of sids) {
334
+ const group = blocks.get(entry.sid) || [];
335
+ group.push(entry);
336
+ blocks.set(entry.sid, group);
115
337
  }
116
- 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(':')}`;
338
+ this.gtidSet = [...blocks.entries()].map(([sid, group]) => {
339
+ group.sort((a, b) => (a.tag || '') < (b.tag || '') ? -1 : 1);
340
+ return sid + group.map(entry => {
341
+ const ranges = entry.intervals.map(interval =>
342
+ interval.start === interval.end - 1n ?
343
+ `${interval.start}` :
344
+ `${interval.start}-${interval.end - 1n}`);
345
+ return (entry.tag ? `:${entry.tag}` : '') + `:${ranges.join(':')}`;
346
+ }).join('');
120
347
  }).join(',');
121
348
  skipEventRemainder(parser, zongji);
122
349
  }
@@ -151,7 +378,7 @@ class Xid extends BinlogEvent {
151
378
  */
152
379
 
153
380
  class Query extends BinlogEvent {
154
- constructor(parser, options) {
381
+ constructor(parser, options, zongji) {
155
382
  super(parser, options);
156
383
 
157
384
  this.slaveProxyId = parser.parseUnsignedNumber(4);
@@ -164,8 +391,20 @@ class Query extends BinlogEvent {
164
391
  this.schema = parser.parseString(this.schemaLength);
165
392
  parser.parseUnsignedNumber(1);
166
393
 
167
- // all the left is the query
168
- this.query = parser.parseString(this.size - 13 - this.statusVarsLength - this.schemaLength - 1);
394
+ if (options.eventType === QUERY_COMPRESSED_EVENT) {
395
+ // MariaDB log_bin_compress=ON: identical to a plain Query event up
396
+ // to and including the schema name's NUL; the query text is
397
+ // replaced by a compressed-payload envelope
398
+ const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
399
+ const envelope = parser.parseBuffer(
400
+ parser._packetEnd - checksumBytes - parser._offset);
401
+ this.query =
402
+ Common.uncompressBinlogEventPayload(envelope).toString();
403
+ skipEventRemainder(parser, zongji);
404
+ } else {
405
+ // all the left is the query
406
+ this.query = parser.parseString(this.size - 13 - this.statusVarsLength - this.schemaLength - 1);
407
+ }
169
408
  }
170
409
 
171
410
  dump() {
@@ -246,8 +485,368 @@ class TableMap extends BinlogEvent {
246
485
  // column meta data length
247
486
  parser.parseLengthCodedNumber();
248
487
  this._readColumnMetadata(parser);
249
- // ignore the rest
488
+ try {
489
+ this._readOptionalMetadata(parser, zongji);
490
+ } catch {
491
+ // A malformed or unrecognised optional metadata block must not
492
+ // break the stream: without it we simply keep using the
493
+ // INFORMATION_SCHEMA metadata path, as before MySQL 8.0.
494
+ this.columnNames = undefined;
495
+ this.signedness = undefined;
496
+ this.primaryKey = undefined;
497
+ this.columnVisibility = undefined;
498
+ this.geometryTypes = undefined;
499
+ this._columnCharsetIds = undefined;
500
+ this._enumSetValues = undefined;
501
+ }
502
+ }
503
+ }
504
+
505
+ // The column type byte for ENUM and SET columns is STRING; their real
506
+ // type arrives in the per-column metadata (see _readColumnMetadata).
507
+ _effectiveColumnType(index) {
508
+ const metadata = this.columnsMetadata[index];
509
+ return (metadata && metadata.type) || this.columnTypes[index];
510
+ }
511
+
512
+ // Parses the optional metadata TLV block that MySQL 8.0+ appends to
513
+ // TABLE_MAP_EVENT (binlog_row_metadata=MINIMAL or FULL). Absent on older
514
+ // servers, in which case this reads nothing.
515
+ _readOptionalMetadata(parser, zongji) {
516
+ // One nullability bit per column sits between the per-column metadata
517
+ // and the optional metadata block
518
+ parser.parseBuffer(Math.floor((this.columnCount + 7) / 8));
519
+
520
+ const end = parser._packetEnd - (zongji && zongji.useChecksum ? 4 : 0);
521
+ if (parser._offset >= end) {
522
+ return;
523
+ }
524
+
525
+ // Optional metadata bitmaps and lists cover column subsets classified
526
+ // by real type, in table-definition order
527
+ const numericIndexes = [];
528
+ const characterIndexes = [];
529
+ const enumSetIndexes = [];
530
+ const enumIndexes = [];
531
+ const setIndexes = [];
532
+ const geometryIndexes = [];
533
+ for (let i = 0; i < this.columnCount; i++) {
534
+ const type = this._effectiveColumnType(i);
535
+ if (NUMERIC_METADATA_TYPES.has(type)) {
536
+ numericIndexes.push(i);
537
+ } else if (type === Common.MysqlTypes.ENUM) {
538
+ enumSetIndexes.push(i);
539
+ enumIndexes.push(i);
540
+ } else if (type === Common.MysqlTypes.SET) {
541
+ enumSetIndexes.push(i);
542
+ setIndexes.push(i);
543
+ } else if (CHARACTER_METADATA_TYPES.has(type)) {
544
+ characterIndexes.push(i);
545
+ } else if (type === Common.MysqlTypes.GEOMETRY) {
546
+ geometryIndexes.push(i);
547
+ }
548
+ }
549
+
550
+ // MSB-first bitmap with one bit per entry of `indexes`; distributes the
551
+ // bit values back to full-table column positions
552
+ const readBitmap = (indexes, target) => {
553
+ const bytes = parser.parseBuffer(Math.floor((indexes.length + 7) / 8));
554
+ indexes.forEach((columnIndex, i) => {
555
+ target[columnIndex] = (bytes[i >> 3] & (0x80 >> (i % 8))) !== 0;
556
+ });
557
+ };
558
+
559
+ // DEFAULT_CHARSET layout: default collation id, then pairs of
560
+ // (position among `indexes`, collation id) for columns that differ
561
+ const readDefaultCharset = (fieldEnd, indexes, target) => {
562
+ const defaultCollation = parser.parseLengthCodedNumber();
563
+ indexes.forEach(columnIndex => {
564
+ target[columnIndex] = defaultCollation;
565
+ });
566
+ while (parser._offset < fieldEnd) {
567
+ const position = parser.parseLengthCodedNumber();
568
+ const collation = parser.parseLengthCodedNumber();
569
+ if (position < indexes.length) {
570
+ target[indexes[position]] = collation;
571
+ }
572
+ }
573
+ };
574
+
575
+ // COLUMN_CHARSET layout: one collation id per entry of `indexes`
576
+ const readColumnCharsets = (fieldEnd, indexes, target) => {
577
+ for (let i = 0; parser._offset < fieldEnd; i++) {
578
+ const collation = parser.parseLengthCodedNumber();
579
+ if (i < indexes.length) {
580
+ target[indexes[i]] = collation;
581
+ }
582
+ }
583
+ };
584
+
585
+ // SET_STR_VALUE/ENUM_STR_VALUE layout, per column of that type: value
586
+ // count, then that many length-coded strings (raw bytes in the column's
587
+ // own charset; decoded in buildColumnSchemas once charsets are known)
588
+ const readTypeValues = (fieldEnd, indexes, target) => {
589
+ for (let i = 0; parser._offset < fieldEnd; i++) {
590
+ const count = parser.parseLengthCodedNumber();
591
+ const values = [];
592
+ for (let j = 0; j < count; j++) {
593
+ values.push(parser.parseBuffer(parser.parseLengthCodedNumber()));
594
+ }
595
+ if (i < indexes.length) {
596
+ target[indexes[i]] = values;
597
+ }
598
+ }
599
+ };
600
+
601
+ const charsetIds = new Array(this.columnCount);
602
+ const Types = OptionalMetadataTypes;
603
+
604
+ while (parser._offset < end) {
605
+ const type = parser.parseUnsignedNumber(1);
606
+ const length = parser.parseLengthCodedNumber();
607
+ const fieldEnd = parser._offset + length;
608
+ if (length === null || fieldEnd > end) {
609
+ throw new Error('Malformed TABLE_MAP_EVENT optional metadata');
610
+ }
611
+
612
+ let handled = true;
613
+ switch (type) {
614
+ case Types.SIGNEDNESS:
615
+ this.signedness = new Array(this.columnCount);
616
+ readBitmap(numericIndexes, this.signedness);
617
+ break;
618
+ case Types.DEFAULT_CHARSET:
619
+ readDefaultCharset(fieldEnd, characterIndexes, charsetIds);
620
+ break;
621
+ case Types.COLUMN_CHARSET:
622
+ readColumnCharsets(fieldEnd, characterIndexes, charsetIds);
623
+ break;
624
+ case Types.COLUMN_NAME: {
625
+ const names = [];
626
+ while (parser._offset < fieldEnd) {
627
+ names.push(parser.parseString(parser.parseLengthCodedNumber()));
628
+ }
629
+ this.columnNames = names;
630
+ break;
631
+ }
632
+ case Types.SET_STR_VALUE:
633
+ this._enumSetValues = this._enumSetValues || new Array(this.columnCount);
634
+ readTypeValues(fieldEnd, setIndexes, this._enumSetValues);
635
+ break;
636
+ case Types.ENUM_STR_VALUE:
637
+ this._enumSetValues = this._enumSetValues || new Array(this.columnCount);
638
+ readTypeValues(fieldEnd, enumIndexes, this._enumSetValues);
639
+ break;
640
+ case Types.ENUM_AND_SET_DEFAULT_CHARSET:
641
+ readDefaultCharset(fieldEnd, enumSetIndexes, charsetIds);
642
+ break;
643
+ case Types.ENUM_AND_SET_COLUMN_CHARSET:
644
+ readColumnCharsets(fieldEnd, enumSetIndexes, charsetIds);
645
+ break;
646
+ case Types.SIMPLE_PRIMARY_KEY: {
647
+ const primaryKey = [];
648
+ while (parser._offset < fieldEnd) {
649
+ primaryKey.push(parser.parseLengthCodedNumber());
650
+ }
651
+ this.primaryKey = primaryKey;
652
+ break;
653
+ }
654
+ case Types.PRIMARY_KEY_WITH_PREFIX: {
655
+ const primaryKey = [];
656
+ while (parser._offset < fieldEnd) {
657
+ const columnIndex = parser.parseLengthCodedNumber();
658
+ // Prefix length in characters; 0 means the whole column
659
+ parser.parseLengthCodedNumber();
660
+ primaryKey.push(columnIndex);
661
+ }
662
+ this.primaryKey = primaryKey;
663
+ break;
664
+ }
665
+ case Types.GEOMETRY_TYPE: {
666
+ this.geometryTypes = new Array(this.columnCount);
667
+ for (let i = 0; parser._offset < fieldEnd; i++) {
668
+ const geometryType = parser.parseLengthCodedNumber();
669
+ if (i < geometryIndexes.length) {
670
+ this.geometryTypes[geometryIndexes[i]] = geometryType;
671
+ }
672
+ }
673
+ break;
674
+ }
675
+ case Types.COLUMN_VISIBILITY:
676
+ this.columnVisibility = new Array(this.columnCount).fill(false);
677
+ readBitmap(
678
+ Array.from({ length: this.columnCount }, (value, i) => i),
679
+ this.columnVisibility);
680
+ break;
681
+ default:
682
+ // Unknown future fields (e.g. VECTOR dimensionality in MySQL 9)
683
+ // are skipped via fieldEnd below
684
+ handled = false;
685
+ }
686
+
687
+ // Every known field type consumes its declared length exactly; a
688
+ // mismatch in either direction means the block is corrupt, so treat
689
+ // all optional metadata as unusable rather than trusting partially
690
+ // garbled values (handled by the caller's catch)
691
+ if (handled ? parser._offset !== fieldEnd : parser._offset > fieldEnd) {
692
+ throw new Error('Malformed TABLE_MAP_EVENT optional metadata');
693
+ }
694
+ parser._offset = fieldEnd;
695
+ }
696
+
697
+ this._columnCharsetIds = charsetIds;
698
+ }
699
+
700
+ // True when the event carries binlog_row_metadata=FULL metadata, i.e.
701
+ // enough to decode rows without consulting INFORMATION_SCHEMA
702
+ hasSelfDescribingMetadata() {
703
+ return this.columnNames !== undefined &&
704
+ this.columnNames.length === this.columnCount;
705
+ }
706
+
707
+ // Classic temporal type codes are ambiguous on MariaDB: a 5.3-era
708
+ // "hires" column (fractional seconds, different byte order and width)
709
+ // is binlogged under the same codes with the same (zero) metadata as a
710
+ // classic column, and even FULL optional metadata carries nothing to
711
+ // tell them apart. Only the table definition can, so tables containing
712
+ // these codes must use the INFORMATION_SCHEMA path.
713
+ hasAmbiguousTemporalColumns() {
714
+ return this.columnTypes.some(type =>
715
+ type === Common.MysqlTypes.TIMESTAMP ||
716
+ type === Common.MysqlTypes.TIME ||
717
+ type === Common.MysqlTypes.DATETIME);
718
+ }
719
+
720
+ // Builds INFORMATION_SCHEMA.COLUMNS-shaped rows from the event's own
721
+ // metadata, so the rest of the pipeline works identically with either
722
+ // metadata source. Only meaningful when hasSelfDescribingMetadata().
723
+ buildColumnSchemas() {
724
+ const schemas = [];
725
+ for (let i = 0; i < this.columnCount; i++) {
726
+ const type = this._effectiveColumnType(i);
727
+ const metadata =
728
+ /** @type {Record<string, any>} */ (this.columnsMetadata[i] || {});
729
+ const charsetId = this._columnCharsetIds && this._columnCharsetIds[i];
730
+ const binary = charsetId === BINARY_COLLATION_ID;
731
+ const charsetName = charsetId === undefined || binary ?
732
+ null : collationToCharset(charsetId);
733
+ const unsigned = this.signedness && this.signedness[i];
734
+
735
+ const schema = {
736
+ COLUMN_NAME: this.columnNames[i],
737
+ COLLATION_NAME: null,
738
+ CHARACTER_SET_NAME: charsetName,
739
+ COLUMN_COMMENT: '',
740
+ COLUMN_TYPE: '',
741
+ };
742
+
743
+ const Mysql = Common.MysqlTypes;
744
+ switch (type) {
745
+ case Mysql.TINY: schema.COLUMN_TYPE = 'tinyint'; break;
746
+ case Mysql.SHORT: schema.COLUMN_TYPE = 'smallint'; break;
747
+ case Mysql.INT24: schema.COLUMN_TYPE = 'mediumint'; break;
748
+ case Mysql.LONG: schema.COLUMN_TYPE = 'int'; break;
749
+ case Mysql.LONGLONG: schema.COLUMN_TYPE = 'bigint'; break;
750
+ case Mysql.FLOAT: schema.COLUMN_TYPE = 'float'; break;
751
+ case Mysql.DOUBLE: schema.COLUMN_TYPE = 'double'; break;
752
+ case Mysql.DECIMAL:
753
+ case Mysql.NEWDECIMAL:
754
+ schema.COLUMN_TYPE =
755
+ `decimal(${metadata.precision},${metadata.decimals})`;
756
+ break;
757
+ case Mysql.YEAR: schema.COLUMN_TYPE = 'year'; break;
758
+ case Mysql.STRING:
759
+ case Mysql.VARCHAR:
760
+ case Mysql.VAR_STRING: {
761
+ // Binlog metadata stores byte widths; character widths follow
762
+ // from the charset's maximum bytes per character
763
+ const isChar = type === Mysql.STRING;
764
+ if (binary) {
765
+ schema.COLUMN_TYPE =
766
+ (isChar ? 'binary' : 'varbinary') +
767
+ `(${metadata['max_length']})`;
768
+ } else {
769
+ const chars =
770
+ metadata['max_length'] / charsetMaxLength(charsetName);
771
+ schema.COLUMN_TYPE = (isChar ? 'char' : 'varchar') +
772
+ `(${Math.floor(chars)})`;
773
+ }
774
+ break;
775
+ }
776
+ case Mysql.TINY_BLOB:
777
+ case Mysql.MEDIUM_BLOB:
778
+ case Mysql.LONG_BLOB:
779
+ case Mysql.BLOB:
780
+ schema.COLUMN_TYPE = (BLOB_TYPE_NAMES[metadata['length_size']] ||
781
+ BLOB_TYPE_NAMES[2])[binary ? 0 : 1];
782
+ break;
783
+ case Mysql.BLOB_COMPRESSED:
784
+ // Same I_S form MariaDB itself reports for COMPRESSED columns
785
+ schema.COLUMN_TYPE = (BLOB_TYPE_NAMES[metadata['length_size']] ||
786
+ BLOB_TYPE_NAMES[2])[binary ? 0 : 1] + ' /*M!100301 COMPRESSED*/';
787
+ break;
788
+ case Mysql.VARCHAR_COMPRESSED: {
789
+ // max_length reserves one byte for the compression header
790
+ const byteLength = metadata['max_length'] - 1;
791
+ if (binary) {
792
+ schema.COLUMN_TYPE =
793
+ `varbinary(${byteLength}) /*M!100301 COMPRESSED*/`;
794
+ } else {
795
+ const chars = byteLength / charsetMaxLength(charsetName);
796
+ schema.COLUMN_TYPE =
797
+ `varchar(${Math.floor(chars)}) /*M!100301 COMPRESSED*/`;
798
+ }
799
+ break;
800
+ }
801
+ case Mysql.ENUM:
802
+ case Mysql.SET: {
803
+ const values = decodeEnumSetValues(
804
+ this._enumSetValues && this._enumSetValues[i], charsetName);
805
+ if (type === Mysql.ENUM) {
806
+ schema.ENUM_VALUES = values;
807
+ } else {
808
+ schema.SET_VALUES = values;
809
+ }
810
+ const keyword = type === Mysql.ENUM ? 'enum' : 'set';
811
+ schema.COLUMN_TYPE = keyword + '(' +
812
+ values.map(value => `'${value.replace(/'/g, "''")}'`).join(',') +
813
+ ')';
814
+ break;
815
+ }
816
+ case Mysql.BIT: schema.COLUMN_TYPE = `bit(${metadata.bits})`; break;
817
+ case Mysql.JSON: schema.COLUMN_TYPE = 'json'; break;
818
+ case Mysql.GEOMETRY:
819
+ schema.COLUMN_TYPE = GEOMETRY_TYPE_NAMES[
820
+ this.geometryTypes && this.geometryTypes[i]] || 'geometry';
821
+ break;
822
+ case Mysql.TIMESTAMP:
823
+ case Mysql.TIMESTAMP2:
824
+ schema.COLUMN_TYPE = withPrecision('timestamp', metadata.decimals);
825
+ break;
826
+ case Mysql.DATETIME:
827
+ case Mysql.DATETIME2:
828
+ schema.COLUMN_TYPE = withPrecision('datetime', metadata.decimals);
829
+ break;
830
+ case Mysql.TIME:
831
+ case Mysql.TIME2:
832
+ schema.COLUMN_TYPE = withPrecision('time', metadata.decimals);
833
+ break;
834
+ case Mysql.DATE:
835
+ case Mysql.NEWDATE:
836
+ schema.COLUMN_TYPE = 'date';
837
+ break;
838
+ }
839
+
840
+ if (unsigned !== undefined && NUMERIC_METADATA_TYPES.has(type)) {
841
+ schema.UNSIGNED = unsigned;
842
+ if (unsigned && type !== Mysql.YEAR) {
843
+ schema.COLUMN_TYPE += ' unsigned';
844
+ }
845
+ }
846
+
847
+ schemas.push(schema);
250
848
  }
849
+ return schemas;
251
850
  }
252
851
 
253
852
  updateColumnInfo() {
@@ -271,6 +870,18 @@ class TableMap extends BinlogEvent {
271
870
  'columns, fetched metadata has ' +
272
871
  `${columnSchemas ? columnSchemas.length : 0}`);
273
872
  }
873
+
874
+ // Even under binlog_row_metadata=MINIMAL the binlog carries exact
875
+ // signedness as of the event's write time; it always wins over
876
+ // inference from the INFORMATION_SCHEMA column definition (see
877
+ // parseAnyInt in common.js)
878
+ if (this.signedness && columnSchemas.length === this.columnCount) {
879
+ for (let i = 0; i < this.columnCount; i++) {
880
+ if (this.signedness[i] !== undefined) {
881
+ columnSchemas[i].UNSIGNED = this.signedness[i];
882
+ }
883
+ }
884
+ }
274
885
  const columns = [];
275
886
  for (let j = 0; j < this.columnCount; j++) {
276
887
  columns.push({
@@ -297,6 +908,7 @@ class TableMap extends BinlogEvent {
297
908
  };
298
909
  break;
299
910
  case Common.MysqlTypes.VARCHAR:
911
+ case Common.MysqlTypes.VARCHAR_COMPRESSED:
300
912
  result = {
301
913
  'max_length': parser.parseUnsignedNumber(2)
302
914
  };
@@ -316,6 +928,7 @@ class TableMap extends BinlogEvent {
316
928
  };
317
929
  break;
318
930
  case Common.MysqlTypes.BLOB:
931
+ case Common.MysqlTypes.BLOB_COMPRESSED:
319
932
  case Common.MysqlTypes.GEOMETRY:
320
933
  case Common.MysqlTypes.JSON:
321
934
  result = {
@@ -374,6 +987,250 @@ class Unknown extends BinlogEvent {
374
987
  }
375
988
  }
376
989
 
990
+ /* Sent by the server instead of real events: while the connection idles
991
+ * (when a heartbeat period is configured), and after transactions were
992
+ * skipped server-side during a GTID dump, so the client's position can
993
+ * advance past them. Carries the current binlog filename; nextPosition
994
+ * holds the advanced position.
995
+ */
996
+ class Heartbeat extends BinlogEvent {
997
+ constructor(parser, options, zongji) {
998
+ super(parser, options);
999
+ const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
1000
+ // MariaDB: when the heartbeat position exceeds 4 GiB the header
1001
+ // log_pos is 0 and a u64 position sub-header precedes the file name
1002
+ // (sql/sql_repl.cc send_heartbeat_event, HB_SUB_HEADER_LEN)
1003
+ if (zongji && zongji.isMariaDb && options.nextPosition === 0 &&
1004
+ parser._packetEnd - checksumBytes - parser._offset >= 8) {
1005
+ this.position = Common.parseUInt64(parser);
1006
+ }
1007
+ this.binlogName = parser.parseString(
1008
+ parser._packetEnd - checksumBytes - parser._offset);
1009
+ skipEventRemainder(parser, zongji);
1010
+ }
1011
+ }
1012
+
1013
+ /* MariaDB GTID_EVENT (code 162): replaces the BEGIN Query event of a
1014
+ * transaction (FL_STANDALONE clear) or marks a standalone group such as a
1015
+ * DDL statement (FL_STANDALONE set). The GTID itself is
1016
+ * domain_id - server_id - seq_no, where server_id comes from the common
1017
+ * event header. Layout: sql/log_event.cc Gtid_log_event; fields after
1018
+ * flags2 are conditional and the data area is zero-padded to the 19-byte
1019
+ * post-header length when shorter.
1020
+ */
1021
+ const MARIADB_GTID_FLAGS2 = {
1022
+ FL_STANDALONE: 0x01,
1023
+ FL_GROUP_COMMIT_ID: 0x02,
1024
+ FL_TRANSACTIONAL: 0x04,
1025
+ FL_ALLOW_PARALLEL: 0x08,
1026
+ FL_WAITED: 0x10,
1027
+ FL_DDL: 0x20,
1028
+ FL_PREPARED_XA: 0x40,
1029
+ FL_COMPLETED_XA: 0x80,
1030
+ };
1031
+ const MARIADB_GTID_FLAGS_EXTRA = {
1032
+ FL_EXTRA_MULTI_ENGINE: 0x01,
1033
+ FL_START_ALTER: 0x02,
1034
+ FL_COMMIT_ALTER: 0x04,
1035
+ FL_ROLLBACK_ALTER: 0x08,
1036
+ FL_EXTRA_THREAD_ID: 0x10,
1037
+ };
1038
+
1039
+ // The parser's own bounds run to the packet end INCLUDING the trailing
1040
+ // CRC32, so a truncated event body could silently consume checksum bytes
1041
+ // as field data; every fixed or length-prefixed read must be bounded by
1042
+ // the checksum-excluded end instead
1043
+ const requireEventBytes = function(parser, end, bytes, typeName) {
1044
+ if (parser._offset + bytes > end) {
1045
+ throw new Error(
1046
+ `Truncated ${typeName} event: ${bytes} bytes needed, ` +
1047
+ `${end - parser._offset} available`);
1048
+ }
1049
+ };
1050
+
1051
+ class MariadbGtid extends BinlogEvent {
1052
+ constructor(parser, options, zongji) {
1053
+ super(parser, options);
1054
+ const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
1055
+ const end = parser._packetEnd - checksumBytes;
1056
+
1057
+ requireEventBytes(parser, end, 13, 'MariadbGtid');
1058
+ this.seqNo = Common.parseUInt64(parser);
1059
+ this.domainId = parser.parseUnsignedNumber(4);
1060
+ this.serverId = options.serverId;
1061
+ this.flags2 = parser.parseUnsignedNumber(1);
1062
+ this.standalone =
1063
+ (this.flags2 & MARIADB_GTID_FLAGS2.FL_STANDALONE) !== 0;
1064
+ this.transactional =
1065
+ (this.flags2 & MARIADB_GTID_FLAGS2.FL_TRANSACTIONAL) !== 0;
1066
+ this.isDdl = (this.flags2 & MARIADB_GTID_FLAGS2.FL_DDL) !== 0;
1067
+ this.preparedXa =
1068
+ (this.flags2 & MARIADB_GTID_FLAGS2.FL_PREPARED_XA) !== 0;
1069
+ this.completedXa =
1070
+ (this.flags2 & MARIADB_GTID_FLAGS2.FL_COMPLETED_XA) !== 0;
1071
+
1072
+ if (this.flags2 & MARIADB_GTID_FLAGS2.FL_GROUP_COMMIT_ID) {
1073
+ requireEventBytes(parser, end, 8, 'MariadbGtid');
1074
+ this.commitId = Common.parseUInt64(parser);
1075
+ }
1076
+ if (this.preparedXa || this.completedXa) {
1077
+ requireEventBytes(parser, end, 6, 'MariadbGtid');
1078
+ this.xidFormatId = parser.parseUnsignedNumber(4);
1079
+ const gtridLength = parser.parseUnsignedNumber(1);
1080
+ const bqualLength = parser.parseUnsignedNumber(1);
1081
+ requireEventBytes(parser, end, gtridLength + bqualLength,
1082
+ 'MariadbGtid');
1083
+ this.xidGtrid = parser.parseBuffer(gtridLength);
1084
+ this.xidBqual = parser.parseBuffer(bqualLength);
1085
+ }
1086
+ // Remaining bytes (if any) start with flags_extra; a zero pad byte
1087
+ // reads as flags_extra = 0, adding nothing
1088
+ if (parser._offset < end) {
1089
+ this.flagsExtra = parser.parseUnsignedNumber(1);
1090
+ if (this.flagsExtra & MARIADB_GTID_FLAGS_EXTRA.FL_EXTRA_MULTI_ENGINE) {
1091
+ requireEventBytes(parser, end, 1, 'MariadbGtid');
1092
+ this.extraEngines = parser.parseUnsignedNumber(1);
1093
+ }
1094
+ if (this.flagsExtra & (MARIADB_GTID_FLAGS_EXTRA.FL_COMMIT_ALTER |
1095
+ MARIADB_GTID_FLAGS_EXTRA.FL_ROLLBACK_ALTER)) {
1096
+ requireEventBytes(parser, end, 8, 'MariadbGtid');
1097
+ this.saSeqNo = Common.parseUInt64(parser);
1098
+ }
1099
+ if ((this.flagsExtra & MARIADB_GTID_FLAGS_EXTRA.FL_EXTRA_THREAD_ID) &&
1100
+ parser._offset + 4 <= end) {
1101
+ this.threadId = parser.parseUnsignedNumber(4);
1102
+ }
1103
+ }
1104
+
1105
+ this.gtid = `${this.domainId}-${this.serverId}-${this.seqNo}`;
1106
+ skipEventRemainder(parser, zongji);
1107
+ }
1108
+ }
1109
+
1110
+ /* MariaDB GTID_LIST_EVENT (code 163): written at the start of every binlog
1111
+ * file with the last GTID per (domain, server) seen so far - MariaDB's
1112
+ * analogue of MySQL's Previous_gtids. Also sent as an artificial event on
1113
+ * GTID connects that seek mid-file. A domain may have several entries (one
1114
+ * per server_id); the last entry of a domain's run is the most recent.
1115
+ */
1116
+ class MariadbGtidList extends BinlogEvent {
1117
+ constructor(parser, options, zongji) {
1118
+ super(parser, options);
1119
+ const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
1120
+ const end = parser._packetEnd - checksumBytes;
1121
+ requireEventBytes(parser, end, 4, 'MariadbGtidList');
1122
+ const val = parser.parseUnsignedNumber(4);
1123
+ this.count = val & 0x0fffffff;
1124
+ // Bit 28: FLAG_UNTIL_REACHED (fake events only); bit 29: FLAG_IGN_GTIDS
1125
+ this.flags = val >>> 28;
1126
+ this.gtids = [];
1127
+ for (let i = 0; i < this.count; i++) {
1128
+ requireEventBytes(parser, end, 16, 'MariadbGtidList');
1129
+ const domainId = parser.parseUnsignedNumber(4);
1130
+ const serverId = parser.parseUnsignedNumber(4);
1131
+ const seqNo = Common.parseUInt64(parser);
1132
+ this.gtids.push({
1133
+ domainId,
1134
+ serverId,
1135
+ seqNo,
1136
+ gtid: `${domainId}-${serverId}-${seqNo}`,
1137
+ });
1138
+ }
1139
+ skipEventRemainder(parser, zongji);
1140
+ }
1141
+ }
1142
+
1143
+ /* MariaDB BINLOG_CHECKPOINT_EVENT (code 161): an XA-recovery marker naming
1144
+ * the oldest binlog file that may still be needed for crash recovery. Not
1145
+ * used in replication; exposed for observability only.
1146
+ */
1147
+ class BinlogCheckpoint extends BinlogEvent {
1148
+ constructor(parser, options, zongji) {
1149
+ super(parser, options);
1150
+ const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
1151
+ const end = parser._packetEnd - checksumBytes;
1152
+ requireEventBytes(parser, end, 4, 'BinlogCheckpoint');
1153
+ const length = parser.parseUnsignedNumber(4);
1154
+ requireEventBytes(parser, end, length, 'BinlogCheckpoint');
1155
+ this.binlogName = parser.parseString(length);
1156
+ skipEventRemainder(parser, zongji);
1157
+ }
1158
+ }
1159
+
1160
+ /* MariaDB ANNOTATE_ROWS_EVENT (code 160): the SQL statement text for the
1161
+ * row events that follow, MariaDB's analogue of MySQL's
1162
+ * ROWS_QUERY_LOG_EVENT. Only sent when the dump requests it (the
1163
+ * BINLOG_SEND_ANNOTATE_ROWS_EVENT flag).
1164
+ */
1165
+ class AnnotateRows extends BinlogEvent {
1166
+ constructor(parser, options, zongji) {
1167
+ super(parser, options);
1168
+ const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
1169
+ this.statement = parser.parseString(
1170
+ parser._packetEnd - checksumBytes - parser._offset);
1171
+ skipEventRemainder(parser, zongji);
1172
+ }
1173
+ }
1174
+
1175
+ /* MySQL ROWS_QUERY_LOG_EVENT (code 29): the SQL statement text for the
1176
+ * row events that follow, sent to every consumer while the server runs
1177
+ * with binlog_rows_query_log_events=ON. The body starts with one length
1178
+ * byte that only holds the statement length mod 256; readers ignore it
1179
+ * and take the text to the end of the event
1180
+ * (sql/log_event.cc Rows_query_log_event).
1181
+ */
1182
+ class RowsQuery extends BinlogEvent {
1183
+ constructor(parser, options, zongji) {
1184
+ super(parser, options);
1185
+ const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
1186
+ const end = parser._packetEnd - checksumBytes;
1187
+ requireEventBytes(parser, end, 1, 'RowsQuery');
1188
+ parser.parseUnsignedNumber(1);
1189
+ this.statement = parser.parseString(end - parser._offset);
1190
+ skipEventRemainder(parser, zongji);
1191
+ }
1192
+ }
1193
+
1194
+ /* MariaDB START_ENCRYPTION_EVENT (code 164): sent once after the format
1195
+ * description event when the binlog file on disk is encrypted. The dump
1196
+ * thread decrypts server-side, so the stream itself is cleartext and this
1197
+ * event is informational; it arrives flagged LOG_EVENT_IGNORABLE_F.
1198
+ */
1199
+ class StartEncryption extends BinlogEvent {
1200
+ constructor(parser, options, zongji) {
1201
+ super(parser, options);
1202
+ const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
1203
+ requireEventBytes(parser, parser._packetEnd - checksumBytes, 17,
1204
+ 'StartEncryption');
1205
+ this.scheme = parser.parseUnsignedNumber(1);
1206
+ this.keyVersion = parser.parseUnsignedNumber(4);
1207
+ this.nonce = parser.parseBuffer(12);
1208
+ skipEventRemainder(parser, zongji);
1209
+ }
1210
+ }
1211
+
1212
+ /* XA_PREPARE_LOG_EVENT (code 38): terminates an XA-prepared event group
1213
+ * (MySQL 5.7+ and MariaDB share the layout). one_phase distinguishes
1214
+ * XA COMMIT ... ONE PHASE, which commits immediately.
1215
+ */
1216
+ class XaPrepare extends BinlogEvent {
1217
+ constructor(parser, options, zongji) {
1218
+ super(parser, options);
1219
+ const checksumBytes = zongji && zongji.useChecksum ? 4 : 0;
1220
+ const end = parser._packetEnd - checksumBytes;
1221
+ requireEventBytes(parser, end, 13, 'XaPrepare');
1222
+ this.onePhase = parser.parseUnsignedNumber(1) !== 0;
1223
+ this.xidFormatId = parser.parseUnsignedNumber(4);
1224
+ const gtridLength = parser.parseUnsignedNumber(4);
1225
+ const bqualLength = parser.parseUnsignedNumber(4);
1226
+ requireEventBytes(parser, end, gtridLength + bqualLength, 'XaPrepare');
1227
+ this.xidGtrid = parser.parseBuffer(gtridLength);
1228
+ this.xidBqual = parser.parseBuffer(bqualLength);
1229
+ skipEventRemainder(parser, zongji);
1230
+ }
1231
+ }
1232
+
1233
+
377
1234
  /* MySQL 8.0.20+ wraps whole transactions in a single compressed event when
378
1235
  * binlog_transaction_compression=ON. The embedded row events are
379
1236
  * zstd-compressed and zongji cannot decode them, so the row changes they
@@ -424,5 +1281,13 @@ export {
424
1281
  PreviousGtids,
425
1282
  TransactionPayload,
426
1283
  PartialUpdateRows,
1284
+ Heartbeat,
1285
+ MariadbGtid,
1286
+ MariadbGtidList,
1287
+ BinlogCheckpoint,
1288
+ AnnotateRows,
1289
+ RowsQuery,
1290
+ StartEncryption,
1291
+ XaPrepare,
427
1292
  Unknown
428
1293
  };