@bytebase/dbhub 1.2.5 → 1.3.1

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/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  > [!NOTE]
2
- > Brought to you by [Bytebase](https://www.bytebase.com/), open-source database governance platform.
2
+ > If you need an enterprise-level database MCP server with built-in guardrails like approval flow, access control, data masking, and audit logging beyond what DBHub offers, check out [Bytebase](https://www.bytebase.com/).
3
3
 
4
4
  <p align="center">
5
5
  <a href="https://www.star-history.com/bytebase/dbhub">
@@ -24,17 +24,17 @@
24
24
  ```bash
25
25
  +------------------+ +--------------+ +------------------+
26
26
  | | | | | |
27
- | | | | | |
28
27
  | Claude Desktop +--->+ +--->+ PostgreSQL |
29
28
  | | | | | |
30
29
  | Claude Code +--->+ +--->+ SQL Server |
31
30
  | | | | | |
32
- | Cursor +--->+ DBHub +--->+ SQLite |
31
+ | Cursor +--->+ DBHub +--->+ Oracle |
33
32
  | | | | | |
34
- | VS Code +--->+ +--->+ MySQL |
33
+ | VS Code +--->+ +--->+ SQLite |
35
34
  | | | | | |
36
- | Copilot CLI +--->+ +--->+ MariaDB |
35
+ | Copilot CLI +--->+ +--->+ MySQL |
37
36
  | | | | | |
37
+ | | | +--->+ MariaDB |
38
38
  | | | | | |
39
39
  +------------------+ +--------------+ +------------------+
40
40
  MCP Clients MCP Server Databases
@@ -43,7 +43,7 @@
43
43
  DBHub is a minimal MCP server: token-efficient, zero-dependency, and just two tools by default with opt-in extras. This lightweight gateway allows MCP-compatible clients to connect to and explore different databases:
44
44
 
45
45
  - **Minimal**: Zero dependency, token efficient with a minimal set of MCP tools to maximize context window
46
- - **Multi-Database**: PostgreSQL, MySQL, MariaDB, SQL Server, and SQLite through a single interface
46
+ - **Multi-Database**: PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, and SQLite through a single interface
47
47
  - **Multi-Connection**: Connect to multiple databases simultaneously with TOML configuration
48
48
  - **Guardrails**: Read-only mode, row limiting, and query timeout to prevent runaway operations
49
49
  - **Secure Access**: SSH tunneling and SSL/TLS encryption
@@ -69,7 +69,7 @@ DBHub loads just 2 tools by default at **1.4k tokens** — 13-14x fewer than alt
69
69
 
70
70
  ## Supported Databases
71
71
 
72
- PostgreSQL, MySQL, SQL Server, MariaDB, and SQLite.
72
+ PostgreSQL, MySQL, SQL Server, MariaDB, Oracle, and SQLite.
73
73
 
74
74
  ## MCP Tools
75
75
 
@@ -6,9 +6,10 @@ import {
6
6
  SafeURL,
7
7
  getDatabaseTypeFromDSN,
8
8
  getDefaultPortForType,
9
+ obfuscateDSNPassword,
9
10
  parseConnectionInfoFromDSN,
10
11
  stripCommentsAndStrings
11
- } from "./chunk-MF6LSVWK.js";
12
+ } from "./chunk-IXC4NQGC.js";
12
13
 
13
14
  // src/tools/builtin-tools.ts
14
15
  var BUILTIN_TOOL_EXECUTE_SQL = "execute_sql";
@@ -660,7 +661,7 @@ function buildDSNFromEnvParams() {
660
661
  return null;
661
662
  }
662
663
  }
663
- const supportedTypes = ["postgres", "postgresql", "mysql", "mariadb", "sqlserver", "sqlite"];
664
+ const supportedTypes = ["postgres", "postgresql", "mysql", "mariadb", "sqlserver", "sqlite", "oracle"];
664
665
  if (!supportedTypes.includes(dbType.toLowerCase())) {
665
666
  throw new Error(`Unsupported DB_TYPE: ${dbType}. Supported types: ${supportedTypes.join(", ")}`);
666
667
  }
@@ -678,6 +679,9 @@ function buildDSNFromEnvParams() {
678
679
  case "sqlserver":
679
680
  port = "1433";
680
681
  break;
682
+ case "oracle":
683
+ port = "1521";
684
+ break;
681
685
  case "sqlite":
682
686
  return {
683
687
  dsn: `sqlite:///${dbName}`,
@@ -821,15 +825,7 @@ function splitTokenList(value) {
821
825
  return value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
822
826
  }
823
827
  function redactDSN(dsn) {
824
- try {
825
- const url = new URL(dsn);
826
- if (url.password) {
827
- url.password = "*******";
828
- }
829
- return url.toString();
830
- } catch (error) {
831
- return dsn.replace(/\/\/([^:]+):([^@]+)@/, "//$1:***@");
832
- }
828
+ return obfuscateDSNPassword(dsn);
833
829
  }
834
830
  function resolveId() {
835
831
  const args = parseCommandLineArgs();
@@ -976,7 +972,7 @@ async function resolveSourceConfigs() {
976
972
  dsnUrl = new SafeURL(dsnResult.dsn);
977
973
  } catch (error) {
978
974
  throw new Error(
979
- `Invalid DSN format: ${dsnResult.dsn}. Expected format: protocol://[user[:password]@]host[:port]/database`
975
+ `Invalid DSN format: ${redactDSN(dsnResult.dsn)}. Expected format: protocol://[user[:password]@]host[:port]/database`
980
976
  );
981
977
  }
982
978
  const protocol = dsnUrl.protocol.replace(":", "");
@@ -991,6 +987,8 @@ async function resolveSourceConfigs() {
991
987
  dbType = "sqlserver";
992
988
  } else if (protocol === "sqlite") {
993
989
  dbType = "sqlite";
990
+ } else if (protocol === "oracle") {
991
+ dbType = "oracle";
994
992
  } else {
995
993
  throw new Error(`Unsupported database type in DSN: ${protocol}`);
996
994
  }
@@ -1288,7 +1286,7 @@ function validateSourceConfig(source, configPath) {
1288
1286
  );
1289
1287
  }
1290
1288
  if (source.type) {
1291
- const validTypes = ["postgres", "mysql", "mariadb", "sqlserver", "sqlite"];
1289
+ const validTypes = ["postgres", "mysql", "mariadb", "sqlserver", "sqlite", "oracle"];
1292
1290
  if (!validTypes.includes(source.type)) {
1293
1291
  throw new Error(
1294
1292
  `Configuration file ${configPath}: source '${source.id}' has invalid type '${source.type}'. Valid types: ${validTypes.join(", ")}`
@@ -1346,6 +1344,18 @@ function validateSourceConfig(source, configPath) {
1346
1344
  );
1347
1345
  }
1348
1346
  }
1347
+ if (source.pool_max_connections !== void 0) {
1348
+ if (source.type !== "postgres") {
1349
+ throw new Error(
1350
+ `Configuration file ${configPath}: source '${source.id}' has 'pool_max_connections' but it is only supported for PostgreSQL sources.`
1351
+ );
1352
+ }
1353
+ if (!Number.isInteger(source.pool_max_connections) || source.pool_max_connections < 1 || source.pool_max_connections > 1e3) {
1354
+ throw new Error(
1355
+ `Configuration file ${configPath}: source '${source.id}' has invalid pool_max_connections. Must be an integer between 1 and 1000.`
1356
+ );
1357
+ }
1358
+ }
1349
1359
  if (source.ssh_port !== void 0) {
1350
1360
  if (typeof source.ssh_port !== "number" || source.ssh_port <= 0 || source.ssh_port > 65535) {
1351
1361
  throw new Error(
@@ -1365,9 +1375,14 @@ function validateSourceConfig(source, configPath) {
1365
1375
  `Configuration file ${configPath}: source '${source.id}' has invalid sslmode '${source.sslmode}'. Valid values: ${validSslModes.join(", ")}`
1366
1376
  );
1367
1377
  }
1368
- if ((source.sslmode === "verify-ca" || source.sslmode === "verify-full") && source.type !== "postgres") {
1378
+ const verifyModesByType = {
1379
+ postgres: ["verify-ca", "verify-full"],
1380
+ oracle: ["verify-full"]
1381
+ };
1382
+ if ((source.sslmode === "verify-ca" || source.sslmode === "verify-full") && !(verifyModesByType[source.type] ?? []).includes(source.sslmode)) {
1383
+ const supported = ["disable", "require", ...verifyModesByType[source.type] ?? []];
1369
1384
  throw new Error(
1370
- `Configuration file ${configPath}: source '${source.id}' has sslmode '${source.sslmode}' which is only supported for PostgreSQL. Valid values for ${source.type}: disable, require`
1385
+ `Configuration file ${configPath}: source '${source.id}' has sslmode '${source.sslmode}' which is not supported for ${source.type}. Valid values for ${source.type}: ${supported.join(", ")}`
1371
1386
  );
1372
1387
  }
1373
1388
  }
@@ -1375,6 +1390,11 @@ function validateSourceConfig(source, configPath) {
1375
1390
  validateDSNFieldConflicts(source, configPath);
1376
1391
  }
1377
1392
  if (source.sslrootcert !== void 0) {
1393
+ if (source.type !== "postgres") {
1394
+ throw new Error(
1395
+ `Configuration file ${configPath}: source '${source.id}' has sslrootcert but it is only supported for PostgreSQL.`
1396
+ );
1397
+ }
1378
1398
  if (source.sslmode !== "verify-ca" && source.sslmode !== "verify-full") {
1379
1399
  throw new Error(
1380
1400
  `Configuration file ${configPath}: source '${source.id}' has sslrootcert but sslmode is '${source.sslmode ?? "not set"}'. sslrootcert requires sslmode 'verify-ca' or 'verify-full'`
@@ -1709,10 +1729,8 @@ async function generateRdsAuthToken(params) {
1709
1729
  username: params.username,
1710
1730
  region: params.region
1711
1731
  };
1712
- if (params.profile) {
1713
- const { fromIni } = await import("@aws-sdk/credential-providers");
1714
- signerConfig.credentials = fromIni({ profile: params.profile });
1715
- }
1732
+ const { fromIni, fromNodeProviderChain } = await import("@aws-sdk/credential-providers");
1733
+ signerConfig.credentials = params.profile ? fromIni({ profile: params.profile, ignoreCache: true }) : fromNodeProviderChain({ ignoreCache: true });
1716
1734
  const signer = new Signer(signerConfig);
1717
1735
  return signer.getAuthToken();
1718
1736
  }
@@ -1727,11 +1745,17 @@ var NETWORK_CODES = /* @__PURE__ */ new Set([
1727
1745
  "ENETUNREACH",
1728
1746
  "ECONNRESET"
1729
1747
  ]);
1748
+ var NETWORK_CODES_BY_TYPE = {
1749
+ // NJS-503: "connection to host ... could not be established" (Thin mode)
1750
+ oracle: /* @__PURE__ */ new Set(["NJS-503"])
1751
+ };
1730
1752
  var AUTH_CODES = {
1731
1753
  postgres: ["28P01", "28000"],
1732
1754
  mysql: ["ER_ACCESS_DENIED_ERROR", 1045, 1698],
1733
1755
  mariadb: ["ER_ACCESS_DENIED_ERROR", 1045, 1698],
1734
1756
  sqlserver: ["ELOGIN"],
1757
+ // ORA-01017: invalid username/password; ORA-28000: account locked
1758
+ oracle: ["ORA-01017", "ORA-28000"],
1735
1759
  sqlite: []
1736
1760
  // no network/auth layer
1737
1761
  };
@@ -1753,7 +1777,7 @@ function classifyConnectionError(error, connectorType, sourceId) {
1753
1777
  return { code: "TUNNEL_FAILED", message: tunnelMessage(sourceId) };
1754
1778
  }
1755
1779
  const code = err.code;
1756
- if (typeof code === "string" && NETWORK_CODES.has(code)) {
1780
+ if (typeof code === "string" && (NETWORK_CODES.has(code) || NETWORK_CODES_BY_TYPE[connectorType]?.has(code))) {
1757
1781
  return { code: "SOURCE_UNREACHABLE", message: unreachableMessage(sourceId) };
1758
1782
  }
1759
1783
  const authCodes = AUTH_CODES[connectorType];
@@ -1951,6 +1975,9 @@ var ConnectorManager = class {
1951
1975
  if (source.query_timeout !== void 0 && connector.id !== "sqlite") {
1952
1976
  config.queryTimeoutSeconds = source.query_timeout;
1953
1977
  }
1978
+ if (source.pool_max_connections !== void 0) {
1979
+ config.poolMaxConnections = source.pool_max_connections;
1980
+ }
1954
1981
  if (source.search_path) {
1955
1982
  config.searchPath = source.search_path;
1956
1983
  }
@@ -2246,24 +2273,29 @@ var PARAMETER_STYLES = {
2246
2273
  // ?, ?, ?
2247
2274
  sqlserver: "named",
2248
2275
  // @p1, @p2, @p3
2249
- sqlite: "positional"
2276
+ sqlite: "positional",
2250
2277
  // ?, ?, ?
2278
+ oracle: "colon"
2279
+ // :1, :2, :3
2251
2280
  };
2252
- function detectParameterStyle(statement) {
2253
- const cleanedSQL = stripCommentsAndStrings(statement);
2281
+ function detectParameterStyle(statement, connectorType) {
2282
+ const cleanedSQL = stripCommentsAndStrings(statement, connectorType);
2254
2283
  if (/\$\d+/.test(cleanedSQL)) {
2255
2284
  return "numbered";
2256
2285
  }
2257
2286
  if (/@p\d+/.test(cleanedSQL)) {
2258
2287
  return "named";
2259
2288
  }
2289
+ if (/(?<!:):\d+/.test(cleanedSQL)) {
2290
+ return "colon";
2291
+ }
2260
2292
  if (/\?/.test(cleanedSQL)) {
2261
2293
  return "positional";
2262
2294
  }
2263
2295
  return "none";
2264
2296
  }
2265
2297
  function validateParameterStyle(statement, connectorType) {
2266
- const detectedStyle = detectParameterStyle(statement);
2298
+ const detectedStyle = detectParameterStyle(statement, connectorType);
2267
2299
  const expectedStyle = PARAMETER_STYLES[connectorType];
2268
2300
  if (detectedStyle === "none") {
2269
2301
  return;
@@ -2272,57 +2304,53 @@ function validateParameterStyle(statement, connectorType) {
2272
2304
  const examples = {
2273
2305
  numbered: "$1, $2, $3",
2274
2306
  positional: "?, ?, ?",
2275
- named: "@p1, @p2, @p3"
2307
+ named: "@p1, @p2, @p3",
2308
+ colon: ":1, :2, :3"
2276
2309
  };
2277
2310
  throw new Error(
2278
2311
  `Invalid parameter syntax for ${connectorType}. Expected ${expectedStyle} style (${examples[expectedStyle]}), but found ${detectedStyle} style in statement.`
2279
2312
  );
2280
2313
  }
2281
2314
  }
2282
- function countParameters(statement) {
2283
- const style = detectParameterStyle(statement);
2284
- const cleanedSQL = stripCommentsAndStrings(statement);
2315
+ function countParameters(statement, connectorType) {
2316
+ const style = detectParameterStyle(statement, connectorType);
2317
+ const cleanedSQL = stripCommentsAndStrings(statement, connectorType);
2285
2318
  switch (style) {
2286
- case "numbered": {
2287
- const matches = cleanedSQL.match(/\$\d+/g);
2288
- if (!matches) return 0;
2289
- const numbers = matches.map((m) => parseInt(m.slice(1), 10));
2290
- const uniqueIndices = Array.from(new Set(numbers)).sort((a, b) => a - b);
2291
- const maxIndex = Math.max(...uniqueIndices);
2292
- for (let i = 1; i <= maxIndex; i++) {
2293
- if (!uniqueIndices.includes(i)) {
2294
- throw new Error(
2295
- `Non-sequential numbered parameters detected. Found placeholders: ${uniqueIndices.map((n) => `$${n}`).join(", ")}. Parameters must be sequential starting from $1 (missing $${i}).`
2296
- );
2297
- }
2298
- }
2299
- return maxIndex;
2300
- }
2301
- case "named": {
2302
- const matches = cleanedSQL.match(/@p\d+/g);
2303
- if (!matches) return 0;
2304
- const numbers = matches.map((m) => parseInt(m.slice(2), 10));
2305
- const uniqueIndices = Array.from(new Set(numbers)).sort((a, b) => a - b);
2306
- const maxIndex = Math.max(...uniqueIndices);
2307
- for (let i = 1; i <= maxIndex; i++) {
2308
- if (!uniqueIndices.includes(i)) {
2309
- throw new Error(
2310
- `Non-sequential named parameters detected. Found placeholders: ${uniqueIndices.map((n) => `@p${n}`).join(", ")}. Parameters must be sequential starting from @p1 (missing @p${i}).`
2311
- );
2312
- }
2313
- }
2314
- return maxIndex;
2315
- }
2316
- case "positional": {
2319
+ case "numbered":
2320
+ return countIndexedParameters(cleanedSQL, /\$\d+/g, "$", "numbered");
2321
+ case "named":
2322
+ return countIndexedParameters(cleanedSQL, /@p\d+/g, "@p", "named");
2323
+ case "colon":
2324
+ return countIndexedParameters(cleanedSQL, /(?<!:):\d+/g, ":", "colon-numbered");
2325
+ case "positional":
2317
2326
  return (cleanedSQL.match(/\?/g) || []).length;
2318
- }
2319
2327
  default:
2320
2328
  return 0;
2321
2329
  }
2322
2330
  }
2331
+ function countIndexedParameters(cleanedSQL, pattern, sigil, styleName) {
2332
+ const matches = cleanedSQL.match(pattern);
2333
+ if (!matches) return 0;
2334
+ const indices = new Set(matches.map((m) => parseInt(m.slice(sigil.length), 10)));
2335
+ if (indices.has(0)) {
2336
+ throw new Error(
2337
+ `Invalid ${styleName} parameter ${sigil}0. Parameters must be sequential starting from ${sigil}1.`
2338
+ );
2339
+ }
2340
+ const maxIndex = Math.max(...indices);
2341
+ for (let i = 1; i <= maxIndex; i++) {
2342
+ if (!indices.has(i)) {
2343
+ const found = Array.from(indices).sort((a, b) => a - b).map((n) => `${sigil}${n}`).join(", ");
2344
+ throw new Error(
2345
+ `Non-sequential ${styleName} parameters detected. Found placeholders: ${found}. Parameters must be sequential starting from ${sigil}1 (missing ${sigil}${i}).`
2346
+ );
2347
+ }
2348
+ }
2349
+ return maxIndex;
2350
+ }
2323
2351
  function validateParameters(statement, parameters, connectorType) {
2324
2352
  validateParameterStyle(statement, connectorType);
2325
- const paramCount = countParameters(statement);
2353
+ const paramCount = countParameters(statement, connectorType);
2326
2354
  const definedCount = parameters?.length || 0;
2327
2355
  if (paramCount !== definedCount) {
2328
2356
  throw new Error(
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-FU2ZJE4E.js";
5
5
  import {
6
6
  obfuscateDSNPassword
7
- } from "./chunk-MF6LSVWK.js";
7
+ } from "./chunk-IXC4NQGC.js";
8
8
 
9
9
  // src/connectors/mysql-family-health-check.ts
10
10
  async function getMySQLFamilyHealthCheck(query) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  stripCommentsAndStrings
3
- } from "./chunk-MF6LSVWK.js";
3
+ } from "./chunk-IXC4NQGC.js";
4
4
 
5
5
  // src/utils/allowed-keywords.ts
6
6
  var allowedKeywords = {
@@ -10,7 +10,12 @@ var allowedKeywords = {
10
10
  sqlite: ["select", "with", "explain", "pragma"],
11
11
  // SQL Server has no native EXPLAIN statement; the connector translates a
12
12
  // leading `EXPLAIN` into a SET SHOWPLAN_XML request (see SQLServerConnector).
13
- sqlserver: ["select", "with", "explain"]
13
+ sqlserver: ["select", "with", "explain"],
14
+ // Oracle's native form is `EXPLAIN PLAN FOR <stmt>`; the connector accepts
15
+ // both that and a bare `EXPLAIN <stmt>` and reads the plan back through
16
+ // DBMS_XPLAN (see OracleConnector.explainQuery). EXPLAIN PLAN only parses
17
+ // the statement, it never executes it.
18
+ oracle: ["select", "with", "explain"]
14
19
  };
15
20
  var mutatingKeywords = [
16
21
  "insert",
@@ -56,12 +61,35 @@ var escapeHatchFunctionKeywords = {
56
61
  mysql: ["load_file", "get_lock", "release_lock", "release_all_locks"],
57
62
  mariadb: ["load_file", "get_lock", "release_lock", "release_all_locks"],
58
63
  postgres: ["pg_read_file", "pg_read_binary_file", "pg_ls_dir"],
59
- sqlserver: sqlServerPassThroughKeywords
64
+ sqlserver: sqlServerPassThroughKeywords,
65
+ // Oracle packages a SELECT can call to reach outside the database, matched
66
+ // as `package.member(` (see escapeHatchCallSuffix).
67
+ oracle: [
68
+ "utl_http",
69
+ "utl_tcp",
70
+ "utl_smtp",
71
+ "utl_inaddr",
72
+ "utl_file",
73
+ "dbms_ldap",
74
+ "dbms_sql",
75
+ "dbms_scheduler",
76
+ "dbms_java",
77
+ "dbms_lob",
78
+ "dbms_xslprocessor",
79
+ "dbms_advisor"
80
+ ]
60
81
  };
82
+ var escapeHatchCallSuffix = {
83
+ oracle: "\\s*\\.\\s*[a-z_][a-z0-9_$#]*\\s*\\("
84
+ };
85
+ var DEFAULT_CALL_SUFFIX = "\\s*\\(";
61
86
  var escapeHatchFunctionPatterns = Object.fromEntries(
62
87
  Object.entries(escapeHatchFunctionKeywords).map(([type, keywords]) => [
63
88
  type,
64
- new RegExp(`\\b(?:${keywords.join("|")})\\s*\\(`, "i")
89
+ new RegExp(
90
+ `\\b(?:${keywords.join("|")})${escapeHatchCallSuffix[type] ?? DEFAULT_CALL_SUFFIX}`,
91
+ "i"
92
+ )
65
93
  ])
66
94
  );
67
95
  function hasEscapeHatchFunction(strippedSQL, connectorType) {
@@ -77,8 +105,12 @@ var mutatingPatterns = {
77
105
  mysql: mutatingPatternWithReplace,
78
106
  mariadb: mutatingPatternWithReplace,
79
107
  sqlite: mutatingPatternWithReplace,
80
- sqlserver: mutatingPatternSqlServer
108
+ sqlserver: mutatingPatternSqlServer,
109
+ oracle: mutatingPattern
81
110
  };
111
+ function hasMutatingKeyword(strippedSQL, connectorType) {
112
+ return (mutatingPatterns[connectorType] ?? mutatingPattern).test(strippedSQL);
113
+ }
82
114
  var selectIntoPattern = /\bselect\b[\s\S]+\binto\b/i;
83
115
  var sqliteReadOnlyArgPragmas = /* @__PURE__ */ new Set([
84
116
  "table_info",
@@ -110,11 +142,8 @@ function checkReadOnly(cleanedSQL, connectorType) {
110
142
  if (hasEscapeHatchFunction(cleanedSQL, connectorType)) {
111
143
  return false;
112
144
  }
113
- if (firstWord === "with") {
114
- const pattern = mutatingPatterns[connectorType] ?? mutatingPattern;
115
- if (pattern.test(cleanedSQL)) {
116
- return false;
117
- }
145
+ if (firstWord === "with" && hasMutatingKeyword(cleanedSQL, connectorType)) {
146
+ return false;
118
147
  }
119
148
  if (firstWord === "pragma" && connectorType === "sqlite") {
120
149
  if (cleanedSQL.includes("=")) {
@@ -147,6 +176,7 @@ export {
147
176
  sqlServerPassThroughKeywords,
148
177
  sqlServerPassThroughPattern,
149
178
  hasEscapeHatchFunction,
179
+ hasMutatingKeyword,
150
180
  isReadOnlySQL,
151
181
  getFirstKeyword
152
182
  };
@@ -188,6 +188,7 @@ function parseConnectionInfoFromDSN(dsn) {
188
188
  return null;
189
189
  }
190
190
  }
191
+ var REDACTED_DSN = "<redacted DSN>";
191
192
  function obfuscateDSNPassword(dsn) {
192
193
  if (!dsn) {
193
194
  return dsn;
@@ -198,6 +199,9 @@ function obfuscateDSNPassword(dsn) {
198
199
  return dsn;
199
200
  }
200
201
  const url = new SafeURL(dsn);
202
+ if (url.port && !/^\d+$/.test(url.port)) {
203
+ return REDACTED_DSN;
204
+ }
201
205
  if (!url.password) {
202
206
  return dsn;
203
207
  }
@@ -222,7 +226,7 @@ function obfuscateDSNPassword(dsn) {
222
226
  }
223
227
  return result;
224
228
  } catch {
225
- return dsn;
229
+ return REDACTED_DSN;
226
230
  }
227
231
  }
228
232
  function getDatabaseTypeFromDSN(dsn) {
@@ -239,7 +243,8 @@ function protocolToConnectorType(protocol) {
239
243
  "mysql": "mysql",
240
244
  "mariadb": "mariadb",
241
245
  "sqlserver": "sqlserver",
242
- "sqlite": "sqlite"
246
+ "sqlite": "sqlite",
247
+ "oracle": "oracle"
243
248
  };
244
249
  return mapping[protocol];
245
250
  }
@@ -249,6 +254,7 @@ function getDefaultPortForType(type) {
249
254
  "mysql": 3306,
250
255
  "mariadb": 3306,
251
256
  "sqlserver": 1433,
257
+ "oracle": 1521,
252
258
  "sqlite": void 0
253
259
  };
254
260
  return ports[type];
@@ -429,12 +435,30 @@ function scanTokenSQLite(sql, i) {
429
435
  function scanTokenSQLServer(sql, i) {
430
436
  return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBracketQuotedIdentifier(sql, i) ?? plainToken(i);
431
437
  }
438
+ function scanOracleAlternativeQuotedString(sql, i) {
439
+ if (sql[i] !== "q" && sql[i] !== "Q" || sql[i + 1] !== "'") {
440
+ return null;
441
+ }
442
+ const open = sql[i + 2];
443
+ if (open === void 0 || open === " " || open === " " || open === "\n") {
444
+ return null;
445
+ }
446
+ const mirrors = { "(": ")", "[": "]", "{": "}", "<": ">" };
447
+ const close = (mirrors[open] ?? open) + "'";
448
+ const closeIdx = sql.indexOf(close, i + 3);
449
+ const end = closeIdx !== -1 ? closeIdx + close.length : sql.length;
450
+ return { type: TokenType.QuotedBlock, end };
451
+ }
452
+ function scanTokenOracle(sql, i) {
453
+ return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanOracleAlternativeQuotedString(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? plainToken(i);
454
+ }
432
455
  var dialectScanners = {
433
456
  postgres: scanTokenPostgres,
434
457
  mysql: scanTokenMySQL,
435
458
  mariadb: scanTokenMySQL,
436
459
  sqlite: scanTokenSQLite,
437
- sqlserver: scanTokenSQLServer
460
+ sqlserver: scanTokenSQLServer,
461
+ oracle: scanTokenOracle
438
462
  };
439
463
  function getScanner(dialect) {
440
464
  return dialect ? dialectScanners[dialect] ?? scanTokenAnsi : scanTokenAnsi;
@@ -479,6 +503,7 @@ function blankCommentsAndStrings(sql, dialect) {
479
503
  }
480
504
  return result;
481
505
  }
506
+ var LEADING_SQL_NOISE = /^(?:\s+|--[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)*/;
482
507
  function splitSQLStatements(sql, dialect) {
483
508
  const scanToken = getScanner(dialect);
484
509
  const statements = [];
@@ -513,5 +538,6 @@ export {
513
538
  getDefaultPortForType,
514
539
  stripCommentsAndStrings,
515
540
  blankCommentsAndStrings,
541
+ LEADING_SQL_NOISE,
516
542
  splitSQLStatements
517
543
  };
@@ -9,6 +9,7 @@ function quoteIdentifier(identifier, dbType) {
9
9
  switch (dbType) {
10
10
  case "postgres":
11
11
  case "sqlite":
12
+ case "oracle":
12
13
  return `"${identifier.replace(/"/g, '""')}"`;
13
14
  case "mysql":
14
15
  case "mariadb":