@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.
@@ -0,0 +1,86 @@
1
+ // Maps MySQL collation ids (as they appear in TABLE_MAP_EVENT optional
2
+ // metadata) to their character set names, matching the
3
+ // INFORMATION_SCHEMA.COLUMNS CHARACTER_SET_NAME values that the
4
+ // metadata-fetch path produces, so both metadata sources look identical
5
+ // to consumers.
6
+ //
7
+ // Generated from INFORMATION_SCHEMA.COLLATIONS and CHARACTER_SETS on
8
+ // MySQL 8.0 and 8.4 (identical sets). Collation ids are append-only and
9
+ // stable across MySQL versions. Regenerate with:
10
+ // docker exec <mysql-container> mysql -u root -p<pw> -N -e \
11
+ // 'SELECT ID, CHARACTER_SET_NAME FROM information_schema.COLLATIONS ORDER BY ID'
12
+ // docker exec <mysql-container> mysql -u root -p<pw> -N -e \
13
+ // 'SELECT CHARACTER_SET_NAME, MAXLEN FROM information_schema.CHARACTER_SETS'
14
+ const collationCharsets = {
15
+ 1: 'big5', 2: 'latin2', 3: 'dec8', 4: 'cp850', 5: 'latin1', 6: 'hp8',
16
+ 7: 'koi8r', 8: 'latin1', 9: 'latin2', 10: 'swe7', 11: 'ascii', 12: 'ujis',
17
+ 13: 'sjis', 14: 'cp1251', 15: 'latin1', 16: 'hebrew', 18: 'tis620', 19: 'euckr',
18
+ 20: 'latin7', 21: 'latin2', 22: 'koi8u', 23: 'cp1251', 24: 'gb2312', 25: 'greek',
19
+ 26: 'cp1250', 27: 'latin2', 28: 'gbk', 29: 'cp1257', 30: 'latin5', 31: 'latin1',
20
+ 32: 'armscii8', 33: 'utf8mb3', 34: 'cp1250', 35: 'ucs2', 36: 'cp866', 37: 'keybcs2',
21
+ 38: 'macce', 39: 'macroman', 40: 'cp852', 41: 'latin7', 42: 'latin7', 43: 'macce',
22
+ 44: 'cp1250', 45: 'utf8mb4', 46: 'utf8mb4', 47: 'latin1', 48: 'latin1', 49: 'latin1',
23
+ 50: 'cp1251', 51: 'cp1251', 52: 'cp1251', 53: 'macroman', 54: 'utf16', 55: 'utf16',
24
+ 56: 'utf16le', 57: 'cp1256', 58: 'cp1257', 59: 'cp1257', 60: 'utf32', 61: 'utf32',
25
+ 62: 'utf16le', 63: 'binary', 64: 'armscii8', 65: 'ascii', 66: 'cp1250', 67: 'cp1256',
26
+ 68: 'cp866', 69: 'dec8', 70: 'greek', 71: 'hebrew', 72: 'hp8', 73: 'keybcs2',
27
+ 74: 'koi8r', 75: 'koi8u', 76: 'utf8mb3', 77: 'latin2', 78: 'latin5', 79: 'latin7',
28
+ 80: 'cp850', 81: 'cp852', 82: 'swe7', 83: 'utf8mb3', 84: 'big5', 85: 'euckr',
29
+ 86: 'gb2312', 87: 'gbk', 88: 'sjis', 89: 'tis620', 90: 'ucs2', 91: 'ujis',
30
+ 92: 'geostd8', 93: 'geostd8', 94: 'latin1', 95: 'cp932', 96: 'cp932', 97: 'eucjpms',
31
+ 98: 'eucjpms', 99: 'cp1250', 101: 'utf16', 102: 'utf16', 103: 'utf16', 104: 'utf16',
32
+ 105: 'utf16', 106: 'utf16', 107: 'utf16', 108: 'utf16', 109: 'utf16', 110: 'utf16',
33
+ 111: 'utf16', 112: 'utf16', 113: 'utf16', 114: 'utf16', 115: 'utf16', 116: 'utf16',
34
+ 117: 'utf16', 118: 'utf16', 119: 'utf16', 120: 'utf16', 121: 'utf16', 122: 'utf16',
35
+ 123: 'utf16', 124: 'utf16', 128: 'ucs2', 129: 'ucs2', 130: 'ucs2', 131: 'ucs2',
36
+ 132: 'ucs2', 133: 'ucs2', 134: 'ucs2', 135: 'ucs2', 136: 'ucs2', 137: 'ucs2',
37
+ 138: 'ucs2', 139: 'ucs2', 140: 'ucs2', 141: 'ucs2', 142: 'ucs2', 143: 'ucs2',
38
+ 144: 'ucs2', 145: 'ucs2', 146: 'ucs2', 147: 'ucs2', 148: 'ucs2', 149: 'ucs2',
39
+ 150: 'ucs2', 151: 'ucs2', 159: 'ucs2', 160: 'utf32', 161: 'utf32', 162: 'utf32',
40
+ 163: 'utf32', 164: 'utf32', 165: 'utf32', 166: 'utf32', 167: 'utf32', 168: 'utf32',
41
+ 169: 'utf32', 170: 'utf32', 171: 'utf32', 172: 'utf32', 173: 'utf32', 174: 'utf32',
42
+ 175: 'utf32', 176: 'utf32', 177: 'utf32', 178: 'utf32', 179: 'utf32', 180: 'utf32',
43
+ 181: 'utf32', 182: 'utf32', 183: 'utf32', 192: 'utf8mb3', 193: 'utf8mb3', 194: 'utf8mb3',
44
+ 195: 'utf8mb3', 196: 'utf8mb3', 197: 'utf8mb3', 198: 'utf8mb3', 199: 'utf8mb3', 200: 'utf8mb3',
45
+ 201: 'utf8mb3', 202: 'utf8mb3', 203: 'utf8mb3', 204: 'utf8mb3', 205: 'utf8mb3', 206: 'utf8mb3',
46
+ 207: 'utf8mb3', 208: 'utf8mb3', 209: 'utf8mb3', 210: 'utf8mb3', 211: 'utf8mb3', 212: 'utf8mb3',
47
+ 213: 'utf8mb3', 214: 'utf8mb3', 215: 'utf8mb3', 223: 'utf8mb3', 224: 'utf8mb4', 225: 'utf8mb4',
48
+ 226: 'utf8mb4', 227: 'utf8mb4', 228: 'utf8mb4', 229: 'utf8mb4', 230: 'utf8mb4', 231: 'utf8mb4',
49
+ 232: 'utf8mb4', 233: 'utf8mb4', 234: 'utf8mb4', 235: 'utf8mb4', 236: 'utf8mb4', 237: 'utf8mb4',
50
+ 238: 'utf8mb4', 239: 'utf8mb4', 240: 'utf8mb4', 241: 'utf8mb4', 242: 'utf8mb4', 243: 'utf8mb4',
51
+ 244: 'utf8mb4', 245: 'utf8mb4', 246: 'utf8mb4', 247: 'utf8mb4', 248: 'gb18030', 249: 'gb18030',
52
+ 250: 'gb18030', 255: 'utf8mb4', 256: 'utf8mb4', 257: 'utf8mb4', 258: 'utf8mb4', 259: 'utf8mb4',
53
+ 260: 'utf8mb4', 261: 'utf8mb4', 262: 'utf8mb4', 263: 'utf8mb4', 264: 'utf8mb4', 265: 'utf8mb4',
54
+ 266: 'utf8mb4', 267: 'utf8mb4', 268: 'utf8mb4', 269: 'utf8mb4', 270: 'utf8mb4', 271: 'utf8mb4',
55
+ 273: 'utf8mb4', 274: 'utf8mb4', 275: 'utf8mb4', 277: 'utf8mb4', 278: 'utf8mb4', 279: 'utf8mb4',
56
+ 280: 'utf8mb4', 281: 'utf8mb4', 282: 'utf8mb4', 283: 'utf8mb4', 284: 'utf8mb4', 285: 'utf8mb4',
57
+ 286: 'utf8mb4', 287: 'utf8mb4', 288: 'utf8mb4', 289: 'utf8mb4', 290: 'utf8mb4', 291: 'utf8mb4',
58
+ 292: 'utf8mb4', 293: 'utf8mb4', 294: 'utf8mb4', 296: 'utf8mb4', 297: 'utf8mb4', 298: 'utf8mb4',
59
+ 300: 'utf8mb4', 303: 'utf8mb4', 304: 'utf8mb4', 305: 'utf8mb4', 306: 'utf8mb4', 307: 'utf8mb4',
60
+ 308: 'utf8mb4', 309: 'utf8mb4', 310: 'utf8mb4', 311: 'utf8mb4', 312: 'utf8mb4', 313: 'utf8mb4',
61
+ 314: 'utf8mb4', 315: 'utf8mb4', 316: 'utf8mb4', 317: 'utf8mb4', 318: 'utf8mb4', 319: 'utf8mb4',
62
+ 320: 'utf8mb4', 321: 'utf8mb4', 322: 'utf8mb4', 323: 'utf8mb4',
63
+ };
64
+
65
+ // Maximum bytes per character, used to recover character display widths
66
+ // from the byte widths stored in binlog column metadata
67
+ const charsetMaxBytesPerChar = {
68
+ armscii8: 1, ascii: 1, big5: 2, binary: 1, cp1250: 1, cp1251: 1,
69
+ cp1256: 1, cp1257: 1, cp850: 1, cp852: 1, cp866: 1, cp932: 2,
70
+ dec8: 1, eucjpms: 3, euckr: 2, gb18030: 4, gb2312: 2, gbk: 2,
71
+ geostd8: 1, greek: 1, hebrew: 1, hp8: 1, keybcs2: 1, koi8r: 1,
72
+ koi8u: 1, latin1: 1, latin2: 1, latin5: 1, latin7: 1, macce: 1,
73
+ macroman: 1, sjis: 2, swe7: 1, tis620: 1, ucs2: 2, ujis: 3,
74
+ utf16: 4, utf16le: 4, utf32: 4, utf8mb3: 3, utf8mb4: 4,
75
+ };
76
+
77
+ // All collations added since MySQL 8.4 are utf8mb4 (the 0900 and uca1400
78
+ // series), so that is the safest guess for ids this table has not caught
79
+ // up with yet.
80
+ export function collationToCharset(id) {
81
+ return collationCharsets[id] || 'utf8mb4';
82
+ }
83
+
84
+ export function charsetMaxLength(charsetName) {
85
+ return charsetMaxBytesPerChar[charsetName] || 4;
86
+ }
package/lib/code_map.js CHANGED
@@ -43,9 +43,27 @@ const CodeEvent = [
43
43
  'XA_PREPARE_LOG_EVENT',
44
44
  'PARTIAL_UPDATE_ROWS_EVENT',
45
45
  'TRANSACTION_PAYLOAD_EVENT',
46
- 'HEARTBEAT_LOG_EVENT_V2'
46
+ 'HEARTBEAT_LOG_EVENT_V2',
47
+ 'GTID_TAGGED_LOG_EVENT'
47
48
  ];
48
49
 
50
+ // MariaDB-specific event codes (sql/log_event.h enum Log_event_type).
51
+ // The GTID names are prefixed to avoid clashing with MySQL's
52
+ // GTID_LOG_EVENT (33): MariaDB's own enum calls 162/163 GTID_EVENT and
53
+ // GTID_LIST_EVENT.
54
+ CodeEvent[160] = 'ANNOTATE_ROWS_EVENT';
55
+ CodeEvent[161] = 'BINLOG_CHECKPOINT_EVENT';
56
+ CodeEvent[162] = 'MARIADB_GTID_EVENT';
57
+ CodeEvent[163] = 'MARIADB_GTID_LIST_EVENT';
58
+ CodeEvent[164] = 'START_ENCRYPTION_EVENT';
59
+ CodeEvent[165] = 'QUERY_COMPRESSED_EVENT';
60
+ CodeEvent[166] = 'WRITE_ROWS_COMPRESSED_EVENT_V1';
61
+ CodeEvent[167] = 'UPDATE_ROWS_COMPRESSED_EVENT_V1';
62
+ CodeEvent[168] = 'DELETE_ROWS_COMPRESSED_EVENT_V1';
63
+ CodeEvent[169] = 'WRITE_ROWS_COMPRESSED_EVENT';
64
+ CodeEvent[170] = 'UPDATE_ROWS_COMPRESSED_EVENT';
65
+ CodeEvent[171] = 'DELETE_ROWS_COMPRESSED_EVENT';
66
+
49
67
  const EventClass = {
50
68
  UNKNOWN_EVENT: events.Unknown,
51
69
  QUERY_EVENT: events.Query,
@@ -53,7 +71,12 @@ const EventClass = {
53
71
  ROTATE_EVENT: events.Rotate,
54
72
  FORMAT_DESCRIPTION_EVENT: events.Format,
55
73
  XID_EVENT: events.Xid,
74
+ HEARTBEAT_LOG_EVENT: events.Heartbeat,
56
75
  GTID_LOG_EVENT: events.Gtid,
76
+ // Tagged GTIDs (MySQL 8.3+) decode inside the plain Gtid class (keyed
77
+ // on options.eventType), so consumers see and filter ordinary 'gtid'
78
+ // events; event.gtid is 'uuid:tag:gno' and event.tag is set
79
+ GTID_TAGGED_LOG_EVENT: events.Gtid,
57
80
  ANONYMOUS_GTID_LOG_EVENT: events.AnonymousGtid,
58
81
  PREVIOUS_GTIDS_LOG_EVENT: events.PreviousGtids,
59
82
  TRANSACTION_PAYLOAD_EVENT: events.TransactionPayload,
@@ -66,6 +89,26 @@ const EventClass = {
66
89
  WRITE_ROWS_EVENT_V2: rowsEvents.WriteRows,
67
90
  UPDATE_ROWS_EVENT_V2: rowsEvents.UpdateRows,
68
91
  DELETE_ROWS_EVENT_V2: rowsEvents.DeleteRows,
92
+
93
+ XA_PREPARE_LOG_EVENT: events.XaPrepare,
94
+ ROWS_QUERY_LOG_EVENT: events.RowsQuery,
95
+
96
+ // MariaDB
97
+ ANNOTATE_ROWS_EVENT: events.AnnotateRows,
98
+ BINLOG_CHECKPOINT_EVENT: events.BinlogCheckpoint,
99
+ MARIADB_GTID_EVENT: events.MariadbGtid,
100
+ MARIADB_GTID_LIST_EVENT: events.MariadbGtidList,
101
+ START_ENCRYPTION_EVENT: events.StartEncryption,
102
+ // log_bin_compress=ON variants decompress transparently inside the
103
+ // plain event classes (keyed on options.eventType), so consumers see
104
+ // and filter ordinary query/writerows/updaterows/deleterows events
105
+ QUERY_COMPRESSED_EVENT: events.Query,
106
+ WRITE_ROWS_COMPRESSED_EVENT_V1: rowsEvents.WriteRows,
107
+ UPDATE_ROWS_COMPRESSED_EVENT_V1: rowsEvents.UpdateRows,
108
+ DELETE_ROWS_COMPRESSED_EVENT_V1: rowsEvents.DeleteRows,
109
+ WRITE_ROWS_COMPRESSED_EVENT: rowsEvents.WriteRows,
110
+ UPDATE_ROWS_COMPRESSED_EVENT: rowsEvents.UpdateRows,
111
+ DELETE_ROWS_COMPRESSED_EVENT: rowsEvents.DeleteRows,
69
112
  };
70
113
 
71
114
  export function getEventClass(code) {
package/lib/common.js CHANGED
@@ -1,3 +1,5 @@
1
+ import zlib from 'zlib';
2
+
1
3
  import iconv from 'iconv-lite';
2
4
  import decodeJson, { convertBigInts, jsonToText } from './json_decode.js';
3
5
  import * as dtDecode from './datetime_decode.js';
@@ -24,6 +26,11 @@ export const MysqlTypes = {
24
26
  TIMESTAMP2: 17,
25
27
  DATETIME2: 18,
26
28
  TIME2: 19,
29
+ // MariaDB-only wire types for VARCHAR/VARBINARY/TEXT/BLOB columns
30
+ // declared with the COMPRESSED attribute ("only used internally for
31
+ // RBR", i.e. they appear in TABLE_MAP column-type arrays)
32
+ BLOB_COMPRESSED: 140,
33
+ VARCHAR_COMPRESSED: 141,
27
34
  // JSON data type added in MySQL 5.7.7
28
35
  JSON: 245,
29
36
  NEWDECIMAL: 246,
@@ -158,7 +165,12 @@ const parseAnyInt = function(parser, column, columnSchema) {
158
165
  result = parseUInt64(parser);
159
166
  break;
160
167
  }
161
- if (columnSchema.COLUMN_TYPE.indexOf('unsigned') === -1) {
168
+ // Binlog optional metadata provides signedness directly (UNSIGNED);
169
+ // otherwise infer it from the INFORMATION_SCHEMA column definition
170
+ const unsigned = columnSchema.UNSIGNED !== undefined ?
171
+ columnSchema.UNSIGNED :
172
+ columnSchema.COLUMN_TYPE.indexOf('unsigned') !== -1;
173
+ if (!unsigned) {
162
174
  const length = size * 8;
163
175
  const int64 = (length == 64);
164
176
  // Flip bits on negative signed integer
@@ -353,6 +365,238 @@ const readTemporalFraction = function(parser, fractionPrecision) {
353
365
  };
354
366
  };
355
367
 
368
+ export function formatUuid(buffer) {
369
+ const hex = buffer.toString('hex');
370
+ return [
371
+ hex.slice(0, 8),
372
+ hex.slice(8, 12),
373
+ hex.slice(12, 16),
374
+ hex.slice(16, 20),
375
+ hex.slice(20)
376
+ ].join('-');
377
+ }
378
+
379
+ // Undoes MariaDB's per-value column compression (VARCHAR/BLOB COMPRESSED,
380
+ // sql/field_comp.cc). Header byte: bits 4-7 = method (0 = stored
381
+ // uncompressed, 8 = zlib), and for zlib bits 0-2 = size of the big-endian
382
+ // original-length field that follows, bit 3 = deflate stream has no zlib
383
+ // wrapper (raw deflate). An empty value is stored with no header at all.
384
+ // Variable-length integer of MySQL's mysql::serialization framework
385
+ // (libs/mysql/serialization/variable_length_integers.h, used by the
386
+ // tagged GTID event of MySQL 8.3+). NOT protobuf varint and NOT the
387
+ // classic length-encoded integer: the total byte count is the number of
388
+ // trailing one-bits of the first byte plus one (1-9); the value is the
389
+ // first byte shifted right by that count, joined with the remaining
390
+ // bytes little-endian shifted left by (8 - count). A 9-byte encoding
391
+ // (first byte 0xFF) carries a full little-endian u64 unshifted.
392
+ // Returns a BigInt; `end` bounds the read (event end minus checksum).
393
+ export function parseSerializationVarint(parser, end, what) {
394
+ if (parser._offset + 1 > end) {
395
+ throw new Error(`Truncated ${what}: varint head past event end`);
396
+ }
397
+ const first = parser.parseUnsignedNumber(1);
398
+ let extra = 0;
399
+ while (extra < 8 && (first >> extra) & 1) {
400
+ extra++;
401
+ }
402
+ if (parser._offset + extra > end) {
403
+ throw new Error(`Truncated ${what}: ${extra + 1}-byte varint ` +
404
+ 'past event end');
405
+ }
406
+ if (extra === 8) {
407
+ let value = 0n;
408
+ for (let i = 0; i < 8; i++) {
409
+ value |= BigInt(parser.parseUnsignedNumber(1)) << BigInt(8 * i);
410
+ }
411
+ return value;
412
+ }
413
+ let rest = 0n;
414
+ for (let i = 0; i < extra; i++) {
415
+ rest |= BigInt(parser.parseUnsignedNumber(1)) << BigInt(8 * i);
416
+ }
417
+ return (BigInt(first) >> BigInt(extra + 1)) |
418
+ (rest << BigInt(8 - (extra + 1)));
419
+ }
420
+
421
+ // Signed variant: value x >= 0 encodes as x << 1, x < 0 as
422
+ // ((-(x + 1)) << 1) | 1 (a zigzag-like mapping)
423
+ export function parseSerializationVarintSigned(parser, end, what) {
424
+ const u = parseSerializationVarint(parser, end, what);
425
+ return (u & 1n) ? -(u >> 1n) - 1n : u >> 1n;
426
+ }
427
+
428
+ // MySQL charset names mostly match iconv-lite encoding names, with four
429
+ // classes of exception: the utf8 aliases; MySQL's "latin1", which is
430
+ // really cp1252 (its 0x80-0x9F range holds printable characters that
431
+ // ISO-8859-1 reserves for controls, and the server converts them as such
432
+ // for query results); the Unicode charsets whose stored byte order is
433
+ // big-endian while the iconv-lite encodings of the same spelling default
434
+ // to little-endian; and names iconv-lite spells differently or lacks
435
+ // (ujis is EUC-JP, eucjpms is its MS-extended variant for which plain
436
+ // EUC-JP is the closest decoder available, macce is Mac Central
437
+ // European). dec8, geostd8, hp8, keybcs2 and swe7 have no iconv-lite
438
+ // decoder at all; those columns come through as raw Buffers.
439
+ const ICONV_CHARSET_OVERRIDES = {
440
+ utf8mb3: 'utf8',
441
+ utf8mb4: 'utf8',
442
+ latin1: 'cp1252',
443
+ ucs2: 'utf16-be',
444
+ utf16: 'utf16-be',
445
+ utf16le: 'utf16-le',
446
+ utf32: 'utf32-be',
447
+ ujis: 'eucjp',
448
+ eucjpms: 'eucjp',
449
+ macce: 'maccenteuro',
450
+ };
451
+
452
+ // Decodes a text column's raw bytes using the column's own character set
453
+ // (INFORMATION_SCHEMA CHARACTER_SET_NAME, or the charset carried by FULL
454
+ // binlog metadata). Returns the buffer unchanged when the charset is
455
+ // unknown to iconv.
456
+ export function decodeTextColumn(buffer, charset) {
457
+ const charsetName = ICONV_CHARSET_OVERRIDES[charset] || charset || 'utf8';
458
+ if (charsetName === 'utf8') {
459
+ return buffer.toString('utf8');
460
+ }
461
+ try {
462
+ // A leading U+FEFF is column data, not a byte-order mark: the
463
+ // charset's byte order is fixed, and the server preserves the
464
+ // character when converting for query results
465
+ return iconv.decode(buffer, charsetName, { stripBOM: false });
466
+ } catch (err) {
467
+ console.warn(err);
468
+ return buffer;
469
+ }
470
+ }
471
+
472
+ const uncompressColumnValue = function(buffer) {
473
+ if (buffer.length === 0) {
474
+ return buffer;
475
+ }
476
+ const header = buffer[0];
477
+ const method = header >> 4;
478
+ if (method === 0) {
479
+ return buffer.subarray(1);
480
+ }
481
+ if (method === 8) {
482
+ const lengthBytes = header & 0x07;
483
+ const compressed = buffer.subarray(1 + lengthBytes);
484
+ return (header & 0x08)
485
+ ? zlib.inflateRawSync(compressed)
486
+ : zlib.inflateSync(compressed);
487
+ }
488
+ throw new Error(
489
+ `Unknown MariaDB column compression method ${method}`);
490
+ };
491
+
492
+ // Undoes the payload envelope of MariaDB's compressed binlog EVENTS
493
+ // (log_bin_compress=ON, event codes 165-171; sql/log_event.cc
494
+ // binlog_get_uncompress_len / binlog_buf_uncompress). Distinct from the
495
+ // per-column COMPRESSED envelope above: header byte has bit 7 always set,
496
+ // bits 4-6 = algorithm (only 0 = zlib is defined), bits 0-2 = size of the
497
+ // big-endian original-length field that follows; then a zlib-wrapped
498
+ // deflate stream.
499
+ export function uncompressBinlogEventPayload(buffer) {
500
+ if (buffer.length < 1) {
501
+ throw new Error('Empty compressed binlog event payload');
502
+ }
503
+ const header = buffer[0];
504
+ const algorithm = (header & 0x70) >> 4;
505
+ if ((header & 0x80) === 0 || algorithm !== 0) {
506
+ throw new Error(
507
+ `Unknown binlog event compression header 0x${header.toString(16)}`);
508
+ }
509
+ const lengthBytes = header & 0x07;
510
+ if (lengthBytes < 1 || lengthBytes > 4 ||
511
+ buffer.length < 1 + lengthBytes) {
512
+ throw new Error(
513
+ `Corrupt binlog event compression header 0x${header.toString(16)}`);
514
+ }
515
+ const originalLength = buffer.readUIntBE(1, lengthBytes);
516
+ const inflated = zlib.inflateSync(buffer.subarray(1 + lengthBytes));
517
+ if (inflated.length !== originalLength) {
518
+ throw new Error(
519
+ `Compressed binlog event inflated to ${inflated.length} bytes, ` +
520
+ `expected ${originalLength}`);
521
+ }
522
+ return inflated;
523
+ }
524
+
525
+ // Dotted-quad text form of a MariaDB INET4 value (4 bytes, network order)
526
+ export function formatInet4(buffer) {
527
+ return `${buffer[0]}.${buffer[1]}.${buffer[2]}.${buffer[3]}`;
528
+ }
529
+
530
+ // Canonical text form of a MariaDB INET6 value (16 bytes, network order),
531
+ // matching the server's own formatting: RFC 5952 (lowercase hex groups,
532
+ // the longest run of two or more zero groups compressed to '::', the
533
+ // leftmost on a tie) with IPv4-mapped (::ffff:a.b.c.d) and IPv4-compatible
534
+ // (::a.b.c.d, except :: and ::1) addresses in dotted form
535
+ export function formatInet6(buffer) {
536
+ const leadingZero = buffer.subarray(0, 10).every(byte => byte === 0);
537
+ if (leadingZero) {
538
+ const v4 = formatInet4(buffer.subarray(12));
539
+ if (buffer[10] === 0xff && buffer[11] === 0xff) {
540
+ return `::ffff:${v4}`;
541
+ }
542
+ if (buffer[10] === 0 && buffer[11] === 0 &&
543
+ buffer.readUInt32BE(12) > 1) {
544
+ return `::${v4}`;
545
+ }
546
+ }
547
+
548
+ const groups = [];
549
+ for (let i = 0; i < 16; i += 2) {
550
+ groups.push(buffer.readUInt16BE(i));
551
+ }
552
+ // Longest run of >=2 zero groups; leftmost wins a tie
553
+ let bestStart = -1, bestLength = 1;
554
+ for (let i = 0; i < 8;) {
555
+ if (groups[i] !== 0) { i++; continue; }
556
+ let j = i;
557
+ while (j < 8 && groups[j] === 0) j++;
558
+ if (j - i > bestLength) {
559
+ bestStart = i;
560
+ bestLength = j - i;
561
+ }
562
+ i = j;
563
+ }
564
+ const hex = (value) => value.toString(16);
565
+ if (bestStart === -1) {
566
+ return groups.map(hex).join(':');
567
+ }
568
+ const head = groups.slice(0, bestStart).map(hex).join(':');
569
+ const tail = groups.slice(bestStart + bestLength).map(hex).join(':');
570
+ return `${head}::${tail}`;
571
+ }
572
+
573
+ // MariaDB 5.3-era "hires" temporals (written under mysql56_temporal_format
574
+ // =OFF, or by tables never rebuilt since 10.0): fractional TIME/DATETIME/
575
+ // TIMESTAMP binlogged under the classic type codes 11/12/7 with no
576
+ // metadata, so the precision is only knowable from the column definition
577
+ // (COLUMN_TYPE reads e.g. 'datetime(3) /* mariadb-5.3 */'). MySQL never
578
+ // produces these codes with a precision: fractional temporals always use
579
+ // TIME2/DATETIME2/TIMESTAMP2.
580
+ const hiresPrecision = function(columnSchema) {
581
+ const match = /^(?:time|datetime|timestamp)\((\d)\)/
582
+ .exec(columnSchema.COLUMN_TYPE);
583
+ return match ? parseInt(match[1], 10) : 0;
584
+ };
585
+
586
+ // Same shape as readTemporalFraction, from a fraction that was stored
587
+ // with exactly `precision` decimal digits
588
+ const hiresFractionObject = function(value, precision) {
589
+ let milliseconds;
590
+ if (precision > 3) {
591
+ milliseconds = Math.floor(value / Math.pow(10, precision - 3));
592
+ } else if (precision < 3) {
593
+ milliseconds = value * Math.pow(10, 3 - precision);
594
+ } else {
595
+ milliseconds = value;
596
+ }
597
+ return { value, precision, milliseconds };
598
+ };
599
+
356
600
  // This function is used to read and interpret non-null values from parser.
357
601
  // The parser object contains the raw value, and functions useful for value
358
602
  // interpretation, but not the underlying column type or how the value ought to
@@ -406,8 +650,11 @@ export function readMysqlValue(
406
650
  low = parser.parseUnsignedNumber(column.metadata.size);
407
651
  }
408
652
 
653
+ // Binlog optional metadata provides the value list directly;
654
+ // otherwise parse it out of the column definition.
409
655
  // Second argument: prefixLen = 4 'set('
410
- choices = parseSetEnumTypeDef(columnSchema.COLUMN_TYPE, 4);
656
+ choices = columnSchema.SET_VALUES ||
657
+ parseSetEnumTypeDef(columnSchema.COLUMN_TYPE, 4);
411
658
  result = '';
412
659
  for (let i = 0; low >= Math.pow(2, i); i++) {
413
660
  if (low & Math.pow(2, i)) result += choices[i] + ',';
@@ -422,27 +669,59 @@ export function readMysqlValue(
422
669
  case MysqlTypes.ENUM:
423
670
  raw = parser.parseUnsignedNumber(column.metadata.size);
424
671
  // Second argument: prefixLen = 5 'enum('
425
- choices = parseSetEnumTypeDef(columnSchema.COLUMN_TYPE, 5);
672
+ choices = columnSchema.ENUM_VALUES ||
673
+ parseSetEnumTypeDef(columnSchema.COLUMN_TYPE, 5);
426
674
  result = choices[raw - 1];
427
675
  break;
428
676
  case MysqlTypes.VAR_STRING:
429
677
  // Never used?
430
678
  result = parser.parseLengthCodedString();
431
679
  break;
680
+ case MysqlTypes.VARCHAR_COMPRESSED: {
681
+ // MariaDB VARCHAR/VARBINARY ... COMPRESSED: length-prefixed like
682
+ // VARCHAR, with a per-value compression envelope around the payload
683
+ const prefixSize = column.metadata['max_length'] > 255 ? 2 : 1;
684
+ size = parser.parseUnsignedNumber(prefixSize);
685
+ buffer = uncompressColumnValue(parser.parseBuffer(size));
686
+ result = columnSchema.COLUMN_TYPE.substr(0, 6) === 'varbin' ?
687
+ buffer : decodeTextColumn(buffer, column.charset);
688
+ break;
689
+ }
432
690
  case MysqlTypes.VARCHAR:
433
691
  case MysqlTypes.STRING: {
434
692
  const prefixSize = column.metadata['max_length'] > 255 ? 2 : 1;
435
693
  size = parser.parseUnsignedNumber(prefixSize);
436
694
  const def = columnSchema.COLUMN_TYPE;
437
695
  const defPrefix = def.substr(0, 6);
438
- if (defPrefix === 'binary') {
696
+ // MariaDB logical types ride on STRING/VARCHAR with a binary
697
+ // charset; only COLUMN_TYPE distinguishes them from BINARY(n) or
698
+ // VARBINARY(n). Text forms match the server's own (and therefore
699
+ // what a mysql2 query returns); VECTOR stays a raw Buffer of
700
+ // little-endian float32s, as in query results. As with BINARY(n),
701
+ // the row image strips trailing zero bytes, so the value is
702
+ // re-padded to the type's fixed width before formatting.
703
+ const readFixedWidth = (width) => {
704
+ const padded = Buffer.alloc(width, 0);
705
+ parser.parseBuffer(size).copy(padded);
706
+ return padded;
707
+ };
708
+ if (def === 'uuid') {
709
+ result = formatUuid(readFixedWidth(16));
710
+ } else if (def === 'inet4') {
711
+ result = formatInet4(readFixedWidth(4));
712
+ } else if (def === 'inet6') {
713
+ result = formatInet6(readFixedWidth(16));
714
+ } else if (defPrefix === 'vector') {
715
+ result = parser.parseBuffer(size);
716
+ } else if (defPrefix === 'binary') {
439
717
  const bufsize = parseInt(def.substr(7, def.length - 2), 10);
440
718
  result = Buffer.alloc(bufsize, 0);
441
719
  parser.parseBuffer(size).copy(result);
442
720
  } else if (defPrefix === 'varbin') {
443
721
  result = parser.parseBuffer(size);
444
722
  } else {
445
- result = parser.parseString(size);
723
+ // Text CHAR/VARCHAR: decode in the column's own charset
724
+ result = decodeTextColumn(parser.parseBuffer(size), column.charset);
446
725
  }
447
726
  break;
448
727
  }
@@ -450,24 +729,20 @@ export function readMysqlValue(
450
729
  case MysqlTypes.MEDIUM_BLOB:
451
730
  case MysqlTypes.LONG_BLOB:
452
731
  case MysqlTypes.BLOB:
732
+ case MysqlTypes.BLOB_COMPRESSED:
453
733
  lengthSize = column.metadata['length_size'];
454
734
  result = parser.parseBuffer(parser.parseUnsignedNumber(lengthSize));
735
+ if (column.type === MysqlTypes.BLOB_COMPRESSED) {
736
+ // MariaDB TEXT/BLOB ... COMPRESSED: the payload wears a per-value
737
+ // compression envelope; the charset handling below then applies
738
+ // to the inflated bytes as for a plain TEXT/BLOB
739
+ result = uncompressColumnValue(result);
740
+ }
455
741
 
456
742
  // Blobs can be sometimes return as String instead of Buffer
457
743
  // e.g. TINYTEXT, MEDIUMTEXT, LONGTEXT, TEXT data types
458
744
  if (column.charset !== null) {
459
- // Javascript UTF8 always allows up to 4 bytes per character
460
- switch (column.charset) {
461
- case 'utf8mb3':
462
- case 'utf8mb4':
463
- column.charset = 'utf8';
464
- break;
465
- }
466
- try {
467
- result = iconv.decode(result, column.charset);
468
- } catch (err) {
469
- console.warn(err);
470
- }
745
+ result = decodeTextColumn(result, column.charset);
471
746
  }
472
747
  break;
473
748
  case MysqlTypes.JSON: {
@@ -504,7 +779,32 @@ export function readMysqlValue(
504
779
  zongji.connection.config.timezone
505
780
  );
506
781
  break;
507
- case MysqlTypes.TIME:
782
+ case MysqlTypes.TIME: {
783
+ const hiresDec = hiresPrecision(columnSchema);
784
+ if (hiresDec > 0) {
785
+ // MariaDB hires TIME: one big-endian unsigned integer of signed
786
+ // total time units offset by zero_point = 3020400 * 10^dec
787
+ // (TIME_MAX_VALUE_SECONDS + 1, scaled), so '00:00:00' stores
788
+ // exactly zero_point and the encoding sorts as unsigned
789
+ const bytes = [3, 4, 4, 5, 5, 5, 6][hiresDec];
790
+ const scale = Math.pow(10, hiresDec);
791
+ let stored = parser._buffer.readUIntBE(parser._offset, bytes);
792
+ parser._offset += bytes;
793
+ stored -= 3020400 * scale;
794
+ isNegative = stored < 0;
795
+ if (isNegative) stored = -stored;
796
+ fraction = hiresFractionObject(stored % scale, hiresDec);
797
+ const totalSeconds = Math.floor(stored / scale);
798
+ hour = Math.floor(totalSeconds / 3600);
799
+ minute = Math.floor((totalSeconds % 3600) / 60);
800
+ second = totalSeconds % 60;
801
+ result = (isNegative ? '-' : '') +
802
+ zeroPad(hour, hour > 99 ? 3 : 2) + ':' +
803
+ zeroPad(minute, 2) + ':' +
804
+ zeroPad(second, 2) +
805
+ dtDecode.getFractionString(fraction);
806
+ break;
807
+ }
508
808
  raw = parseUInt24(parser);
509
809
 
510
810
  isNegative = (raw & (1 << 23)) !== 0;
@@ -520,6 +820,7 @@ export function readMysqlValue(
520
820
  zeroPad(minute, 2) + ':' +
521
821
  zeroPad(second, 2);
522
822
  break;
823
+ }
523
824
  case MysqlTypes.TIME2:
524
825
  raw = readIntBE(parser._buffer, parser._offset, 3);
525
826
  parser._offset += 3;
@@ -545,7 +846,41 @@ export function readMysqlValue(
545
846
  result += dtDecode.getFractionString(fraction);
546
847
  }
547
848
  break;
548
- case MysqlTypes.DATETIME:
849
+ case MysqlTypes.DATETIME: {
850
+ const hiresDec = hiresPrecision(columnSchema);
851
+ if (hiresDec > 0) {
852
+ // MariaDB hires DATETIME: one big-endian unsigned integer of
853
+ // pack_time() scaled to dec fractional digits; mixed radix, not
854
+ // the YYYYMMDDHHMMSS decimal of the classic format and not the
855
+ // DATETIME2 bit layout. Up to 8 bytes (beyond 2^53), so split in
856
+ // BigInt.
857
+ const bytes = [5, 6, 6, 7, 7, 7, 8][hiresDec];
858
+ let packed = 0n;
859
+ for (let i = 0; i < bytes; i++) {
860
+ packed = (packed << 8n) | BigInt(parser._buffer[parser._offset + i]);
861
+ }
862
+ parser._offset += bytes;
863
+ const scale = 10n ** BigInt(hiresDec);
864
+ fraction = hiresFractionObject(Number(packed % scale), hiresDec);
865
+ let units = packed / scale;
866
+ const secondPart = Number(units % 60n); units /= 60n;
867
+ const minutePart = Number(units % 60n); units /= 60n;
868
+ const hourPart = Number(units % 24n); units /= 24n;
869
+ const dayPart = Number(units % 32n); units /= 32n;
870
+ const monthPart = Number(units % 13n); units /= 13n;
871
+ result = dtDecode.getDateTime(
872
+ zongji.connection.config.dateStrings,
873
+ Number(units), // year
874
+ monthPart,
875
+ dayPart,
876
+ hourPart,
877
+ minutePart,
878
+ secondPart,
879
+ fraction,
880
+ zongji.connection.config.timezone
881
+ );
882
+ break;
883
+ }
549
884
  raw = parseUInt64(parser);
550
885
  date = Math.floor(raw / 1000000);
551
886
  time = raw % 1000000;
@@ -561,6 +896,7 @@ export function readMysqlValue(
561
896
  zongji.connection.config.timezone
562
897
  );
563
898
  break;
899
+ }
564
900
  case MysqlTypes.DATETIME2: {
565
901
  // Overlapping high-low to get all data in 32-bit numbers
566
902
  const rawHigh = readIntBE(parser._buffer, parser._offset, 4);
@@ -582,7 +918,26 @@ export function readMysqlValue(
582
918
  );
583
919
  break;
584
920
  }
585
- case MysqlTypes.TIMESTAMP:
921
+ case MysqlTypes.TIMESTAMP: {
922
+ const hiresDec = hiresPrecision(columnSchema);
923
+ if (hiresDec > 0) {
924
+ // MariaDB hires TIMESTAMP: big-endian epoch seconds (the classic
925
+ // format is little-endian) followed by a big-endian fraction of
926
+ // exactly dec digits
927
+ raw = parser._buffer.readUInt32BE(parser._offset);
928
+ parser._offset += 4;
929
+ const fracBytes = [0, 1, 1, 2, 2, 3, 3][hiresDec];
930
+ fraction = hiresFractionObject(
931
+ parser._buffer.readUIntBE(parser._offset, fracBytes), hiresDec);
932
+ parser._offset += fracBytes;
933
+ result = dtDecode.getTimeStamp(
934
+ zongji.connection.config.dateStrings,
935
+ raw,
936
+ fraction,
937
+ zongji.connection.config.timezone
938
+ );
939
+ break;
940
+ }
586
941
  raw = parser.parseUnsignedNumber(4);
587
942
  result = dtDecode.getTimeStamp(
588
943
  zongji.connection.config.dateStrings,
@@ -591,6 +946,7 @@ export function readMysqlValue(
591
946
  zongji.connection.config.timezone
592
947
  );
593
948
  break;
949
+ }
594
950
  case MysqlTypes.TIMESTAMP2:
595
951
  raw = readIntBE(parser._buffer, parser._offset, 4);
596
952
  parser._offset += 4;