@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/dist/public/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>DBHub - Minimal Database MCP Server</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-BRlIJaqn.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-BJ-1UrcV.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
@@ -2,9 +2,9 @@ import {
|
|
|
2
2
|
ToolRegistry,
|
|
3
3
|
getToolRegistry,
|
|
4
4
|
initializeToolRegistry
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-77BJNE6E.js";
|
|
6
6
|
import "./chunk-WVVMH6FJ.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-IXC4NQGC.js";
|
|
8
8
|
export {
|
|
9
9
|
ToolRegistry,
|
|
10
10
|
getToolRegistry,
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
quoteIdentifier
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-X2EC47W4.js";
|
|
4
4
|
import {
|
|
5
5
|
SQLRowLimiter,
|
|
6
6
|
closeQuietly
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-YIGJHEU6.js";
|
|
8
|
+
import "./chunk-ABTVMHD6.js";
|
|
8
9
|
import {
|
|
9
10
|
ConnectorRegistry,
|
|
10
11
|
SafeURL,
|
|
11
12
|
obfuscateDSNPassword,
|
|
12
|
-
splitSQLStatements
|
|
13
|
-
|
|
13
|
+
splitSQLStatements,
|
|
14
|
+
stripCommentsAndStrings
|
|
15
|
+
} from "./chunk-IXC4NQGC.js";
|
|
14
16
|
|
|
15
17
|
// src/connectors/sqlite/suppress-experimental-warning.ts
|
|
16
18
|
var installed = false;
|
|
@@ -321,6 +323,16 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
321
323
|
"SQLite does not support stored procedures. Functions are defined programmatically through the SQLite API, not stored in the database."
|
|
322
324
|
);
|
|
323
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Whether a statement returns rows (and so must run via `all()` rather than
|
|
328
|
+
* `run()`). Classified on the comment/string-stripped text so a leading
|
|
329
|
+
* attribution comment cannot hide a SELECT, matching the row limiter and the
|
|
330
|
+
* read-only classifier.
|
|
331
|
+
*/
|
|
332
|
+
returnsRows(statement) {
|
|
333
|
+
const stripped = stripCommentsAndStrings(statement, "sqlite").toLowerCase().trim();
|
|
334
|
+
return stripped.startsWith("select") || stripped.startsWith("with") || stripped.startsWith("explain") || stripped.startsWith("analyze") || stripped.startsWith("pragma") && (stripped.includes("table_info") || stripped.includes("index_info") || stripped.includes("index_list") || stripped.includes("foreign_key_list"));
|
|
335
|
+
}
|
|
324
336
|
async executeSQL(sql, options, parameters) {
|
|
325
337
|
if (!this.db) {
|
|
326
338
|
throw new Error("Not connected to SQLite database");
|
|
@@ -332,13 +344,13 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
332
344
|
const statements = splitSQLStatements(sql, "sqlite");
|
|
333
345
|
if (statements.length === 1) {
|
|
334
346
|
let processedStatement = statements[0];
|
|
335
|
-
const
|
|
336
|
-
const isReadStatement = trimmedStatement.startsWith("select") || trimmedStatement.startsWith("with") || trimmedStatement.startsWith("explain") || trimmedStatement.startsWith("analyze") || trimmedStatement.startsWith("pragma") && (trimmedStatement.includes("table_info") || trimmedStatement.includes("index_info") || trimmedStatement.includes("index_list") || trimmedStatement.includes("foreign_key_list"));
|
|
347
|
+
const isReadStatement = this.returnsRows(statements[0]);
|
|
337
348
|
let probeApplied = false;
|
|
338
349
|
if (options.maxRows) {
|
|
339
350
|
const rewrite = SQLRowLimiter.applyMaxRowsWithTruncationProbe(
|
|
340
351
|
processedStatement,
|
|
341
|
-
options.maxRows
|
|
352
|
+
options.maxRows,
|
|
353
|
+
"sqlite"
|
|
342
354
|
);
|
|
343
355
|
processedStatement = rewrite.sql;
|
|
344
356
|
probeApplied = rewrite.probeApplied;
|
|
@@ -366,8 +378,7 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
366
378
|
const readStatements = [];
|
|
367
379
|
const writeStatements = [];
|
|
368
380
|
for (const statement of statements) {
|
|
369
|
-
|
|
370
|
-
if (trimmedStatement.startsWith("select") || trimmedStatement.startsWith("with") || trimmedStatement.startsWith("explain") || trimmedStatement.startsWith("analyze") || trimmedStatement.startsWith("pragma") && (trimmedStatement.includes("table_info") || trimmedStatement.includes("index_info") || trimmedStatement.includes("index_list") || trimmedStatement.includes("foreign_key_list"))) {
|
|
381
|
+
if (this.returnsRows(statement)) {
|
|
371
382
|
readStatements.push(statement);
|
|
372
383
|
} else {
|
|
373
384
|
writeStatements.push(statement);
|
|
@@ -387,7 +398,8 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
387
398
|
}
|
|
388
399
|
const { sql: processedStatement, probeApplied } = SQLRowLimiter.applyMaxRowsWithTruncationProbe(
|
|
389
400
|
statement,
|
|
390
|
-
options.maxRows
|
|
401
|
+
options.maxRows,
|
|
402
|
+
"sqlite"
|
|
391
403
|
);
|
|
392
404
|
const rows = this.prepare(processedStatement).all();
|
|
393
405
|
const resultSet = { sql: statement, rows, rowCount: rows.length };
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
sqlServerDynamicSqlKeywords,
|
|
3
|
-
sqlServerDynamicSqlPattern,
|
|
4
|
-
sqlServerPassThroughKeywords,
|
|
5
|
-
sqlServerPassThroughPattern
|
|
6
|
-
} from "./chunk-IQRBLAB4.js";
|
|
7
1
|
import {
|
|
8
2
|
isDriverNotInstalled
|
|
9
3
|
} from "./chunk-WVVMH6FJ.js";
|
|
@@ -14,14 +8,21 @@ import {
|
|
|
14
8
|
import {
|
|
15
9
|
SQLRowLimiter,
|
|
16
10
|
closeQuietly
|
|
17
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-YIGJHEU6.js";
|
|
12
|
+
import {
|
|
13
|
+
sqlServerDynamicSqlKeywords,
|
|
14
|
+
sqlServerDynamicSqlPattern,
|
|
15
|
+
sqlServerPassThroughKeywords,
|
|
16
|
+
sqlServerPassThroughPattern
|
|
17
|
+
} from "./chunk-ABTVMHD6.js";
|
|
18
18
|
import {
|
|
19
19
|
ConnectorRegistry,
|
|
20
|
+
LEADING_SQL_NOISE,
|
|
20
21
|
SafeURL,
|
|
21
22
|
obfuscateDSNPassword,
|
|
22
23
|
splitSQLStatements,
|
|
23
24
|
stripCommentsAndStrings
|
|
24
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-IXC4NQGC.js";
|
|
25
26
|
|
|
26
27
|
// src/connectors/sqlserver/index.ts
|
|
27
28
|
import sql from "mssql";
|
|
@@ -551,9 +552,7 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
551
552
|
if (!this.connection) {
|
|
552
553
|
throw new Error("Not connected to SQL Server database");
|
|
553
554
|
}
|
|
554
|
-
const afterNoise = sqlQuery.
|
|
555
|
-
sqlQuery.match(_SQLServerConnector.LEADING_NOISE)[0].length
|
|
556
|
-
);
|
|
555
|
+
const afterNoise = sqlQuery.replace(LEADING_SQL_NOISE, "");
|
|
557
556
|
if (/^explain\b/i.test(afterNoise)) {
|
|
558
557
|
const { analyze, query } = _SQLServerConnector.parseExplainPrefix(
|
|
559
558
|
afterNoise.slice("explain".length).trim()
|
|
@@ -1013,13 +1012,6 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
1013
1012
|
return null;
|
|
1014
1013
|
}
|
|
1015
1014
|
};
|
|
1016
|
-
/**
|
|
1017
|
-
* Leading whitespace and SQL comments to skip before looking for a keyword.
|
|
1018
|
-
* The read-only validator strips comments before checking the first keyword,
|
|
1019
|
-
* so the connector must skip them too; otherwise an EXPLAIN preceded by a
|
|
1020
|
-
* comment passes validation but reaches the server untranslated.
|
|
1021
|
-
*/
|
|
1022
|
-
_SQLServerConnector.LEADING_NOISE = /^(?:\s+|--[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)*/;
|
|
1023
1015
|
/** Boolean spellings PostgreSQL accepts for an EXPLAIN option. */
|
|
1024
1016
|
_SQLServerConnector.EXPLAIN_ON = /^(?:true|on|1)$/i;
|
|
1025
1017
|
_SQLServerConnector.EXPLAIN_OFF = /^(?:false|off|0)$/i;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebase/dbhub",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"mcpName": "io.github.bytebase/dbhub",
|
|
5
|
-
"description": "Minimal, token-efficient Database MCP Server for PostgreSQL, MySQL, SQL Server, SQLite, MariaDB",
|
|
5
|
+
"description": "Minimal, token-efficient Database MCP Server for PostgreSQL, MySQL, SQL Server, Oracle, SQLite, MariaDB",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/bytebase/dbhub.git"
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"mariadb": "^3.4.0",
|
|
62
62
|
"mssql": "^11.0.1",
|
|
63
63
|
"mysql2": "^3.13.0",
|
|
64
|
+
"oracledb": "^7.0.1",
|
|
64
65
|
"pg": "^8.13.3"
|
|
65
66
|
},
|
|
66
67
|
"devDependencies": {
|
|
@@ -69,10 +70,12 @@
|
|
|
69
70
|
"@testcontainers/mariadb": "^11.0.3",
|
|
70
71
|
"@testcontainers/mssqlserver": "^11.0.3",
|
|
71
72
|
"@testcontainers/mysql": "^11.0.3",
|
|
73
|
+
"@testcontainers/oraclefree": "^11.14.0",
|
|
72
74
|
"@testcontainers/postgresql": "^11.0.3",
|
|
73
75
|
"@types/express": "^4.17.21",
|
|
74
76
|
"@types/mssql": "^9.1.7",
|
|
75
77
|
"@types/node": "^22.13.10",
|
|
78
|
+
"@types/oracledb": "^7.0.3",
|
|
76
79
|
"@types/pg": "^8.11.11",
|
|
77
80
|
"@types/ssh2": "^1.15.5",
|
|
78
81
|
"concurrently": "^9.2.1",
|
package/dist/chunk-6QETXS44.js
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
blankCommentsAndStrings,
|
|
3
|
-
stripCommentsAndStrings
|
|
4
|
-
} from "./chunk-MF6LSVWK.js";
|
|
5
|
-
|
|
6
|
-
// src/utils/sql-row-limiter.ts
|
|
7
|
-
var SQLRowLimiter = class {
|
|
8
|
-
/**
|
|
9
|
-
* Check if a SQL statement is a SELECT query that can benefit from row limiting
|
|
10
|
-
* Only handles SELECT queries
|
|
11
|
-
*/
|
|
12
|
-
static isSelectQuery(sql) {
|
|
13
|
-
const trimmed = sql.trim().toLowerCase();
|
|
14
|
-
return trimmed.startsWith("select");
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Check if a SQL statement already has a LIMIT clause.
|
|
18
|
-
* Strips comments and string literals first to avoid false positives.
|
|
19
|
-
*/
|
|
20
|
-
static hasLimitClause(sql) {
|
|
21
|
-
const cleanedSQL = stripCommentsAndStrings(sql);
|
|
22
|
-
const limitRegex = /\blimit\s+(?:\d+|\$\d+|\?|@p\d+)/i;
|
|
23
|
-
return limitRegex.test(cleanedSQL);
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Check if a SQL statement already has a TOP clause (SQL Server).
|
|
27
|
-
* Strips comments and string literals first to avoid false positives.
|
|
28
|
-
*/
|
|
29
|
-
static hasTopClause(sql) {
|
|
30
|
-
const cleanedSQL = stripCommentsAndStrings(sql);
|
|
31
|
-
const topRegex = /\bselect\s+top\s+\d+/i;
|
|
32
|
-
return topRegex.test(cleanedSQL);
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Extract existing LIMIT value from SQL if present.
|
|
36
|
-
* Strips comments and string literals first to avoid false positives.
|
|
37
|
-
*/
|
|
38
|
-
static extractLimitValue(sql) {
|
|
39
|
-
const cleanedSQL = stripCommentsAndStrings(sql);
|
|
40
|
-
const limitMatch = cleanedSQL.match(/\blimit\s+(\d+)/i);
|
|
41
|
-
if (limitMatch) {
|
|
42
|
-
return parseInt(limitMatch[1], 10);
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Extract existing TOP value from SQL if present (SQL Server).
|
|
48
|
-
* Strips comments and string literals first to avoid false positives.
|
|
49
|
-
*/
|
|
50
|
-
static extractTopValue(sql) {
|
|
51
|
-
const cleanedSQL = stripCommentsAndStrings(sql);
|
|
52
|
-
const topMatch = cleanedSQL.match(/\bselect\s+top\s+(\d+)/i);
|
|
53
|
-
if (topMatch) {
|
|
54
|
-
return parseInt(topMatch[1], 10);
|
|
55
|
-
}
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Add or modify LIMIT clause in a SQL statement
|
|
60
|
-
*/
|
|
61
|
-
static applyLimitToQuery(sql, maxRows) {
|
|
62
|
-
const existingLimit = this.extractLimitValue(sql);
|
|
63
|
-
if (existingLimit !== null) {
|
|
64
|
-
const effectiveLimit = Math.min(existingLimit, maxRows);
|
|
65
|
-
return sql.replace(/\blimit\s+\d+/i, `LIMIT ${effectiveLimit}`);
|
|
66
|
-
} else {
|
|
67
|
-
const trimmed = sql.trim();
|
|
68
|
-
const hasSemicolon = trimmed.endsWith(";");
|
|
69
|
-
const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
|
|
70
|
-
return `${sqlWithoutSemicolon}
|
|
71
|
-
LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Scan blanked (comment/string-free, length-preserving) SQL for parenthesis
|
|
76
|
-
* depth, invoking onMatch for every regex hit at depth 0 (i.e. not nested
|
|
77
|
-
* inside a subquery, function call, or window OVER clause).
|
|
78
|
-
*/
|
|
79
|
-
static scanTopLevel(blankedSQL, regex, onMatch) {
|
|
80
|
-
let depth = 0;
|
|
81
|
-
let m;
|
|
82
|
-
while ((m = regex.exec(blankedSQL)) !== null) {
|
|
83
|
-
const token = m[0];
|
|
84
|
-
if (token === "(") {
|
|
85
|
-
depth++;
|
|
86
|
-
} else if (token === ")") {
|
|
87
|
-
depth--;
|
|
88
|
-
} else if (depth === 0) {
|
|
89
|
-
onMatch(m);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Check if a SQL statement combines multiple SELECTs with a set operator
|
|
95
|
-
* (UNION [ALL], INTERSECT, EXCEPT) at the top level — i.e. not nested
|
|
96
|
-
* inside a subquery already wrapped in parentheses. Strips comments and
|
|
97
|
-
* string literals first to avoid false positives.
|
|
98
|
-
*/
|
|
99
|
-
static hasSetOperator(sql) {
|
|
100
|
-
const blankedSQL = blankCommentsAndStrings(sql, "sqlserver");
|
|
101
|
-
let found = false;
|
|
102
|
-
this.scanTopLevel(blankedSQL, /\(|\)|\bunion\b|\bintersect\b|\bexcept\b/gi, () => {
|
|
103
|
-
found = true;
|
|
104
|
-
});
|
|
105
|
-
return found;
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Find the start index of a top-level trailing ORDER BY clause (not one
|
|
109
|
-
* nested inside a subquery or a window function's OVER (...) clause).
|
|
110
|
-
* Returns -1 if none exists. Operates on the original (non-blanked) SQL
|
|
111
|
-
* length, since blankCommentsAndStrings preserves length/position.
|
|
112
|
-
*/
|
|
113
|
-
static findTopLevelOrderByIndex(sql) {
|
|
114
|
-
const blankedSQL = blankCommentsAndStrings(sql, "sqlserver");
|
|
115
|
-
let lastIndex = -1;
|
|
116
|
-
this.scanTopLevel(blankedSQL, /\(|\)|\border\s+by\b/gi, (m) => {
|
|
117
|
-
lastIndex = m.index;
|
|
118
|
-
});
|
|
119
|
-
return lastIndex;
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Add or modify TOP clause in a SQL statement (SQL Server)
|
|
123
|
-
*/
|
|
124
|
-
static applyTopToQuery(sql, maxRows) {
|
|
125
|
-
if (this.hasSetOperator(sql)) {
|
|
126
|
-
const trimmed = sql.trim();
|
|
127
|
-
const hasSemicolon = trimmed.endsWith(";");
|
|
128
|
-
const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
|
|
129
|
-
const orderByIndex = this.findTopLevelOrderByIndex(sqlWithoutSemicolon);
|
|
130
|
-
if (orderByIndex !== -1) {
|
|
131
|
-
const innerSql = sqlWithoutSemicolon.slice(0, orderByIndex).trimEnd();
|
|
132
|
-
const orderByClause = sqlWithoutSemicolon.slice(orderByIndex).trim();
|
|
133
|
-
return `SELECT TOP ${maxRows} * FROM (${innerSql}
|
|
134
|
-
) AS subq ${orderByClause}${hasSemicolon ? ";" : ""}`;
|
|
135
|
-
}
|
|
136
|
-
return `SELECT TOP ${maxRows} * FROM (${sqlWithoutSemicolon}
|
|
137
|
-
) AS subq${hasSemicolon ? ";" : ""}`;
|
|
138
|
-
}
|
|
139
|
-
const existingTop = this.extractTopValue(sql);
|
|
140
|
-
if (existingTop !== null) {
|
|
141
|
-
const effectiveTop = Math.min(existingTop, maxRows);
|
|
142
|
-
return sql.replace(/\bselect\s+top\s+\d+/i, `SELECT TOP ${effectiveTop}`);
|
|
143
|
-
}
|
|
144
|
-
return sql.replace(/\bselect\s+/i, `SELECT TOP ${maxRows} `);
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Check if a LIMIT clause uses a parameter placeholder (not a literal number).
|
|
148
|
-
* Strips comments and string literals first to avoid false positives.
|
|
149
|
-
*/
|
|
150
|
-
static hasParameterizedLimit(sql) {
|
|
151
|
-
const cleanedSQL = stripCommentsAndStrings(sql);
|
|
152
|
-
const parameterizedLimitRegex = /\blimit\s+(?:\$\d+|\?|@p\d+)/i;
|
|
153
|
-
return parameterizedLimitRegex.test(cleanedSQL);
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Apply maxRows limit to a SELECT query only
|
|
157
|
-
*
|
|
158
|
-
* This method is used by PostgreSQL, MySQL, MariaDB, and SQLite connectors which all support
|
|
159
|
-
* the LIMIT clause syntax. SQL Server uses applyMaxRowsForSQLServer() instead with TOP syntax.
|
|
160
|
-
*
|
|
161
|
-
* For parameterized LIMIT clauses (e.g., LIMIT $1 or LIMIT ?), we wrap the query in a subquery
|
|
162
|
-
* to enforce max_rows as a hard cap, since the parameter value is not known until runtime.
|
|
163
|
-
*/
|
|
164
|
-
static applyMaxRows(sql, maxRows) {
|
|
165
|
-
if (!maxRows || !this.isSelectQuery(sql)) {
|
|
166
|
-
return sql;
|
|
167
|
-
}
|
|
168
|
-
if (this.hasParameterizedLimit(sql)) {
|
|
169
|
-
const trimmed = sql.trim();
|
|
170
|
-
const hasSemicolon = trimmed.endsWith(";");
|
|
171
|
-
const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
|
|
172
|
-
return `SELECT * FROM (${sqlWithoutSemicolon}
|
|
173
|
-
) AS subq LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
|
|
174
|
-
}
|
|
175
|
-
return this.applyLimitToQuery(sql, maxRows);
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Apply maxRows limit to a SELECT query using SQL Server TOP syntax
|
|
179
|
-
*/
|
|
180
|
-
static applyMaxRowsForSQLServer(sql, maxRows) {
|
|
181
|
-
if (!maxRows || !this.isSelectQuery(sql)) {
|
|
182
|
-
return sql;
|
|
183
|
-
}
|
|
184
|
-
return this.applyTopToQuery(sql, maxRows);
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Like applyMaxRows, but requests one probe row (maxRows + 1) whenever the
|
|
188
|
-
* cap is the binding constraint, so callers can detect truncation exactly
|
|
189
|
-
* instead of guessing from `rowCount === maxRows` (which a table with
|
|
190
|
-
* exactly maxRows rows also produces).
|
|
191
|
-
*
|
|
192
|
-
* `probeApplied` is false when the cap cannot fire: no maxRows, not a
|
|
193
|
-
* SELECT, or the query's own literal LIMIT is already within the cap (the
|
|
194
|
-
* user asked for fewer rows than the cap — that is their limit, not
|
|
195
|
-
* truncation).
|
|
196
|
-
*/
|
|
197
|
-
static applyMaxRowsWithTruncationProbe(sql, maxRows) {
|
|
198
|
-
if (!maxRows || !this.isSelectQuery(sql)) {
|
|
199
|
-
return { sql, probeApplied: false };
|
|
200
|
-
}
|
|
201
|
-
if (!this.hasParameterizedLimit(sql)) {
|
|
202
|
-
const existingLimit = this.extractLimitValue(sql);
|
|
203
|
-
if (existingLimit !== null && existingLimit <= maxRows) {
|
|
204
|
-
return { sql, probeApplied: false };
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return { sql: this.applyMaxRows(sql, maxRows + 1), probeApplied: true };
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* SQL Server variant of applyMaxRowsWithTruncationProbe, using TOP syntax.
|
|
211
|
-
* A set-operator statement is always wrapped (any TOP on an individual
|
|
212
|
-
* branch caps only that branch, not the combined output), so the probe
|
|
213
|
-
* applies there regardless of branch-level TOPs.
|
|
214
|
-
*/
|
|
215
|
-
static applyMaxRowsForSQLServerWithTruncationProbe(sql, maxRows) {
|
|
216
|
-
if (!maxRows || !this.isSelectQuery(sql)) {
|
|
217
|
-
return { sql, probeApplied: false };
|
|
218
|
-
}
|
|
219
|
-
if (!this.hasSetOperator(sql)) {
|
|
220
|
-
const existingTop = this.extractTopValue(sql);
|
|
221
|
-
if (existingTop !== null && existingTop <= maxRows) {
|
|
222
|
-
return { sql, probeApplied: false };
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
return { sql: this.applyMaxRowsForSQLServer(sql, maxRows + 1), probeApplied: true };
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Post-process a result set produced by a truncation-probe rewrite: when
|
|
229
|
-
* the probe row came back (more than maxRows rows), drop it, clamp the
|
|
230
|
-
* count to maxRows, and mark the set truncated so consumers can tell a
|
|
231
|
-
* capped result from a complete one.
|
|
232
|
-
*/
|
|
233
|
-
static flagTruncation(resultSet, maxRows, probeApplied) {
|
|
234
|
-
if (!probeApplied || !maxRows || resultSet.rows.length <= maxRows) {
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
resultSet.rows = resultSet.rows.slice(0, maxRows);
|
|
238
|
-
resultSet.rowCount = maxRows;
|
|
239
|
-
resultSet.truncated = true;
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
// src/utils/resource-cleanup.ts
|
|
244
|
-
async function closeQuietly(close) {
|
|
245
|
-
try {
|
|
246
|
-
await close();
|
|
247
|
-
} catch {
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
export {
|
|
252
|
-
SQLRowLimiter,
|
|
253
|
-
closeQuietly
|
|
254
|
-
};
|