@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,270 @@
1
+ import {
2
+ MissingDatabaseError,
3
+ getDefaultSchema,
4
+ getMySQLFamilyHealthCheck,
5
+ getSchemas,
6
+ getStoredProcedureDetail,
7
+ getStoredProcedures,
8
+ getTableComment,
9
+ getTableIndexes,
10
+ getTableSchema,
11
+ getTables,
12
+ getViews,
13
+ isTiDBVersion,
14
+ parseQueryResultSets,
15
+ requireDatabaseInDSN,
16
+ tableExists,
17
+ withReadOnlyTransaction
18
+ } from "./chunk-SJ5BRPQA.js";
19
+ import {
20
+ clientQueryTimeoutMs,
21
+ isClientSideTimeout,
22
+ withClientQueryDeadline
23
+ } from "./chunk-5XSBOK3W.js";
24
+ import "./chunk-FU2ZJE4E.js";
25
+ import "./chunk-X2EC47W4.js";
26
+ import {
27
+ SQLRowLimiter,
28
+ closeQuietly
29
+ } from "./chunk-X5OCFRNL.js";
30
+ import "./chunk-55X2VY5D.js";
31
+ import {
32
+ ConnectorRegistry,
33
+ SafeURL,
34
+ obfuscateDSNPassword,
35
+ splitSQLStatements
36
+ } from "./chunk-IXC4NQGC.js";
37
+
38
+ // src/connectors/mariadb/index.ts
39
+ import * as mariadb from "mariadb";
40
+ var MariadbDSNParser = class {
41
+ async parse(dsn, config) {
42
+ const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
43
+ const queryTimeoutSeconds = config?.queryTimeoutSeconds;
44
+ if (!this.isValidDSN(dsn)) {
45
+ const obfuscatedDSN = obfuscateDSNPassword(dsn);
46
+ const expectedFormat = this.getSampleDSN();
47
+ throw new Error(
48
+ `Invalid MariaDB DSN format.
49
+ Provided: ${obfuscatedDSN}
50
+ Expected: ${expectedFormat}`
51
+ );
52
+ }
53
+ try {
54
+ const url = new SafeURL(dsn);
55
+ const database = url.pathname ? url.pathname.substring(1) : "";
56
+ requireDatabaseInDSN(database, dsn, "MariaDB");
57
+ const connectionConfig = {
58
+ host: url.hostname,
59
+ port: url.port ? parseInt(url.port) : 3306,
60
+ database,
61
+ user: url.username,
62
+ password: url.password,
63
+ multipleStatements: true,
64
+ // Enable native multi-statement support
65
+ ...connectionTimeoutSeconds !== void 0 && {
66
+ connectTimeout: connectionTimeoutSeconds * 1e3
67
+ },
68
+ // Server-side limit: the driver applies `queryTimeout` on each new
69
+ // connection as the session variable max_statement_time.
70
+ ...queryTimeoutSeconds !== void 0 && {
71
+ queryTimeout: queryTimeoutSeconds * 1e3
72
+ },
73
+ // Controls how the driver interprets DATETIME values ("Z", "local", or "±HH:MM").
74
+ ...config?.timezone !== void 0 && {
75
+ timezone: config.timezone
76
+ },
77
+ // Connection character set (e.g. "utf8mb4") / collation (e.g.
78
+ // "utf8mb4_general_ci"). The mariadb driver exposes both as distinct
79
+ // options, but a configured collation is authoritative (it implies its
80
+ // character set) and the driver ignores `collation` when `charset` is also
81
+ // passed. So forward the collation when present, otherwise the charset
82
+ // (which uses that character set's default collation).
83
+ ...config?.collation !== void 0 ? { collation: config.collation } : config?.charset !== void 0 ? { charset: config.charset } : {}
84
+ };
85
+ url.forEachSearchParam((value, key) => {
86
+ if (key === "sslmode") {
87
+ if (value === "disable") {
88
+ connectionConfig.ssl = void 0;
89
+ } else if (value === "require") {
90
+ connectionConfig.ssl = { rejectUnauthorized: false };
91
+ } else {
92
+ connectionConfig.ssl = {};
93
+ }
94
+ }
95
+ });
96
+ if (url.password && url.password.includes("X-Amz-Credential")) {
97
+ if (connectionConfig.ssl === void 0) {
98
+ connectionConfig.ssl = { rejectUnauthorized: false };
99
+ }
100
+ }
101
+ return connectionConfig;
102
+ } catch (error) {
103
+ if (error instanceof MissingDatabaseError) {
104
+ throw error;
105
+ }
106
+ throw new Error(
107
+ `Failed to parse MariaDB DSN: ${error instanceof Error ? error.message : String(error)}`
108
+ );
109
+ }
110
+ }
111
+ getSampleDSN() {
112
+ return "mariadb://root:password@localhost:3306/db?sslmode=require";
113
+ }
114
+ isValidDSN(dsn) {
115
+ try {
116
+ return dsn.startsWith("mariadb://");
117
+ } catch (error) {
118
+ return false;
119
+ }
120
+ }
121
+ };
122
+ var MariaDBConnector = class _MariaDBConnector {
123
+ constructor() {
124
+ this.id = "mariadb";
125
+ this.name = "MariaDB";
126
+ this.dsnParser = new MariadbDSNParser();
127
+ this.pool = null;
128
+ // Source ID is set by ConnectorManager after cloning
129
+ this.sourceId = "default";
130
+ // TiDB speaks the MySQL protocol but rejects `START TRANSACTION READ ONLY`
131
+ // unless tidb_enable_noop_functions is on. Detected once at connect time.
132
+ this.supportsReadOnlyTransaction = true;
133
+ }
134
+ getId() {
135
+ return this.sourceId;
136
+ }
137
+ clone() {
138
+ return new _MariaDBConnector();
139
+ }
140
+ async connect(dsn, initScript, config) {
141
+ try {
142
+ const connectionConfig = await this.dsnParser.parse(dsn, config);
143
+ this.clientQueryTimeoutMs = config?.queryTimeoutSeconds !== void 0 ? clientQueryTimeoutMs(config.queryTimeoutSeconds * 1e3) : void 0;
144
+ this.pool = mariadb.createPool(connectionConfig);
145
+ this.pool.on("error", (err) => {
146
+ console.error(
147
+ `MariaDB pool (source "${this.sourceId}"): background connection error, pool will retry:`,
148
+ err.message
149
+ );
150
+ });
151
+ const rows = await this.pool.query("SELECT VERSION() AS version");
152
+ this.supportsReadOnlyTransaction = !isTiDBVersion(rows?.[0]?.version);
153
+ } catch (err) {
154
+ if (this.pool) {
155
+ const pool = this.pool;
156
+ this.pool = null;
157
+ await closeQuietly(() => pool.end());
158
+ }
159
+ console.error("Failed to connect to MariaDB database:", err);
160
+ throw err;
161
+ }
162
+ }
163
+ async disconnect() {
164
+ if (this.pool) {
165
+ await this.pool.end();
166
+ this.pool = null;
167
+ }
168
+ }
169
+ /**
170
+ * Row-array query adapter over the pool for the shared MySQL-family
171
+ * introspection and health-check code. Throws when not connected, so the
172
+ * shared functions never see a missing pool. The mariadb driver already
173
+ * resolves to the rows themselves.
174
+ */
175
+ requireQuery() {
176
+ if (!this.pool) {
177
+ throw new Error("Not connected to database");
178
+ }
179
+ const pool = this.pool;
180
+ return (sql, params) => params === void 0 ? pool.query(sql) : pool.query(sql, params);
181
+ }
182
+ async getSchemas() {
183
+ return getSchemas(this.requireQuery());
184
+ }
185
+ async getTables(schema) {
186
+ return getTables(this.requireQuery(), schema);
187
+ }
188
+ async getViews(schema) {
189
+ return getViews(this.requireQuery(), schema);
190
+ }
191
+ async tableExists(tableName, schema) {
192
+ return tableExists(this.requireQuery(), tableName, schema);
193
+ }
194
+ async getTableIndexes(tableName, schema) {
195
+ return getTableIndexes(this.requireQuery(), tableName, schema);
196
+ }
197
+ async getTableSchema(tableName, schema) {
198
+ return getTableSchema(this.requireQuery(), tableName, schema);
199
+ }
200
+ async getTableComment(tableName, schema) {
201
+ return getTableComment(this.requireQuery(), tableName, schema);
202
+ }
203
+ async getHealthCheck() {
204
+ return getMySQLFamilyHealthCheck(this.requireQuery());
205
+ }
206
+ async getStoredProcedures(schema, routineType) {
207
+ return getStoredProcedures(this.requireQuery(), schema, routineType);
208
+ }
209
+ async getStoredProcedureDetail(procedureName, schema) {
210
+ return getStoredProcedureDetail(this.requireQuery(), procedureName, schema);
211
+ }
212
+ async getDefaultSchema() {
213
+ return getDefaultSchema(this.requireQuery());
214
+ }
215
+ async executeSQL(sql, options, parameters) {
216
+ if (!this.pool) {
217
+ throw new Error("Not connected to database");
218
+ }
219
+ const conn = await this.pool.getConnection();
220
+ let isConnectionDiscarded = false;
221
+ try {
222
+ return await withReadOnlyTransaction(
223
+ conn,
224
+ options.readonly,
225
+ this.supportsReadOnlyTransaction,
226
+ async () => {
227
+ const statements = splitSQLStatements(sql, "mariadb");
228
+ let processedSQL = sql;
229
+ let probes = [];
230
+ if (options.maxRows) {
231
+ const rewrites = statements.map(
232
+ (statement) => SQLRowLimiter.applyMaxRowsWithTruncationProbe(statement, options.maxRows, "mariadb")
233
+ );
234
+ probes = rewrites.map((rewrite) => rewrite.probeApplied);
235
+ processedSQL = rewrites.map((rewrite) => rewrite.sql).join("; ");
236
+ if (sql.trim().endsWith(";")) {
237
+ processedSQL += ";";
238
+ }
239
+ }
240
+ const results = await withClientQueryDeadline(
241
+ parameters && parameters.length > 0 ? conn.query(processedSQL, parameters) : conn.query(processedSQL),
242
+ this.clientQueryTimeoutMs
243
+ );
244
+ const resultSets = parseQueryResultSets(results, statements);
245
+ if (resultSets.length === probes.length) {
246
+ resultSets.forEach(
247
+ (set, index) => SQLRowLimiter.flagTruncation(set, options.maxRows, probes[index])
248
+ );
249
+ }
250
+ return { resultSets };
251
+ }
252
+ );
253
+ } catch (error) {
254
+ if (isClientSideTimeout(error)) {
255
+ isConnectionDiscarded = true;
256
+ conn.destroy();
257
+ }
258
+ throw error;
259
+ } finally {
260
+ if (!isConnectionDiscarded) {
261
+ conn.release();
262
+ }
263
+ }
264
+ }
265
+ };
266
+ var mariadbConnector = new MariaDBConnector();
267
+ ConnectorRegistry.register(mariadbConnector);
268
+ export {
269
+ MariaDBConnector
270
+ };
@@ -0,0 +1,312 @@
1
+ import {
2
+ MissingDatabaseError,
3
+ getDefaultSchema,
4
+ getMySQLFamilyHealthCheck,
5
+ getSchemas,
6
+ getStoredProcedureDetail,
7
+ getStoredProcedures,
8
+ getTableComment,
9
+ getTableIndexes,
10
+ getTableSchema,
11
+ getTables,
12
+ getViews,
13
+ isTiDBVersion,
14
+ parseQueryResultSets,
15
+ requireDatabaseInDSN,
16
+ tableExists,
17
+ withReadOnlyTransaction
18
+ } from "./chunk-SJ5BRPQA.js";
19
+ import {
20
+ CANCEL_QUERY_TIMEOUT_MS,
21
+ clientQueryTimeoutMs,
22
+ isClientSideTimeout
23
+ } from "./chunk-5XSBOK3W.js";
24
+ import "./chunk-FU2ZJE4E.js";
25
+ import "./chunk-X2EC47W4.js";
26
+ import {
27
+ SQLRowLimiter,
28
+ closeQuietly
29
+ } from "./chunk-X5OCFRNL.js";
30
+ import "./chunk-55X2VY5D.js";
31
+ import {
32
+ ConnectorRegistry,
33
+ SafeURL,
34
+ obfuscateDSNPassword,
35
+ splitSQLStatements
36
+ } from "./chunk-IXC4NQGC.js";
37
+
38
+ // src/connectors/mysql/index.ts
39
+ import mysql from "mysql2/promise";
40
+ var MySQLDSNParser = class {
41
+ async parse(dsn, config) {
42
+ const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
43
+ const timezone = config?.timezone;
44
+ const charset = config?.charset;
45
+ const collation = config?.collation;
46
+ if (!this.isValidDSN(dsn)) {
47
+ const obfuscatedDSN = obfuscateDSNPassword(dsn);
48
+ const expectedFormat = this.getSampleDSN();
49
+ throw new Error(
50
+ `Invalid MySQL DSN format.
51
+ Provided: ${obfuscatedDSN}
52
+ Expected: ${expectedFormat}`
53
+ );
54
+ }
55
+ try {
56
+ const url = new SafeURL(dsn);
57
+ const database = url.pathname ? url.pathname.substring(1) : "";
58
+ requireDatabaseInDSN(database, dsn, "MySQL");
59
+ const config2 = {
60
+ host: url.hostname,
61
+ port: url.port ? parseInt(url.port) : 3306,
62
+ database,
63
+ user: url.username,
64
+ password: url.password,
65
+ multipleStatements: true,
66
+ // Enable native multi-statement support
67
+ supportBigNumbers: true
68
+ // Return BIGINT as string when value exceeds Number.MAX_SAFE_INTEGER
69
+ };
70
+ url.forEachSearchParam((value, key) => {
71
+ if (key === "sslmode") {
72
+ if (value === "disable") {
73
+ config2.ssl = void 0;
74
+ } else if (value === "require") {
75
+ config2.ssl = { rejectUnauthorized: false };
76
+ } else {
77
+ config2.ssl = {};
78
+ }
79
+ }
80
+ });
81
+ if (connectionTimeoutSeconds !== void 0) {
82
+ config2.connectTimeout = connectionTimeoutSeconds * 1e3;
83
+ }
84
+ if (timezone !== void 0) {
85
+ config2.timezone = timezone;
86
+ }
87
+ const charsetOrCollation = collation ?? charset;
88
+ if (charsetOrCollation !== void 0) {
89
+ config2.charset = charsetOrCollation;
90
+ }
91
+ if (url.password && url.password.includes("X-Amz-Credential")) {
92
+ config2.authPlugins = {
93
+ mysql_clear_password: () => () => {
94
+ return Buffer.from(url.password + "\0");
95
+ }
96
+ };
97
+ if (config2.ssl === void 0) {
98
+ config2.ssl = { rejectUnauthorized: false };
99
+ }
100
+ }
101
+ return config2;
102
+ } catch (error) {
103
+ if (error instanceof MissingDatabaseError) {
104
+ throw error;
105
+ }
106
+ throw new Error(
107
+ `Failed to parse MySQL DSN: ${error instanceof Error ? error.message : String(error)}`
108
+ );
109
+ }
110
+ }
111
+ getSampleDSN() {
112
+ return "mysql://root:password@localhost:3306/mysql?sslmode=require";
113
+ }
114
+ isValidDSN(dsn) {
115
+ try {
116
+ return dsn.startsWith("mysql://");
117
+ } catch (error) {
118
+ return false;
119
+ }
120
+ }
121
+ };
122
+ var MySQLConnector = class _MySQLConnector {
123
+ constructor() {
124
+ this.id = "mysql";
125
+ this.name = "MySQL";
126
+ this.dsnParser = new MySQLDSNParser();
127
+ this.pool = null;
128
+ // Source ID is set by ConnectorManager after cloning
129
+ this.sourceId = "default";
130
+ // TiDB speaks the MySQL protocol but rejects `START TRANSACTION READ ONLY`
131
+ // unless tidb_enable_noop_functions is on. Detected once at connect time.
132
+ this.supportsReadOnlyTransaction = true;
133
+ }
134
+ getId() {
135
+ return this.sourceId;
136
+ }
137
+ clone() {
138
+ return new _MySQLConnector();
139
+ }
140
+ async connect(dsn, initScript, config) {
141
+ try {
142
+ const connectionOptions = await this.dsnParser.parse(dsn, config);
143
+ this.pool = mysql.createPool(connectionOptions);
144
+ if (config?.queryTimeoutSeconds !== void 0) {
145
+ const queryTimeoutMs = Math.ceil(config.queryTimeoutSeconds * 1e3);
146
+ this.queryTimeoutMs = queryTimeoutMs;
147
+ const setServerLimit = `SET SESSION max_execution_time = ${queryTimeoutMs}`;
148
+ this.pool.on(
149
+ "connection",
150
+ (connection) => {
151
+ connection.query(setServerLimit, () => {
152
+ });
153
+ }
154
+ );
155
+ }
156
+ const [rows] = await this.pool.query("SELECT VERSION() AS version");
157
+ this.supportsReadOnlyTransaction = !isTiDBVersion(rows[0]?.version);
158
+ } catch (err) {
159
+ if (this.pool) {
160
+ const pool = this.pool;
161
+ this.pool = null;
162
+ await closeQuietly(() => pool.end());
163
+ }
164
+ console.error("Failed to connect to MySQL database:", err);
165
+ throw err;
166
+ }
167
+ }
168
+ async disconnect() {
169
+ if (this.pool) {
170
+ await this.pool.end();
171
+ this.pool = null;
172
+ }
173
+ }
174
+ /**
175
+ * Row-array query adapter over the pool for the shared MySQL-family
176
+ * introspection and health-check code. Throws when not connected, so the
177
+ * shared functions never see a missing pool. mysql2 resolves to
178
+ * [rows, fields]; only the rows are handed on.
179
+ */
180
+ requireQuery() {
181
+ if (!this.pool) {
182
+ throw new Error("Not connected to database");
183
+ }
184
+ const pool = this.pool;
185
+ return async (sql, params) => {
186
+ const [rows] = await (params === void 0 ? pool.query(sql) : pool.query(sql, params));
187
+ return rows;
188
+ };
189
+ }
190
+ async getSchemas() {
191
+ return getSchemas(this.requireQuery());
192
+ }
193
+ async getTables(schema) {
194
+ return getTables(this.requireQuery(), schema);
195
+ }
196
+ async getViews(schema) {
197
+ return getViews(this.requireQuery(), schema);
198
+ }
199
+ async tableExists(tableName, schema) {
200
+ return tableExists(this.requireQuery(), tableName, schema);
201
+ }
202
+ async getTableIndexes(tableName, schema) {
203
+ return getTableIndexes(this.requireQuery(), tableName, schema);
204
+ }
205
+ async getTableSchema(tableName, schema) {
206
+ return getTableSchema(this.requireQuery(), tableName, schema);
207
+ }
208
+ async getTableComment(tableName, schema) {
209
+ return getTableComment(this.requireQuery(), tableName, schema);
210
+ }
211
+ async getHealthCheck() {
212
+ return getMySQLFamilyHealthCheck(this.requireQuery());
213
+ }
214
+ async getStoredProcedures(schema, routineType) {
215
+ return getStoredProcedures(this.requireQuery(), schema, routineType);
216
+ }
217
+ async getStoredProcedureDetail(procedureName, schema) {
218
+ return getStoredProcedureDetail(this.requireQuery(), procedureName, schema);
219
+ }
220
+ async getDefaultSchema() {
221
+ return getDefaultSchema(this.requireQuery());
222
+ }
223
+ async executeSQL(sql, options, parameters) {
224
+ if (!this.pool) {
225
+ throw new Error("Not connected to database");
226
+ }
227
+ const conn = await this.pool.getConnection();
228
+ const threadId = conn.threadId;
229
+ let isConnectionDiscarded = false;
230
+ const timeout = this.queryTimeoutMs !== void 0 ? clientQueryTimeoutMs(this.queryTimeoutMs) : void 0;
231
+ try {
232
+ return await withReadOnlyTransaction(
233
+ conn,
234
+ options.readonly,
235
+ this.supportsReadOnlyTransaction,
236
+ async () => {
237
+ const statements = splitSQLStatements(sql, "mysql");
238
+ let processedSQL = sql;
239
+ let probes = [];
240
+ if (options.maxRows) {
241
+ const rewrites = statements.map(
242
+ (statement) => SQLRowLimiter.applyMaxRowsWithTruncationProbe(statement, options.maxRows, "mysql")
243
+ );
244
+ probes = rewrites.map((rewrite) => rewrite.probeApplied);
245
+ processedSQL = rewrites.map((rewrite) => rewrite.sql).join("; ");
246
+ if (sql.trim().endsWith(";")) {
247
+ processedSQL += ";";
248
+ }
249
+ }
250
+ let results;
251
+ if (parameters && parameters.length > 0) {
252
+ results = await conn.query({ sql: processedSQL, timeout }, parameters);
253
+ } else {
254
+ results = await conn.query({ sql: processedSQL, timeout });
255
+ }
256
+ const [firstResult] = results;
257
+ const resultSets = parseQueryResultSets(firstResult, statements);
258
+ if (resultSets.length === probes.length) {
259
+ resultSets.forEach(
260
+ (set, index) => SQLRowLimiter.flagTruncation(set, options.maxRows, probes[index])
261
+ );
262
+ }
263
+ return { resultSets };
264
+ }
265
+ );
266
+ } catch (error) {
267
+ if (isClientSideTimeout(error)) {
268
+ isConnectionDiscarded = true;
269
+ conn.destroy();
270
+ await this.killQuery(threadId);
271
+ }
272
+ throw error;
273
+ } finally {
274
+ if (!isConnectionDiscarded) {
275
+ conn.release();
276
+ }
277
+ }
278
+ }
279
+ /**
280
+ * Best-effort server-side kill for a query abandoned by mysql2's client-side
281
+ * timeout. Uses a separate connection: the original connection's command
282
+ * queue is stuck behind the abandoned statement (see isClientSideTimeout)
283
+ * and cannot itself be used to send KILL QUERY.
284
+ *
285
+ * Bounded by its own short timeout (CANCEL_QUERY_TIMEOUT_MS), independent
286
+ * of the user's query_timeout.
287
+ */
288
+ async killQuery(threadId) {
289
+ if (!this.pool) return;
290
+ let killer;
291
+ let killerPoisoned = false;
292
+ try {
293
+ killer = await this.pool.getConnection();
294
+ await killer.query({ sql: `KILL QUERY ${threadId}`, timeout: CANCEL_QUERY_TIMEOUT_MS });
295
+ } catch (error) {
296
+ killerPoisoned = isClientSideTimeout(error);
297
+ } finally {
298
+ if (killer) {
299
+ if (killerPoisoned) {
300
+ killer.destroy();
301
+ } else {
302
+ killer.release();
303
+ }
304
+ }
305
+ }
306
+ }
307
+ };
308
+ var mysqlConnector = new MySQLConnector();
309
+ ConnectorRegistry.register(mysqlConnector);
310
+ export {
311
+ MySQLConnector
312
+ };
@@ -5,10 +5,10 @@ import {
5
5
  import {
6
6
  SQLRowLimiter,
7
7
  closeQuietly
8
- } from "./chunk-YIGJHEU6.js";
8
+ } from "./chunk-X5OCFRNL.js";
9
9
  import {
10
10
  isReadOnlySQL
11
- } from "./chunk-ABTVMHD6.js";
11
+ } from "./chunk-55X2VY5D.js";
12
12
  import {
13
13
  ConnectorRegistry,
14
14
  LEADING_SQL_NOISE,