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