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