@bytebase/dbhub 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,601 +0,0 @@
1
- import {
2
- MissingDatabaseError,
3
- getMySQLFamilyHealthCheck,
4
- isClientSideTimeout,
5
- isTiDBVersion,
6
- parseQueryResultSets,
7
- requireDatabaseInDSN,
8
- withReadOnlyTransaction
9
- } from "./chunk-6SZKW6GD.js";
10
- import "./chunk-FU2ZJE4E.js";
11
- import {
12
- quoteIdentifier
13
- } from "./chunk-X2EC47W4.js";
14
- import {
15
- SQLRowLimiter,
16
- closeQuietly
17
- } from "./chunk-YIGJHEU6.js";
18
- import "./chunk-ABTVMHD6.js";
19
- import {
20
- ConnectorRegistry,
21
- SafeURL,
22
- obfuscateDSNPassword,
23
- splitSQLStatements
24
- } from "./chunk-IXC4NQGC.js";
25
-
26
- // src/connectors/mysql/index.ts
27
- import mysql from "mysql2/promise";
28
- var MySQLDSNParser = class {
29
- async parse(dsn, config) {
30
- const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
31
- const timezone = config?.timezone;
32
- const charset = config?.charset;
33
- const collation = config?.collation;
34
- if (!this.isValidDSN(dsn)) {
35
- const obfuscatedDSN = obfuscateDSNPassword(dsn);
36
- const expectedFormat = this.getSampleDSN();
37
- throw new Error(
38
- `Invalid MySQL DSN format.
39
- Provided: ${obfuscatedDSN}
40
- Expected: ${expectedFormat}`
41
- );
42
- }
43
- try {
44
- const url = new SafeURL(dsn);
45
- const database = url.pathname ? url.pathname.substring(1) : "";
46
- requireDatabaseInDSN(database, dsn, "MySQL");
47
- const config2 = {
48
- host: url.hostname,
49
- port: url.port ? parseInt(url.port) : 3306,
50
- database,
51
- user: url.username,
52
- password: url.password,
53
- multipleStatements: true,
54
- // Enable native multi-statement support
55
- supportBigNumbers: true
56
- // Return BIGINT as string when value exceeds Number.MAX_SAFE_INTEGER
57
- };
58
- url.forEachSearchParam((value, key) => {
59
- if (key === "sslmode") {
60
- if (value === "disable") {
61
- config2.ssl = void 0;
62
- } else if (value === "require") {
63
- config2.ssl = { rejectUnauthorized: false };
64
- } else {
65
- config2.ssl = {};
66
- }
67
- }
68
- });
69
- if (connectionTimeoutSeconds !== void 0) {
70
- config2.connectTimeout = connectionTimeoutSeconds * 1e3;
71
- }
72
- if (timezone !== void 0) {
73
- config2.timezone = timezone;
74
- }
75
- const charsetOrCollation = collation ?? charset;
76
- if (charsetOrCollation !== void 0) {
77
- config2.charset = charsetOrCollation;
78
- }
79
- if (url.password && url.password.includes("X-Amz-Credential")) {
80
- config2.authPlugins = {
81
- mysql_clear_password: () => () => {
82
- return Buffer.from(url.password + "\0");
83
- }
84
- };
85
- if (config2.ssl === void 0) {
86
- config2.ssl = { rejectUnauthorized: false };
87
- }
88
- }
89
- return config2;
90
- } catch (error) {
91
- if (error instanceof MissingDatabaseError) {
92
- throw error;
93
- }
94
- throw new Error(
95
- `Failed to parse MySQL DSN: ${error instanceof Error ? error.message : String(error)}`
96
- );
97
- }
98
- }
99
- getSampleDSN() {
100
- return "mysql://root:password@localhost:3306/mysql?sslmode=require";
101
- }
102
- isValidDSN(dsn) {
103
- try {
104
- return dsn.startsWith("mysql://");
105
- } catch (error) {
106
- return false;
107
- }
108
- }
109
- };
110
- var _MySQLConnector = class _MySQLConnector {
111
- constructor() {
112
- this.id = "mysql";
113
- this.name = "MySQL";
114
- this.dsnParser = new MySQLDSNParser();
115
- this.pool = null;
116
- // Source ID is set by ConnectorManager after cloning
117
- this.sourceId = "default";
118
- // TiDB speaks the MySQL protocol but rejects `START TRANSACTION READ ONLY`
119
- // unless tidb_enable_noop_functions is on. Detected once at connect time.
120
- this.supportsReadOnlyTransaction = true;
121
- }
122
- getId() {
123
- return this.sourceId;
124
- }
125
- clone() {
126
- return new _MySQLConnector();
127
- }
128
- async connect(dsn, initScript, config) {
129
- try {
130
- const connectionOptions = await this.dsnParser.parse(dsn, config);
131
- this.pool = mysql.createPool(connectionOptions);
132
- if (config?.queryTimeoutSeconds !== void 0) {
133
- this.queryTimeoutMs = config.queryTimeoutSeconds * 1e3;
134
- }
135
- const [rows] = await this.pool.query("SELECT VERSION() AS version");
136
- this.supportsReadOnlyTransaction = !isTiDBVersion(rows[0]?.version);
137
- } catch (err) {
138
- if (this.pool) {
139
- const pool = this.pool;
140
- this.pool = null;
141
- await closeQuietly(() => pool.end());
142
- }
143
- console.error("Failed to connect to MySQL database:", err);
144
- throw err;
145
- }
146
- }
147
- async disconnect() {
148
- if (this.pool) {
149
- await this.pool.end();
150
- this.pool = null;
151
- }
152
- }
153
- async getSchemas() {
154
- if (!this.pool) {
155
- throw new Error("Not connected to database");
156
- }
157
- try {
158
- const [rows] = await this.pool.query(`
159
- SELECT SCHEMA_NAME
160
- FROM INFORMATION_SCHEMA.SCHEMATA
161
- WHERE SCHEMA_NAME NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
162
- ORDER BY SCHEMA_NAME
163
- `);
164
- return rows.map((row) => row.SCHEMA_NAME);
165
- } catch (error) {
166
- console.error("Error getting schemas:", error);
167
- throw error;
168
- }
169
- }
170
- async getTables(schema) {
171
- if (!this.pool) {
172
- throw new Error("Not connected to database");
173
- }
174
- try {
175
- const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
176
- const queryParams = schema ? [schema] : [];
177
- const [rows] = await this.pool.query(
178
- `
179
- SELECT TABLE_NAME
180
- FROM INFORMATION_SCHEMA.TABLES
181
- ${schemaClause}
182
- AND TABLE_TYPE = 'BASE TABLE'
183
- ORDER BY TABLE_NAME
184
- `,
185
- queryParams
186
- );
187
- return rows.map((row) => row.TABLE_NAME);
188
- } catch (error) {
189
- console.error("Error getting tables:", error);
190
- throw error;
191
- }
192
- }
193
- async getViews(schema) {
194
- if (!this.pool) {
195
- throw new Error("Not connected to database");
196
- }
197
- try {
198
- const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
199
- const queryParams = schema ? [schema] : [];
200
- const [rows] = await this.pool.query(
201
- `
202
- SELECT TABLE_NAME
203
- FROM INFORMATION_SCHEMA.TABLES
204
- ${schemaClause}
205
- AND TABLE_TYPE = 'VIEW'
206
- ORDER BY TABLE_NAME
207
- `,
208
- queryParams
209
- );
210
- return rows.map((row) => row.TABLE_NAME);
211
- } catch (error) {
212
- console.error("Error getting views:", error);
213
- throw error;
214
- }
215
- }
216
- async tableExists(tableName, schema) {
217
- if (!this.pool) {
218
- throw new Error("Not connected to database");
219
- }
220
- try {
221
- const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
222
- const queryParams = schema ? [schema, tableName] : [tableName];
223
- const [rows] = await this.pool.query(
224
- `
225
- SELECT COUNT(*) AS COUNT
226
- FROM INFORMATION_SCHEMA.TABLES
227
- ${schemaClause}
228
- AND TABLE_NAME = ?
229
- `,
230
- queryParams
231
- );
232
- return rows[0].COUNT > 0;
233
- } catch (error) {
234
- console.error("Error checking if table exists:", error);
235
- throw error;
236
- }
237
- }
238
- async getTableIndexes(tableName, schema) {
239
- if (!this.pool) {
240
- throw new Error("Not connected to database");
241
- }
242
- try {
243
- const schemaClause = schema ? "TABLE_SCHEMA = ?" : "TABLE_SCHEMA = DATABASE()";
244
- const queryParams = schema ? [schema, tableName] : [tableName];
245
- const [indexRows] = await this.pool.query(
246
- `
247
- SELECT
248
- INDEX_NAME,
249
- COLUMN_NAME,
250
- NON_UNIQUE,
251
- SEQ_IN_INDEX
252
- FROM
253
- INFORMATION_SCHEMA.STATISTICS
254
- WHERE
255
- ${schemaClause}
256
- AND TABLE_NAME = ?
257
- ORDER BY
258
- INDEX_NAME,
259
- SEQ_IN_INDEX
260
- `,
261
- queryParams
262
- );
263
- const indexMap = /* @__PURE__ */ new Map();
264
- for (const row of indexRows) {
265
- const indexName = row.INDEX_NAME;
266
- const columnName = row.COLUMN_NAME;
267
- const isUnique = row.NON_UNIQUE === 0;
268
- const isPrimary = indexName === "PRIMARY";
269
- if (!indexMap.has(indexName)) {
270
- indexMap.set(indexName, {
271
- columns: [],
272
- is_unique: isUnique,
273
- is_primary: isPrimary
274
- });
275
- }
276
- const indexInfo = indexMap.get(indexName);
277
- indexInfo.columns.push(columnName);
278
- }
279
- const results = [];
280
- indexMap.forEach((indexInfo, indexName) => {
281
- results.push({
282
- index_name: indexName,
283
- column_names: indexInfo.columns,
284
- is_unique: indexInfo.is_unique,
285
- is_primary: indexInfo.is_primary
286
- });
287
- });
288
- return results;
289
- } catch (error) {
290
- console.error("Error getting table indexes:", error);
291
- throw error;
292
- }
293
- }
294
- async getTableSchema(tableName, schema) {
295
- if (!this.pool) {
296
- throw new Error("Not connected to database");
297
- }
298
- try {
299
- const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
300
- const queryParams = schema ? [schema, tableName] : [tableName];
301
- const [rows] = await this.pool.query(
302
- `
303
- SELECT
304
- COLUMN_NAME as column_name,
305
- DATA_TYPE as data_type,
306
- IS_NULLABLE as is_nullable,
307
- COLUMN_DEFAULT as column_default,
308
- COLUMN_COMMENT as description
309
- FROM INFORMATION_SCHEMA.COLUMNS
310
- ${schemaClause}
311
- AND TABLE_NAME = ?
312
- ORDER BY ORDINAL_POSITION
313
- `,
314
- queryParams
315
- );
316
- return rows.map((row) => ({
317
- ...row,
318
- description: row.description || null
319
- }));
320
- } catch (error) {
321
- console.error("Error getting table schema:", error);
322
- throw error;
323
- }
324
- }
325
- async getTableComment(tableName, schema) {
326
- if (!this.pool) {
327
- throw new Error("Not connected to database");
328
- }
329
- try {
330
- const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
331
- const queryParams = schema ? [schema, tableName] : [tableName];
332
- const [rows] = await this.pool.query(
333
- `
334
- SELECT TABLE_COMMENT
335
- FROM INFORMATION_SCHEMA.TABLES
336
- ${schemaClause}
337
- AND TABLE_NAME = ?
338
- `,
339
- queryParams
340
- );
341
- if (rows.length > 0) {
342
- return rows[0].TABLE_COMMENT || null;
343
- }
344
- return null;
345
- } catch (error) {
346
- return null;
347
- }
348
- }
349
- async getHealthCheck() {
350
- if (!this.pool) {
351
- throw new Error("Not connected to database");
352
- }
353
- const pool = this.pool;
354
- return getMySQLFamilyHealthCheck(async (sql) => {
355
- const [rows] = await pool.query(sql);
356
- return rows;
357
- });
358
- }
359
- async getStoredProcedures(schema, routineType) {
360
- if (!this.pool) {
361
- throw new Error("Not connected to database");
362
- }
363
- try {
364
- const schemaClause = schema ? "WHERE ROUTINE_SCHEMA = ?" : "WHERE ROUTINE_SCHEMA = DATABASE()";
365
- const queryParams = schema ? [schema] : [];
366
- let typeFilter = "";
367
- if (routineType === "function") {
368
- typeFilter = " AND ROUTINE_TYPE = 'FUNCTION'";
369
- } else if (routineType === "procedure") {
370
- typeFilter = " AND ROUTINE_TYPE = 'PROCEDURE'";
371
- }
372
- const [rows] = await this.pool.query(
373
- `
374
- SELECT ROUTINE_NAME
375
- FROM INFORMATION_SCHEMA.ROUTINES
376
- ${schemaClause}${typeFilter}
377
- ORDER BY ROUTINE_NAME
378
- `,
379
- queryParams
380
- );
381
- return rows.map((row) => row.ROUTINE_NAME);
382
- } catch (error) {
383
- console.error("Error getting stored procedures:", error);
384
- throw error;
385
- }
386
- }
387
- async getStoredProcedureDetail(procedureName, schema) {
388
- if (!this.pool) {
389
- throw new Error("Not connected to database");
390
- }
391
- try {
392
- const schemaClause = schema ? "WHERE r.ROUTINE_SCHEMA = ?" : "WHERE r.ROUTINE_SCHEMA = DATABASE()";
393
- const queryParams = schema ? [schema, procedureName] : [procedureName];
394
- const [rows] = await this.pool.query(
395
- `
396
- SELECT
397
- r.ROUTINE_NAME AS procedure_name,
398
- CASE
399
- WHEN r.ROUTINE_TYPE = 'PROCEDURE' THEN 'procedure'
400
- ELSE 'function'
401
- END AS procedure_type,
402
- LOWER(r.ROUTINE_TYPE) AS routine_type,
403
- r.ROUTINE_DEFINITION,
404
- r.DTD_IDENTIFIER AS return_type,
405
- (
406
- SELECT GROUP_CONCAT(
407
- CONCAT(p.PARAMETER_NAME, ' ', p.PARAMETER_MODE, ' ', p.DATA_TYPE)
408
- ORDER BY p.ORDINAL_POSITION
409
- SEPARATOR ', '
410
- )
411
- FROM INFORMATION_SCHEMA.PARAMETERS p
412
- WHERE p.SPECIFIC_SCHEMA = r.ROUTINE_SCHEMA
413
- AND p.SPECIFIC_NAME = r.ROUTINE_NAME
414
- AND p.PARAMETER_NAME IS NOT NULL
415
- ) AS parameter_list
416
- FROM INFORMATION_SCHEMA.ROUTINES r
417
- ${schemaClause}
418
- AND r.ROUTINE_NAME = ?
419
- `,
420
- queryParams
421
- );
422
- if (rows.length === 0) {
423
- const schemaName = schema || "current schema";
424
- throw new Error(`Stored procedure '${procedureName}' not found in ${schemaName}`);
425
- }
426
- const procedure = rows[0];
427
- let definition = procedure.ROUTINE_DEFINITION;
428
- try {
429
- const schemaValue = schema || await this.getCurrentSchema();
430
- const quotedSchema = quoteIdentifier(schemaValue, "mysql");
431
- const quotedProcName = quoteIdentifier(procedureName, "mysql");
432
- if (procedure.procedure_type === "procedure") {
433
- try {
434
- const [defRows] = await this.pool.query(`
435
- SHOW CREATE PROCEDURE ${quotedSchema}.${quotedProcName}
436
- `);
437
- if (defRows && defRows.length > 0) {
438
- definition = defRows[0]["Create Procedure"];
439
- }
440
- } catch (err) {
441
- console.error(`Error getting procedure definition with SHOW CREATE: ${err}`);
442
- }
443
- } else {
444
- try {
445
- const [defRows] = await this.pool.query(`
446
- SHOW CREATE FUNCTION ${quotedSchema}.${quotedProcName}
447
- `);
448
- if (defRows && defRows.length > 0) {
449
- definition = defRows[0]["Create Function"];
450
- }
451
- } catch (innerErr) {
452
- console.error(`Error getting function definition with SHOW CREATE: ${innerErr}`);
453
- }
454
- }
455
- if (!definition) {
456
- const [bodyRows] = await this.pool.query(
457
- `
458
- SELECT ROUTINE_DEFINITION, ROUTINE_BODY
459
- FROM INFORMATION_SCHEMA.ROUTINES
460
- WHERE ROUTINE_SCHEMA = ? AND ROUTINE_NAME = ?
461
- `,
462
- [schemaValue, procedureName]
463
- );
464
- if (bodyRows && bodyRows.length > 0) {
465
- if (bodyRows[0].ROUTINE_DEFINITION) {
466
- definition = bodyRows[0].ROUTINE_DEFINITION;
467
- } else if (bodyRows[0].ROUTINE_BODY) {
468
- definition = bodyRows[0].ROUTINE_BODY;
469
- }
470
- }
471
- }
472
- } catch (error) {
473
- console.error(`Error getting procedure/function details: ${error}`);
474
- }
475
- return {
476
- procedure_name: procedure.procedure_name,
477
- procedure_type: procedure.procedure_type,
478
- language: "sql",
479
- // MySQL procedures are generally in SQL
480
- parameter_list: procedure.parameter_list || "",
481
- return_type: procedure.routine_type === "function" ? procedure.return_type : void 0,
482
- definition: definition || void 0
483
- };
484
- } catch (error) {
485
- console.error("Error getting stored procedure detail:", error);
486
- throw error;
487
- }
488
- }
489
- // Helper method to get current schema (database) name
490
- async getCurrentSchema() {
491
- const [rows] = await this.pool.query("SELECT DATABASE() AS DB");
492
- return rows[0].DB;
493
- }
494
- /**
495
- * Default search scope = the database named in the DSN. DATABASE() returns
496
- * null when the connection was opened without a database, in which case
497
- * callers fall back to the full server-wide schema list.
498
- */
499
- async getDefaultSchema() {
500
- if (!this.pool) {
501
- throw new Error("Not connected to database");
502
- }
503
- const [rows] = await this.pool.query("SELECT DATABASE() AS DB");
504
- return rows[0]?.DB ?? null;
505
- }
506
- async executeSQL(sql, options, parameters) {
507
- if (!this.pool) {
508
- throw new Error("Not connected to database");
509
- }
510
- const conn = await this.pool.getConnection();
511
- const threadId = conn.threadId;
512
- let isConnectionPoisoned = false;
513
- try {
514
- return await withReadOnlyTransaction(
515
- conn,
516
- options.readonly,
517
- this.supportsReadOnlyTransaction,
518
- async () => {
519
- const statements = splitSQLStatements(sql, "mysql");
520
- let processedSQL = sql;
521
- let probes = [];
522
- if (options.maxRows) {
523
- const rewrites = statements.map(
524
- (statement) => SQLRowLimiter.applyMaxRowsWithTruncationProbe(statement, options.maxRows, "mysql")
525
- );
526
- probes = rewrites.map((rewrite) => rewrite.probeApplied);
527
- processedSQL = rewrites.map((rewrite) => rewrite.sql).join("; ");
528
- if (sql.trim().endsWith(";")) {
529
- processedSQL += ";";
530
- }
531
- }
532
- let results;
533
- if (parameters && parameters.length > 0) {
534
- results = await conn.query({ sql: processedSQL, timeout: this.queryTimeoutMs }, parameters);
535
- } else {
536
- results = await conn.query({ sql: processedSQL, timeout: this.queryTimeoutMs });
537
- }
538
- const [firstResult] = results;
539
- const resultSets = parseQueryResultSets(firstResult, statements);
540
- if (resultSets.length === probes.length) {
541
- resultSets.forEach(
542
- (set, index) => SQLRowLimiter.flagTruncation(set, options.maxRows, probes[index])
543
- );
544
- }
545
- return { resultSets };
546
- }
547
- );
548
- } catch (error) {
549
- if (isClientSideTimeout(error)) {
550
- isConnectionPoisoned = true;
551
- await this.killQuery(threadId);
552
- }
553
- throw error;
554
- } finally {
555
- if (isConnectionPoisoned) {
556
- conn.destroy();
557
- } else {
558
- conn.release();
559
- }
560
- }
561
- }
562
- /**
563
- * Best-effort server-side kill for a query abandoned by mysql2's client-side
564
- * timeout. Uses a separate connection: the original connection's command
565
- * queue is stuck behind the abandoned statement (see isClientSideTimeout)
566
- * and cannot itself be used to send KILL QUERY.
567
- *
568
- * Bounded by its own short timeout, independent of the user's (possibly
569
- * long or unset) query_timeout — KILL QUERY is metadata-only and should
570
- * return almost immediately on a healthy server, so cleanup must not stall
571
- * indefinitely if it doesn't.
572
- */
573
- async killQuery(threadId) {
574
- if (!this.pool) return;
575
- let killer;
576
- let killerPoisoned = false;
577
- try {
578
- killer = await this.pool.getConnection();
579
- await killer.query({ sql: `KILL QUERY ${threadId}`, timeout: _MySQLConnector.KILL_QUERY_TIMEOUT_MS });
580
- } catch (error) {
581
- killerPoisoned = isClientSideTimeout(error);
582
- } finally {
583
- if (killer) {
584
- if (killerPoisoned) {
585
- killer.destroy();
586
- } else {
587
- killer.release();
588
- }
589
- }
590
- }
591
- }
592
- };
593
- // Bounds the KILL QUERY cleanup call issued after a client-side query
594
- // timeout; see killQuery.
595
- _MySQLConnector.KILL_QUERY_TIMEOUT_MS = 5e3;
596
- var MySQLConnector = _MySQLConnector;
597
- var mysqlConnector = new MySQLConnector();
598
- ConnectorRegistry.register(mysqlConnector);
599
- export {
600
- MySQLConnector
601
- };