@bytebase/dbhub 0.23.0 → 1.0.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 +6 -0
- package/dist/{chunk-RTB262PR.js → chunk-DUCNPMNW.js} +54 -1
- package/dist/chunk-JO7YQPBQ.js +130 -0
- package/dist/{chunk-COEDRFSK.js → chunk-MQR6VX5F.js} +54 -40
- package/dist/{chunk-BXXHWEQS.js → chunk-S2V6RZ3C.js} +14 -3
- package/dist/index.js +104 -154
- package/dist/{mariadb-SPFBL46R.js → mariadb-72C2BNCG.js} +51 -48
- package/dist/{mysql-MZWCPISB.js → mysql-CDUKJMNN.js} +52 -49
- package/dist/{postgres-BKBCGDZS.js → postgres-YAYHDXVD.js} +9 -15
- package/dist/{registry-ZME64VB2.js → registry-ZU6ZFNQ2.js} +1 -1
- package/dist/{sqlite-CQ4LCBB6.js → sqlite-5M7SDAQZ.js} +11 -19
- package/dist/{sqlserver-65AGCEJX.js → sqlserver-COKW7RIK.js} +285 -81
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -84,6 +84,10 @@ npx @bytebase/dbhub@latest --transport http --port 8080 --dsn "postgres://user:p
|
|
|
84
84
|
|
|
85
85
|
Download `dbhub-<version>.mcpb` from the [latest release](https://github.com/bytebase/dbhub/releases/latest) and install it in any [MCPB-compatible client](https://github.com/modelcontextprotocol/mcpb) — Claude Desktop (double-click, or drag into Settings → Extensions), Claude Code, or MCP for Windows — then enter your database connection string. The bundle runs locally over stdio, is **read-only by design** (writes are rejected and the database session is set to read-only at the engine level), and needs no remote endpoint or OAuth setup — ideal for giving non-technical teammates curated, read-only database access. Pair it with a least-privilege, read-only database account. See the [MCP Bundle guide](https://dbhub.ai/mcpb) for details and for packaging your own bundle.
|
|
86
86
|
|
|
87
|
+
**Claude Code Plugin:**
|
|
88
|
+
|
|
89
|
+
For Claude Code, the [DBHub plugin](https://dbhub.ai/claude-code-plugin) registers the MCP server with one install command — Claude Code prompts for your connection string and stores it in secure storage — and adds `/dbhub:setup` and `/dbhub:explore` skills. Read-only by design, like the MCP Bundle.
|
|
90
|
+
|
|
87
91
|
**Demo Mode:**
|
|
88
92
|
|
|
89
93
|
```bash
|
|
@@ -99,6 +103,8 @@ npx @bytebase/dbhub@latest --transport http --host 127.0.0.1 --port 8080 --demo
|
|
|
99
103
|
> The HTTP transport defaults to `--host 0.0.0.0`, exposing DBHub on every network interface. For production, bind to `127.0.0.1` and front DBHub with a reverse proxy (nginx/Caddy) or firewall — DBHub does not authenticate HTTP clients.
|
|
100
104
|
>
|
|
101
105
|
> The HTTP transport also has built-in DNS-rebinding protection: it only accepts requests whose `Host` is loopback, this machine's own hostname/IPs, or a name you allow via [`--allowed-hosts`](https://dbhub.ai/config/command-line#allowed-hosts). If a client behind a reverse proxy or custom DNS name gets a `403`, add that hostname with `--allowed-hosts`.
|
|
106
|
+
>
|
|
107
|
+
> DBHub serves both MCP protocol eras on the same `/mcp` endpoint: the stateless **2026-07-28** revision (cacheable `tools/list`, no session state — safe behind round-robin load balancers) and the 2025-era Streamable HTTP protocol for older clients. 2026-era clients send the standard `Mcp-Method` / `Mcp-Name` headers on every request, so a fronting gateway or WAF can route and rate-limit `execute_sql` separately from `search_objects` without parsing JSON bodies.
|
|
102
108
|
|
|
103
109
|
See [Command-Line Options](https://dbhub.ai/config/command-line) for all available parameters.
|
|
104
110
|
|
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
obfuscateDSNPassword
|
|
3
|
+
} from "./chunk-JEZZN2YZ.js";
|
|
4
|
+
|
|
5
|
+
// src/utils/dsn-database.ts
|
|
6
|
+
var MissingDatabaseError = class extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "MissingDatabaseError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function requireDatabaseInDSN(database, dsn, label) {
|
|
13
|
+
if (database) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
throw new MissingDatabaseError(
|
|
17
|
+
`${label} DSN must name a database.
|
|
18
|
+
Provided: ${obfuscateDSNPassword(dsn)}
|
|
19
|
+
Add the database to the DSN, e.g. ...:3306/mydb
|
|
20
|
+
To work with several databases, define one [[sources]] entry per database in a TOML config file: https://dbhub.ai/config/toml`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
1
24
|
// src/utils/multi-statement-result-parser.ts
|
|
2
25
|
function isMetadataObject(element) {
|
|
3
26
|
if (!element || typeof element !== "object" || Array.isArray(element)) {
|
|
@@ -54,7 +77,37 @@ function parseQueryResults(results) {
|
|
|
54
77
|
return results;
|
|
55
78
|
}
|
|
56
79
|
|
|
80
|
+
// src/utils/readonly-transaction.ts
|
|
81
|
+
async function withReadOnlyTransaction(conn, readonly, supportsReadOnlyTransaction, execute) {
|
|
82
|
+
if (!readonly) {
|
|
83
|
+
return execute();
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
await conn.query(
|
|
87
|
+
supportsReadOnlyTransaction ? "START TRANSACTION READ ONLY" : "START TRANSACTION"
|
|
88
|
+
);
|
|
89
|
+
const result = await execute();
|
|
90
|
+
await conn.query(supportsReadOnlyTransaction ? "COMMIT" : "ROLLBACK");
|
|
91
|
+
return result;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
try {
|
|
94
|
+
await conn.query("ROLLBACK");
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/utils/server-flavor.ts
|
|
102
|
+
function isTiDBVersion(version) {
|
|
103
|
+
return typeof version === "string" && /tidb/i.test(version);
|
|
104
|
+
}
|
|
105
|
+
|
|
57
106
|
export {
|
|
107
|
+
MissingDatabaseError,
|
|
108
|
+
requireDatabaseInDSN,
|
|
58
109
|
extractAffectedRows,
|
|
59
|
-
parseQueryResults
|
|
110
|
+
parseQueryResults,
|
|
111
|
+
withReadOnlyTransaction,
|
|
112
|
+
isTiDBVersion
|
|
60
113
|
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
stripCommentsAndStrings
|
|
3
|
+
} from "./chunk-JEZZN2YZ.js";
|
|
4
|
+
|
|
5
|
+
// src/utils/allowed-keywords.ts
|
|
6
|
+
var allowedKeywords = {
|
|
7
|
+
postgres: ["select", "with", "explain", "show"],
|
|
8
|
+
mysql: ["select", "with", "explain", "show", "describe", "desc"],
|
|
9
|
+
mariadb: ["select", "with", "explain", "show", "describe", "desc"],
|
|
10
|
+
sqlite: ["select", "with", "explain", "pragma"],
|
|
11
|
+
// SQL Server has no native EXPLAIN statement; the connector translates a
|
|
12
|
+
// leading `EXPLAIN` into a SET SHOWPLAN_XML request (see SQLServerConnector).
|
|
13
|
+
sqlserver: ["select", "with", "explain"]
|
|
14
|
+
};
|
|
15
|
+
var mutatingKeywords = [
|
|
16
|
+
"insert",
|
|
17
|
+
"update",
|
|
18
|
+
"delete",
|
|
19
|
+
"drop",
|
|
20
|
+
"alter",
|
|
21
|
+
"create",
|
|
22
|
+
"truncate",
|
|
23
|
+
"merge",
|
|
24
|
+
"grant",
|
|
25
|
+
"revoke",
|
|
26
|
+
"rename"
|
|
27
|
+
];
|
|
28
|
+
var mutatingPattern = new RegExp(
|
|
29
|
+
`\\b(?:${mutatingKeywords.join("|")})\\b`,
|
|
30
|
+
"i"
|
|
31
|
+
);
|
|
32
|
+
var mutatingPatternWithReplace = new RegExp(
|
|
33
|
+
`\\b(?:${mutatingKeywords.join("|")}|replace\\s+(?:(?:low_priority|delayed)\\s+)?into)\\b`,
|
|
34
|
+
"i"
|
|
35
|
+
);
|
|
36
|
+
var sqlServerDynamicSqlKeywords = [
|
|
37
|
+
"execute",
|
|
38
|
+
"exec",
|
|
39
|
+
"sp_executesql",
|
|
40
|
+
"xp_cmdshell"
|
|
41
|
+
];
|
|
42
|
+
var sqlServerDynamicSqlPattern = new RegExp(
|
|
43
|
+
`\\b(?:${sqlServerDynamicSqlKeywords.join("|")})\\b`,
|
|
44
|
+
"i"
|
|
45
|
+
);
|
|
46
|
+
var sqlServerPassThroughKeywords = [
|
|
47
|
+
"openquery",
|
|
48
|
+
"openrowset",
|
|
49
|
+
"opendatasource"
|
|
50
|
+
];
|
|
51
|
+
var sqlServerPassThroughPattern = new RegExp(
|
|
52
|
+
`\\b(?:${sqlServerPassThroughKeywords.join("|")})\\s*\\(`,
|
|
53
|
+
"i"
|
|
54
|
+
);
|
|
55
|
+
var mutatingPatternSqlServer = new RegExp(
|
|
56
|
+
`\\b(?:${[...mutatingKeywords, ...sqlServerDynamicSqlKeywords].join("|")})\\b`,
|
|
57
|
+
"i"
|
|
58
|
+
);
|
|
59
|
+
var mutatingPatterns = {
|
|
60
|
+
postgres: mutatingPattern,
|
|
61
|
+
mysql: mutatingPatternWithReplace,
|
|
62
|
+
mariadb: mutatingPatternWithReplace,
|
|
63
|
+
sqlite: mutatingPatternWithReplace,
|
|
64
|
+
sqlserver: mutatingPatternSqlServer
|
|
65
|
+
};
|
|
66
|
+
var selectIntoPattern = /\bselect\b[\s\S]+\binto\b/i;
|
|
67
|
+
var sqliteReadOnlyArgPragmas = /* @__PURE__ */ new Set([
|
|
68
|
+
"table_info",
|
|
69
|
+
"index_info",
|
|
70
|
+
"index_list",
|
|
71
|
+
"foreign_key_list"
|
|
72
|
+
]);
|
|
73
|
+
var sqlitePragmaParenPattern = /^pragma\s+(?:[a-z0-9_]+\.)?([a-z0-9_]+)\s*\(/;
|
|
74
|
+
var explainAnalyzePattern = /^explain\s+(?:\([^)]*\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)[^)]*\)|\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)(?:\s+verbose\b)?)/i;
|
|
75
|
+
function isReadOnlySQL(sql, connectorType) {
|
|
76
|
+
return checkReadOnly(
|
|
77
|
+
stripCommentsAndStrings(sql, connectorType).trim().toLowerCase(),
|
|
78
|
+
connectorType
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
function checkReadOnly(cleanedSQL, connectorType) {
|
|
82
|
+
if (!cleanedSQL) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const firstWord = cleanedSQL.match(/\S+/)?.[0] ?? "";
|
|
86
|
+
const keywordList = allowedKeywords[connectorType] || [];
|
|
87
|
+
if (!keywordList.includes(firstWord)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (connectorType === "sqlserver" && sqlServerPassThroughPattern.test(cleanedSQL)) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (firstWord === "with") {
|
|
94
|
+
const pattern = mutatingPatterns[connectorType] ?? mutatingPattern;
|
|
95
|
+
if (pattern.test(cleanedSQL)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (firstWord === "pragma" && connectorType === "sqlite") {
|
|
100
|
+
if (cleanedSQL.includes("=")) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const parenMatch = cleanedSQL.match(sqlitePragmaParenPattern);
|
|
104
|
+
if (parenMatch && !sqliteReadOnlyArgPragmas.has(parenMatch[1])) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if ((firstWord === "select" || firstWord === "with") && selectIntoPattern.test(cleanedSQL)) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
if (firstWord === "explain") {
|
|
112
|
+
const m = explainAnalyzePattern.exec(cleanedSQL);
|
|
113
|
+
if (m) {
|
|
114
|
+
const afterExplain = cleanedSQL.slice(m[0].length).trim();
|
|
115
|
+
if (afterExplain && !checkReadOnly(afterExplain, connectorType)) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export {
|
|
124
|
+
allowedKeywords,
|
|
125
|
+
sqlServerDynamicSqlKeywords,
|
|
126
|
+
sqlServerDynamicSqlPattern,
|
|
127
|
+
sqlServerPassThroughKeywords,
|
|
128
|
+
sqlServerPassThroughPattern,
|
|
129
|
+
isReadOnlySQL
|
|
130
|
+
};
|
|
@@ -580,55 +580,59 @@ function parseCommandLineArgs() {
|
|
|
580
580
|
}
|
|
581
581
|
return parsedManually;
|
|
582
582
|
}
|
|
583
|
-
function
|
|
583
|
+
function findEnvFile() {
|
|
584
584
|
const isDevelopment = process.env.NODE_ENV === "development" || process.argv[1]?.includes("tsx");
|
|
585
585
|
const envFileNames = isDevelopment ? [".env.local", ".env"] : [".env"];
|
|
586
586
|
const envPaths = [];
|
|
587
587
|
for (const fileName of envFileNames) {
|
|
588
588
|
envPaths.push(
|
|
589
|
-
fileName,
|
|
589
|
+
path.join(process.cwd(), fileName),
|
|
590
590
|
// Current working directory
|
|
591
|
-
path.join(__dirname, "..", "..", fileName)
|
|
591
|
+
path.join(__dirname, "..", "..", fileName)
|
|
592
592
|
// Two levels up (src/config -> src -> root)
|
|
593
|
-
path.join(process.cwd(), fileName)
|
|
594
|
-
// Explicit current working directory
|
|
595
593
|
);
|
|
596
594
|
}
|
|
597
595
|
for (const envPath of envPaths) {
|
|
598
|
-
console.error(`Checking for env file: ${envPath}`);
|
|
599
596
|
if (fs.existsSync(envPath)) {
|
|
600
|
-
|
|
601
|
-
if (process.env.READONLY !== void 0) {
|
|
602
|
-
console.error("\nERROR: READONLY environment variable is no longer supported.");
|
|
603
|
-
console.error("Use dbhub.toml with [[tools]] configuration instead:\n");
|
|
604
|
-
console.error(" [[sources]]");
|
|
605
|
-
console.error(' id = "default"');
|
|
606
|
-
console.error(' dsn = "..."\n');
|
|
607
|
-
console.error(" [[tools]]");
|
|
608
|
-
console.error(' name = "execute_sql"');
|
|
609
|
-
console.error(' source = "default"');
|
|
610
|
-
console.error(" readonly = true\n");
|
|
611
|
-
console.error("See https://dbhub.ai/tools/execute-sql#read-only-mode for details.\n");
|
|
612
|
-
process.exit(1);
|
|
613
|
-
}
|
|
614
|
-
if (process.env.MAX_ROWS !== void 0) {
|
|
615
|
-
console.error("\nERROR: MAX_ROWS environment variable is no longer supported.");
|
|
616
|
-
console.error("Use dbhub.toml with [[tools]] configuration instead:\n");
|
|
617
|
-
console.error(" [[sources]]");
|
|
618
|
-
console.error(' id = "default"');
|
|
619
|
-
console.error(' dsn = "..."\n');
|
|
620
|
-
console.error(" [[tools]]");
|
|
621
|
-
console.error(' name = "execute_sql"');
|
|
622
|
-
console.error(' source = "default"');
|
|
623
|
-
console.error(" max_rows = 1000\n");
|
|
624
|
-
console.error("See https://dbhub.ai/tools/execute-sql#row-limiting for details.\n");
|
|
625
|
-
process.exit(1);
|
|
626
|
-
}
|
|
627
|
-
return path.basename(envPath);
|
|
597
|
+
return envPath;
|
|
628
598
|
}
|
|
629
599
|
}
|
|
630
600
|
return null;
|
|
631
601
|
}
|
|
602
|
+
function loadEnvFiles() {
|
|
603
|
+
const envPath = findEnvFile();
|
|
604
|
+
if (envPath) {
|
|
605
|
+
dotenv.config({ path: envPath });
|
|
606
|
+
if (process.env.READONLY !== void 0) {
|
|
607
|
+
console.error("\nERROR: READONLY environment variable is no longer supported.");
|
|
608
|
+
console.error("Use dbhub.toml with [[tools]] configuration instead:\n");
|
|
609
|
+
console.error(" [[sources]]");
|
|
610
|
+
console.error(' id = "default"');
|
|
611
|
+
console.error(' dsn = "..."\n');
|
|
612
|
+
console.error(" [[tools]]");
|
|
613
|
+
console.error(' name = "execute_sql"');
|
|
614
|
+
console.error(' source = "default"');
|
|
615
|
+
console.error(" readonly = true\n");
|
|
616
|
+
console.error("See https://dbhub.ai/tools/execute-sql#read-only-mode for details.\n");
|
|
617
|
+
process.exit(1);
|
|
618
|
+
}
|
|
619
|
+
if (process.env.MAX_ROWS !== void 0) {
|
|
620
|
+
console.error("\nERROR: MAX_ROWS environment variable is no longer supported.");
|
|
621
|
+
console.error("Use dbhub.toml with [[tools]] configuration instead:\n");
|
|
622
|
+
console.error(" [[sources]]");
|
|
623
|
+
console.error(' id = "default"');
|
|
624
|
+
console.error(' dsn = "..."\n');
|
|
625
|
+
console.error(" [[tools]]");
|
|
626
|
+
console.error(' name = "execute_sql"');
|
|
627
|
+
console.error(' source = "default"');
|
|
628
|
+
console.error(" max_rows = 1000\n");
|
|
629
|
+
console.error("See https://dbhub.ai/tools/execute-sql#row-limiting for details.\n");
|
|
630
|
+
process.exit(1);
|
|
631
|
+
}
|
|
632
|
+
return path.basename(envPath);
|
|
633
|
+
}
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
632
636
|
function isDemoMode() {
|
|
633
637
|
const args = parseCommandLineArgs();
|
|
634
638
|
return args.demo === "true";
|
|
@@ -923,6 +927,9 @@ function resolveSSHConfig() {
|
|
|
923
927
|
};
|
|
924
928
|
}
|
|
925
929
|
async function resolveSourceConfigs() {
|
|
930
|
+
if (parseCommandLineArgs().config) {
|
|
931
|
+
loadEnvFiles();
|
|
932
|
+
}
|
|
926
933
|
if (!isDemoMode()) {
|
|
927
934
|
const tomlConfig = loadTomlConfig();
|
|
928
935
|
if (tomlConfig) {
|
|
@@ -932,6 +939,11 @@ async function resolveSourceConfigs() {
|
|
|
932
939
|
"The --id flag cannot be used with TOML configuration. TOML config defines source IDs directly. Either remove the --id flag or use command-line DSN configuration instead."
|
|
933
940
|
);
|
|
934
941
|
}
|
|
942
|
+
if (parseCommandLineArgs().dsn) {
|
|
943
|
+
throw new Error(
|
|
944
|
+
`The --dsn flag cannot be used with TOML configuration (${tomlConfig.source}). TOML config defines database sources directly and supports multiple databases, while a DSN configures a single database. Either remove the --dsn flag or drop the --config flag.`
|
|
945
|
+
);
|
|
946
|
+
}
|
|
935
947
|
return tomlConfig;
|
|
936
948
|
}
|
|
937
949
|
}
|
|
@@ -1039,8 +1051,9 @@ function loadTomlConfig() {
|
|
|
1039
1051
|
}
|
|
1040
1052
|
function resolveTomlConfigPath() {
|
|
1041
1053
|
const args = parseCommandLineArgs();
|
|
1042
|
-
|
|
1043
|
-
|
|
1054
|
+
const configValue = requireFlagValue("config", args, "./dbhub.toml");
|
|
1055
|
+
if (configValue) {
|
|
1056
|
+
const configPath = expandHomeDir(configValue);
|
|
1044
1057
|
if (!fs2.existsSync(configPath)) {
|
|
1045
1058
|
throw new Error(
|
|
1046
1059
|
`Configuration file specified by --config flag not found: ${configPath}`
|
|
@@ -1048,10 +1061,6 @@ function resolveTomlConfigPath() {
|
|
|
1048
1061
|
}
|
|
1049
1062
|
return configPath;
|
|
1050
1063
|
}
|
|
1051
|
-
const defaultConfigPath = path2.join(process.cwd(), "dbhub.toml");
|
|
1052
|
-
if (fs2.existsSync(defaultConfigPath)) {
|
|
1053
|
-
return defaultConfigPath;
|
|
1054
|
-
}
|
|
1055
1064
|
return null;
|
|
1056
1065
|
}
|
|
1057
1066
|
function validateTomlConfig(config, configPath) {
|
|
@@ -1206,6 +1215,11 @@ function validateDSNFieldConflicts(source, configPath) {
|
|
|
1206
1215
|
if (source.database && info.database && source.database !== info.database) {
|
|
1207
1216
|
conflict("database", source.database, info.database);
|
|
1208
1217
|
}
|
|
1218
|
+
if (source.database && !info.database) {
|
|
1219
|
+
throw new Error(
|
|
1220
|
+
`Configuration file ${configPath}: source '${source.id}' has a 'database' field but the DSN names no database. The field is ignored at connection time \u2014 add the database to the DSN, or use individual connection parameters instead of a DSN.`
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1209
1223
|
if (source.user && info.user && source.user !== info.user) {
|
|
1210
1224
|
conflict("user", source.user, info.user);
|
|
1211
1225
|
}
|
|
@@ -66,7 +66,8 @@ var SQLRowLimiter = class {
|
|
|
66
66
|
const trimmed = sql.trim();
|
|
67
67
|
const hasSemicolon = trimmed.endsWith(";");
|
|
68
68
|
const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
|
|
69
|
-
return `${sqlWithoutSemicolon}
|
|
69
|
+
return `${sqlWithoutSemicolon}
|
|
70
|
+
LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
/**
|
|
@@ -107,7 +108,8 @@ var SQLRowLimiter = class {
|
|
|
107
108
|
const trimmed = sql.trim();
|
|
108
109
|
const hasSemicolon = trimmed.endsWith(";");
|
|
109
110
|
const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
|
|
110
|
-
return `SELECT * FROM (${sqlWithoutSemicolon}
|
|
111
|
+
return `SELECT * FROM (${sqlWithoutSemicolon}
|
|
112
|
+
) AS subq LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
|
|
111
113
|
}
|
|
112
114
|
return this.applyLimitToQuery(sql, maxRows);
|
|
113
115
|
}
|
|
@@ -122,6 +124,15 @@ var SQLRowLimiter = class {
|
|
|
122
124
|
}
|
|
123
125
|
};
|
|
124
126
|
|
|
127
|
+
// src/utils/resource-cleanup.ts
|
|
128
|
+
async function closeQuietly(close) {
|
|
129
|
+
try {
|
|
130
|
+
await close();
|
|
131
|
+
} catch {
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
125
135
|
export {
|
|
126
|
-
SQLRowLimiter
|
|
136
|
+
SQLRowLimiter,
|
|
137
|
+
closeQuietly
|
|
127
138
|
};
|