@bytebase/dbhub 1.2.4 → 1.3.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/README.md +7 -7
- package/dist/{chunk-A4TSDCPY.js → chunk-6SZKW6GD.js} +1 -1
- package/dist/{chunk-F5TPYAVO.js → chunk-77BJNE6E.js} +121 -63
- package/dist/{chunk-IQRBLAB4.js → chunk-ABTVMHD6.js} +40 -10
- package/dist/{chunk-MF6LSVWK.js → chunk-IXC4NQGC.js} +29 -3
- package/dist/{chunk-JFWX35TB.js → chunk-X2EC47W4.js} +1 -0
- package/dist/chunk-YIGJHEU6.js +343 -0
- package/dist/index.js +16 -15
- package/dist/{mariadb-EVJ5HLYH.js → mariadb-L7Z55ROY.js} +6 -5
- package/dist/{mysql-GXDUNLJ6.js → mysql-XXIHWQGR.js} +6 -5
- package/dist/oracle-OUKBU52X.js +754 -0
- package/dist/{postgres-M3W3Y6F5.js → postgres-6XJ7V4J4.js} +13 -6
- package/dist/public/assets/{index-BdBaFSHd.js → index-BRlIJaqn.js} +27 -27
- package/dist/public/index.html +1 -1
- package/dist/{registry-6ABK3M6V.js → registry-P4IGJ56H.js} +2 -2
- package/dist/{sqlite-ASYMU5BI.js → sqlite-AO2BKIC6.js} +22 -10
- package/dist/{sqlserver-WNVSBFJ3.js → sqlserver-AJUK6L55.js} +10 -18
- package/package.json +5 -2
- package/dist/chunk-6QETXS44.js +0 -254
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
> [!NOTE]
|
|
2
|
-
>
|
|
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 +--->+
|
|
31
|
+
| Cursor +--->+ DBHub +--->+ Oracle |
|
|
33
32
|
| | | | | |
|
|
34
|
-
| VS Code +--->+ +--->+
|
|
33
|
+
| VS Code +--->+ +--->+ SQLite |
|
|
35
34
|
| | | | | |
|
|
36
|
-
| Copilot CLI +--->+ +--->+
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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'`
|
|
@@ -1727,11 +1747,17 @@ var NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
|
1727
1747
|
"ENETUNREACH",
|
|
1728
1748
|
"ECONNRESET"
|
|
1729
1749
|
]);
|
|
1750
|
+
var NETWORK_CODES_BY_TYPE = {
|
|
1751
|
+
// NJS-503: "connection to host ... could not be established" (Thin mode)
|
|
1752
|
+
oracle: /* @__PURE__ */ new Set(["NJS-503"])
|
|
1753
|
+
};
|
|
1730
1754
|
var AUTH_CODES = {
|
|
1731
1755
|
postgres: ["28P01", "28000"],
|
|
1732
1756
|
mysql: ["ER_ACCESS_DENIED_ERROR", 1045, 1698],
|
|
1733
1757
|
mariadb: ["ER_ACCESS_DENIED_ERROR", 1045, 1698],
|
|
1734
1758
|
sqlserver: ["ELOGIN"],
|
|
1759
|
+
// ORA-01017: invalid username/password; ORA-28000: account locked
|
|
1760
|
+
oracle: ["ORA-01017", "ORA-28000"],
|
|
1735
1761
|
sqlite: []
|
|
1736
1762
|
// no network/auth layer
|
|
1737
1763
|
};
|
|
@@ -1753,7 +1779,7 @@ function classifyConnectionError(error, connectorType, sourceId) {
|
|
|
1753
1779
|
return { code: "TUNNEL_FAILED", message: tunnelMessage(sourceId) };
|
|
1754
1780
|
}
|
|
1755
1781
|
const code = err.code;
|
|
1756
|
-
if (typeof code === "string" && NETWORK_CODES.has(code)) {
|
|
1782
|
+
if (typeof code === "string" && (NETWORK_CODES.has(code) || NETWORK_CODES_BY_TYPE[connectorType]?.has(code))) {
|
|
1757
1783
|
return { code: "SOURCE_UNREACHABLE", message: unreachableMessage(sourceId) };
|
|
1758
1784
|
}
|
|
1759
1785
|
const authCodes = AUTH_CODES[connectorType];
|
|
@@ -1832,7 +1858,7 @@ var ConnectorManager = class {
|
|
|
1832
1858
|
if (!lazySource) {
|
|
1833
1859
|
if (sourceId) {
|
|
1834
1860
|
throw new Error(
|
|
1835
|
-
`Source '${sourceId}' not found. Available sources: ${this.
|
|
1861
|
+
`Source '${sourceId}' not found. Available sources: ${this.getAvailableSourceIds().join(", ")}`
|
|
1836
1862
|
);
|
|
1837
1863
|
} else {
|
|
1838
1864
|
throw new Error("No sources configured. Call connectWithSources() first.");
|
|
@@ -1871,6 +1897,7 @@ var ConnectorManager = class {
|
|
|
1871
1897
|
const dsn = await this.buildConnectionDSN(source);
|
|
1872
1898
|
console.error(` - ${sourceId}: ${redactDSN(dsn)}`);
|
|
1873
1899
|
let actualDSN = dsn;
|
|
1900
|
+
let tunnel;
|
|
1874
1901
|
if (source.ssh_host) {
|
|
1875
1902
|
const sshConfigPath = getDefaultSSHConfigPath();
|
|
1876
1903
|
let resolvedSSHConfig = null;
|
|
@@ -1914,7 +1941,7 @@ var ConnectorManager = class {
|
|
|
1914
1941
|
const url = new URL(dsn);
|
|
1915
1942
|
const targetHost = url.hostname;
|
|
1916
1943
|
const targetPort = parseInt(url.port) || this.getDefaultPort(dsn);
|
|
1917
|
-
|
|
1944
|
+
tunnel = new SSHTunnel();
|
|
1918
1945
|
let tunnelInfo;
|
|
1919
1946
|
try {
|
|
1920
1947
|
tunnelInfo = await tunnel.establish(sshConfig, {
|
|
@@ -1950,6 +1977,9 @@ var ConnectorManager = class {
|
|
|
1950
1977
|
if (source.query_timeout !== void 0 && connector.id !== "sqlite") {
|
|
1951
1978
|
config.queryTimeoutSeconds = source.query_timeout;
|
|
1952
1979
|
}
|
|
1980
|
+
if (source.pool_max_connections !== void 0) {
|
|
1981
|
+
config.poolMaxConnections = source.pool_max_connections;
|
|
1982
|
+
}
|
|
1953
1983
|
if (source.search_path) {
|
|
1954
1984
|
config.searchPath = source.search_path;
|
|
1955
1985
|
}
|
|
@@ -1962,7 +1992,19 @@ var ConnectorManager = class {
|
|
|
1962
1992
|
if (source.collation) {
|
|
1963
1993
|
config.collation = source.collation;
|
|
1964
1994
|
}
|
|
1965
|
-
|
|
1995
|
+
try {
|
|
1996
|
+
await connector.connect(actualDSN, source.init_script, config);
|
|
1997
|
+
} catch (error) {
|
|
1998
|
+
if (tunnel) {
|
|
1999
|
+
this.sshTunnels.delete(sourceId);
|
|
2000
|
+
try {
|
|
2001
|
+
await tunnel.close();
|
|
2002
|
+
} catch (closeError) {
|
|
2003
|
+
console.error(`Error closing SSH tunnel for source '${sourceId}':`, closeError);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
throw error;
|
|
2007
|
+
}
|
|
1966
2008
|
this.connectors.set(sourceId, connector);
|
|
1967
2009
|
if (!this.sourceIds.includes(sourceId)) {
|
|
1968
2010
|
this.sourceIds.push(sourceId);
|
|
@@ -2012,7 +2054,7 @@ var ConnectorManager = class {
|
|
|
2012
2054
|
if (!connector) {
|
|
2013
2055
|
if (sourceId) {
|
|
2014
2056
|
throw new Error(
|
|
2015
|
-
`Source '${sourceId}' not found. Available sources: ${this.
|
|
2057
|
+
`Source '${sourceId}' not found. Available sources: ${this.getAvailableSourceIds().join(", ")}`
|
|
2016
2058
|
);
|
|
2017
2059
|
} else {
|
|
2018
2060
|
throw new Error("No sources connected. Call connectWithSources() first.");
|
|
@@ -2049,6 +2091,14 @@ var ConnectorManager = class {
|
|
|
2049
2091
|
getSourceIds() {
|
|
2050
2092
|
return [...this.sourceIds];
|
|
2051
2093
|
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Source IDs that can actually serve a request: connected, or registered for
|
|
2096
|
+
* lazy (re)connection on first use. Used for error messages so a source that
|
|
2097
|
+
* is neither is not reported as available.
|
|
2098
|
+
*/
|
|
2099
|
+
getAvailableSourceIds() {
|
|
2100
|
+
return this.sourceIds.filter((id) => this.connectors.has(id) || this.lazySources.has(id));
|
|
2101
|
+
}
|
|
2052
2102
|
/** Get all available source IDs */
|
|
2053
2103
|
static getAvailableSourceIds() {
|
|
2054
2104
|
if (!managerInstance) {
|
|
@@ -2126,7 +2176,7 @@ var ConnectorManager = class {
|
|
|
2126
2176
|
error
|
|
2127
2177
|
);
|
|
2128
2178
|
} finally {
|
|
2129
|
-
if (!this.isDisconnecting && this.
|
|
2179
|
+
if (!this.isDisconnecting && this.connectors.has(sourceId)) {
|
|
2130
2180
|
this.scheduleIamRefresh(source);
|
|
2131
2181
|
}
|
|
2132
2182
|
}
|
|
@@ -2153,7 +2203,14 @@ var ConnectorManager = class {
|
|
|
2153
2203
|
if (this.isDisconnecting) {
|
|
2154
2204
|
return;
|
|
2155
2205
|
}
|
|
2156
|
-
|
|
2206
|
+
try {
|
|
2207
|
+
await this.connectSource(source);
|
|
2208
|
+
} catch (error) {
|
|
2209
|
+
if (!this.isDisconnecting && this.sourceConfigs.has(sourceId)) {
|
|
2210
|
+
this.lazySources.set(sourceId, source);
|
|
2211
|
+
}
|
|
2212
|
+
throw error;
|
|
2213
|
+
}
|
|
2157
2214
|
}
|
|
2158
2215
|
/**
|
|
2159
2216
|
* Build a connection DSN, optionally replacing password with
|
|
@@ -2218,24 +2275,29 @@ var PARAMETER_STYLES = {
|
|
|
2218
2275
|
// ?, ?, ?
|
|
2219
2276
|
sqlserver: "named",
|
|
2220
2277
|
// @p1, @p2, @p3
|
|
2221
|
-
sqlite: "positional"
|
|
2278
|
+
sqlite: "positional",
|
|
2222
2279
|
// ?, ?, ?
|
|
2280
|
+
oracle: "colon"
|
|
2281
|
+
// :1, :2, :3
|
|
2223
2282
|
};
|
|
2224
|
-
function detectParameterStyle(statement) {
|
|
2225
|
-
const cleanedSQL = stripCommentsAndStrings(statement);
|
|
2283
|
+
function detectParameterStyle(statement, connectorType) {
|
|
2284
|
+
const cleanedSQL = stripCommentsAndStrings(statement, connectorType);
|
|
2226
2285
|
if (/\$\d+/.test(cleanedSQL)) {
|
|
2227
2286
|
return "numbered";
|
|
2228
2287
|
}
|
|
2229
2288
|
if (/@p\d+/.test(cleanedSQL)) {
|
|
2230
2289
|
return "named";
|
|
2231
2290
|
}
|
|
2291
|
+
if (/(?<!:):\d+/.test(cleanedSQL)) {
|
|
2292
|
+
return "colon";
|
|
2293
|
+
}
|
|
2232
2294
|
if (/\?/.test(cleanedSQL)) {
|
|
2233
2295
|
return "positional";
|
|
2234
2296
|
}
|
|
2235
2297
|
return "none";
|
|
2236
2298
|
}
|
|
2237
2299
|
function validateParameterStyle(statement, connectorType) {
|
|
2238
|
-
const detectedStyle = detectParameterStyle(statement);
|
|
2300
|
+
const detectedStyle = detectParameterStyle(statement, connectorType);
|
|
2239
2301
|
const expectedStyle = PARAMETER_STYLES[connectorType];
|
|
2240
2302
|
if (detectedStyle === "none") {
|
|
2241
2303
|
return;
|
|
@@ -2244,57 +2306,53 @@ function validateParameterStyle(statement, connectorType) {
|
|
|
2244
2306
|
const examples = {
|
|
2245
2307
|
numbered: "$1, $2, $3",
|
|
2246
2308
|
positional: "?, ?, ?",
|
|
2247
|
-
named: "@p1, @p2, @p3"
|
|
2309
|
+
named: "@p1, @p2, @p3",
|
|
2310
|
+
colon: ":1, :2, :3"
|
|
2248
2311
|
};
|
|
2249
2312
|
throw new Error(
|
|
2250
2313
|
`Invalid parameter syntax for ${connectorType}. Expected ${expectedStyle} style (${examples[expectedStyle]}), but found ${detectedStyle} style in statement.`
|
|
2251
2314
|
);
|
|
2252
2315
|
}
|
|
2253
2316
|
}
|
|
2254
|
-
function countParameters(statement) {
|
|
2255
|
-
const style = detectParameterStyle(statement);
|
|
2256
|
-
const cleanedSQL = stripCommentsAndStrings(statement);
|
|
2317
|
+
function countParameters(statement, connectorType) {
|
|
2318
|
+
const style = detectParameterStyle(statement, connectorType);
|
|
2319
|
+
const cleanedSQL = stripCommentsAndStrings(statement, connectorType);
|
|
2257
2320
|
switch (style) {
|
|
2258
|
-
case "numbered":
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
if (!uniqueIndices.includes(i)) {
|
|
2266
|
-
throw new Error(
|
|
2267
|
-
`Non-sequential numbered parameters detected. Found placeholders: ${uniqueIndices.map((n) => `$${n}`).join(", ")}. Parameters must be sequential starting from $1 (missing $${i}).`
|
|
2268
|
-
);
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
|
-
return maxIndex;
|
|
2272
|
-
}
|
|
2273
|
-
case "named": {
|
|
2274
|
-
const matches = cleanedSQL.match(/@p\d+/g);
|
|
2275
|
-
if (!matches) return 0;
|
|
2276
|
-
const numbers = matches.map((m) => parseInt(m.slice(2), 10));
|
|
2277
|
-
const uniqueIndices = Array.from(new Set(numbers)).sort((a, b) => a - b);
|
|
2278
|
-
const maxIndex = Math.max(...uniqueIndices);
|
|
2279
|
-
for (let i = 1; i <= maxIndex; i++) {
|
|
2280
|
-
if (!uniqueIndices.includes(i)) {
|
|
2281
|
-
throw new Error(
|
|
2282
|
-
`Non-sequential named parameters detected. Found placeholders: ${uniqueIndices.map((n) => `@p${n}`).join(", ")}. Parameters must be sequential starting from @p1 (missing @p${i}).`
|
|
2283
|
-
);
|
|
2284
|
-
}
|
|
2285
|
-
}
|
|
2286
|
-
return maxIndex;
|
|
2287
|
-
}
|
|
2288
|
-
case "positional": {
|
|
2321
|
+
case "numbered":
|
|
2322
|
+
return countIndexedParameters(cleanedSQL, /\$\d+/g, "$", "numbered");
|
|
2323
|
+
case "named":
|
|
2324
|
+
return countIndexedParameters(cleanedSQL, /@p\d+/g, "@p", "named");
|
|
2325
|
+
case "colon":
|
|
2326
|
+
return countIndexedParameters(cleanedSQL, /(?<!:):\d+/g, ":", "colon-numbered");
|
|
2327
|
+
case "positional":
|
|
2289
2328
|
return (cleanedSQL.match(/\?/g) || []).length;
|
|
2290
|
-
}
|
|
2291
2329
|
default:
|
|
2292
2330
|
return 0;
|
|
2293
2331
|
}
|
|
2294
2332
|
}
|
|
2333
|
+
function countIndexedParameters(cleanedSQL, pattern, sigil, styleName) {
|
|
2334
|
+
const matches = cleanedSQL.match(pattern);
|
|
2335
|
+
if (!matches) return 0;
|
|
2336
|
+
const indices = new Set(matches.map((m) => parseInt(m.slice(sigil.length), 10)));
|
|
2337
|
+
if (indices.has(0)) {
|
|
2338
|
+
throw new Error(
|
|
2339
|
+
`Invalid ${styleName} parameter ${sigil}0. Parameters must be sequential starting from ${sigil}1.`
|
|
2340
|
+
);
|
|
2341
|
+
}
|
|
2342
|
+
const maxIndex = Math.max(...indices);
|
|
2343
|
+
for (let i = 1; i <= maxIndex; i++) {
|
|
2344
|
+
if (!indices.has(i)) {
|
|
2345
|
+
const found = Array.from(indices).sort((a, b) => a - b).map((n) => `${sigil}${n}`).join(", ");
|
|
2346
|
+
throw new Error(
|
|
2347
|
+
`Non-sequential ${styleName} parameters detected. Found placeholders: ${found}. Parameters must be sequential starting from ${sigil}1 (missing ${sigil}${i}).`
|
|
2348
|
+
);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
return maxIndex;
|
|
2352
|
+
}
|
|
2295
2353
|
function validateParameters(statement, parameters, connectorType) {
|
|
2296
2354
|
validateParameterStyle(statement, connectorType);
|
|
2297
|
-
const paramCount = countParameters(statement);
|
|
2355
|
+
const paramCount = countParameters(statement, connectorType);
|
|
2298
2356
|
const definedCount = parameters?.length || 0;
|
|
2299
2357
|
if (paramCount !== definedCount) {
|
|
2300
2358
|
throw new Error(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
stripCommentsAndStrings
|
|
3
|
-
} from "./chunk-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
};
|