@bytebase/dbhub 1.1.0 → 1.2.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 +33 -4
- package/dist/{chunk-GSSLHJB6.js → chunk-2DFLBX4D.js} +38 -4
- package/dist/chunk-6QETXS44.js +254 -0
- package/dist/{chunk-NIILHXTG.js → chunk-A4TSDCPY.js} +26 -37
- package/dist/{chunk-EPOKWSFC.js → chunk-IQRBLAB4.js} +1 -1
- package/dist/{chunk-JEZZN2YZ.js → chunk-MF6LSVWK.js} +19 -1
- package/dist/index.js +77 -26
- package/dist/{mariadb-DUDFBY5W.js → mariadb-DQ6P57IX.js} +17 -12
- package/dist/{mysql-265M3KBD.js → mysql-GXDUNLJ6.js} +66 -14
- package/dist/{postgres-5ZOFYV6X.js → postgres-2FHS64EO.js} +33 -15
- package/dist/public/assets/{index-DBYlgGks.js → index-DOZ5HOZ9.js} +21 -20
- package/dist/public/index.html +1 -1
- package/dist/{registry-JYRRNC3E.js → registry-D2QVU6AG.js} +2 -2
- package/dist/{sqlite-5M7SDAQZ.js → sqlite-ASYMU5BI.js} +28 -19
- package/dist/{sqlserver-PSAZRH7G.js → sqlserver-WNVSBFJ3.js} +90 -14
- package/package.json +5 -2
- package/dist/chunk-S2V6RZ3C.js +0 -138
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-DOZ5HOZ9.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-2DFLBX4D.js";
|
|
6
6
|
import "./chunk-WVVMH6FJ.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-MF6LSVWK.js";
|
|
8
8
|
export {
|
|
9
9
|
ToolRegistry,
|
|
10
10
|
getToolRegistry,
|
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
SQLRowLimiter,
|
|
6
6
|
closeQuietly
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-6QETXS44.js";
|
|
8
8
|
import {
|
|
9
9
|
ConnectorRegistry,
|
|
10
10
|
SafeURL,
|
|
11
11
|
obfuscateDSNPassword,
|
|
12
12
|
splitSQLStatements
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-MF6LSVWK.js";
|
|
14
14
|
|
|
15
15
|
// src/connectors/sqlite/suppress-experimental-warning.ts
|
|
16
16
|
var installed = false;
|
|
@@ -334,17 +334,20 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
334
334
|
let processedStatement = statements[0];
|
|
335
335
|
const trimmedStatement = statements[0].toLowerCase().trim();
|
|
336
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"));
|
|
337
|
+
let probeApplied = false;
|
|
337
338
|
if (options.maxRows) {
|
|
338
|
-
|
|
339
|
+
const rewrite = SQLRowLimiter.applyMaxRowsWithTruncationProbe(
|
|
340
|
+
processedStatement,
|
|
341
|
+
options.maxRows
|
|
342
|
+
);
|
|
343
|
+
processedStatement = rewrite.sql;
|
|
344
|
+
probeApplied = rewrite.probeApplied;
|
|
339
345
|
}
|
|
340
346
|
if (isReadStatement) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const rows = this.prepare(processedStatement).all();
|
|
346
|
-
return { rows, rowCount: rows.length };
|
|
347
|
-
}
|
|
347
|
+
const rows = parameters && parameters.length > 0 ? this.prepare(processedStatement).all(...parameters) : this.prepare(processedStatement).all();
|
|
348
|
+
const resultSet = { sql: statements[0], rows, rowCount: rows.length };
|
|
349
|
+
SQLRowLimiter.flagTruncation(resultSet, options.maxRows, probeApplied);
|
|
350
|
+
return { resultSets: [resultSet] };
|
|
348
351
|
} else {
|
|
349
352
|
let result;
|
|
350
353
|
if (parameters && parameters.length > 0) {
|
|
@@ -352,7 +355,9 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
352
355
|
} else {
|
|
353
356
|
result = this.prepare(processedStatement).run();
|
|
354
357
|
}
|
|
355
|
-
return {
|
|
358
|
+
return {
|
|
359
|
+
resultSets: [{ sql: statements[0], rows: [], rowCount: Number(result.changes) }]
|
|
360
|
+
};
|
|
356
361
|
}
|
|
357
362
|
} else {
|
|
358
363
|
if (parameters && parameters.length > 0) {
|
|
@@ -368,24 +373,28 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
368
373
|
writeStatements.push(statement);
|
|
369
374
|
}
|
|
370
375
|
}
|
|
371
|
-
|
|
376
|
+
const resultSets = [];
|
|
372
377
|
for (const statement of writeStatements) {
|
|
373
378
|
if (options.readonly) {
|
|
374
379
|
this.db.exec("PRAGMA query_only = ON");
|
|
375
380
|
}
|
|
376
381
|
const result = this.prepare(statement).run();
|
|
377
|
-
|
|
382
|
+
resultSets.push({ sql: statement, rows: [], rowCount: Number(result.changes) });
|
|
378
383
|
}
|
|
379
|
-
|
|
380
|
-
for (let statement of readStatements) {
|
|
384
|
+
for (const statement of readStatements) {
|
|
381
385
|
if (options.readonly) {
|
|
382
386
|
this.db.exec("PRAGMA query_only = ON");
|
|
383
387
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
388
|
+
const { sql: processedStatement, probeApplied } = SQLRowLimiter.applyMaxRowsWithTruncationProbe(
|
|
389
|
+
statement,
|
|
390
|
+
options.maxRows
|
|
391
|
+
);
|
|
392
|
+
const rows = this.prepare(processedStatement).all();
|
|
393
|
+
const resultSet = { sql: statement, rows, rowCount: rows.length };
|
|
394
|
+
SQLRowLimiter.flagTruncation(resultSet, options.maxRows, probeApplied);
|
|
395
|
+
resultSets.push(resultSet);
|
|
387
396
|
}
|
|
388
|
-
return {
|
|
397
|
+
return { resultSets };
|
|
389
398
|
}
|
|
390
399
|
} finally {
|
|
391
400
|
if (options.readonly) {
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
sqlServerDynamicSqlPattern,
|
|
4
4
|
sqlServerPassThroughKeywords,
|
|
5
5
|
sqlServerPassThroughPattern
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-IQRBLAB4.js";
|
|
7
7
|
import {
|
|
8
8
|
isDriverNotInstalled
|
|
9
9
|
} from "./chunk-WVVMH6FJ.js";
|
|
@@ -14,13 +14,14 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
SQLRowLimiter,
|
|
16
16
|
closeQuietly
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-6QETXS44.js";
|
|
18
18
|
import {
|
|
19
19
|
ConnectorRegistry,
|
|
20
20
|
SafeURL,
|
|
21
21
|
obfuscateDSNPassword,
|
|
22
|
+
splitSQLStatements,
|
|
22
23
|
stripCommentsAndStrings
|
|
23
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-MF6LSVWK.js";
|
|
24
25
|
|
|
25
26
|
// src/connectors/sqlserver/index.ts
|
|
26
27
|
import sql from "mssql";
|
|
@@ -561,11 +562,24 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
561
562
|
}
|
|
562
563
|
try {
|
|
563
564
|
let processedSQL = sqlQuery;
|
|
565
|
+
let probeApplied = false;
|
|
564
566
|
if (options.maxRows) {
|
|
565
|
-
|
|
567
|
+
const rewrite = SQLRowLimiter.applyMaxRowsForSQLServerWithTruncationProbe(
|
|
568
|
+
sqlQuery,
|
|
569
|
+
options.maxRows
|
|
570
|
+
);
|
|
571
|
+
processedSQL = rewrite.sql;
|
|
572
|
+
probeApplied = rewrite.probeApplied;
|
|
566
573
|
}
|
|
574
|
+
const isSingleStatement = splitSQLStatements(processedSQL, "sqlserver").length === 1;
|
|
567
575
|
if (options.readonly) {
|
|
568
|
-
return await this.executeReadOnly(
|
|
576
|
+
return await this.executeReadOnly(
|
|
577
|
+
processedSQL,
|
|
578
|
+
parameters,
|
|
579
|
+
isSingleStatement ? sqlQuery : void 0,
|
|
580
|
+
options.maxRows,
|
|
581
|
+
probeApplied
|
|
582
|
+
);
|
|
569
583
|
}
|
|
570
584
|
const request = this.connection.request();
|
|
571
585
|
const messages = [];
|
|
@@ -583,15 +597,62 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
583
597
|
);
|
|
584
598
|
_SQLServerConnector.bindParameters(request, parameters);
|
|
585
599
|
const result = await request.query(processedSQL);
|
|
600
|
+
const resultSets = _SQLServerConnector.buildResultSets(
|
|
601
|
+
result.recordsets,
|
|
602
|
+
result.rowsAffected,
|
|
603
|
+
isSingleStatement ? sqlQuery : void 0
|
|
604
|
+
);
|
|
605
|
+
if (resultSets.length > 0) {
|
|
606
|
+
SQLRowLimiter.flagTruncation(resultSets[0], options.maxRows, probeApplied);
|
|
607
|
+
}
|
|
586
608
|
return {
|
|
587
|
-
|
|
588
|
-
rowCount: result.rowsAffected[0] || 0,
|
|
609
|
+
resultSets,
|
|
589
610
|
...messages.length > 0 ? { messages } : {}
|
|
590
611
|
};
|
|
591
612
|
} catch (error) {
|
|
592
613
|
throw new Error(`Failed to execute query: ${error.message}`);
|
|
593
614
|
}
|
|
594
615
|
}
|
|
616
|
+
/**
|
|
617
|
+
* Builds one result set per SELECT-producing statement in the batch, plus
|
|
618
|
+
* (if any) a trailing result set summarizing the write-only statements.
|
|
619
|
+
*
|
|
620
|
+
* node-mssql only pushes a `recordsets` entry for statements that produce
|
|
621
|
+
* columns (SELECT); INSERT/UPDATE/DELETE statements in the same batch
|
|
622
|
+
* contribute to `rowsAffected` only, with no way to recover which
|
|
623
|
+
* `rowsAffected` entry belongs to which statement from the driver's final
|
|
624
|
+
* arrays (`recordsets`/`rowsAffected` aren't index-aligned - see the
|
|
625
|
+
* tedious `doneHandler`, which always pushes to `rowsAffected` but only
|
|
626
|
+
* conditionally to `recordsets`). So a batch of pure writes collapses to a
|
|
627
|
+
* single summed result set, and a batch mixing writes with selects gets
|
|
628
|
+
* one result set per select plus one trailing "writes" set for the rest -
|
|
629
|
+
* not a truly per-statement breakdown, but no read statement's rows are
|
|
630
|
+
* ever merged with another's, which is what mattered for #380.
|
|
631
|
+
*
|
|
632
|
+
* `sourceSql`, when given, is attributed to the single resulting set - it
|
|
633
|
+
* must be omitted (pass `undefined`) unless the caller has already
|
|
634
|
+
* confirmed the batch is unambiguously one statement, since for a genuine
|
|
635
|
+
* multi-statement batch there's no reliable way to say which source
|
|
636
|
+
* statement a given recordset (or the trailing writes set) came from.
|
|
637
|
+
*/
|
|
638
|
+
static buildResultSets(recordsets, rowsAffected, sourceSql) {
|
|
639
|
+
const sets = (recordsets ?? []).map((recordset) => {
|
|
640
|
+
const rows = recordset ?? [];
|
|
641
|
+
return { rows, rowCount: rows.length };
|
|
642
|
+
});
|
|
643
|
+
const totalAffected = (rowsAffected ?? []).reduce((total, count) => total + (count ?? 0), 0);
|
|
644
|
+
const accountedFor = sets.reduce((total, set) => total + set.rowCount, 0);
|
|
645
|
+
const writesOnly = totalAffected - accountedFor;
|
|
646
|
+
if (sets.length === 0) {
|
|
647
|
+
sets.push({ rows: [], rowCount: totalAffected });
|
|
648
|
+
} else if (writesOnly > 0) {
|
|
649
|
+
sets.push({ rows: [], rowCount: writesOnly });
|
|
650
|
+
}
|
|
651
|
+
if (sets.length === 1 && sourceSql !== void 0) {
|
|
652
|
+
sets[0].sql = sourceSql;
|
|
653
|
+
}
|
|
654
|
+
return sets;
|
|
655
|
+
}
|
|
595
656
|
/**
|
|
596
657
|
* Bind positional parameters as @p1, @p2, ... inferring the SQL Server type
|
|
597
658
|
* from each JavaScript value. Shared by every execution path so they cannot
|
|
@@ -673,7 +734,7 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
673
734
|
* Dangerous constructs are rejected before the transaction opens; see
|
|
674
735
|
* assertNoReadOnlyEscapes.
|
|
675
736
|
*/
|
|
676
|
-
async executeReadOnly(processedSQL, parameters) {
|
|
737
|
+
async executeReadOnly(processedSQL, parameters, sourceSql, maxRows, probeApplied) {
|
|
677
738
|
this.assertNoReadOnlyEscapes(processedSQL, { transactionControl: true });
|
|
678
739
|
const transaction = new sql.Transaction(this.connection);
|
|
679
740
|
await transaction.begin();
|
|
@@ -709,9 +770,16 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
709
770
|
}
|
|
710
771
|
}
|
|
711
772
|
}
|
|
773
|
+
const resultSets = _SQLServerConnector.buildResultSets(
|
|
774
|
+
result.recordsets,
|
|
775
|
+
result.rowsAffected,
|
|
776
|
+
sourceSql
|
|
777
|
+
);
|
|
778
|
+
if (resultSets.length > 0) {
|
|
779
|
+
SQLRowLimiter.flagTruncation(resultSets[0], maxRows, probeApplied);
|
|
780
|
+
}
|
|
712
781
|
return {
|
|
713
|
-
|
|
714
|
-
rowCount: result.rowsAffected[0] || 0,
|
|
782
|
+
resultSets,
|
|
715
783
|
...messages.length > 0 ? { messages } : {}
|
|
716
784
|
};
|
|
717
785
|
}
|
|
@@ -756,8 +824,12 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
756
824
|
const planRow = planResult.recordset?.[0];
|
|
757
825
|
const planXml = planRow ? Object.values(planRow)[0] : null;
|
|
758
826
|
return {
|
|
759
|
-
|
|
760
|
-
|
|
827
|
+
resultSets: [
|
|
828
|
+
{
|
|
829
|
+
rows: planXml != null ? [{ plan: planXml }] : [],
|
|
830
|
+
rowCount: planXml != null ? 1 : 0
|
|
831
|
+
}
|
|
832
|
+
]
|
|
761
833
|
};
|
|
762
834
|
} catch (error) {
|
|
763
835
|
throw new Error(`Failed to explain query: ${error.message}`);
|
|
@@ -878,8 +950,12 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
878
950
|
}
|
|
879
951
|
const planXml = _SQLServerConnector.extractPlanXml(planResult);
|
|
880
952
|
return {
|
|
881
|
-
|
|
882
|
-
|
|
953
|
+
resultSets: [
|
|
954
|
+
{
|
|
955
|
+
rows: planXml != null ? [{ plan: planXml }] : [],
|
|
956
|
+
rowCount: planXml != null ? 1 : 0
|
|
957
|
+
}
|
|
958
|
+
]
|
|
883
959
|
};
|
|
884
960
|
} catch (error) {
|
|
885
961
|
throw new Error(`Failed to explain analyze query: ${error.message}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebase/dbhub",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"mcpName": "io.github.bytebase/dbhub",
|
|
5
5
|
"description": "Minimal, token-efficient Database MCP Server for PostgreSQL, MySQL, SQL Server, SQLite, MariaDB",
|
|
6
6
|
"repository": {
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
"test:unit": "vitest run --project unit",
|
|
34
34
|
"test:watch": "vitest",
|
|
35
35
|
"test:integration": "vitest run --project integration",
|
|
36
|
-
"test:build": "node scripts/smoke-test-build.mjs"
|
|
36
|
+
"test:build": "node scripts/smoke-test-build.mjs",
|
|
37
|
+
"sync-version": "node scripts/sync-version.mjs",
|
|
38
|
+
"version": "pnpm run sync-version && git add plugin/.claude-plugin/plugin.json plugin/.mcp.json server.json"
|
|
37
39
|
},
|
|
38
40
|
"engines": {
|
|
39
41
|
"node": ">=22.5.0"
|
|
@@ -53,6 +55,7 @@
|
|
|
53
55
|
"zod": "^4.2.0"
|
|
54
56
|
},
|
|
55
57
|
"optionalDependencies": {
|
|
58
|
+
"@aws-sdk/credential-providers": "^3.1001.0",
|
|
56
59
|
"@aws-sdk/rds-signer": "^3.1001.0",
|
|
57
60
|
"@azure/identity": "^4.8.0",
|
|
58
61
|
"mariadb": "^3.4.0",
|
package/dist/chunk-S2V6RZ3C.js
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
stripCommentsAndStrings
|
|
3
|
-
} from "./chunk-JEZZN2YZ.js";
|
|
4
|
-
|
|
5
|
-
// src/utils/sql-row-limiter.ts
|
|
6
|
-
var SQLRowLimiter = class {
|
|
7
|
-
/**
|
|
8
|
-
* Check if a SQL statement is a SELECT query that can benefit from row limiting
|
|
9
|
-
* Only handles SELECT queries
|
|
10
|
-
*/
|
|
11
|
-
static isSelectQuery(sql) {
|
|
12
|
-
const trimmed = sql.trim().toLowerCase();
|
|
13
|
-
return trimmed.startsWith("select");
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Check if a SQL statement already has a LIMIT clause.
|
|
17
|
-
* Strips comments and string literals first to avoid false positives.
|
|
18
|
-
*/
|
|
19
|
-
static hasLimitClause(sql) {
|
|
20
|
-
const cleanedSQL = stripCommentsAndStrings(sql);
|
|
21
|
-
const limitRegex = /\blimit\s+(?:\d+|\$\d+|\?|@p\d+)/i;
|
|
22
|
-
return limitRegex.test(cleanedSQL);
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Check if a SQL statement already has a TOP clause (SQL Server).
|
|
26
|
-
* Strips comments and string literals first to avoid false positives.
|
|
27
|
-
*/
|
|
28
|
-
static hasTopClause(sql) {
|
|
29
|
-
const cleanedSQL = stripCommentsAndStrings(sql);
|
|
30
|
-
const topRegex = /\bselect\s+top\s+\d+/i;
|
|
31
|
-
return topRegex.test(cleanedSQL);
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Extract existing LIMIT value from SQL if present.
|
|
35
|
-
* Strips comments and string literals first to avoid false positives.
|
|
36
|
-
*/
|
|
37
|
-
static extractLimitValue(sql) {
|
|
38
|
-
const cleanedSQL = stripCommentsAndStrings(sql);
|
|
39
|
-
const limitMatch = cleanedSQL.match(/\blimit\s+(\d+)/i);
|
|
40
|
-
if (limitMatch) {
|
|
41
|
-
return parseInt(limitMatch[1], 10);
|
|
42
|
-
}
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Extract existing TOP value from SQL if present (SQL Server).
|
|
47
|
-
* Strips comments and string literals first to avoid false positives.
|
|
48
|
-
*/
|
|
49
|
-
static extractTopValue(sql) {
|
|
50
|
-
const cleanedSQL = stripCommentsAndStrings(sql);
|
|
51
|
-
const topMatch = cleanedSQL.match(/\bselect\s+top\s+(\d+)/i);
|
|
52
|
-
if (topMatch) {
|
|
53
|
-
return parseInt(topMatch[1], 10);
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Add or modify LIMIT clause in a SQL statement
|
|
59
|
-
*/
|
|
60
|
-
static applyLimitToQuery(sql, maxRows) {
|
|
61
|
-
const existingLimit = this.extractLimitValue(sql);
|
|
62
|
-
if (existingLimit !== null) {
|
|
63
|
-
const effectiveLimit = Math.min(existingLimit, maxRows);
|
|
64
|
-
return sql.replace(/\blimit\s+\d+/i, `LIMIT ${effectiveLimit}`);
|
|
65
|
-
} else {
|
|
66
|
-
const trimmed = sql.trim();
|
|
67
|
-
const hasSemicolon = trimmed.endsWith(";");
|
|
68
|
-
const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
|
|
69
|
-
return `${sqlWithoutSemicolon}
|
|
70
|
-
LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Add or modify TOP clause in a SQL statement (SQL Server)
|
|
75
|
-
*/
|
|
76
|
-
static applyTopToQuery(sql, maxRows) {
|
|
77
|
-
const existingTop = this.extractTopValue(sql);
|
|
78
|
-
if (existingTop !== null) {
|
|
79
|
-
const effectiveTop = Math.min(existingTop, maxRows);
|
|
80
|
-
return sql.replace(/\bselect\s+top\s+\d+/i, `SELECT TOP ${effectiveTop}`);
|
|
81
|
-
} else {
|
|
82
|
-
return sql.replace(/\bselect\s+/i, `SELECT TOP ${maxRows} `);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Check if a LIMIT clause uses a parameter placeholder (not a literal number).
|
|
87
|
-
* Strips comments and string literals first to avoid false positives.
|
|
88
|
-
*/
|
|
89
|
-
static hasParameterizedLimit(sql) {
|
|
90
|
-
const cleanedSQL = stripCommentsAndStrings(sql);
|
|
91
|
-
const parameterizedLimitRegex = /\blimit\s+(?:\$\d+|\?|@p\d+)/i;
|
|
92
|
-
return parameterizedLimitRegex.test(cleanedSQL);
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Apply maxRows limit to a SELECT query only
|
|
96
|
-
*
|
|
97
|
-
* This method is used by PostgreSQL, MySQL, MariaDB, and SQLite connectors which all support
|
|
98
|
-
* the LIMIT clause syntax. SQL Server uses applyMaxRowsForSQLServer() instead with TOP syntax.
|
|
99
|
-
*
|
|
100
|
-
* For parameterized LIMIT clauses (e.g., LIMIT $1 or LIMIT ?), we wrap the query in a subquery
|
|
101
|
-
* to enforce max_rows as a hard cap, since the parameter value is not known until runtime.
|
|
102
|
-
*/
|
|
103
|
-
static applyMaxRows(sql, maxRows) {
|
|
104
|
-
if (!maxRows || !this.isSelectQuery(sql)) {
|
|
105
|
-
return sql;
|
|
106
|
-
}
|
|
107
|
-
if (this.hasParameterizedLimit(sql)) {
|
|
108
|
-
const trimmed = sql.trim();
|
|
109
|
-
const hasSemicolon = trimmed.endsWith(";");
|
|
110
|
-
const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
|
|
111
|
-
return `SELECT * FROM (${sqlWithoutSemicolon}
|
|
112
|
-
) AS subq LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
|
|
113
|
-
}
|
|
114
|
-
return this.applyLimitToQuery(sql, maxRows);
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Apply maxRows limit to a SELECT query using SQL Server TOP syntax
|
|
118
|
-
*/
|
|
119
|
-
static applyMaxRowsForSQLServer(sql, maxRows) {
|
|
120
|
-
if (!maxRows || !this.isSelectQuery(sql)) {
|
|
121
|
-
return sql;
|
|
122
|
-
}
|
|
123
|
-
return this.applyTopToQuery(sql, maxRows);
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// src/utils/resource-cleanup.ts
|
|
128
|
-
async function closeQuietly(close) {
|
|
129
|
-
try {
|
|
130
|
-
await close();
|
|
131
|
-
} catch {
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export {
|
|
136
|
-
SQLRowLimiter,
|
|
137
|
-
closeQuietly
|
|
138
|
-
};
|