@bytebase/dbhub 0.16.0 → 0.17.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.
@@ -1339,6 +1339,18 @@ function validateSourceConfig(source, configPath) {
1339
1339
  );
1340
1340
  }
1341
1341
  }
1342
+ if (source.search_path !== void 0) {
1343
+ if (source.type !== "postgres") {
1344
+ throw new Error(
1345
+ `Configuration file ${configPath}: source '${source.id}' has 'search_path' but it is only supported for PostgreSQL sources.`
1346
+ );
1347
+ }
1348
+ if (typeof source.search_path !== "string" || source.search_path.trim().length === 0) {
1349
+ throw new Error(
1350
+ `Configuration file ${configPath}: source '${source.id}' has invalid search_path. Must be a non-empty string of comma-separated schema names (e.g., "myschema,public").`
1351
+ );
1352
+ }
1353
+ }
1342
1354
  if (source.readonly !== void 0) {
1343
1355
  throw new Error(
1344
1356
  `Configuration file ${configPath}: source '${source.id}' has 'readonly' field, but readonly must be configured per-tool, not per-source. Move 'readonly' to [[tools]] configuration instead.`
@@ -1606,6 +1618,9 @@ var ConnectorManager = class {
1606
1618
  if (source.readonly !== void 0) {
1607
1619
  config.readonly = source.readonly;
1608
1620
  }
1621
+ if (source.search_path) {
1622
+ config.searchPath = source.search_path;
1623
+ }
1609
1624
  await connector.connect(actualDSN, source.init_script, config);
1610
1625
  this.connectors.set(sourceId, connector);
1611
1626
  if (!this.sourceIds.includes(sourceId)) {
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  resolveSourceConfigs,
17
17
  resolveTransport,
18
18
  stripCommentsAndStrings
19
- } from "./chunk-IBBG4PSO.js";
19
+ } from "./chunk-YKDZH7G5.js";
20
20
 
21
21
  // src/connectors/postgres/index.ts
22
22
  import pg from "pg";
@@ -141,6 +141,36 @@ var SQLRowLimiter = class {
141
141
  }
142
142
  };
143
143
 
144
+ // src/utils/identifier-quoter.ts
145
+ function quoteIdentifier(identifier, dbType) {
146
+ if (/[\0\x08\x09\x1a\n\r]/.test(identifier)) {
147
+ throw new Error(`Invalid identifier: contains control characters: ${identifier}`);
148
+ }
149
+ if (!identifier) {
150
+ throw new Error("Identifier cannot be empty");
151
+ }
152
+ switch (dbType) {
153
+ case "postgres":
154
+ case "sqlite":
155
+ return `"${identifier.replace(/"/g, '""')}"`;
156
+ case "mysql":
157
+ case "mariadb":
158
+ return `\`${identifier.replace(/`/g, "``")}\``;
159
+ case "sqlserver":
160
+ return `[${identifier.replace(/]/g, "]]")}]`;
161
+ default:
162
+ return `"${identifier.replace(/"/g, '""')}"`;
163
+ }
164
+ }
165
+ function quoteQualifiedIdentifier(tableName, schemaName, dbType) {
166
+ const quotedTable = quoteIdentifier(tableName, dbType);
167
+ if (schemaName) {
168
+ const quotedSchema = quoteIdentifier(schemaName, dbType);
169
+ return `${quotedSchema}.${quotedTable}`;
170
+ }
171
+ return quotedTable;
172
+ }
173
+
144
174
  // src/connectors/postgres/index.ts
145
175
  var { Pool } = pg;
146
176
  var PostgresDSNParser = class {
@@ -209,6 +239,8 @@ var PostgresConnector = class _PostgresConnector {
209
239
  this.pool = null;
210
240
  // Source ID is set by ConnectorManager after cloning
211
241
  this.sourceId = "default";
242
+ // Default schema for discovery methods (first entry from search_path, or "public")
243
+ this.defaultSchema = "public";
212
244
  }
213
245
  getId() {
214
246
  return this.sourceId;
@@ -217,11 +249,21 @@ var PostgresConnector = class _PostgresConnector {
217
249
  return new _PostgresConnector();
218
250
  }
219
251
  async connect(dsn, initScript, config) {
252
+ this.defaultSchema = "public";
220
253
  try {
221
254
  const poolConfig = await this.dsnParser.parse(dsn, config);
222
255
  if (config?.readonly) {
223
256
  poolConfig.options = (poolConfig.options || "") + " -c default_transaction_read_only=on";
224
257
  }
258
+ if (config?.searchPath) {
259
+ const schemas = config.searchPath.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
260
+ if (schemas.length > 0) {
261
+ this.defaultSchema = schemas[0];
262
+ const quotedSchemas = schemas.map((s) => quoteIdentifier(s, "postgres"));
263
+ const optionsValue = quotedSchemas.join(",").replace(/\\/g, "\\\\").replace(/ /g, "\\ ");
264
+ poolConfig.options = (poolConfig.options || "") + ` -c search_path=${optionsValue}`;
265
+ }
266
+ }
225
267
  this.pool = new Pool(poolConfig);
226
268
  const client = await this.pool.connect();
227
269
  client.release();
@@ -259,7 +301,7 @@ var PostgresConnector = class _PostgresConnector {
259
301
  }
260
302
  const client = await this.pool.connect();
261
303
  try {
262
- const schemaToUse = schema || "public";
304
+ const schemaToUse = schema || this.defaultSchema;
263
305
  const result = await client.query(
264
306
  `
265
307
  SELECT table_name
@@ -280,7 +322,7 @@ var PostgresConnector = class _PostgresConnector {
280
322
  }
281
323
  const client = await this.pool.connect();
282
324
  try {
283
- const schemaToUse = schema || "public";
325
+ const schemaToUse = schema || this.defaultSchema;
284
326
  const result = await client.query(
285
327
  `
286
328
  SELECT EXISTS (
@@ -302,7 +344,7 @@ var PostgresConnector = class _PostgresConnector {
302
344
  }
303
345
  const client = await this.pool.connect();
304
346
  try {
305
- const schemaToUse = schema || "public";
347
+ const schemaToUse = schema || this.defaultSchema;
306
348
  const result = await client.query(
307
349
  `
308
350
  SELECT
@@ -350,18 +392,27 @@ var PostgresConnector = class _PostgresConnector {
350
392
  }
351
393
  const client = await this.pool.connect();
352
394
  try {
353
- const schemaToUse = schema || "public";
395
+ const schemaToUse = schema || this.defaultSchema;
354
396
  const result = await client.query(
355
397
  `
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
398
+ SELECT
399
+ c.column_name,
400
+ c.data_type,
401
+ c.is_nullable,
402
+ c.column_default,
403
+ pgd.description
404
+ FROM information_schema.columns c
405
+ LEFT JOIN pg_catalog.pg_namespace nsp
406
+ ON nsp.nspname = c.table_schema
407
+ LEFT JOIN pg_catalog.pg_class cls
408
+ ON cls.relnamespace = nsp.oid
409
+ AND cls.relname = c.table_name
410
+ LEFT JOIN pg_catalog.pg_description pgd
411
+ ON pgd.objoid = cls.oid
412
+ AND pgd.objsubid = c.ordinal_position
413
+ WHERE c.table_schema = $1
414
+ AND c.table_name = $2
415
+ ORDER BY c.ordinal_position
365
416
  `,
366
417
  [schemaToUse, tableName]
367
418
  );
@@ -370,22 +421,82 @@ var PostgresConnector = class _PostgresConnector {
370
421
  client.release();
371
422
  }
372
423
  }
373
- async getStoredProcedures(schema) {
424
+ async getTableRowCount(tableName, schema) {
374
425
  if (!this.pool) {
375
426
  throw new Error("Not connected to database");
376
427
  }
377
428
  const client = await this.pool.connect();
378
429
  try {
379
- const schemaToUse = schema || "public";
430
+ const schemaToUse = schema || this.defaultSchema;
380
431
  const result = await client.query(
381
432
  `
382
- SELECT
433
+ SELECT c.reltuples::bigint as count
434
+ FROM pg_class c
435
+ JOIN pg_namespace n ON n.oid = c.relnamespace
436
+ WHERE c.relname = $1
437
+ AND n.nspname = $2
438
+ AND c.relkind IN ('r','p','m','f')
439
+ `,
440
+ [tableName, schemaToUse]
441
+ );
442
+ if (result.rows.length > 0) {
443
+ const count = Number(result.rows[0].count);
444
+ return count >= 0 ? count : null;
445
+ }
446
+ return null;
447
+ } finally {
448
+ client.release();
449
+ }
450
+ }
451
+ async getTableComment(tableName, schema) {
452
+ if (!this.pool) {
453
+ throw new Error("Not connected to database");
454
+ }
455
+ const client = await this.pool.connect();
456
+ try {
457
+ const schemaToUse = schema || this.defaultSchema;
458
+ const result = await client.query(
459
+ `
460
+ SELECT obj_description(c.oid) as table_comment
461
+ FROM pg_catalog.pg_class c
462
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
463
+ WHERE c.relname = $1
464
+ AND n.nspname = $2
465
+ AND c.relkind IN ('r','p','m','f')
466
+ `,
467
+ [tableName, schemaToUse]
468
+ );
469
+ if (result.rows.length > 0) {
470
+ return result.rows[0].table_comment || null;
471
+ }
472
+ return null;
473
+ } finally {
474
+ client.release();
475
+ }
476
+ }
477
+ async getStoredProcedures(schema, routineType) {
478
+ if (!this.pool) {
479
+ throw new Error("Not connected to database");
480
+ }
481
+ const client = await this.pool.connect();
482
+ try {
483
+ const schemaToUse = schema || this.defaultSchema;
484
+ const params = [schemaToUse];
485
+ let typeFilter = "";
486
+ if (routineType === "function") {
487
+ typeFilter = " AND routine_type = 'FUNCTION'";
488
+ } else if (routineType === "procedure") {
489
+ typeFilter = " AND routine_type = 'PROCEDURE'";
490
+ }
491
+ const result = await client.query(
492
+ `
493
+ SELECT
383
494
  routine_name
384
495
  FROM information_schema.routines
385
- WHERE routine_schema = $1
496
+ WHERE routine_schema = $1${typeFilter}
386
497
  ORDER BY routine_name
387
498
  `,
388
- [schemaToUse]
499
+ params
389
500
  );
390
501
  return result.rows.map((row) => row.routine_name);
391
502
  } finally {
@@ -398,7 +509,7 @@ var PostgresConnector = class _PostgresConnector {
398
509
  }
399
510
  const client = await this.pool.connect();
400
511
  try {
401
- const schemaToUse = schema || "public";
512
+ const schemaToUse = schema || this.defaultSchema;
402
513
  const result = await client.query(
403
514
  `
404
515
  SELECT
@@ -789,33 +900,78 @@ var SQLServerConnector = class _SQLServerConnector {
789
900
  const schemaToUse = schema || "dbo";
790
901
  const request = this.connection.request().input("tableName", sql.VarChar, tableName).input("schema", sql.VarChar, schemaToUse);
791
902
  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
903
+ SELECT c.COLUMN_NAME as column_name,
904
+ c.DATA_TYPE as data_type,
905
+ c.IS_NULLABLE as is_nullable,
906
+ c.COLUMN_DEFAULT as column_default,
907
+ ep.value as description
908
+ FROM INFORMATION_SCHEMA.COLUMNS c
909
+ LEFT JOIN sys.columns sc
910
+ ON sc.name = c.COLUMN_NAME
911
+ AND sc.object_id = OBJECT_ID(QUOTENAME(c.TABLE_SCHEMA) + '.' + QUOTENAME(c.TABLE_NAME))
912
+ LEFT JOIN sys.extended_properties ep
913
+ ON ep.major_id = sc.object_id
914
+ AND ep.minor_id = sc.column_id
915
+ AND ep.name = 'MS_Description'
916
+ WHERE c.TABLE_NAME = @tableName
917
+ AND c.TABLE_SCHEMA = @schema
918
+ ORDER BY c.ORDINAL_POSITION
800
919
  `;
801
920
  const result = await request.query(query);
802
- return result.recordset;
921
+ return result.recordset.map((row) => ({
922
+ ...row,
923
+ description: row.description || null
924
+ }));
803
925
  } catch (error) {
804
926
  throw new Error(`Failed to get schema for table ${tableName}: ${error.message}`);
805
927
  }
806
928
  }
807
- async getStoredProcedures(schema) {
929
+ async getTableComment(tableName, schema) {
930
+ if (!this.connection) {
931
+ throw new Error("Not connected to SQL Server database");
932
+ }
933
+ try {
934
+ const schemaToUse = schema || "dbo";
935
+ const request = this.connection.request().input("tableName", sql.VarChar, tableName).input("schema", sql.VarChar, schemaToUse);
936
+ const query = `
937
+ SELECT ep.value as table_comment
938
+ FROM sys.extended_properties ep
939
+ JOIN sys.tables t ON ep.major_id = t.object_id
940
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
941
+ WHERE ep.minor_id = 0
942
+ AND ep.name = 'MS_Description'
943
+ AND t.name = @tableName
944
+ AND s.name = @schema
945
+ `;
946
+ const result = await request.query(query);
947
+ if (result.recordset.length > 0) {
948
+ return result.recordset[0].table_comment || null;
949
+ }
950
+ return null;
951
+ } catch (error) {
952
+ return null;
953
+ }
954
+ }
955
+ async getStoredProcedures(schema, routineType) {
808
956
  if (!this.connection) {
809
957
  throw new Error("Not connected to SQL Server database");
810
958
  }
811
959
  try {
812
960
  const schemaToUse = schema || "dbo";
813
961
  const request = this.connection.request().input("schema", sql.VarChar, schemaToUse);
962
+ let typeFilter;
963
+ if (routineType === "function") {
964
+ typeFilter = "AND ROUTINE_TYPE = 'FUNCTION'";
965
+ } else if (routineType === "procedure") {
966
+ typeFilter = "AND ROUTINE_TYPE = 'PROCEDURE'";
967
+ } else {
968
+ typeFilter = "AND (ROUTINE_TYPE = 'PROCEDURE' OR ROUTINE_TYPE = 'FUNCTION')";
969
+ }
814
970
  const query = `
815
971
  SELECT ROUTINE_NAME
816
972
  FROM INFORMATION_SCHEMA.ROUTINES
817
973
  WHERE ROUTINE_SCHEMA = @schema
818
- AND (ROUTINE_TYPE = 'PROCEDURE' OR ROUTINE_TYPE = 'FUNCTION')
974
+ ${typeFilter}
819
975
  ORDER BY ROUTINE_NAME
820
976
  `;
821
977
  const result = await request.query(query);
@@ -948,38 +1104,6 @@ ConnectorRegistry.register(sqlServerConnector);
948
1104
 
949
1105
  // src/connectors/sqlite/index.ts
950
1106
  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
1107
  var SQLiteDSNParser = class {
984
1108
  async parse(dsn, config) {
985
1109
  if (!this.isValidDSN(dsn)) {
@@ -1181,14 +1305,15 @@ var SQLiteConnector = class _SQLiteConnector {
1181
1305
  data_type: row.type,
1182
1306
  // In SQLite, primary key columns are automatically NOT NULL even if notnull=0
1183
1307
  is_nullable: row.notnull === 1 || row.pk > 0 ? "NO" : "YES",
1184
- column_default: row.dflt_value
1308
+ column_default: row.dflt_value,
1309
+ description: null
1185
1310
  }));
1186
1311
  return columns;
1187
1312
  } catch (error) {
1188
1313
  throw error;
1189
1314
  }
1190
1315
  }
1191
- async getStoredProcedures(schema) {
1316
+ async getStoredProcedures(schema, routineType) {
1192
1317
  if (!this.db) {
1193
1318
  throw new Error("Not connected to SQLite database");
1194
1319
  }
@@ -1566,11 +1691,12 @@ var MySQLConnector = class _MySQLConnector {
1566
1691
  const queryParams = schema ? [schema, tableName] : [tableName];
1567
1692
  const [rows] = await this.pool.query(
1568
1693
  `
1569
- SELECT
1570
- COLUMN_NAME as column_name,
1571
- DATA_TYPE as data_type,
1694
+ SELECT
1695
+ COLUMN_NAME as column_name,
1696
+ DATA_TYPE as data_type,
1572
1697
  IS_NULLABLE as is_nullable,
1573
- COLUMN_DEFAULT as column_default
1698
+ COLUMN_DEFAULT as column_default,
1699
+ COLUMN_COMMENT as description
1574
1700
  FROM INFORMATION_SCHEMA.COLUMNS
1575
1701
  ${schemaClause}
1576
1702
  AND TABLE_NAME = ?
@@ -1578,24 +1704,57 @@ var MySQLConnector = class _MySQLConnector {
1578
1704
  `,
1579
1705
  queryParams
1580
1706
  );
1581
- return rows;
1707
+ return rows.map((row) => ({
1708
+ ...row,
1709
+ description: row.description || null
1710
+ }));
1582
1711
  } catch (error) {
1583
1712
  console.error("Error getting table schema:", error);
1584
1713
  throw error;
1585
1714
  }
1586
1715
  }
1587
- async getStoredProcedures(schema) {
1716
+ async getTableComment(tableName, schema) {
1717
+ if (!this.pool) {
1718
+ throw new Error("Not connected to database");
1719
+ }
1720
+ try {
1721
+ const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
1722
+ const queryParams = schema ? [schema, tableName] : [tableName];
1723
+ const [rows] = await this.pool.query(
1724
+ `
1725
+ SELECT TABLE_COMMENT
1726
+ FROM INFORMATION_SCHEMA.TABLES
1727
+ ${schemaClause}
1728
+ AND TABLE_NAME = ?
1729
+ `,
1730
+ queryParams
1731
+ );
1732
+ if (rows.length > 0) {
1733
+ return rows[0].TABLE_COMMENT || null;
1734
+ }
1735
+ return null;
1736
+ } catch (error) {
1737
+ return null;
1738
+ }
1739
+ }
1740
+ async getStoredProcedures(schema, routineType) {
1588
1741
  if (!this.pool) {
1589
1742
  throw new Error("Not connected to database");
1590
1743
  }
1591
1744
  try {
1592
1745
  const schemaClause = schema ? "WHERE ROUTINE_SCHEMA = ?" : "WHERE ROUTINE_SCHEMA = DATABASE()";
1593
1746
  const queryParams = schema ? [schema] : [];
1747
+ let typeFilter = "";
1748
+ if (routineType === "function") {
1749
+ typeFilter = " AND ROUTINE_TYPE = 'FUNCTION'";
1750
+ } else if (routineType === "procedure") {
1751
+ typeFilter = " AND ROUTINE_TYPE = 'PROCEDURE'";
1752
+ }
1594
1753
  const [rows] = await this.pool.query(
1595
1754
  `
1596
1755
  SELECT ROUTINE_NAME
1597
1756
  FROM INFORMATION_SCHEMA.ROUTINES
1598
- ${schemaClause}
1757
+ ${schemaClause}${typeFilter}
1599
1758
  ORDER BY ROUTINE_NAME
1600
1759
  `,
1601
1760
  queryParams
@@ -1757,7 +1916,7 @@ var mysqlConnector = new MySQLConnector();
1757
1916
  ConnectorRegistry.register(mysqlConnector);
1758
1917
 
1759
1918
  // src/connectors/mariadb/index.ts
1760
- import mariadb from "mariadb";
1919
+ import * as mariadb from "mariadb";
1761
1920
  var MariadbDSNParser = class {
1762
1921
  async parse(dsn, config) {
1763
1922
  const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
@@ -1979,11 +2138,12 @@ var MariaDBConnector = class _MariaDBConnector {
1979
2138
  const queryParams = schema ? [schema, tableName] : [tableName];
1980
2139
  const rows = await this.pool.query(
1981
2140
  `
1982
- SELECT
1983
- COLUMN_NAME as column_name,
1984
- DATA_TYPE as data_type,
2141
+ SELECT
2142
+ COLUMN_NAME as column_name,
2143
+ DATA_TYPE as data_type,
1985
2144
  IS_NULLABLE as is_nullable,
1986
- COLUMN_DEFAULT as column_default
2145
+ COLUMN_DEFAULT as column_default,
2146
+ COLUMN_COMMENT as description
1987
2147
  FROM INFORMATION_SCHEMA.COLUMNS
1988
2148
  ${schemaClause}
1989
2149
  AND TABLE_NAME = ?
@@ -1991,24 +2151,57 @@ var MariaDBConnector = class _MariaDBConnector {
1991
2151
  `,
1992
2152
  queryParams
1993
2153
  );
1994
- return rows;
2154
+ return rows.map((row) => ({
2155
+ ...row,
2156
+ description: row.description || null
2157
+ }));
1995
2158
  } catch (error) {
1996
2159
  console.error("Error getting table schema:", error);
1997
2160
  throw error;
1998
2161
  }
1999
2162
  }
2000
- async getStoredProcedures(schema) {
2163
+ async getTableComment(tableName, schema) {
2164
+ if (!this.pool) {
2165
+ throw new Error("Not connected to database");
2166
+ }
2167
+ try {
2168
+ const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
2169
+ const queryParams = schema ? [schema, tableName] : [tableName];
2170
+ const rows = await this.pool.query(
2171
+ `
2172
+ SELECT TABLE_COMMENT
2173
+ FROM INFORMATION_SCHEMA.TABLES
2174
+ ${schemaClause}
2175
+ AND TABLE_NAME = ?
2176
+ `,
2177
+ queryParams
2178
+ );
2179
+ if (rows.length > 0) {
2180
+ return rows[0].TABLE_COMMENT || null;
2181
+ }
2182
+ return null;
2183
+ } catch (error) {
2184
+ return null;
2185
+ }
2186
+ }
2187
+ async getStoredProcedures(schema, routineType) {
2001
2188
  if (!this.pool) {
2002
2189
  throw new Error("Not connected to database");
2003
2190
  }
2004
2191
  try {
2005
2192
  const schemaClause = schema ? "WHERE ROUTINE_SCHEMA = ?" : "WHERE ROUTINE_SCHEMA = DATABASE()";
2006
2193
  const queryParams = schema ? [schema] : [];
2194
+ let typeFilter = "";
2195
+ if (routineType === "function") {
2196
+ typeFilter = " AND ROUTINE_TYPE = 'FUNCTION'";
2197
+ } else if (routineType === "procedure") {
2198
+ typeFilter = " AND ROUTINE_TYPE = 'PROCEDURE'";
2199
+ }
2007
2200
  const rows = await this.pool.query(
2008
2201
  `
2009
2202
  SELECT ROUTINE_NAME
2010
2203
  FROM INFORMATION_SCHEMA.ROUTINES
2011
- ${schemaClause}
2204
+ ${schemaClause}${typeFilter}
2012
2205
  ORDER BY ROUTINE_NAME
2013
2206
  `,
2014
2207
  queryParams
@@ -2389,7 +2582,7 @@ function createExecuteSqlToolHandler(sourceId) {
2389
2582
  // src/tools/search-objects.ts
2390
2583
  import { z as z2 } from "zod";
2391
2584
  var searchDatabaseObjectsSchema = {
2392
- object_type: z2.enum(["schema", "table", "column", "procedure", "index"]).describe("Object type to search"),
2585
+ object_type: z2.enum(["schema", "table", "column", "procedure", "function", "index"]).describe("Object type to search"),
2393
2586
  pattern: z2.string().optional().default("%").describe("LIKE pattern (% = any chars, _ = one char). Default: %"),
2394
2587
  schema: z2.string().optional().describe("Filter to schema"),
2395
2588
  table: z2.string().optional().describe("Filter to table (requires schema; column/index only)"),
@@ -2402,6 +2595,9 @@ function likePatternToRegex(pattern) {
2402
2595
  }
2403
2596
  async function getTableRowCount(connector, tableName, schemaName) {
2404
2597
  try {
2598
+ if (connector.getTableRowCount) {
2599
+ return await connector.getTableRowCount(tableName, schemaName);
2600
+ }
2405
2601
  const qualifiedTable = quoteQualifiedIdentifier(tableName, schemaName, connector.id);
2406
2602
  const countQuery = `SELECT COUNT(*) as count FROM ${qualifiedTable}`;
2407
2603
  const result = await connector.executeSQL(countQuery, { maxRows: 1 });
@@ -2413,6 +2609,16 @@ async function getTableRowCount(connector, tableName, schemaName) {
2413
2609
  }
2414
2610
  return null;
2415
2611
  }
2612
+ async function getTableComment(connector, tableName, schemaName) {
2613
+ try {
2614
+ if (connector.getTableComment) {
2615
+ return await connector.getTableComment(tableName, schemaName);
2616
+ }
2617
+ return null;
2618
+ } catch (error) {
2619
+ return null;
2620
+ }
2621
+ }
2416
2622
  async function searchSchemas(connector, pattern, detailLevel, limit) {
2417
2623
  const schemas = await connector.getSchemas();
2418
2624
  const regex = likePatternToRegex(pattern);
@@ -2463,11 +2669,13 @@ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit
2463
2669
  try {
2464
2670
  const columns = await connector.getTableSchema(tableName, schemaName);
2465
2671
  const rowCount = await getTableRowCount(connector, tableName, schemaName);
2672
+ const comment = await getTableComment(connector, tableName, schemaName);
2466
2673
  results.push({
2467
2674
  name: tableName,
2468
2675
  schema: schemaName,
2469
2676
  column_count: columns.length,
2470
- row_count: rowCount
2677
+ row_count: rowCount,
2678
+ ...comment ? { comment } : {}
2471
2679
  });
2472
2680
  } catch (error) {
2473
2681
  results.push({
@@ -2482,16 +2690,19 @@ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit
2482
2690
  const columns = await connector.getTableSchema(tableName, schemaName);
2483
2691
  const indexes = await connector.getTableIndexes(tableName, schemaName);
2484
2692
  const rowCount = await getTableRowCount(connector, tableName, schemaName);
2693
+ const comment = await getTableComment(connector, tableName, schemaName);
2485
2694
  results.push({
2486
2695
  name: tableName,
2487
2696
  schema: schemaName,
2488
2697
  column_count: columns.length,
2489
2698
  row_count: rowCount,
2699
+ ...comment ? { comment } : {},
2490
2700
  columns: columns.map((col) => ({
2491
2701
  name: col.column_name,
2492
2702
  type: col.data_type,
2493
2703
  nullable: col.is_nullable === "YES",
2494
- default: col.column_default
2704
+ default: col.column_default,
2705
+ ...col.description ? { description: col.description } : {}
2495
2706
  })),
2496
2707
  indexes: indexes.map((idx) => ({
2497
2708
  name: idx.index_name,
@@ -2553,7 +2764,8 @@ async function searchColumns(connector, pattern, schemaFilter, tableFilter, deta
2553
2764
  schema: schemaName,
2554
2765
  type: column.data_type,
2555
2766
  nullable: column.is_nullable === "YES",
2556
- default: column.column_default
2767
+ default: column.column_default,
2768
+ ...column.description ? { description: column.description } : {}
2557
2769
  });
2558
2770
  }
2559
2771
  }
@@ -2567,7 +2779,7 @@ async function searchColumns(connector, pattern, schemaFilter, tableFilter, deta
2567
2779
  }
2568
2780
  return results;
2569
2781
  }
2570
- async function searchProcedures(connector, pattern, schemaFilter, detailLevel, limit) {
2782
+ async function searchProcedures(connector, pattern, schemaFilter, detailLevel, limit, routineType) {
2571
2783
  const regex = likePatternToRegex(pattern);
2572
2784
  const results = [];
2573
2785
  let schemasToSearch;
@@ -2579,7 +2791,7 @@ async function searchProcedures(connector, pattern, schemaFilter, detailLevel, l
2579
2791
  for (const schemaName of schemasToSearch) {
2580
2792
  if (results.length >= limit) break;
2581
2793
  try {
2582
- const procedures = await connector.getStoredProcedures(schemaName);
2794
+ const procedures = await connector.getStoredProcedures(schemaName, routineType);
2583
2795
  const matched = procedures.filter((proc) => regex.test(proc));
2584
2796
  for (const procName of matched) {
2585
2797
  if (results.length >= limit) break;
@@ -2716,7 +2928,10 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
2716
2928
  results = await searchColumns(connector, pattern, schema, table, detail_level, limit);
2717
2929
  break;
2718
2930
  case "procedure":
2719
- results = await searchProcedures(connector, pattern, schema, detail_level, limit);
2931
+ results = await searchProcedures(connector, pattern, schema, detail_level, limit, "procedure");
2932
+ break;
2933
+ case "function":
2934
+ results = await searchProcedures(connector, pattern, schema, detail_level, limit, "function");
2720
2935
  break;
2721
2936
  case "index":
2722
2937
  results = await searchIndexes(connector, pattern, schema, table, detail_level, limit);
@@ -2836,7 +3051,7 @@ function getSearchObjectsMetadata(sourceId) {
2836
3051
  const isSingleSource = sourceIds.length === 1;
2837
3052
  const toolName = isSingleSource ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
2838
3053
  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`;
3054
+ 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
3055
  return {
2841
3056
  name: toolName,
2842
3057
  description,
@@ -3392,7 +3607,7 @@ See documentation for more details on configuring database connections.
3392
3607
  const sources = sourceConfigsData.sources;
3393
3608
  console.error(`Configuration source: ${sourceConfigsData.source}`);
3394
3609
  await connectorManager.connectWithSources(sources);
3395
- const { initializeToolRegistry } = await import("./registry-D77Y4CIA.js");
3610
+ const { initializeToolRegistry } = await import("./registry-7HJVUJCM.js");
3396
3611
  initializeToolRegistry({
3397
3612
  sources: sourceConfigsData.sources,
3398
3613
  tools: sourceConfigsData.tools
@@ -3430,9 +3645,6 @@ See documentation for more details on configuring database connections.
3430
3645
  app.use(express.json());
3431
3646
  app.use((req, res, next) => {
3432
3647
  const origin = req.headers.origin;
3433
- if (origin && !origin.startsWith("http://localhost") && !origin.startsWith("https://localhost")) {
3434
- return res.status(403).json({ error: "Forbidden origin" });
3435
- }
3436
3648
  res.header("Access-Control-Allow-Origin", origin || "http://localhost");
3437
3649
  res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
3438
3650
  res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
@@ -2,7 +2,7 @@ import {
2
2
  ToolRegistry,
3
3
  getToolRegistry,
4
4
  initializeToolRegistry
5
- } from "./chunk-IBBG4PSO.js";
5
+ } from "./chunk-YKDZH7G5.js";
6
6
  export {
7
7
  ToolRegistry,
8
8
  getToolRegistry,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebase/dbhub",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "mcpName": "io.github.bytebase/dbhub",
5
5
  "description": "Minimal, token-efficient Database MCP Server for PostgreSQL, MySQL, SQL Server, SQLite, MariaDB",
6
6
  "repository": {