@bytebase/dbhub 0.16.1 → 0.18.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/dist/index.js CHANGED
@@ -8,15 +8,19 @@ import {
8
8
  getDatabaseTypeFromDSN,
9
9
  getDefaultPortForType,
10
10
  getToolRegistry,
11
+ initializeToolRegistry,
11
12
  isDemoMode,
13
+ loadTomlConfig,
12
14
  mapArgumentsToArray,
13
15
  obfuscateDSNPassword,
14
16
  parseConnectionInfoFromDSN,
15
17
  resolvePort,
16
18
  resolveSourceConfigs,
19
+ resolveTomlConfigPath,
17
20
  resolveTransport,
21
+ splitSQLStatements,
18
22
  stripCommentsAndStrings
19
- } from "./chunk-IBBG4PSO.js";
23
+ } from "./chunk-WCXOWHL3.js";
20
24
 
21
25
  // src/connectors/postgres/index.ts
22
26
  import pg from "pg";
@@ -141,6 +145,36 @@ var SQLRowLimiter = class {
141
145
  }
142
146
  };
143
147
 
148
+ // src/utils/identifier-quoter.ts
149
+ function quoteIdentifier(identifier, dbType) {
150
+ if (/[\0\x08\x09\x1a\n\r]/.test(identifier)) {
151
+ throw new Error(`Invalid identifier: contains control characters: ${identifier}`);
152
+ }
153
+ if (!identifier) {
154
+ throw new Error("Identifier cannot be empty");
155
+ }
156
+ switch (dbType) {
157
+ case "postgres":
158
+ case "sqlite":
159
+ return `"${identifier.replace(/"/g, '""')}"`;
160
+ case "mysql":
161
+ case "mariadb":
162
+ return `\`${identifier.replace(/`/g, "``")}\``;
163
+ case "sqlserver":
164
+ return `[${identifier.replace(/]/g, "]]")}]`;
165
+ default:
166
+ return `"${identifier.replace(/"/g, '""')}"`;
167
+ }
168
+ }
169
+ function quoteQualifiedIdentifier(tableName, schemaName, dbType) {
170
+ const quotedTable = quoteIdentifier(tableName, dbType);
171
+ if (schemaName) {
172
+ const quotedSchema = quoteIdentifier(schemaName, dbType);
173
+ return `${quotedSchema}.${quotedTable}`;
174
+ }
175
+ return quotedTable;
176
+ }
177
+
144
178
  // src/connectors/postgres/index.ts
145
179
  var { Pool } = pg;
146
180
  var PostgresDSNParser = class {
@@ -209,6 +243,8 @@ var PostgresConnector = class _PostgresConnector {
209
243
  this.pool = null;
210
244
  // Source ID is set by ConnectorManager after cloning
211
245
  this.sourceId = "default";
246
+ // Default schema for discovery methods (first entry from search_path, or "public")
247
+ this.defaultSchema = "public";
212
248
  }
213
249
  getId() {
214
250
  return this.sourceId;
@@ -217,11 +253,21 @@ var PostgresConnector = class _PostgresConnector {
217
253
  return new _PostgresConnector();
218
254
  }
219
255
  async connect(dsn, initScript, config) {
256
+ this.defaultSchema = "public";
220
257
  try {
221
258
  const poolConfig = await this.dsnParser.parse(dsn, config);
222
259
  if (config?.readonly) {
223
260
  poolConfig.options = (poolConfig.options || "") + " -c default_transaction_read_only=on";
224
261
  }
262
+ if (config?.searchPath) {
263
+ const schemas = config.searchPath.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
264
+ if (schemas.length > 0) {
265
+ this.defaultSchema = schemas[0];
266
+ const quotedSchemas = schemas.map((s) => quoteIdentifier(s, "postgres"));
267
+ const optionsValue = quotedSchemas.join(",").replace(/\\/g, "\\\\").replace(/ /g, "\\ ");
268
+ poolConfig.options = (poolConfig.options || "") + ` -c search_path=${optionsValue}`;
269
+ }
270
+ }
225
271
  this.pool = new Pool(poolConfig);
226
272
  const client = await this.pool.connect();
227
273
  client.release();
@@ -259,7 +305,7 @@ var PostgresConnector = class _PostgresConnector {
259
305
  }
260
306
  const client = await this.pool.connect();
261
307
  try {
262
- const schemaToUse = schema || "public";
308
+ const schemaToUse = schema || this.defaultSchema;
263
309
  const result = await client.query(
264
310
  `
265
311
  SELECT table_name
@@ -280,7 +326,7 @@ var PostgresConnector = class _PostgresConnector {
280
326
  }
281
327
  const client = await this.pool.connect();
282
328
  try {
283
- const schemaToUse = schema || "public";
329
+ const schemaToUse = schema || this.defaultSchema;
284
330
  const result = await client.query(
285
331
  `
286
332
  SELECT EXISTS (
@@ -302,7 +348,7 @@ var PostgresConnector = class _PostgresConnector {
302
348
  }
303
349
  const client = await this.pool.connect();
304
350
  try {
305
- const schemaToUse = schema || "public";
351
+ const schemaToUse = schema || this.defaultSchema;
306
352
  const result = await client.query(
307
353
  `
308
354
  SELECT
@@ -350,18 +396,27 @@ var PostgresConnector = class _PostgresConnector {
350
396
  }
351
397
  const client = await this.pool.connect();
352
398
  try {
353
- const schemaToUse = schema || "public";
399
+ const schemaToUse = schema || this.defaultSchema;
354
400
  const result = await client.query(
355
401
  `
356
- SELECT
357
- column_name,
358
- data_type,
359
- is_nullable,
360
- column_default
361
- FROM information_schema.columns
362
- WHERE table_schema = $1
363
- AND table_name = $2
364
- ORDER BY ordinal_position
402
+ SELECT
403
+ c.column_name,
404
+ c.data_type,
405
+ c.is_nullable,
406
+ c.column_default,
407
+ pgd.description
408
+ FROM information_schema.columns c
409
+ LEFT JOIN pg_catalog.pg_namespace nsp
410
+ ON nsp.nspname = c.table_schema
411
+ LEFT JOIN pg_catalog.pg_class cls
412
+ ON cls.relnamespace = nsp.oid
413
+ AND cls.relname = c.table_name
414
+ LEFT JOIN pg_catalog.pg_description pgd
415
+ ON pgd.objoid = cls.oid
416
+ AND pgd.objsubid = c.ordinal_position
417
+ WHERE c.table_schema = $1
418
+ AND c.table_name = $2
419
+ ORDER BY c.ordinal_position
365
420
  `,
366
421
  [schemaToUse, tableName]
367
422
  );
@@ -370,22 +425,82 @@ var PostgresConnector = class _PostgresConnector {
370
425
  client.release();
371
426
  }
372
427
  }
373
- async getStoredProcedures(schema) {
428
+ async getTableRowCount(tableName, schema) {
374
429
  if (!this.pool) {
375
430
  throw new Error("Not connected to database");
376
431
  }
377
432
  const client = await this.pool.connect();
378
433
  try {
379
- const schemaToUse = schema || "public";
434
+ const schemaToUse = schema || this.defaultSchema;
380
435
  const result = await client.query(
381
436
  `
382
- SELECT
437
+ SELECT c.reltuples::bigint as count
438
+ FROM pg_class c
439
+ JOIN pg_namespace n ON n.oid = c.relnamespace
440
+ WHERE c.relname = $1
441
+ AND n.nspname = $2
442
+ AND c.relkind IN ('r','p','m','f')
443
+ `,
444
+ [tableName, schemaToUse]
445
+ );
446
+ if (result.rows.length > 0) {
447
+ const count = Number(result.rows[0].count);
448
+ return count >= 0 ? count : null;
449
+ }
450
+ return null;
451
+ } finally {
452
+ client.release();
453
+ }
454
+ }
455
+ async getTableComment(tableName, schema) {
456
+ if (!this.pool) {
457
+ throw new Error("Not connected to database");
458
+ }
459
+ const client = await this.pool.connect();
460
+ try {
461
+ const schemaToUse = schema || this.defaultSchema;
462
+ const result = await client.query(
463
+ `
464
+ SELECT obj_description(c.oid) as table_comment
465
+ FROM pg_catalog.pg_class c
466
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
467
+ WHERE c.relname = $1
468
+ AND n.nspname = $2
469
+ AND c.relkind IN ('r','p','m','f')
470
+ `,
471
+ [tableName, schemaToUse]
472
+ );
473
+ if (result.rows.length > 0) {
474
+ return result.rows[0].table_comment || null;
475
+ }
476
+ return null;
477
+ } finally {
478
+ client.release();
479
+ }
480
+ }
481
+ async getStoredProcedures(schema, routineType) {
482
+ if (!this.pool) {
483
+ throw new Error("Not connected to database");
484
+ }
485
+ const client = await this.pool.connect();
486
+ try {
487
+ const schemaToUse = schema || this.defaultSchema;
488
+ const params = [schemaToUse];
489
+ let typeFilter = "";
490
+ if (routineType === "function") {
491
+ typeFilter = " AND routine_type = 'FUNCTION'";
492
+ } else if (routineType === "procedure") {
493
+ typeFilter = " AND routine_type = 'PROCEDURE'";
494
+ }
495
+ const result = await client.query(
496
+ `
497
+ SELECT
383
498
  routine_name
384
499
  FROM information_schema.routines
385
- WHERE routine_schema = $1
500
+ WHERE routine_schema = $1${typeFilter}
386
501
  ORDER BY routine_name
387
502
  `,
388
- [schemaToUse]
503
+ params
389
504
  );
390
505
  return result.rows.map((row) => row.routine_name);
391
506
  } finally {
@@ -398,7 +513,7 @@ var PostgresConnector = class _PostgresConnector {
398
513
  }
399
514
  const client = await this.pool.connect();
400
515
  try {
401
- const schemaToUse = schema || "public";
516
+ const schemaToUse = schema || this.defaultSchema;
402
517
  const result = await client.query(
403
518
  `
404
519
  SELECT
@@ -474,7 +589,7 @@ var PostgresConnector = class _PostgresConnector {
474
589
  }
475
590
  const client = await this.pool.connect();
476
591
  try {
477
- const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
592
+ const statements = splitSQLStatements(sql2, "postgres");
478
593
  if (statements.length === 1) {
479
594
  const processedStatement = SQLRowLimiter.applyMaxRows(statements[0], options.maxRows);
480
595
  let result;
@@ -789,33 +904,78 @@ var SQLServerConnector = class _SQLServerConnector {
789
904
  const schemaToUse = schema || "dbo";
790
905
  const request = this.connection.request().input("tableName", sql.VarChar, tableName).input("schema", sql.VarChar, schemaToUse);
791
906
  const query = `
792
- SELECT COLUMN_NAME as column_name,
793
- DATA_TYPE as data_type,
794
- IS_NULLABLE as is_nullable,
795
- COLUMN_DEFAULT as column_default
796
- FROM INFORMATION_SCHEMA.COLUMNS
797
- WHERE TABLE_NAME = @tableName
798
- AND TABLE_SCHEMA = @schema
799
- ORDER BY ORDINAL_POSITION
907
+ SELECT c.COLUMN_NAME as column_name,
908
+ c.DATA_TYPE as data_type,
909
+ c.IS_NULLABLE as is_nullable,
910
+ c.COLUMN_DEFAULT as column_default,
911
+ ep.value as description
912
+ FROM INFORMATION_SCHEMA.COLUMNS c
913
+ LEFT JOIN sys.columns sc
914
+ ON sc.name = c.COLUMN_NAME
915
+ AND sc.object_id = OBJECT_ID(QUOTENAME(c.TABLE_SCHEMA) + '.' + QUOTENAME(c.TABLE_NAME))
916
+ LEFT JOIN sys.extended_properties ep
917
+ ON ep.major_id = sc.object_id
918
+ AND ep.minor_id = sc.column_id
919
+ AND ep.name = 'MS_Description'
920
+ WHERE c.TABLE_NAME = @tableName
921
+ AND c.TABLE_SCHEMA = @schema
922
+ ORDER BY c.ORDINAL_POSITION
800
923
  `;
801
924
  const result = await request.query(query);
802
- return result.recordset;
925
+ return result.recordset.map((row) => ({
926
+ ...row,
927
+ description: row.description || null
928
+ }));
803
929
  } catch (error) {
804
930
  throw new Error(`Failed to get schema for table ${tableName}: ${error.message}`);
805
931
  }
806
932
  }
807
- async getStoredProcedures(schema) {
933
+ async getTableComment(tableName, schema) {
934
+ if (!this.connection) {
935
+ throw new Error("Not connected to SQL Server database");
936
+ }
937
+ try {
938
+ const schemaToUse = schema || "dbo";
939
+ const request = this.connection.request().input("tableName", sql.VarChar, tableName).input("schema", sql.VarChar, schemaToUse);
940
+ const query = `
941
+ SELECT ep.value as table_comment
942
+ FROM sys.extended_properties ep
943
+ JOIN sys.tables t ON ep.major_id = t.object_id
944
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
945
+ WHERE ep.minor_id = 0
946
+ AND ep.name = 'MS_Description'
947
+ AND t.name = @tableName
948
+ AND s.name = @schema
949
+ `;
950
+ const result = await request.query(query);
951
+ if (result.recordset.length > 0) {
952
+ return result.recordset[0].table_comment || null;
953
+ }
954
+ return null;
955
+ } catch (error) {
956
+ return null;
957
+ }
958
+ }
959
+ async getStoredProcedures(schema, routineType) {
808
960
  if (!this.connection) {
809
961
  throw new Error("Not connected to SQL Server database");
810
962
  }
811
963
  try {
812
964
  const schemaToUse = schema || "dbo";
813
965
  const request = this.connection.request().input("schema", sql.VarChar, schemaToUse);
966
+ let typeFilter;
967
+ if (routineType === "function") {
968
+ typeFilter = "AND ROUTINE_TYPE = 'FUNCTION'";
969
+ } else if (routineType === "procedure") {
970
+ typeFilter = "AND ROUTINE_TYPE = 'PROCEDURE'";
971
+ } else {
972
+ typeFilter = "AND (ROUTINE_TYPE = 'PROCEDURE' OR ROUTINE_TYPE = 'FUNCTION')";
973
+ }
814
974
  const query = `
815
975
  SELECT ROUTINE_NAME
816
976
  FROM INFORMATION_SCHEMA.ROUTINES
817
977
  WHERE ROUTINE_SCHEMA = @schema
818
- AND (ROUTINE_TYPE = 'PROCEDURE' OR ROUTINE_TYPE = 'FUNCTION')
978
+ ${typeFilter}
819
979
  ORDER BY ROUTINE_NAME
820
980
  `;
821
981
  const result = await request.query(query);
@@ -948,38 +1108,6 @@ ConnectorRegistry.register(sqlServerConnector);
948
1108
 
949
1109
  // src/connectors/sqlite/index.ts
950
1110
  import Database from "better-sqlite3";
951
-
952
- // src/utils/identifier-quoter.ts
953
- function quoteIdentifier(identifier, dbType) {
954
- if (/[\0\x08\x09\x1a\n\r]/.test(identifier)) {
955
- throw new Error(`Invalid identifier: contains control characters: ${identifier}`);
956
- }
957
- if (!identifier) {
958
- throw new Error("Identifier cannot be empty");
959
- }
960
- switch (dbType) {
961
- case "postgres":
962
- case "sqlite":
963
- return `"${identifier.replace(/"/g, '""')}"`;
964
- case "mysql":
965
- case "mariadb":
966
- return `\`${identifier.replace(/`/g, "``")}\``;
967
- case "sqlserver":
968
- return `[${identifier.replace(/]/g, "]]")}]`;
969
- default:
970
- return `"${identifier.replace(/"/g, '""')}"`;
971
- }
972
- }
973
- function quoteQualifiedIdentifier(tableName, schemaName, dbType) {
974
- const quotedTable = quoteIdentifier(tableName, dbType);
975
- if (schemaName) {
976
- const quotedSchema = quoteIdentifier(schemaName, dbType);
977
- return `${quotedSchema}.${quotedTable}`;
978
- }
979
- return quotedTable;
980
- }
981
-
982
- // src/connectors/sqlite/index.ts
983
1111
  var SQLiteDSNParser = class {
984
1112
  async parse(dsn, config) {
985
1113
  if (!this.isValidDSN(dsn)) {
@@ -1181,14 +1309,15 @@ var SQLiteConnector = class _SQLiteConnector {
1181
1309
  data_type: row.type,
1182
1310
  // In SQLite, primary key columns are automatically NOT NULL even if notnull=0
1183
1311
  is_nullable: row.notnull === 1 || row.pk > 0 ? "NO" : "YES",
1184
- column_default: row.dflt_value
1312
+ column_default: row.dflt_value,
1313
+ description: null
1185
1314
  }));
1186
1315
  return columns;
1187
1316
  } catch (error) {
1188
1317
  throw error;
1189
1318
  }
1190
1319
  }
1191
- async getStoredProcedures(schema) {
1320
+ async getStoredProcedures(schema, routineType) {
1192
1321
  if (!this.db) {
1193
1322
  throw new Error("Not connected to SQLite database");
1194
1323
  }
@@ -1207,7 +1336,7 @@ var SQLiteConnector = class _SQLiteConnector {
1207
1336
  throw new Error("Not connected to SQLite database");
1208
1337
  }
1209
1338
  try {
1210
- const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
1339
+ const statements = splitSQLStatements(sql2, "sqlite");
1211
1340
  if (statements.length === 1) {
1212
1341
  let processedStatement = statements[0];
1213
1342
  const trimmedStatement = statements[0].toLowerCase().trim();
@@ -1566,11 +1695,12 @@ var MySQLConnector = class _MySQLConnector {
1566
1695
  const queryParams = schema ? [schema, tableName] : [tableName];
1567
1696
  const [rows] = await this.pool.query(
1568
1697
  `
1569
- SELECT
1570
- COLUMN_NAME as column_name,
1571
- DATA_TYPE as data_type,
1698
+ SELECT
1699
+ COLUMN_NAME as column_name,
1700
+ DATA_TYPE as data_type,
1572
1701
  IS_NULLABLE as is_nullable,
1573
- COLUMN_DEFAULT as column_default
1702
+ COLUMN_DEFAULT as column_default,
1703
+ COLUMN_COMMENT as description
1574
1704
  FROM INFORMATION_SCHEMA.COLUMNS
1575
1705
  ${schemaClause}
1576
1706
  AND TABLE_NAME = ?
@@ -1578,24 +1708,57 @@ var MySQLConnector = class _MySQLConnector {
1578
1708
  `,
1579
1709
  queryParams
1580
1710
  );
1581
- return rows;
1711
+ return rows.map((row) => ({
1712
+ ...row,
1713
+ description: row.description || null
1714
+ }));
1582
1715
  } catch (error) {
1583
1716
  console.error("Error getting table schema:", error);
1584
1717
  throw error;
1585
1718
  }
1586
1719
  }
1587
- async getStoredProcedures(schema) {
1720
+ async getTableComment(tableName, schema) {
1721
+ if (!this.pool) {
1722
+ throw new Error("Not connected to database");
1723
+ }
1724
+ try {
1725
+ const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
1726
+ const queryParams = schema ? [schema, tableName] : [tableName];
1727
+ const [rows] = await this.pool.query(
1728
+ `
1729
+ SELECT TABLE_COMMENT
1730
+ FROM INFORMATION_SCHEMA.TABLES
1731
+ ${schemaClause}
1732
+ AND TABLE_NAME = ?
1733
+ `,
1734
+ queryParams
1735
+ );
1736
+ if (rows.length > 0) {
1737
+ return rows[0].TABLE_COMMENT || null;
1738
+ }
1739
+ return null;
1740
+ } catch (error) {
1741
+ return null;
1742
+ }
1743
+ }
1744
+ async getStoredProcedures(schema, routineType) {
1588
1745
  if (!this.pool) {
1589
1746
  throw new Error("Not connected to database");
1590
1747
  }
1591
1748
  try {
1592
1749
  const schemaClause = schema ? "WHERE ROUTINE_SCHEMA = ?" : "WHERE ROUTINE_SCHEMA = DATABASE()";
1593
1750
  const queryParams = schema ? [schema] : [];
1751
+ let typeFilter = "";
1752
+ if (routineType === "function") {
1753
+ typeFilter = " AND ROUTINE_TYPE = 'FUNCTION'";
1754
+ } else if (routineType === "procedure") {
1755
+ typeFilter = " AND ROUTINE_TYPE = 'PROCEDURE'";
1756
+ }
1594
1757
  const [rows] = await this.pool.query(
1595
1758
  `
1596
1759
  SELECT ROUTINE_NAME
1597
1760
  FROM INFORMATION_SCHEMA.ROUTINES
1598
- ${schemaClause}
1761
+ ${schemaClause}${typeFilter}
1599
1762
  ORDER BY ROUTINE_NAME
1600
1763
  `,
1601
1764
  queryParams
@@ -1719,7 +1882,7 @@ var MySQLConnector = class _MySQLConnector {
1719
1882
  try {
1720
1883
  let processedSQL = sql2;
1721
1884
  if (options.maxRows) {
1722
- const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
1885
+ const statements = splitSQLStatements(sql2, "mysql");
1723
1886
  const processedStatements = statements.map(
1724
1887
  (statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
1725
1888
  );
@@ -1979,11 +2142,12 @@ var MariaDBConnector = class _MariaDBConnector {
1979
2142
  const queryParams = schema ? [schema, tableName] : [tableName];
1980
2143
  const rows = await this.pool.query(
1981
2144
  `
1982
- SELECT
1983
- COLUMN_NAME as column_name,
1984
- DATA_TYPE as data_type,
2145
+ SELECT
2146
+ COLUMN_NAME as column_name,
2147
+ DATA_TYPE as data_type,
1985
2148
  IS_NULLABLE as is_nullable,
1986
- COLUMN_DEFAULT as column_default
2149
+ COLUMN_DEFAULT as column_default,
2150
+ COLUMN_COMMENT as description
1987
2151
  FROM INFORMATION_SCHEMA.COLUMNS
1988
2152
  ${schemaClause}
1989
2153
  AND TABLE_NAME = ?
@@ -1991,24 +2155,57 @@ var MariaDBConnector = class _MariaDBConnector {
1991
2155
  `,
1992
2156
  queryParams
1993
2157
  );
1994
- return rows;
2158
+ return rows.map((row) => ({
2159
+ ...row,
2160
+ description: row.description || null
2161
+ }));
1995
2162
  } catch (error) {
1996
2163
  console.error("Error getting table schema:", error);
1997
2164
  throw error;
1998
2165
  }
1999
2166
  }
2000
- async getStoredProcedures(schema) {
2167
+ async getTableComment(tableName, schema) {
2168
+ if (!this.pool) {
2169
+ throw new Error("Not connected to database");
2170
+ }
2171
+ try {
2172
+ const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
2173
+ const queryParams = schema ? [schema, tableName] : [tableName];
2174
+ const rows = await this.pool.query(
2175
+ `
2176
+ SELECT TABLE_COMMENT
2177
+ FROM INFORMATION_SCHEMA.TABLES
2178
+ ${schemaClause}
2179
+ AND TABLE_NAME = ?
2180
+ `,
2181
+ queryParams
2182
+ );
2183
+ if (rows.length > 0) {
2184
+ return rows[0].TABLE_COMMENT || null;
2185
+ }
2186
+ return null;
2187
+ } catch (error) {
2188
+ return null;
2189
+ }
2190
+ }
2191
+ async getStoredProcedures(schema, routineType) {
2001
2192
  if (!this.pool) {
2002
2193
  throw new Error("Not connected to database");
2003
2194
  }
2004
2195
  try {
2005
2196
  const schemaClause = schema ? "WHERE ROUTINE_SCHEMA = ?" : "WHERE ROUTINE_SCHEMA = DATABASE()";
2006
2197
  const queryParams = schema ? [schema] : [];
2198
+ let typeFilter = "";
2199
+ if (routineType === "function") {
2200
+ typeFilter = " AND ROUTINE_TYPE = 'FUNCTION'";
2201
+ } else if (routineType === "procedure") {
2202
+ typeFilter = " AND ROUTINE_TYPE = 'PROCEDURE'";
2203
+ }
2007
2204
  const rows = await this.pool.query(
2008
2205
  `
2009
2206
  SELECT ROUTINE_NAME
2010
2207
  FROM INFORMATION_SCHEMA.ROUTINES
2011
- ${schemaClause}
2208
+ ${schemaClause}${typeFilter}
2012
2209
  ORDER BY ROUTINE_NAME
2013
2210
  `,
2014
2211
  queryParams
@@ -2132,7 +2329,7 @@ var MariaDBConnector = class _MariaDBConnector {
2132
2329
  try {
2133
2330
  let processedSQL = sql2;
2134
2331
  if (options.maxRows) {
2135
- const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
2332
+ const statements = splitSQLStatements(sql2, "mariadb");
2136
2333
  const processedStatements = statements.map(
2137
2334
  (statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
2138
2335
  );
@@ -2235,7 +2432,7 @@ var allowedKeywords = {
2235
2432
  sqlserver: ["select", "with", "explain", "showplan"]
2236
2433
  };
2237
2434
  function isReadOnlySQL(sql2, connectorType) {
2238
- const cleanedSQL = stripCommentsAndStrings(sql2).trim().toLowerCase();
2435
+ const cleanedSQL = stripCommentsAndStrings(sql2, connectorType).trim().toLowerCase();
2239
2436
  if (!cleanedSQL) {
2240
2437
  return true;
2241
2438
  }
@@ -2328,11 +2525,8 @@ function trackToolRequest(metadata, startTime, extra, success, error) {
2328
2525
  var executeSqlSchema = {
2329
2526
  sql: z.string().describe("SQL to execute (multiple statements separated by ;)")
2330
2527
  };
2331
- function splitSQLStatements(sql2) {
2332
- return sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
2333
- }
2334
2528
  function areAllStatementsReadOnly(sql2, connectorType) {
2335
- const statements = splitSQLStatements(sql2);
2529
+ const statements = splitSQLStatements(sql2, connectorType);
2336
2530
  return statements.every((statement) => isReadOnlySQL(statement, connectorType));
2337
2531
  }
2338
2532
  function createExecuteSqlToolHandler(sourceId) {
@@ -2389,7 +2583,7 @@ function createExecuteSqlToolHandler(sourceId) {
2389
2583
  // src/tools/search-objects.ts
2390
2584
  import { z as z2 } from "zod";
2391
2585
  var searchDatabaseObjectsSchema = {
2392
- object_type: z2.enum(["schema", "table", "column", "procedure", "index"]).describe("Object type to search"),
2586
+ object_type: z2.enum(["schema", "table", "column", "procedure", "function", "index"]).describe("Object type to search"),
2393
2587
  pattern: z2.string().optional().default("%").describe("LIKE pattern (% = any chars, _ = one char). Default: %"),
2394
2588
  schema: z2.string().optional().describe("Filter to schema"),
2395
2589
  table: z2.string().optional().describe("Filter to table (requires schema; column/index only)"),
@@ -2402,6 +2596,9 @@ function likePatternToRegex(pattern) {
2402
2596
  }
2403
2597
  async function getTableRowCount(connector, tableName, schemaName) {
2404
2598
  try {
2599
+ if (connector.getTableRowCount) {
2600
+ return await connector.getTableRowCount(tableName, schemaName);
2601
+ }
2405
2602
  const qualifiedTable = quoteQualifiedIdentifier(tableName, schemaName, connector.id);
2406
2603
  const countQuery = `SELECT COUNT(*) as count FROM ${qualifiedTable}`;
2407
2604
  const result = await connector.executeSQL(countQuery, { maxRows: 1 });
@@ -2413,6 +2610,16 @@ async function getTableRowCount(connector, tableName, schemaName) {
2413
2610
  }
2414
2611
  return null;
2415
2612
  }
2613
+ async function getTableComment(connector, tableName, schemaName) {
2614
+ try {
2615
+ if (connector.getTableComment) {
2616
+ return await connector.getTableComment(tableName, schemaName);
2617
+ }
2618
+ return null;
2619
+ } catch (error) {
2620
+ return null;
2621
+ }
2622
+ }
2416
2623
  async function searchSchemas(connector, pattern, detailLevel, limit) {
2417
2624
  const schemas = await connector.getSchemas();
2418
2625
  const regex = likePatternToRegex(pattern);
@@ -2463,11 +2670,13 @@ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit
2463
2670
  try {
2464
2671
  const columns = await connector.getTableSchema(tableName, schemaName);
2465
2672
  const rowCount = await getTableRowCount(connector, tableName, schemaName);
2673
+ const comment = await getTableComment(connector, tableName, schemaName);
2466
2674
  results.push({
2467
2675
  name: tableName,
2468
2676
  schema: schemaName,
2469
2677
  column_count: columns.length,
2470
- row_count: rowCount
2678
+ row_count: rowCount,
2679
+ ...comment ? { comment } : {}
2471
2680
  });
2472
2681
  } catch (error) {
2473
2682
  results.push({
@@ -2482,16 +2691,19 @@ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit
2482
2691
  const columns = await connector.getTableSchema(tableName, schemaName);
2483
2692
  const indexes = await connector.getTableIndexes(tableName, schemaName);
2484
2693
  const rowCount = await getTableRowCount(connector, tableName, schemaName);
2694
+ const comment = await getTableComment(connector, tableName, schemaName);
2485
2695
  results.push({
2486
2696
  name: tableName,
2487
2697
  schema: schemaName,
2488
2698
  column_count: columns.length,
2489
2699
  row_count: rowCount,
2700
+ ...comment ? { comment } : {},
2490
2701
  columns: columns.map((col) => ({
2491
2702
  name: col.column_name,
2492
2703
  type: col.data_type,
2493
2704
  nullable: col.is_nullable === "YES",
2494
- default: col.column_default
2705
+ default: col.column_default,
2706
+ ...col.description ? { description: col.description } : {}
2495
2707
  })),
2496
2708
  indexes: indexes.map((idx) => ({
2497
2709
  name: idx.index_name,
@@ -2553,7 +2765,8 @@ async function searchColumns(connector, pattern, schemaFilter, tableFilter, deta
2553
2765
  schema: schemaName,
2554
2766
  type: column.data_type,
2555
2767
  nullable: column.is_nullable === "YES",
2556
- default: column.column_default
2768
+ default: column.column_default,
2769
+ ...column.description ? { description: column.description } : {}
2557
2770
  });
2558
2771
  }
2559
2772
  }
@@ -2567,7 +2780,7 @@ async function searchColumns(connector, pattern, schemaFilter, tableFilter, deta
2567
2780
  }
2568
2781
  return results;
2569
2782
  }
2570
- async function searchProcedures(connector, pattern, schemaFilter, detailLevel, limit) {
2783
+ async function searchProcedures(connector, pattern, schemaFilter, detailLevel, limit, routineType) {
2571
2784
  const regex = likePatternToRegex(pattern);
2572
2785
  const results = [];
2573
2786
  let schemasToSearch;
@@ -2579,7 +2792,7 @@ async function searchProcedures(connector, pattern, schemaFilter, detailLevel, l
2579
2792
  for (const schemaName of schemasToSearch) {
2580
2793
  if (results.length >= limit) break;
2581
2794
  try {
2582
- const procedures = await connector.getStoredProcedures(schemaName);
2795
+ const procedures = await connector.getStoredProcedures(schemaName, routineType);
2583
2796
  const matched = procedures.filter((proc) => regex.test(proc));
2584
2797
  for (const procName of matched) {
2585
2798
  if (results.length >= limit) break;
@@ -2716,7 +2929,10 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
2716
2929
  results = await searchColumns(connector, pattern, schema, table, detail_level, limit);
2717
2930
  break;
2718
2931
  case "procedure":
2719
- results = await searchProcedures(connector, pattern, schema, detail_level, limit);
2932
+ results = await searchProcedures(connector, pattern, schema, detail_level, limit, "procedure");
2933
+ break;
2934
+ case "function":
2935
+ results = await searchProcedures(connector, pattern, schema, detail_level, limit, "function");
2720
2936
  break;
2721
2937
  case "index":
2722
2938
  results = await searchIndexes(connector, pattern, schema, table, detail_level, limit);
@@ -2836,7 +3052,7 @@ function getSearchObjectsMetadata(sourceId) {
2836
3052
  const isSingleSource = sourceIds.length === 1;
2837
3053
  const toolName = isSingleSource ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
2838
3054
  const title = isSingleSource ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
2839
- const description = isSingleSource ? `Search and list database objects (schemas, tables, columns, procedures, indexes) on the ${dbType} database` : `Search and list database objects (schemas, tables, columns, procedures, indexes) on the '${sourceId}' ${dbType} database`;
3055
+ const description = isSingleSource ? `Search and list database objects (schemas, tables, columns, procedures, functions, indexes) on the ${dbType} database` : `Search and list database objects (schemas, tables, columns, procedures, functions, indexes) on the '${sourceId}' ${dbType} database`;
2840
3056
  return {
2841
3057
  name: toolName,
2842
3058
  description,
@@ -3340,6 +3556,95 @@ function buildSourceDisplayInfo(sourceConfigs, getToolsForSource2, isDemo) {
3340
3556
  });
3341
3557
  }
3342
3558
 
3559
+ // src/utils/config-watcher.ts
3560
+ import fs from "fs";
3561
+ var DEBOUNCE_MS = 500;
3562
+ function startConfigWatcher(options) {
3563
+ const { connectorManager, initialTools } = options;
3564
+ const configPath = resolveTomlConfigPath();
3565
+ if (!configPath) {
3566
+ return null;
3567
+ }
3568
+ let debounceTimer = null;
3569
+ let isReloading = false;
3570
+ let reloadPending = false;
3571
+ let lastGoodSources = connectorManager.getAllSourceConfigs();
3572
+ let lastGoodTools = initialTools;
3573
+ const scheduleReload = () => {
3574
+ if (debounceTimer) {
3575
+ clearTimeout(debounceTimer);
3576
+ }
3577
+ debounceTimer = setTimeout(reload, DEBOUNCE_MS);
3578
+ };
3579
+ const reload = async () => {
3580
+ if (isReloading) {
3581
+ reloadPending = true;
3582
+ return;
3583
+ }
3584
+ isReloading = true;
3585
+ reloadPending = false;
3586
+ try {
3587
+ console.error(`
3588
+ Detected change in ${configPath}, reloading configuration...`);
3589
+ const newConfig = loadTomlConfig();
3590
+ if (!newConfig) {
3591
+ console.error("Config reload: failed to load TOML config, keeping existing connections.");
3592
+ return;
3593
+ }
3594
+ const oldSources = lastGoodSources;
3595
+ const oldTools = lastGoodTools;
3596
+ await connectorManager.disconnect();
3597
+ try {
3598
+ await connectorManager.connectWithSources(newConfig.sources);
3599
+ initializeToolRegistry({
3600
+ sources: newConfig.sources,
3601
+ tools: newConfig.tools
3602
+ });
3603
+ lastGoodSources = newConfig.sources;
3604
+ lastGoodTools = newConfig.tools;
3605
+ console.error("Configuration reloaded successfully.");
3606
+ } catch (connectError) {
3607
+ console.error("Failed to connect with new config, rolling back:", connectError);
3608
+ try {
3609
+ await connectorManager.disconnect();
3610
+ } catch {
3611
+ }
3612
+ try {
3613
+ await connectorManager.connectWithSources(oldSources);
3614
+ initializeToolRegistry({ sources: oldSources, tools: oldTools });
3615
+ console.error("Rolled back to previous configuration.");
3616
+ } catch (rollbackError) {
3617
+ console.error("Rollback also failed, server has no active connections:", rollbackError);
3618
+ }
3619
+ }
3620
+ } catch (error) {
3621
+ console.error("Config reload failed, keeping existing connections:", error);
3622
+ } finally {
3623
+ isReloading = false;
3624
+ if (reloadPending) {
3625
+ reloadPending = false;
3626
+ scheduleReload();
3627
+ }
3628
+ }
3629
+ };
3630
+ const watcher = fs.watch(configPath, (eventType) => {
3631
+ if (eventType === "change") {
3632
+ scheduleReload();
3633
+ }
3634
+ });
3635
+ watcher.unref?.();
3636
+ watcher.on("error", (err) => {
3637
+ console.error("Config file watcher error:", err);
3638
+ });
3639
+ console.error(`Watching ${configPath} for changes (hot reload enabled)`);
3640
+ return () => {
3641
+ if (debounceTimer) {
3642
+ clearTimeout(debounceTimer);
3643
+ }
3644
+ watcher.close();
3645
+ };
3646
+ }
3647
+
3343
3648
  // src/server.ts
3344
3649
  var __filename = fileURLToPath(import.meta.url);
3345
3650
  var __dirname = path.dirname(__filename);
@@ -3392,12 +3697,16 @@ See documentation for more details on configuring database connections.
3392
3697
  const sources = sourceConfigsData.sources;
3393
3698
  console.error(`Configuration source: ${sourceConfigsData.source}`);
3394
3699
  await connectorManager.connectWithSources(sources);
3395
- const { initializeToolRegistry } = await import("./registry-D77Y4CIA.js");
3396
- initializeToolRegistry({
3700
+ const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-NTKAVQCA.js");
3701
+ initializeToolRegistry2({
3397
3702
  sources: sourceConfigsData.sources,
3398
3703
  tools: sourceConfigsData.tools
3399
3704
  });
3400
3705
  console.error("Tool registry initialized");
3706
+ const stopConfigWatcher = startConfigWatcher({
3707
+ connectorManager,
3708
+ initialTools: sourceConfigsData.tools
3709
+ });
3401
3710
  const createServer = () => {
3402
3711
  const server = new McpServer({
3403
3712
  name: SERVER_NAME,
@@ -3425,6 +3734,9 @@ See documentation for more details on configuring database connections.
3425
3734
  isDemo
3426
3735
  );
3427
3736
  console.error(generateStartupTable(sourceDisplayInfos));
3737
+ process.on("exit", () => {
3738
+ stopConfigWatcher?.();
3739
+ });
3428
3740
  if (transportData.type === "http") {
3429
3741
  const app = express();
3430
3742
  app.use(express.json());