@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.
@@ -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,
@@ -51,6 +58,14 @@ export function unsignedInt64ToNumberOrString(high, low) {
51
58
  return value <= MAX_SAFE_BIGINT ? Number(value) : value.toString();
52
59
  }
53
60
 
61
+ // An exact BigInt to the Number-or-exact-string convention used for
62
+ // 64-bit values: a Number within the safe integer range, otherwise an
63
+ // exact decimal string.
64
+ export function bigintToNumberOrString(value) {
65
+ return (value >= MIN_SAFE_BIGINT && value <= MAX_SAFE_BIGINT) ?
66
+ Number(value) : value.toString();
67
+ }
68
+
54
69
  // Combine two unsigned 32-bit halves into an exact signed (two's
55
70
  // complement) 64-bit value. Returns a Number when within the safe integer
56
71
  // range, otherwise an exact string.
@@ -158,7 +173,12 @@ const parseAnyInt = function(parser, column, columnSchema) {
158
173
  result = parseUInt64(parser);
159
174
  break;
160
175
  }
161
- if (columnSchema.COLUMN_TYPE.indexOf('unsigned') === -1) {
176
+ // Binlog optional metadata provides signedness directly (UNSIGNED);
177
+ // otherwise infer it from the INFORMATION_SCHEMA column definition
178
+ const unsigned = columnSchema.UNSIGNED !== undefined ?
179
+ columnSchema.UNSIGNED :
180
+ columnSchema.COLUMN_TYPE.indexOf('unsigned') !== -1;
181
+ if (!unsigned) {
162
182
  const length = size * 8;
163
183
  const int64 = (length == 64);
164
184
  // Flip bits on negative signed integer
@@ -353,6 +373,238 @@ const readTemporalFraction = function(parser, fractionPrecision) {
353
373
  };
354
374
  };
355
375
 
376
+ export function formatUuid(buffer) {
377
+ const hex = buffer.toString('hex');
378
+ return [
379
+ hex.slice(0, 8),
380
+ hex.slice(8, 12),
381
+ hex.slice(12, 16),
382
+ hex.slice(16, 20),
383
+ hex.slice(20)
384
+ ].join('-');
385
+ }
386
+
387
+ // Undoes MariaDB's per-value column compression (VARCHAR/BLOB COMPRESSED,
388
+ // sql/field_comp.cc). Header byte: bits 4-7 = method (0 = stored
389
+ // uncompressed, 8 = zlib), and for zlib bits 0-2 = size of the big-endian
390
+ // original-length field that follows, bit 3 = deflate stream has no zlib
391
+ // wrapper (raw deflate). An empty value is stored with no header at all.
392
+ // Variable-length integer of MySQL's mysql::serialization framework
393
+ // (libs/mysql/serialization/variable_length_integers.h, used by the
394
+ // tagged GTID event of MySQL 8.3+). NOT protobuf varint and NOT the
395
+ // classic length-encoded integer: the total byte count is the number of
396
+ // trailing one-bits of the first byte plus one (1-9); the value is the
397
+ // first byte shifted right by that count, joined with the remaining
398
+ // bytes little-endian shifted left by (8 - count). A 9-byte encoding
399
+ // (first byte 0xFF) carries a full little-endian u64 unshifted.
400
+ // Returns a BigInt; `end` bounds the read (event end minus checksum).
401
+ export function parseSerializationVarint(parser, end, what) {
402
+ if (parser._offset + 1 > end) {
403
+ throw new Error(`Truncated ${what}: varint head past event end`);
404
+ }
405
+ const first = parser.parseUnsignedNumber(1);
406
+ let extra = 0;
407
+ while (extra < 8 && (first >> extra) & 1) {
408
+ extra++;
409
+ }
410
+ if (parser._offset + extra > end) {
411
+ throw new Error(`Truncated ${what}: ${extra + 1}-byte varint ` +
412
+ 'past event end');
413
+ }
414
+ if (extra === 8) {
415
+ let value = 0n;
416
+ for (let i = 0; i < 8; i++) {
417
+ value |= BigInt(parser.parseUnsignedNumber(1)) << BigInt(8 * i);
418
+ }
419
+ return value;
420
+ }
421
+ let rest = 0n;
422
+ for (let i = 0; i < extra; i++) {
423
+ rest |= BigInt(parser.parseUnsignedNumber(1)) << BigInt(8 * i);
424
+ }
425
+ return (BigInt(first) >> BigInt(extra + 1)) |
426
+ (rest << BigInt(8 - (extra + 1)));
427
+ }
428
+
429
+ // Signed variant: value x >= 0 encodes as x << 1, x < 0 as
430
+ // ((-(x + 1)) << 1) | 1 (a zigzag-like mapping)
431
+ export function parseSerializationVarintSigned(parser, end, what) {
432
+ const u = parseSerializationVarint(parser, end, what);
433
+ return (u & 1n) ? -(u >> 1n) - 1n : u >> 1n;
434
+ }
435
+
436
+ // MySQL charset names mostly match iconv-lite encoding names, with four
437
+ // classes of exception: the utf8 aliases; MySQL's "latin1", which is
438
+ // really cp1252 (its 0x80-0x9F range holds printable characters that
439
+ // ISO-8859-1 reserves for controls, and the server converts them as such
440
+ // for query results); the Unicode charsets whose stored byte order is
441
+ // big-endian while the iconv-lite encodings of the same spelling default
442
+ // to little-endian; and names iconv-lite spells differently or lacks
443
+ // (ujis is EUC-JP, eucjpms is its MS-extended variant for which plain
444
+ // EUC-JP is the closest decoder available, macce is Mac Central
445
+ // European). dec8, geostd8, hp8, keybcs2 and swe7 have no iconv-lite
446
+ // decoder at all; those columns come through as raw Buffers.
447
+ const ICONV_CHARSET_OVERRIDES = {
448
+ utf8mb3: 'utf8',
449
+ utf8mb4: 'utf8',
450
+ latin1: 'cp1252',
451
+ ucs2: 'utf16-be',
452
+ utf16: 'utf16-be',
453
+ utf16le: 'utf16-le',
454
+ utf32: 'utf32-be',
455
+ ujis: 'eucjp',
456
+ eucjpms: 'eucjp',
457
+ macce: 'maccenteuro',
458
+ };
459
+
460
+ // Decodes a text column's raw bytes using the column's own character set
461
+ // (INFORMATION_SCHEMA CHARACTER_SET_NAME, or the charset carried by FULL
462
+ // binlog metadata). Returns the buffer unchanged when the charset is
463
+ // unknown to iconv.
464
+ export function decodeTextColumn(buffer, charset) {
465
+ const charsetName = ICONV_CHARSET_OVERRIDES[charset] || charset || 'utf8';
466
+ if (charsetName === 'utf8') {
467
+ return buffer.toString('utf8');
468
+ }
469
+ try {
470
+ // A leading U+FEFF is column data, not a byte-order mark: the
471
+ // charset's byte order is fixed, and the server preserves the
472
+ // character when converting for query results
473
+ return iconv.decode(buffer, charsetName, { stripBOM: false });
474
+ } catch (err) {
475
+ console.warn(err);
476
+ return buffer;
477
+ }
478
+ }
479
+
480
+ const uncompressColumnValue = function(buffer) {
481
+ if (buffer.length === 0) {
482
+ return buffer;
483
+ }
484
+ const header = buffer[0];
485
+ const method = header >> 4;
486
+ if (method === 0) {
487
+ return buffer.subarray(1);
488
+ }
489
+ if (method === 8) {
490
+ const lengthBytes = header & 0x07;
491
+ const compressed = buffer.subarray(1 + lengthBytes);
492
+ return (header & 0x08)
493
+ ? zlib.inflateRawSync(compressed)
494
+ : zlib.inflateSync(compressed);
495
+ }
496
+ throw new Error(
497
+ `Unknown MariaDB column compression method ${method}`);
498
+ };
499
+
500
+ // Undoes the payload envelope of MariaDB's compressed binlog EVENTS
501
+ // (log_bin_compress=ON, event codes 165-171; sql/log_event.cc
502
+ // binlog_get_uncompress_len / binlog_buf_uncompress). Distinct from the
503
+ // per-column COMPRESSED envelope above: header byte has bit 7 always set,
504
+ // bits 4-6 = algorithm (only 0 = zlib is defined), bits 0-2 = size of the
505
+ // big-endian original-length field that follows; then a zlib-wrapped
506
+ // deflate stream.
507
+ export function uncompressBinlogEventPayload(buffer) {
508
+ if (buffer.length < 1) {
509
+ throw new Error('Empty compressed binlog event payload');
510
+ }
511
+ const header = buffer[0];
512
+ const algorithm = (header & 0x70) >> 4;
513
+ if ((header & 0x80) === 0 || algorithm !== 0) {
514
+ throw new Error(
515
+ `Unknown binlog event compression header 0x${header.toString(16)}`);
516
+ }
517
+ const lengthBytes = header & 0x07;
518
+ if (lengthBytes < 1 || lengthBytes > 4 ||
519
+ buffer.length < 1 + lengthBytes) {
520
+ throw new Error(
521
+ `Corrupt binlog event compression header 0x${header.toString(16)}`);
522
+ }
523
+ const originalLength = buffer.readUIntBE(1, lengthBytes);
524
+ const inflated = zlib.inflateSync(buffer.subarray(1 + lengthBytes));
525
+ if (inflated.length !== originalLength) {
526
+ throw new Error(
527
+ `Compressed binlog event inflated to ${inflated.length} bytes, ` +
528
+ `expected ${originalLength}`);
529
+ }
530
+ return inflated;
531
+ }
532
+
533
+ // Dotted-quad text form of a MariaDB INET4 value (4 bytes, network order)
534
+ export function formatInet4(buffer) {
535
+ return `${buffer[0]}.${buffer[1]}.${buffer[2]}.${buffer[3]}`;
536
+ }
537
+
538
+ // Canonical text form of a MariaDB INET6 value (16 bytes, network order),
539
+ // matching the server's own formatting: RFC 5952 (lowercase hex groups,
540
+ // the longest run of two or more zero groups compressed to '::', the
541
+ // leftmost on a tie) with IPv4-mapped (::ffff:a.b.c.d) and IPv4-compatible
542
+ // (::a.b.c.d, except :: and ::1) addresses in dotted form
543
+ export function formatInet6(buffer) {
544
+ const leadingZero = buffer.subarray(0, 10).every(byte => byte === 0);
545
+ if (leadingZero) {
546
+ const v4 = formatInet4(buffer.subarray(12));
547
+ if (buffer[10] === 0xff && buffer[11] === 0xff) {
548
+ return `::ffff:${v4}`;
549
+ }
550
+ if (buffer[10] === 0 && buffer[11] === 0 &&
551
+ buffer.readUInt32BE(12) > 1) {
552
+ return `::${v4}`;
553
+ }
554
+ }
555
+
556
+ const groups = [];
557
+ for (let i = 0; i < 16; i += 2) {
558
+ groups.push(buffer.readUInt16BE(i));
559
+ }
560
+ // Longest run of >=2 zero groups; leftmost wins a tie
561
+ let bestStart = -1, bestLength = 1;
562
+ for (let i = 0; i < 8;) {
563
+ if (groups[i] !== 0) { i++; continue; }
564
+ let j = i;
565
+ while (j < 8 && groups[j] === 0) j++;
566
+ if (j - i > bestLength) {
567
+ bestStart = i;
568
+ bestLength = j - i;
569
+ }
570
+ i = j;
571
+ }
572
+ const hex = (value) => value.toString(16);
573
+ if (bestStart === -1) {
574
+ return groups.map(hex).join(':');
575
+ }
576
+ const head = groups.slice(0, bestStart).map(hex).join(':');
577
+ const tail = groups.slice(bestStart + bestLength).map(hex).join(':');
578
+ return `${head}::${tail}`;
579
+ }
580
+
581
+ // MariaDB 5.3-era "hires" temporals (written under mysql56_temporal_format
582
+ // =OFF, or by tables never rebuilt since 10.0): fractional TIME/DATETIME/
583
+ // TIMESTAMP binlogged under the classic type codes 11/12/7 with no
584
+ // metadata, so the precision is only knowable from the column definition
585
+ // (COLUMN_TYPE reads e.g. 'datetime(3) /* mariadb-5.3 */'). MySQL never
586
+ // produces these codes with a precision: fractional temporals always use
587
+ // TIME2/DATETIME2/TIMESTAMP2.
588
+ const hiresPrecision = function(columnSchema) {
589
+ const match = /^(?:time|datetime|timestamp)\((\d)\)/
590
+ .exec(columnSchema.COLUMN_TYPE);
591
+ return match ? parseInt(match[1], 10) : 0;
592
+ };
593
+
594
+ // Same shape as readTemporalFraction, from a fraction that was stored
595
+ // with exactly `precision` decimal digits
596
+ const hiresFractionObject = function(value, precision) {
597
+ let milliseconds;
598
+ if (precision > 3) {
599
+ milliseconds = Math.floor(value / Math.pow(10, precision - 3));
600
+ } else if (precision < 3) {
601
+ milliseconds = value * Math.pow(10, 3 - precision);
602
+ } else {
603
+ milliseconds = value;
604
+ }
605
+ return { value, precision, milliseconds };
606
+ };
607
+
356
608
  // This function is used to read and interpret non-null values from parser.
357
609
  // The parser object contains the raw value, and functions useful for value
358
610
  // interpretation, but not the underlying column type or how the value ought to
@@ -406,8 +658,11 @@ export function readMysqlValue(
406
658
  low = parser.parseUnsignedNumber(column.metadata.size);
407
659
  }
408
660
 
661
+ // Binlog optional metadata provides the value list directly;
662
+ // otherwise parse it out of the column definition.
409
663
  // Second argument: prefixLen = 4 'set('
410
- choices = parseSetEnumTypeDef(columnSchema.COLUMN_TYPE, 4);
664
+ choices = columnSchema.SET_VALUES ||
665
+ parseSetEnumTypeDef(columnSchema.COLUMN_TYPE, 4);
411
666
  result = '';
412
667
  for (let i = 0; low >= Math.pow(2, i); i++) {
413
668
  if (low & Math.pow(2, i)) result += choices[i] + ',';
@@ -422,27 +677,59 @@ export function readMysqlValue(
422
677
  case MysqlTypes.ENUM:
423
678
  raw = parser.parseUnsignedNumber(column.metadata.size);
424
679
  // Second argument: prefixLen = 5 'enum('
425
- choices = parseSetEnumTypeDef(columnSchema.COLUMN_TYPE, 5);
680
+ choices = columnSchema.ENUM_VALUES ||
681
+ parseSetEnumTypeDef(columnSchema.COLUMN_TYPE, 5);
426
682
  result = choices[raw - 1];
427
683
  break;
428
684
  case MysqlTypes.VAR_STRING:
429
685
  // Never used?
430
686
  result = parser.parseLengthCodedString();
431
687
  break;
688
+ case MysqlTypes.VARCHAR_COMPRESSED: {
689
+ // MariaDB VARCHAR/VARBINARY ... COMPRESSED: length-prefixed like
690
+ // VARCHAR, with a per-value compression envelope around the payload
691
+ const prefixSize = column.metadata['max_length'] > 255 ? 2 : 1;
692
+ size = parser.parseUnsignedNumber(prefixSize);
693
+ buffer = uncompressColumnValue(parser.parseBuffer(size));
694
+ result = columnSchema.COLUMN_TYPE.substr(0, 6) === 'varbin' ?
695
+ buffer : decodeTextColumn(buffer, column.charset);
696
+ break;
697
+ }
432
698
  case MysqlTypes.VARCHAR:
433
699
  case MysqlTypes.STRING: {
434
700
  const prefixSize = column.metadata['max_length'] > 255 ? 2 : 1;
435
701
  size = parser.parseUnsignedNumber(prefixSize);
436
702
  const def = columnSchema.COLUMN_TYPE;
437
703
  const defPrefix = def.substr(0, 6);
438
- if (defPrefix === 'binary') {
704
+ // MariaDB logical types ride on STRING/VARCHAR with a binary
705
+ // charset; only COLUMN_TYPE distinguishes them from BINARY(n) or
706
+ // VARBINARY(n). Text forms match the server's own (and therefore
707
+ // what a mysql2 query returns); VECTOR stays a raw Buffer of
708
+ // little-endian float32s, as in query results. As with BINARY(n),
709
+ // the row image strips trailing zero bytes, so the value is
710
+ // re-padded to the type's fixed width before formatting.
711
+ const readFixedWidth = (width) => {
712
+ const padded = Buffer.alloc(width, 0);
713
+ parser.parseBuffer(size).copy(padded);
714
+ return padded;
715
+ };
716
+ if (def === 'uuid') {
717
+ result = formatUuid(readFixedWidth(16));
718
+ } else if (def === 'inet4') {
719
+ result = formatInet4(readFixedWidth(4));
720
+ } else if (def === 'inet6') {
721
+ result = formatInet6(readFixedWidth(16));
722
+ } else if (defPrefix === 'vector') {
723
+ result = parser.parseBuffer(size);
724
+ } else if (defPrefix === 'binary') {
439
725
  const bufsize = parseInt(def.substr(7, def.length - 2), 10);
440
726
  result = Buffer.alloc(bufsize, 0);
441
727
  parser.parseBuffer(size).copy(result);
442
728
  } else if (defPrefix === 'varbin') {
443
729
  result = parser.parseBuffer(size);
444
730
  } else {
445
- result = parser.parseString(size);
731
+ // Text CHAR/VARCHAR: decode in the column's own charset
732
+ result = decodeTextColumn(parser.parseBuffer(size), column.charset);
446
733
  }
447
734
  break;
448
735
  }
@@ -450,24 +737,20 @@ export function readMysqlValue(
450
737
  case MysqlTypes.MEDIUM_BLOB:
451
738
  case MysqlTypes.LONG_BLOB:
452
739
  case MysqlTypes.BLOB:
740
+ case MysqlTypes.BLOB_COMPRESSED:
453
741
  lengthSize = column.metadata['length_size'];
454
742
  result = parser.parseBuffer(parser.parseUnsignedNumber(lengthSize));
743
+ if (column.type === MysqlTypes.BLOB_COMPRESSED) {
744
+ // MariaDB TEXT/BLOB ... COMPRESSED: the payload wears a per-value
745
+ // compression envelope; the charset handling below then applies
746
+ // to the inflated bytes as for a plain TEXT/BLOB
747
+ result = uncompressColumnValue(result);
748
+ }
455
749
 
456
750
  // Blobs can be sometimes return as String instead of Buffer
457
751
  // e.g. TINYTEXT, MEDIUMTEXT, LONGTEXT, TEXT data types
458
752
  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
- }
753
+ result = decodeTextColumn(result, column.charset);
471
754
  }
472
755
  break;
473
756
  case MysqlTypes.JSON: {
@@ -504,7 +787,32 @@ export function readMysqlValue(
504
787
  zongji.connection.config.timezone
505
788
  );
506
789
  break;
507
- case MysqlTypes.TIME:
790
+ case MysqlTypes.TIME: {
791
+ const hiresDec = hiresPrecision(columnSchema);
792
+ if (hiresDec > 0) {
793
+ // MariaDB hires TIME: one big-endian unsigned integer of signed
794
+ // total time units offset by zero_point = 3020400 * 10^dec
795
+ // (TIME_MAX_VALUE_SECONDS + 1, scaled), so '00:00:00' stores
796
+ // exactly zero_point and the encoding sorts as unsigned
797
+ const bytes = [3, 4, 4, 5, 5, 5, 6][hiresDec];
798
+ const scale = Math.pow(10, hiresDec);
799
+ let stored = parser._buffer.readUIntBE(parser._offset, bytes);
800
+ parser._offset += bytes;
801
+ stored -= 3020400 * scale;
802
+ isNegative = stored < 0;
803
+ if (isNegative) stored = -stored;
804
+ fraction = hiresFractionObject(stored % scale, hiresDec);
805
+ const totalSeconds = Math.floor(stored / scale);
806
+ hour = Math.floor(totalSeconds / 3600);
807
+ minute = Math.floor((totalSeconds % 3600) / 60);
808
+ second = totalSeconds % 60;
809
+ result = (isNegative ? '-' : '') +
810
+ zeroPad(hour, hour > 99 ? 3 : 2) + ':' +
811
+ zeroPad(minute, 2) + ':' +
812
+ zeroPad(second, 2) +
813
+ dtDecode.getFractionString(fraction);
814
+ break;
815
+ }
508
816
  raw = parseUInt24(parser);
509
817
 
510
818
  isNegative = (raw & (1 << 23)) !== 0;
@@ -520,6 +828,7 @@ export function readMysqlValue(
520
828
  zeroPad(minute, 2) + ':' +
521
829
  zeroPad(second, 2);
522
830
  break;
831
+ }
523
832
  case MysqlTypes.TIME2:
524
833
  raw = readIntBE(parser._buffer, parser._offset, 3);
525
834
  parser._offset += 3;
@@ -545,7 +854,41 @@ export function readMysqlValue(
545
854
  result += dtDecode.getFractionString(fraction);
546
855
  }
547
856
  break;
548
- case MysqlTypes.DATETIME:
857
+ case MysqlTypes.DATETIME: {
858
+ const hiresDec = hiresPrecision(columnSchema);
859
+ if (hiresDec > 0) {
860
+ // MariaDB hires DATETIME: one big-endian unsigned integer of
861
+ // pack_time() scaled to dec fractional digits; mixed radix, not
862
+ // the YYYYMMDDHHMMSS decimal of the classic format and not the
863
+ // DATETIME2 bit layout. Up to 8 bytes (beyond 2^53), so split in
864
+ // BigInt.
865
+ const bytes = [5, 6, 6, 7, 7, 7, 8][hiresDec];
866
+ let packed = 0n;
867
+ for (let i = 0; i < bytes; i++) {
868
+ packed = (packed << 8n) | BigInt(parser._buffer[parser._offset + i]);
869
+ }
870
+ parser._offset += bytes;
871
+ const scale = 10n ** BigInt(hiresDec);
872
+ fraction = hiresFractionObject(Number(packed % scale), hiresDec);
873
+ let units = packed / scale;
874
+ const secondPart = Number(units % 60n); units /= 60n;
875
+ const minutePart = Number(units % 60n); units /= 60n;
876
+ const hourPart = Number(units % 24n); units /= 24n;
877
+ const dayPart = Number(units % 32n); units /= 32n;
878
+ const monthPart = Number(units % 13n); units /= 13n;
879
+ result = dtDecode.getDateTime(
880
+ zongji.connection.config.dateStrings,
881
+ Number(units), // year
882
+ monthPart,
883
+ dayPart,
884
+ hourPart,
885
+ minutePart,
886
+ secondPart,
887
+ fraction,
888
+ zongji.connection.config.timezone
889
+ );
890
+ break;
891
+ }
549
892
  raw = parseUInt64(parser);
550
893
  date = Math.floor(raw / 1000000);
551
894
  time = raw % 1000000;
@@ -561,6 +904,7 @@ export function readMysqlValue(
561
904
  zongji.connection.config.timezone
562
905
  );
563
906
  break;
907
+ }
564
908
  case MysqlTypes.DATETIME2: {
565
909
  // Overlapping high-low to get all data in 32-bit numbers
566
910
  const rawHigh = readIntBE(parser._buffer, parser._offset, 4);
@@ -582,7 +926,26 @@ export function readMysqlValue(
582
926
  );
583
927
  break;
584
928
  }
585
- case MysqlTypes.TIMESTAMP:
929
+ case MysqlTypes.TIMESTAMP: {
930
+ const hiresDec = hiresPrecision(columnSchema);
931
+ if (hiresDec > 0) {
932
+ // MariaDB hires TIMESTAMP: big-endian epoch seconds (the classic
933
+ // format is little-endian) followed by a big-endian fraction of
934
+ // exactly dec digits
935
+ raw = parser._buffer.readUInt32BE(parser._offset);
936
+ parser._offset += 4;
937
+ const fracBytes = [0, 1, 1, 2, 2, 3, 3][hiresDec];
938
+ fraction = hiresFractionObject(
939
+ parser._buffer.readUIntBE(parser._offset, fracBytes), hiresDec);
940
+ parser._offset += fracBytes;
941
+ result = dtDecode.getTimeStamp(
942
+ zongji.connection.config.dateStrings,
943
+ raw,
944
+ fraction,
945
+ zongji.connection.config.timezone
946
+ );
947
+ break;
948
+ }
586
949
  raw = parser.parseUnsignedNumber(4);
587
950
  result = dtDecode.getTimeStamp(
588
951
  zongji.connection.config.dateStrings,
@@ -591,6 +954,7 @@ export function readMysqlValue(
591
954
  zongji.connection.config.timezone
592
955
  );
593
956
  break;
957
+ }
594
958
  case MysqlTypes.TIMESTAMP2:
595
959
  raw = readIntBE(parser._buffer, parser._offset, 4);
596
960
  parser._offset += 4;