@bytebase/dbhub 1.3.1 → 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.
@@ -0,0 +1,500 @@
1
+ import {
2
+ isClientSideTimeout
3
+ } from "./chunk-5XSBOK3W.js";
4
+ import {
5
+ computeHitRatioPct,
6
+ toNullableNumber
7
+ } from "./chunk-FU2ZJE4E.js";
8
+ import {
9
+ quoteIdentifier
10
+ } from "./chunk-X2EC47W4.js";
11
+ import {
12
+ obfuscateDSNPassword
13
+ } from "./chunk-IXC4NQGC.js";
14
+
15
+ // src/connectors/mysql-family-health-check.ts
16
+ async function getMySQLFamilyHealthCheck(query) {
17
+ const [processlistRows, maxConnRows, bufferPoolRows, trxOutcome] = await Promise.all([
18
+ query(`
19
+ SELECT
20
+ COUNT(*) AS total,
21
+ SUM(CASE WHEN COMMAND != 'Sleep' THEN 1 ELSE 0 END) AS active,
22
+ SUM(CASE WHEN COMMAND = 'Sleep' THEN 1 ELSE 0 END) AS idle,
23
+ MAX(CASE WHEN COMMAND != 'Sleep' THEN TIME ELSE NULL END) AS longest_active_query_seconds
24
+ FROM information_schema.PROCESSLIST
25
+ WHERE ID <> CONNECTION_ID()
26
+ `),
27
+ query(`SHOW VARIABLES LIKE 'max_connections'`),
28
+ query(`
29
+ SHOW GLOBAL STATUS WHERE Variable_name IN ('Innodb_buffer_pool_read_requests', 'Innodb_buffer_pool_reads')
30
+ `),
31
+ // Joining PROCESSLIST against INNODB_TRX to detect idle-in-transaction
32
+ // sessions requires the PROCESS privilege - unlike a plain PROCESSLIST
33
+ // select, which silently narrows to the caller's own session instead of
34
+ // erroring. Run concurrently with the other queries above, but resolve
35
+ // rather than reject on failure so it degrades instead of failing the
36
+ // whole health check.
37
+ query(`
38
+ SELECT
39
+ COUNT(*) AS idle_in_transaction,
40
+ MAX(TIMESTAMPDIFF(SECOND, t.trx_started, NOW())) AS longest_idle_in_transaction_seconds
41
+ FROM information_schema.INNODB_TRX t
42
+ JOIN information_schema.PROCESSLIST p ON p.ID = t.trx_mysql_thread_id
43
+ WHERE p.COMMAND = 'Sleep'
44
+ `).then(
45
+ (rows) => ({ ok: true, rows }),
46
+ () => ({ ok: false })
47
+ )
48
+ ]);
49
+ const notes = [];
50
+ let idleInTransaction = 0;
51
+ let longestIdleInTransactionSeconds = null;
52
+ if (trxOutcome.ok) {
53
+ idleInTransaction = Number(trxOutcome.rows[0].idle_in_transaction);
54
+ longestIdleInTransactionSeconds = toNullableNumber(trxOutcome.rows[0].longest_idle_in_transaction_seconds);
55
+ } else {
56
+ notes.push(
57
+ "Connected user lacks the PROCESS privilege: PROCESSLIST visibility is restricted to the connecting user's own sessions, and this diagnostic session is excluded from the count, so connection counts and active-query duration may under-report (down to 0). Idle-in-transaction detection is unavailable."
58
+ );
59
+ }
60
+ const proc = processlistRows[0];
61
+ const bufferPoolStats = Object.fromEntries(bufferPoolRows.map((row) => [row.Variable_name, row.Value]));
62
+ const readRequests = Number(bufferPoolStats.Innodb_buffer_pool_read_requests ?? 0);
63
+ const reads = Number(bufferPoolStats.Innodb_buffer_pool_reads ?? 0);
64
+ return {
65
+ connections: {
66
+ total: Number(proc.total),
67
+ active: Number(proc.active),
68
+ idle: Number(proc.idle),
69
+ idleInTransaction,
70
+ // MySQL/MariaDB's InnoDB has no equivalent of Postgres's "idle in
71
+ // transaction (aborted)" state - a failed statement doesn't leave the
72
+ // session's transaction in a distinct aborted-but-open state the way
73
+ // Postgres does - so idleInTransactionAborted is left undefined.
74
+ maxConnections: maxConnRows.length > 0 ? Number(maxConnRows[0].Value) : null,
75
+ longestIdleInTransactionSeconds,
76
+ longestActiveQuerySeconds: toNullableNumber(proc.longest_active_query_seconds)
77
+ },
78
+ bufferCache: {
79
+ hitRatioPct: computeHitRatioPct(readRequests, reads),
80
+ blocksHit: readRequests - reads,
81
+ blocksRead: reads
82
+ },
83
+ ...notes.length > 0 ? { notes } : {}
84
+ };
85
+ }
86
+
87
+ // src/connectors/mysql-family-introspection.ts
88
+ async function getSchemas(query) {
89
+ try {
90
+ const rows = await query(`
91
+ SELECT SCHEMA_NAME
92
+ FROM INFORMATION_SCHEMA.SCHEMATA
93
+ WHERE SCHEMA_NAME NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
94
+ ORDER BY SCHEMA_NAME
95
+ `);
96
+ return rows.map((row) => row.SCHEMA_NAME);
97
+ } catch (error) {
98
+ console.error("Error getting schemas:", error);
99
+ throw error;
100
+ }
101
+ }
102
+ async function getTables(query, schema) {
103
+ try {
104
+ const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
105
+ const queryParams = schema ? [schema] : [];
106
+ const rows = await query(
107
+ `
108
+ SELECT TABLE_NAME
109
+ FROM INFORMATION_SCHEMA.TABLES
110
+ ${schemaClause}
111
+ AND TABLE_TYPE = 'BASE TABLE'
112
+ ORDER BY TABLE_NAME
113
+ `,
114
+ queryParams
115
+ );
116
+ return rows.map((row) => row.TABLE_NAME);
117
+ } catch (error) {
118
+ console.error("Error getting tables:", error);
119
+ throw error;
120
+ }
121
+ }
122
+ async function getViews(query, schema) {
123
+ try {
124
+ const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
125
+ const queryParams = schema ? [schema] : [];
126
+ const rows = await query(
127
+ `
128
+ SELECT TABLE_NAME
129
+ FROM INFORMATION_SCHEMA.TABLES
130
+ ${schemaClause}
131
+ AND TABLE_TYPE = 'VIEW'
132
+ ORDER BY TABLE_NAME
133
+ `,
134
+ queryParams
135
+ );
136
+ return rows.map((row) => row.TABLE_NAME);
137
+ } catch (error) {
138
+ console.error("Error getting views:", error);
139
+ throw error;
140
+ }
141
+ }
142
+ async function tableExists(query, tableName, schema) {
143
+ try {
144
+ const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
145
+ const queryParams = schema ? [schema, tableName] : [tableName];
146
+ const rows = await query(
147
+ `
148
+ SELECT COUNT(*) AS COUNT
149
+ FROM INFORMATION_SCHEMA.TABLES
150
+ ${schemaClause}
151
+ AND TABLE_NAME = ?
152
+ `,
153
+ queryParams
154
+ );
155
+ return rows[0].COUNT > 0;
156
+ } catch (error) {
157
+ console.error("Error checking if table exists:", error);
158
+ throw error;
159
+ }
160
+ }
161
+ async function getTableIndexes(query, tableName, schema) {
162
+ try {
163
+ const schemaClause = schema ? "TABLE_SCHEMA = ?" : "TABLE_SCHEMA = DATABASE()";
164
+ const queryParams = schema ? [schema, tableName] : [tableName];
165
+ const indexRows = await query(
166
+ `
167
+ SELECT
168
+ INDEX_NAME,
169
+ COLUMN_NAME,
170
+ NON_UNIQUE,
171
+ SEQ_IN_INDEX
172
+ FROM
173
+ INFORMATION_SCHEMA.STATISTICS
174
+ WHERE
175
+ ${schemaClause}
176
+ AND TABLE_NAME = ?
177
+ ORDER BY
178
+ INDEX_NAME,
179
+ SEQ_IN_INDEX
180
+ `,
181
+ queryParams
182
+ );
183
+ const indexMap = /* @__PURE__ */ new Map();
184
+ for (const row of indexRows) {
185
+ const indexName = row.INDEX_NAME;
186
+ const columnName = row.COLUMN_NAME;
187
+ const isUnique = row.NON_UNIQUE === 0;
188
+ const isPrimary = indexName === "PRIMARY";
189
+ if (!indexMap.has(indexName)) {
190
+ indexMap.set(indexName, {
191
+ columns: [],
192
+ is_unique: isUnique,
193
+ is_primary: isPrimary
194
+ });
195
+ }
196
+ const indexInfo = indexMap.get(indexName);
197
+ indexInfo.columns.push(columnName);
198
+ }
199
+ const results = [];
200
+ indexMap.forEach((indexInfo, indexName) => {
201
+ results.push({
202
+ index_name: indexName,
203
+ column_names: indexInfo.columns,
204
+ is_unique: indexInfo.is_unique,
205
+ is_primary: indexInfo.is_primary
206
+ });
207
+ });
208
+ return results;
209
+ } catch (error) {
210
+ console.error("Error getting table indexes:", error);
211
+ throw error;
212
+ }
213
+ }
214
+ async function getTableSchema(query, tableName, schema) {
215
+ try {
216
+ const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
217
+ const queryParams = schema ? [schema, tableName] : [tableName];
218
+ const rows = await query(
219
+ `
220
+ SELECT
221
+ COLUMN_NAME as column_name,
222
+ DATA_TYPE as data_type,
223
+ IS_NULLABLE as is_nullable,
224
+ COLUMN_DEFAULT as column_default,
225
+ COLUMN_COMMENT as description
226
+ FROM INFORMATION_SCHEMA.COLUMNS
227
+ ${schemaClause}
228
+ AND TABLE_NAME = ?
229
+ ORDER BY ORDINAL_POSITION
230
+ `,
231
+ queryParams
232
+ );
233
+ return rows.map((row) => ({
234
+ ...row,
235
+ description: row.description || null
236
+ }));
237
+ } catch (error) {
238
+ console.error("Error getting table schema:", error);
239
+ throw error;
240
+ }
241
+ }
242
+ async function getTableComment(query, tableName, schema) {
243
+ try {
244
+ const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
245
+ const queryParams = schema ? [schema, tableName] : [tableName];
246
+ const rows = await query(
247
+ `
248
+ SELECT TABLE_COMMENT
249
+ FROM INFORMATION_SCHEMA.TABLES
250
+ ${schemaClause}
251
+ AND TABLE_NAME = ?
252
+ `,
253
+ queryParams
254
+ );
255
+ if (rows.length > 0) {
256
+ return rows[0].TABLE_COMMENT || null;
257
+ }
258
+ return null;
259
+ } catch (error) {
260
+ return null;
261
+ }
262
+ }
263
+ async function getStoredProcedures(query, schema, routineType) {
264
+ try {
265
+ const schemaClause = schema ? "WHERE ROUTINE_SCHEMA = ?" : "WHERE ROUTINE_SCHEMA = DATABASE()";
266
+ const queryParams = schema ? [schema] : [];
267
+ let typeFilter = "";
268
+ if (routineType === "function") {
269
+ typeFilter = " AND ROUTINE_TYPE = 'FUNCTION'";
270
+ } else if (routineType === "procedure") {
271
+ typeFilter = " AND ROUTINE_TYPE = 'PROCEDURE'";
272
+ }
273
+ const rows = await query(
274
+ `
275
+ SELECT ROUTINE_NAME
276
+ FROM INFORMATION_SCHEMA.ROUTINES
277
+ ${schemaClause}${typeFilter}
278
+ ORDER BY ROUTINE_NAME
279
+ `,
280
+ queryParams
281
+ );
282
+ return rows.map((row) => row.ROUTINE_NAME);
283
+ } catch (error) {
284
+ console.error("Error getting stored procedures:", error);
285
+ throw error;
286
+ }
287
+ }
288
+ async function getStoredProcedureDetail(query, procedureName, schema) {
289
+ try {
290
+ const schemaClause = schema ? "WHERE r.ROUTINE_SCHEMA = ?" : "WHERE r.ROUTINE_SCHEMA = DATABASE()";
291
+ const queryParams = schema ? [schema, procedureName] : [procedureName];
292
+ const rows = await query(
293
+ `
294
+ SELECT
295
+ r.ROUTINE_NAME AS procedure_name,
296
+ CASE
297
+ WHEN r.ROUTINE_TYPE = 'PROCEDURE' THEN 'procedure'
298
+ ELSE 'function'
299
+ END AS procedure_type,
300
+ LOWER(r.ROUTINE_TYPE) AS routine_type,
301
+ r.ROUTINE_DEFINITION,
302
+ r.DTD_IDENTIFIER AS return_type,
303
+ (
304
+ SELECT GROUP_CONCAT(
305
+ CONCAT(p.PARAMETER_NAME, ' ', p.PARAMETER_MODE, ' ', p.DATA_TYPE)
306
+ ORDER BY p.ORDINAL_POSITION
307
+ SEPARATOR ', '
308
+ )
309
+ FROM INFORMATION_SCHEMA.PARAMETERS p
310
+ WHERE p.SPECIFIC_SCHEMA = r.ROUTINE_SCHEMA
311
+ AND p.SPECIFIC_NAME = r.ROUTINE_NAME
312
+ AND p.PARAMETER_NAME IS NOT NULL
313
+ ) AS parameter_list
314
+ FROM INFORMATION_SCHEMA.ROUTINES r
315
+ ${schemaClause}
316
+ AND r.ROUTINE_NAME = ?
317
+ `,
318
+ queryParams
319
+ );
320
+ if (rows.length === 0) {
321
+ const schemaName = schema || "current schema";
322
+ throw new Error(`Stored procedure '${procedureName}' not found in ${schemaName}`);
323
+ }
324
+ const procedure = rows[0];
325
+ let definition = procedure.ROUTINE_DEFINITION;
326
+ try {
327
+ const schemaValue = schema || await getCurrentSchema(query);
328
+ const quotedSchema = quoteIdentifier(schemaValue, "mysql");
329
+ const quotedProcName = quoteIdentifier(procedureName, "mysql");
330
+ if (procedure.procedure_type === "procedure") {
331
+ try {
332
+ const defRows = await query(`
333
+ SHOW CREATE PROCEDURE ${quotedSchema}.${quotedProcName}
334
+ `);
335
+ if (defRows && defRows.length > 0) {
336
+ definition = defRows[0]["Create Procedure"];
337
+ }
338
+ } catch (err) {
339
+ console.error(`Error getting procedure definition with SHOW CREATE: ${err}`);
340
+ }
341
+ } else {
342
+ try {
343
+ const defRows = await query(`
344
+ SHOW CREATE FUNCTION ${quotedSchema}.${quotedProcName}
345
+ `);
346
+ if (defRows && defRows.length > 0) {
347
+ definition = defRows[0]["Create Function"];
348
+ }
349
+ } catch (innerErr) {
350
+ console.error(`Error getting function definition with SHOW CREATE: ${innerErr}`);
351
+ }
352
+ }
353
+ if (!definition) {
354
+ const bodyRows = await query(
355
+ `
356
+ SELECT ROUTINE_DEFINITION, ROUTINE_BODY
357
+ FROM INFORMATION_SCHEMA.ROUTINES
358
+ WHERE ROUTINE_SCHEMA = ? AND ROUTINE_NAME = ?
359
+ `,
360
+ [schemaValue, procedureName]
361
+ );
362
+ if (bodyRows && bodyRows.length > 0) {
363
+ if (bodyRows[0].ROUTINE_DEFINITION) {
364
+ definition = bodyRows[0].ROUTINE_DEFINITION;
365
+ } else if (bodyRows[0].ROUTINE_BODY) {
366
+ definition = bodyRows[0].ROUTINE_BODY;
367
+ }
368
+ }
369
+ }
370
+ } catch (error) {
371
+ console.error(`Error getting procedure/function details: ${error}`);
372
+ }
373
+ return {
374
+ procedure_name: procedure.procedure_name,
375
+ procedure_type: procedure.procedure_type,
376
+ language: "sql",
377
+ // MySQL/MariaDB procedures are generally in SQL
378
+ parameter_list: procedure.parameter_list || "",
379
+ return_type: procedure.routine_type === "function" ? procedure.return_type : void 0,
380
+ definition: definition || void 0
381
+ };
382
+ } catch (error) {
383
+ console.error("Error getting stored procedure detail:", error);
384
+ throw error;
385
+ }
386
+ }
387
+ async function getCurrentSchema(query) {
388
+ const rows = await query("SELECT DATABASE() AS DB");
389
+ return rows[0].DB;
390
+ }
391
+ async function getDefaultSchema(query) {
392
+ const rows = await query("SELECT DATABASE() AS DB");
393
+ return rows[0]?.DB ?? null;
394
+ }
395
+
396
+ // src/utils/dsn-database.ts
397
+ var MissingDatabaseError = class extends Error {
398
+ constructor(message) {
399
+ super(message);
400
+ this.name = "MissingDatabaseError";
401
+ }
402
+ };
403
+ function requireDatabaseInDSN(database, dsn, label) {
404
+ if (database) {
405
+ return;
406
+ }
407
+ throw new MissingDatabaseError(
408
+ `${label} DSN must name a database.
409
+ Provided: ${obfuscateDSNPassword(dsn)}
410
+ Add the database to the DSN, e.g. ...:3306/mydb
411
+ To work with several databases, define one [[sources]] entry per database in a TOML config file: https://dbhub.ai/config/toml`
412
+ );
413
+ }
414
+
415
+ // src/utils/multi-statement-result-parser.ts
416
+ function isMetadataObject(element) {
417
+ if (!element || typeof element !== "object" || Array.isArray(element)) {
418
+ return false;
419
+ }
420
+ return "affectedRows" in element || "insertId" in element || "fieldCount" in element || "warningStatus" in element;
421
+ }
422
+ function isMultiStatementResult(results) {
423
+ if (!Array.isArray(results) || results.length === 0) {
424
+ return false;
425
+ }
426
+ const firstElement = results[0];
427
+ return isMetadataObject(firstElement) || Array.isArray(firstElement);
428
+ }
429
+ function buildResultSetsFromMultiStatement(results, statementTexts) {
430
+ const sql = statementTexts?.length === results.length ? statementTexts : void 0;
431
+ return results.map((result, index) => {
432
+ if (Array.isArray(result)) {
433
+ return { sql: sql?.[index], rows: result, rowCount: result.length };
434
+ }
435
+ if (isMetadataObject(result)) {
436
+ return { sql: sql?.[index], rows: [], rowCount: result.affectedRows || 0 };
437
+ }
438
+ return { sql: sql?.[index], rows: [], rowCount: 0 };
439
+ });
440
+ }
441
+ function parseQueryResultSets(results, statementTexts) {
442
+ const singleStatementSql = statementTexts?.length === 1 ? statementTexts[0] : void 0;
443
+ if (isMetadataObject(results)) {
444
+ return [{ sql: singleStatementSql, rows: [], rowCount: results.affectedRows || 0 }];
445
+ }
446
+ if (!Array.isArray(results)) {
447
+ return [{ sql: singleStatementSql, rows: [], rowCount: 0 }];
448
+ }
449
+ if (isMultiStatementResult(results)) {
450
+ return buildResultSetsFromMultiStatement(results, statementTexts);
451
+ }
452
+ return [{ sql: singleStatementSql, rows: results, rowCount: results.length }];
453
+ }
454
+
455
+ // src/utils/readonly-transaction.ts
456
+ async function withReadOnlyTransaction(conn, readonly, supportsReadOnlyTransaction, execute) {
457
+ if (!readonly) {
458
+ return execute();
459
+ }
460
+ try {
461
+ await conn.query(
462
+ supportsReadOnlyTransaction ? "START TRANSACTION READ ONLY" : "START TRANSACTION"
463
+ );
464
+ const result = await execute();
465
+ await conn.query(supportsReadOnlyTransaction ? "COMMIT" : "ROLLBACK");
466
+ return result;
467
+ } catch (error) {
468
+ if (!isClientSideTimeout(error)) {
469
+ try {
470
+ await conn.query("ROLLBACK");
471
+ } catch {
472
+ }
473
+ }
474
+ throw error;
475
+ }
476
+ }
477
+
478
+ // src/utils/server-flavor.ts
479
+ function isTiDBVersion(version) {
480
+ return typeof version === "string" && /tidb/i.test(version);
481
+ }
482
+
483
+ export {
484
+ getMySQLFamilyHealthCheck,
485
+ getSchemas,
486
+ getTables,
487
+ getViews,
488
+ tableExists,
489
+ getTableIndexes,
490
+ getTableSchema,
491
+ getTableComment,
492
+ getStoredProcedures,
493
+ getStoredProcedureDetail,
494
+ getDefaultSchema,
495
+ MissingDatabaseError,
496
+ requireDatabaseInDSN,
497
+ parseQueryResultSets,
498
+ withReadOnlyTransaction,
499
+ isTiDBVersion
500
+ };