@bytebase/dbhub 0.23.0 → 0.24.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/dist/{chunk-BXXHWEQS.js → chunk-D2BIPC7V.js} +10 -1
- 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/index.js +14 -107
- package/dist/{mariadb-SPFBL46R.js → mariadb-VZMU7LYA.js} +58 -45
- package/dist/{mysql-MZWCPISB.js → mysql-MHIZK2UY.js} +59 -46
- package/dist/{postgres-BKBCGDZS.js → postgres-ZQFXCBYA.js} +8 -2
- package/dist/{registry-ZME64VB2.js → registry-ZU6ZFNQ2.js} +1 -1
- package/dist/{sqlite-CQ4LCBB6.js → sqlite-JH5CNV5E.js} +8 -2
- package/dist/{sqlserver-65AGCEJX.js → sqlserver-LHWZ7WOP.js} +61 -20
- package/package.json +1 -1
|
@@ -122,6 +122,15 @@ var SQLRowLimiter = class {
|
|
|
122
122
|
}
|
|
123
123
|
};
|
|
124
124
|
|
|
125
|
+
// src/utils/resource-cleanup.ts
|
|
126
|
+
async function closeQuietly(close) {
|
|
127
|
+
try {
|
|
128
|
+
await close();
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
125
133
|
export {
|
|
126
|
-
SQLRowLimiter
|
|
134
|
+
SQLRowLimiter,
|
|
135
|
+
closeQuietly
|
|
127
136
|
};
|
|
@@ -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
|
}
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,11 @@ import {
|
|
|
15
15
|
resolveSourceConfigs,
|
|
16
16
|
resolveTomlConfigPath,
|
|
17
17
|
resolveTransport
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-MQR6VX5F.js";
|
|
19
|
+
import {
|
|
20
|
+
allowedKeywords,
|
|
21
|
+
isReadOnlySQL
|
|
22
|
+
} from "./chunk-JO7YQPBQ.js";
|
|
19
23
|
import {
|
|
20
24
|
loadConnectors
|
|
21
25
|
} from "./chunk-WVVMH6FJ.js";
|
|
@@ -27,8 +31,7 @@ import {
|
|
|
27
31
|
getDatabaseTypeFromDSN,
|
|
28
32
|
getDefaultPortForType,
|
|
29
33
|
parseConnectionInfoFromDSN,
|
|
30
|
-
splitSQLStatements
|
|
31
|
-
stripCommentsAndStrings
|
|
34
|
+
splitSQLStatements
|
|
32
35
|
} from "./chunk-JEZZN2YZ.js";
|
|
33
36
|
|
|
34
37
|
// src/server.ts
|
|
@@ -95,102 +98,6 @@ function createToolSuccessResponse(data, meta = {}) {
|
|
|
95
98
|
};
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
// src/utils/allowed-keywords.ts
|
|
99
|
-
var allowedKeywords = {
|
|
100
|
-
postgres: ["select", "with", "explain", "show"],
|
|
101
|
-
mysql: ["select", "with", "explain", "show", "describe", "desc"],
|
|
102
|
-
mariadb: ["select", "with", "explain", "show", "describe", "desc"],
|
|
103
|
-
sqlite: ["select", "with", "explain", "pragma"],
|
|
104
|
-
// SQL Server has no native EXPLAIN statement; the connector translates a
|
|
105
|
-
// leading `EXPLAIN` into a SET SHOWPLAN_XML request (see SQLServerConnector).
|
|
106
|
-
sqlserver: ["select", "with", "explain"]
|
|
107
|
-
};
|
|
108
|
-
var mutatingKeywords = [
|
|
109
|
-
"insert",
|
|
110
|
-
"update",
|
|
111
|
-
"delete",
|
|
112
|
-
"drop",
|
|
113
|
-
"alter",
|
|
114
|
-
"create",
|
|
115
|
-
"truncate",
|
|
116
|
-
"merge",
|
|
117
|
-
"grant",
|
|
118
|
-
"revoke",
|
|
119
|
-
"rename"
|
|
120
|
-
];
|
|
121
|
-
var mutatingPattern = new RegExp(
|
|
122
|
-
`\\b(?:${mutatingKeywords.join("|")})\\b`,
|
|
123
|
-
"i"
|
|
124
|
-
);
|
|
125
|
-
var mutatingPatternWithReplace = new RegExp(
|
|
126
|
-
`\\b(?:${mutatingKeywords.join("|")}|replace\\s+(?:(?:low_priority|delayed)\\s+)?into)\\b`,
|
|
127
|
-
"i"
|
|
128
|
-
);
|
|
129
|
-
var mutatingPatternSqlServer = new RegExp(
|
|
130
|
-
`\\b(?:${[...mutatingKeywords, "execute", "exec", "sp_executesql", "xp_cmdshell"].join("|")})\\b`,
|
|
131
|
-
"i"
|
|
132
|
-
);
|
|
133
|
-
var mutatingPatterns = {
|
|
134
|
-
postgres: mutatingPattern,
|
|
135
|
-
mysql: mutatingPatternWithReplace,
|
|
136
|
-
mariadb: mutatingPatternWithReplace,
|
|
137
|
-
sqlite: mutatingPatternWithReplace,
|
|
138
|
-
sqlserver: mutatingPatternSqlServer
|
|
139
|
-
};
|
|
140
|
-
var selectIntoPattern = /\bselect\b[\s\S]+\binto\b/i;
|
|
141
|
-
var sqliteReadOnlyArgPragmas = /* @__PURE__ */ new Set([
|
|
142
|
-
"table_info",
|
|
143
|
-
"index_info",
|
|
144
|
-
"index_list",
|
|
145
|
-
"foreign_key_list"
|
|
146
|
-
]);
|
|
147
|
-
var sqlitePragmaParenPattern = /^pragma\s+(?:[a-z0-9_]+\.)?([a-z0-9_]+)\s*\(/;
|
|
148
|
-
var explainAnalyzePattern = /^explain\s+(?:\([^)]*\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)[^)]*\)|\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)(?:\s+verbose\b)?)/i;
|
|
149
|
-
function isReadOnlySQL(sql, connectorType) {
|
|
150
|
-
return checkReadOnly(
|
|
151
|
-
stripCommentsAndStrings(sql, connectorType).trim().toLowerCase(),
|
|
152
|
-
connectorType
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
function checkReadOnly(cleanedSQL, connectorType) {
|
|
156
|
-
if (!cleanedSQL) {
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
const firstWord = cleanedSQL.match(/\S+/)?.[0] ?? "";
|
|
160
|
-
const keywordList = allowedKeywords[connectorType] || [];
|
|
161
|
-
if (!keywordList.includes(firstWord)) {
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
if (firstWord === "with") {
|
|
165
|
-
const pattern = mutatingPatterns[connectorType] ?? mutatingPattern;
|
|
166
|
-
if (pattern.test(cleanedSQL)) {
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
if (firstWord === "pragma" && connectorType === "sqlite") {
|
|
171
|
-
if (cleanedSQL.includes("=")) {
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
const parenMatch = cleanedSQL.match(sqlitePragmaParenPattern);
|
|
175
|
-
if (parenMatch && !sqliteReadOnlyArgPragmas.has(parenMatch[1])) {
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
if ((firstWord === "select" || firstWord === "with") && selectIntoPattern.test(cleanedSQL)) {
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
if (firstWord === "explain") {
|
|
183
|
-
const m = explainAnalyzePattern.exec(cleanedSQL);
|
|
184
|
-
if (m) {
|
|
185
|
-
const afterExplain = cleanedSQL.slice(m[0].length).trim();
|
|
186
|
-
if (afterExplain && !checkReadOnly(afterExplain, connectorType)) {
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return true;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
101
|
// src/requests/store.ts
|
|
195
102
|
var RequestStore = class {
|
|
196
103
|
constructor() {
|
|
@@ -377,7 +284,7 @@ async function getTableRowCount(connector, tableName, schemaName) {
|
|
|
377
284
|
}
|
|
378
285
|
const qualifiedTable = quoteQualifiedIdentifier(tableName, schemaName, connector.id);
|
|
379
286
|
const countQuery = `SELECT COUNT(*) as count FROM ${qualifiedTable}`;
|
|
380
|
-
const result = await connector.executeSQL(countQuery, { maxRows: 1 });
|
|
287
|
+
const result = await connector.executeSQL(countQuery, { maxRows: 1, readonly: true });
|
|
381
288
|
if (result.rows && result.rows.length > 0) {
|
|
382
289
|
return Number(result.rows[0].count || result.rows[0].COUNT || 0);
|
|
383
290
|
}
|
|
@@ -510,7 +417,7 @@ async function searchViews(connector, pattern, schemaFilter, detailLevel, limit)
|
|
|
510
417
|
if (schemaFilter) {
|
|
511
418
|
schemasToSearch = [schemaFilter];
|
|
512
419
|
} else {
|
|
513
|
-
schemasToSearch = await connector
|
|
420
|
+
schemasToSearch = await resolveDefaultSchemas(connector);
|
|
514
421
|
}
|
|
515
422
|
for (const schemaName of schemasToSearch) {
|
|
516
423
|
if (results.length >= limit) break;
|
|
@@ -1676,7 +1583,7 @@ See documentation for more details on configuring database connections.
|
|
|
1676
1583
|
const sources = sourceConfigsData.sources;
|
|
1677
1584
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
1678
1585
|
await connectorManager.connectWithSources(sources);
|
|
1679
|
-
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-
|
|
1586
|
+
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-ZU6ZFNQ2.js");
|
|
1680
1587
|
initializeToolRegistry2({
|
|
1681
1588
|
sources: sourceConfigsData.sources,
|
|
1682
1589
|
tools: sourceConfigsData.tools
|
|
@@ -1830,11 +1737,11 @@ See documentation for more details on configuring database connections.
|
|
|
1830
1737
|
|
|
1831
1738
|
// src/index.ts
|
|
1832
1739
|
var connectorModules = [
|
|
1833
|
-
{ load: () => import("./postgres-
|
|
1834
|
-
{ load: () => import("./sqlserver-
|
|
1835
|
-
{ load: () => import("./sqlite-
|
|
1836
|
-
{ load: () => import("./mysql-
|
|
1837
|
-
{ load: () => import("./mariadb-
|
|
1740
|
+
{ load: () => import("./postgres-ZQFXCBYA.js"), name: "PostgreSQL", driver: "pg" },
|
|
1741
|
+
{ load: () => import("./sqlserver-LHWZ7WOP.js"), name: "SQL Server", driver: "mssql" },
|
|
1742
|
+
{ load: () => import("./sqlite-JH5CNV5E.js"), name: "SQLite", driver: "node:sqlite" },
|
|
1743
|
+
{ load: () => import("./mysql-MHIZK2UY.js"), name: "MySQL", driver: "mysql2" },
|
|
1744
|
+
{ load: () => import("./mariadb-VZMU7LYA.js"), name: "MariaDB", driver: "mariadb" }
|
|
1838
1745
|
];
|
|
1839
1746
|
loadConnectors(connectorModules).then(() => main()).catch((error) => {
|
|
1840
1747
|
console.error("Fatal error:", error);
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
+
MissingDatabaseError,
|
|
2
3
|
extractAffectedRows,
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
isTiDBVersion,
|
|
5
|
+
parseQueryResults,
|
|
6
|
+
requireDatabaseInDSN,
|
|
7
|
+
withReadOnlyTransaction
|
|
8
|
+
} from "./chunk-DUCNPMNW.js";
|
|
5
9
|
import {
|
|
6
10
|
quoteIdentifier
|
|
7
11
|
} from "./chunk-JFWX35TB.js";
|
|
8
12
|
import {
|
|
9
|
-
SQLRowLimiter
|
|
10
|
-
|
|
13
|
+
SQLRowLimiter,
|
|
14
|
+
closeQuietly
|
|
15
|
+
} from "./chunk-D2BIPC7V.js";
|
|
11
16
|
import {
|
|
12
17
|
ConnectorRegistry,
|
|
13
18
|
SafeURL,
|
|
@@ -32,11 +37,12 @@ Expected: ${expectedFormat}`
|
|
|
32
37
|
}
|
|
33
38
|
try {
|
|
34
39
|
const url = new SafeURL(dsn);
|
|
40
|
+
const database = url.pathname ? url.pathname.substring(1) : "";
|
|
41
|
+
requireDatabaseInDSN(database, dsn, "MariaDB");
|
|
35
42
|
const connectionConfig = {
|
|
36
43
|
host: url.hostname,
|
|
37
44
|
port: url.port ? parseInt(url.port) : 3306,
|
|
38
|
-
database
|
|
39
|
-
// Remove leading '/' if exists
|
|
45
|
+
database,
|
|
40
46
|
user: url.username,
|
|
41
47
|
password: url.password,
|
|
42
48
|
multipleStatements: true,
|
|
@@ -77,6 +83,9 @@ Expected: ${expectedFormat}`
|
|
|
77
83
|
}
|
|
78
84
|
return connectionConfig;
|
|
79
85
|
} catch (error) {
|
|
86
|
+
if (error instanceof MissingDatabaseError) {
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
80
89
|
throw new Error(
|
|
81
90
|
`Failed to parse MariaDB DSN: ${error instanceof Error ? error.message : String(error)}`
|
|
82
91
|
);
|
|
@@ -101,6 +110,9 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
101
110
|
this.pool = null;
|
|
102
111
|
// Source ID is set by ConnectorManager after cloning
|
|
103
112
|
this.sourceId = "default";
|
|
113
|
+
// TiDB speaks the MySQL protocol but rejects `START TRANSACTION READ ONLY`
|
|
114
|
+
// unless tidb_enable_noop_functions is on. Detected once at connect time.
|
|
115
|
+
this.supportsReadOnlyTransaction = true;
|
|
104
116
|
}
|
|
105
117
|
getId() {
|
|
106
118
|
return this.sourceId;
|
|
@@ -112,8 +124,14 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
112
124
|
try {
|
|
113
125
|
const connectionConfig = await this.dsnParser.parse(dsn, config);
|
|
114
126
|
this.pool = mariadb.createPool(connectionConfig);
|
|
115
|
-
await this.pool.query("SELECT
|
|
127
|
+
const rows = await this.pool.query("SELECT VERSION() AS version");
|
|
128
|
+
this.supportsReadOnlyTransaction = !isTiDBVersion(rows?.[0]?.version);
|
|
116
129
|
} catch (err) {
|
|
130
|
+
if (this.pool) {
|
|
131
|
+
const pool = this.pool;
|
|
132
|
+
this.pool = null;
|
|
133
|
+
await closeQuietly(() => pool.end());
|
|
134
|
+
}
|
|
117
135
|
console.error("Failed to connect to MariaDB database:", err);
|
|
118
136
|
throw err;
|
|
119
137
|
}
|
|
@@ -473,46 +491,41 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
473
491
|
}
|
|
474
492
|
const conn = await this.pool.getConnection();
|
|
475
493
|
try {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
494
|
+
return await withReadOnlyTransaction(
|
|
495
|
+
conn,
|
|
496
|
+
options.readonly,
|
|
497
|
+
this.supportsReadOnlyTransaction,
|
|
498
|
+
async () => {
|
|
499
|
+
let processedSQL = sql;
|
|
500
|
+
if (options.maxRows) {
|
|
501
|
+
const statements = splitSQLStatements(sql, "mariadb");
|
|
502
|
+
const processedStatements = statements.map(
|
|
503
|
+
(statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
|
|
504
|
+
);
|
|
505
|
+
processedSQL = processedStatements.join("; ");
|
|
506
|
+
if (sql.trim().endsWith(";")) {
|
|
507
|
+
processedSQL += ";";
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
let results;
|
|
511
|
+
if (parameters && parameters.length > 0) {
|
|
512
|
+
try {
|
|
513
|
+
results = await conn.query(processedSQL, parameters);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
console.error(`[MariaDB executeSQL] ERROR: ${error.message}`);
|
|
516
|
+
console.error(`[MariaDB executeSQL] SQL: ${processedSQL}`);
|
|
517
|
+
console.error(`[MariaDB executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
results = await conn.query(processedSQL);
|
|
522
|
+
}
|
|
523
|
+
const rows = parseQueryResults(results);
|
|
524
|
+
const rowCount = extractAffectedRows(results);
|
|
525
|
+
return { rows, rowCount };
|
|
499
526
|
}
|
|
500
|
-
|
|
501
|
-
results = await conn.query(processedSQL);
|
|
502
|
-
}
|
|
503
|
-
const rows = parseQueryResults(results);
|
|
504
|
-
const rowCount = extractAffectedRows(results);
|
|
505
|
-
if (options.readonly) {
|
|
506
|
-
await conn.query("COMMIT");
|
|
507
|
-
}
|
|
508
|
-
return { rows, rowCount };
|
|
527
|
+
);
|
|
509
528
|
} catch (error) {
|
|
510
|
-
if (options.readonly) {
|
|
511
|
-
try {
|
|
512
|
-
await conn.query("ROLLBACK");
|
|
513
|
-
} catch {
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
529
|
console.error("Error executing query:", error);
|
|
517
530
|
throw error;
|
|
518
531
|
} finally {
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
+
MissingDatabaseError,
|
|
2
3
|
extractAffectedRows,
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
isTiDBVersion,
|
|
5
|
+
parseQueryResults,
|
|
6
|
+
requireDatabaseInDSN,
|
|
7
|
+
withReadOnlyTransaction
|
|
8
|
+
} from "./chunk-DUCNPMNW.js";
|
|
5
9
|
import {
|
|
6
10
|
quoteIdentifier
|
|
7
11
|
} from "./chunk-JFWX35TB.js";
|
|
8
12
|
import {
|
|
9
|
-
SQLRowLimiter
|
|
10
|
-
|
|
13
|
+
SQLRowLimiter,
|
|
14
|
+
closeQuietly
|
|
15
|
+
} from "./chunk-D2BIPC7V.js";
|
|
11
16
|
import {
|
|
12
17
|
ConnectorRegistry,
|
|
13
18
|
SafeURL,
|
|
@@ -34,11 +39,12 @@ Expected: ${expectedFormat}`
|
|
|
34
39
|
}
|
|
35
40
|
try {
|
|
36
41
|
const url = new SafeURL(dsn);
|
|
42
|
+
const database = url.pathname ? url.pathname.substring(1) : "";
|
|
43
|
+
requireDatabaseInDSN(database, dsn, "MySQL");
|
|
37
44
|
const config2 = {
|
|
38
45
|
host: url.hostname,
|
|
39
46
|
port: url.port ? parseInt(url.port) : 3306,
|
|
40
|
-
database
|
|
41
|
-
// Remove leading '/' if exists
|
|
47
|
+
database,
|
|
42
48
|
user: url.username,
|
|
43
49
|
password: url.password,
|
|
44
50
|
multipleStatements: true,
|
|
@@ -79,6 +85,9 @@ Expected: ${expectedFormat}`
|
|
|
79
85
|
}
|
|
80
86
|
return config2;
|
|
81
87
|
} catch (error) {
|
|
88
|
+
if (error instanceof MissingDatabaseError) {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
82
91
|
throw new Error(
|
|
83
92
|
`Failed to parse MySQL DSN: ${error instanceof Error ? error.message : String(error)}`
|
|
84
93
|
);
|
|
@@ -103,6 +112,9 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
103
112
|
this.pool = null;
|
|
104
113
|
// Source ID is set by ConnectorManager after cloning
|
|
105
114
|
this.sourceId = "default";
|
|
115
|
+
// TiDB speaks the MySQL protocol but rejects `START TRANSACTION READ ONLY`
|
|
116
|
+
// unless tidb_enable_noop_functions is on. Detected once at connect time.
|
|
117
|
+
this.supportsReadOnlyTransaction = true;
|
|
106
118
|
}
|
|
107
119
|
getId() {
|
|
108
120
|
return this.sourceId;
|
|
@@ -117,8 +129,14 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
117
129
|
if (config?.queryTimeoutSeconds !== void 0) {
|
|
118
130
|
this.queryTimeoutMs = config.queryTimeoutSeconds * 1e3;
|
|
119
131
|
}
|
|
120
|
-
const [rows] = await this.pool.query("SELECT
|
|
132
|
+
const [rows] = await this.pool.query("SELECT VERSION() AS version");
|
|
133
|
+
this.supportsReadOnlyTransaction = !isTiDBVersion(rows[0]?.version);
|
|
121
134
|
} catch (err) {
|
|
135
|
+
if (this.pool) {
|
|
136
|
+
const pool = this.pool;
|
|
137
|
+
this.pool = null;
|
|
138
|
+
await closeQuietly(() => pool.end());
|
|
139
|
+
}
|
|
122
140
|
console.error("Failed to connect to MySQL database:", err);
|
|
123
141
|
throw err;
|
|
124
142
|
}
|
|
@@ -478,47 +496,42 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
478
496
|
}
|
|
479
497
|
const conn = await this.pool.getConnection();
|
|
480
498
|
try {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
499
|
+
return await withReadOnlyTransaction(
|
|
500
|
+
conn,
|
|
501
|
+
options.readonly,
|
|
502
|
+
this.supportsReadOnlyTransaction,
|
|
503
|
+
async () => {
|
|
504
|
+
let processedSQL = sql;
|
|
505
|
+
if (options.maxRows) {
|
|
506
|
+
const statements = splitSQLStatements(sql, "mysql");
|
|
507
|
+
const processedStatements = statements.map(
|
|
508
|
+
(statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
|
|
509
|
+
);
|
|
510
|
+
processedSQL = processedStatements.join("; ");
|
|
511
|
+
if (sql.trim().endsWith(";")) {
|
|
512
|
+
processedSQL += ";";
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
let results;
|
|
516
|
+
if (parameters && parameters.length > 0) {
|
|
517
|
+
try {
|
|
518
|
+
results = await conn.query({ sql: processedSQL, timeout: this.queryTimeoutMs }, parameters);
|
|
519
|
+
} catch (error) {
|
|
520
|
+
console.error(`[MySQL executeSQL] ERROR: ${error.message}`);
|
|
521
|
+
console.error(`[MySQL executeSQL] SQL: ${processedSQL}`);
|
|
522
|
+
console.error(`[MySQL executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
} else {
|
|
526
|
+
results = await conn.query({ sql: processedSQL, timeout: this.queryTimeoutMs });
|
|
527
|
+
}
|
|
528
|
+
const [firstResult] = results;
|
|
529
|
+
const rows = parseQueryResults(firstResult);
|
|
530
|
+
const rowCount = extractAffectedRows(firstResult);
|
|
531
|
+
return { rows, rowCount };
|
|
504
532
|
}
|
|
505
|
-
|
|
506
|
-
results = await conn.query({ sql: processedSQL, timeout: this.queryTimeoutMs });
|
|
507
|
-
}
|
|
508
|
-
const [firstResult] = results;
|
|
509
|
-
const rows = parseQueryResults(firstResult);
|
|
510
|
-
const rowCount = extractAffectedRows(firstResult);
|
|
511
|
-
if (options.readonly) {
|
|
512
|
-
await conn.query("COMMIT");
|
|
513
|
-
}
|
|
514
|
-
return { rows, rowCount };
|
|
533
|
+
);
|
|
515
534
|
} catch (error) {
|
|
516
|
-
if (options.readonly) {
|
|
517
|
-
try {
|
|
518
|
-
await conn.query("ROLLBACK");
|
|
519
|
-
} catch {
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
535
|
console.error("Error executing query:", error);
|
|
523
536
|
throw error;
|
|
524
537
|
} finally {
|
|
@@ -2,8 +2,9 @@ import {
|
|
|
2
2
|
quoteIdentifier
|
|
3
3
|
} from "./chunk-JFWX35TB.js";
|
|
4
4
|
import {
|
|
5
|
-
SQLRowLimiter
|
|
6
|
-
|
|
5
|
+
SQLRowLimiter,
|
|
6
|
+
closeQuietly
|
|
7
|
+
} from "./chunk-D2BIPC7V.js";
|
|
7
8
|
import {
|
|
8
9
|
ConnectorRegistry,
|
|
9
10
|
SafeURL,
|
|
@@ -148,6 +149,11 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
148
149
|
const client = await this.pool.connect();
|
|
149
150
|
client.release();
|
|
150
151
|
} catch (err) {
|
|
152
|
+
if (this.pool) {
|
|
153
|
+
const pool = this.pool;
|
|
154
|
+
this.pool = null;
|
|
155
|
+
await closeQuietly(() => pool.end());
|
|
156
|
+
}
|
|
151
157
|
console.error("Failed to connect to PostgreSQL database:", err);
|
|
152
158
|
throw err;
|
|
153
159
|
}
|
|
@@ -2,8 +2,9 @@ import {
|
|
|
2
2
|
quoteIdentifier
|
|
3
3
|
} from "./chunk-JFWX35TB.js";
|
|
4
4
|
import {
|
|
5
|
-
SQLRowLimiter
|
|
6
|
-
|
|
5
|
+
SQLRowLimiter,
|
|
6
|
+
closeQuietly
|
|
7
|
+
} from "./chunk-D2BIPC7V.js";
|
|
7
8
|
import {
|
|
8
9
|
ConnectorRegistry,
|
|
9
10
|
SafeURL,
|
|
@@ -150,6 +151,11 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
150
151
|
this.db.exec(initScript);
|
|
151
152
|
}
|
|
152
153
|
} catch (error) {
|
|
154
|
+
if (this.db) {
|
|
155
|
+
const db = this.db;
|
|
156
|
+
this.db = null;
|
|
157
|
+
await closeQuietly(() => db.close());
|
|
158
|
+
}
|
|
153
159
|
console.error("Failed to connect to SQLite database:", error);
|
|
154
160
|
throw error;
|
|
155
161
|
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sqlServerDynamicSqlKeywords,
|
|
3
|
+
sqlServerDynamicSqlPattern,
|
|
4
|
+
sqlServerPassThroughKeywords,
|
|
5
|
+
sqlServerPassThroughPattern
|
|
6
|
+
} from "./chunk-JO7YQPBQ.js";
|
|
1
7
|
import {
|
|
2
8
|
isDriverNotInstalled
|
|
3
9
|
} from "./chunk-WVVMH6FJ.js";
|
|
4
10
|
import {
|
|
5
|
-
SQLRowLimiter
|
|
6
|
-
|
|
11
|
+
SQLRowLimiter,
|
|
12
|
+
closeQuietly
|
|
13
|
+
} from "./chunk-D2BIPC7V.js";
|
|
7
14
|
import {
|
|
8
15
|
ConnectorRegistry,
|
|
9
16
|
SafeURL,
|
|
@@ -156,8 +163,14 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
156
163
|
if (!this.config.options) {
|
|
157
164
|
this.config.options = {};
|
|
158
165
|
}
|
|
159
|
-
this.connection =
|
|
166
|
+
this.connection = new sql.ConnectionPool(this.config);
|
|
167
|
+
await this.connection.connect();
|
|
160
168
|
} catch (error) {
|
|
169
|
+
if (this.connection) {
|
|
170
|
+
const connection = this.connection;
|
|
171
|
+
this.connection = void 0;
|
|
172
|
+
await closeQuietly(() => connection.close());
|
|
173
|
+
}
|
|
161
174
|
throw error;
|
|
162
175
|
}
|
|
163
176
|
}
|
|
@@ -462,7 +475,7 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
462
475
|
sqlQuery.match(_SQLServerConnector.LEADING_NOISE)[0].length
|
|
463
476
|
);
|
|
464
477
|
if (/^explain\b/i.test(afterNoise)) {
|
|
465
|
-
return this.explainQuery(afterNoise.slice("explain".length).trim());
|
|
478
|
+
return this.explainQuery(afterNoise.slice("explain".length).trim(), options.readonly);
|
|
466
479
|
}
|
|
467
480
|
try {
|
|
468
481
|
let processedSQL = sqlQuery;
|
|
@@ -529,30 +542,55 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
529
542
|
}
|
|
530
543
|
}
|
|
531
544
|
/**
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
* transaction mode, so this is the defense-in-depth backstop behind the
|
|
535
|
-
* keyword classifier.
|
|
545
|
+
* Reject the constructs that escape SQL Server's read-only guards, for use by
|
|
546
|
+
* both read-only execution paths.
|
|
536
547
|
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
* -
|
|
541
|
-
* -
|
|
542
|
-
*
|
|
548
|
+
* - Dynamic SQL (sqlServerDynamicSqlKeywords): can carry hidden COMMIT/ROLLBACK
|
|
549
|
+
* inside string literals that stripCommentsAndStrings removes
|
|
550
|
+
* - Pass-through data sources (sqlServerPassThroughKeywords): execute on a
|
|
551
|
+
* remote or ad-hoc source, so a local rollback never reaches them
|
|
552
|
+
* - COMMIT/ROLLBACK, when `transactionControl` is set: would end the wrapping
|
|
553
|
+
* transaction, letting writes persist. Only meaningful for the transaction
|
|
554
|
+
* path; the EXPLAIN path opens no transaction of its own.
|
|
555
|
+
*
|
|
556
|
+
* Both keyword lists are imported from the read-only classifier rather than
|
|
557
|
+
* redeclared, so the classifier and these backstops cannot drift apart.
|
|
558
|
+
*
|
|
559
|
+
* Note the COMMIT/ROLLBACK check is SQL Server-only by design. MySQL/MariaDB
|
|
560
|
+
* wrap batches in a transaction too, but there `commit`, `prepare` and
|
|
561
|
+
* `execute` are absent from their allow-lists in allowedKeywords, and
|
|
562
|
+
* execute-sql.ts requires every split statement to pass the classifier — so a
|
|
563
|
+
* transaction-control statement can never reach their backstop.
|
|
543
564
|
*/
|
|
544
|
-
|
|
545
|
-
const cleaned = stripCommentsAndStrings(
|
|
546
|
-
if (/\b(?:commit|rollback)\b/.test(cleaned)) {
|
|
565
|
+
assertNoReadOnlyEscapes(sqlText, { transactionControl = false } = {}) {
|
|
566
|
+
const cleaned = stripCommentsAndStrings(sqlText, "sqlserver").toLowerCase();
|
|
567
|
+
if (transactionControl && /\b(?:commit|rollback)\b/.test(cleaned)) {
|
|
547
568
|
throw new Error(
|
|
548
569
|
"Read-only mode: transaction control statements (COMMIT, ROLLBACK) are not allowed"
|
|
549
570
|
);
|
|
550
571
|
}
|
|
551
|
-
if (
|
|
572
|
+
if (sqlServerDynamicSqlPattern.test(cleaned)) {
|
|
573
|
+
throw new Error(
|
|
574
|
+
`Read-only mode: dynamic SQL execution (${sqlServerDynamicSqlKeywords.map((k) => k.toUpperCase()).join(", ")}) is not allowed`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
if (sqlServerPassThroughPattern.test(cleaned)) {
|
|
552
578
|
throw new Error(
|
|
553
|
-
|
|
579
|
+
`Read-only mode: pass-through data sources (${sqlServerPassThroughKeywords.map((k) => k.toUpperCase()).join(", ")}) are not allowed`
|
|
554
580
|
);
|
|
555
581
|
}
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Execute a query inside a transaction that always rolls back, preventing
|
|
585
|
+
* any modifications from persisting. SQL Server has no native READ ONLY
|
|
586
|
+
* transaction mode, so this is the defense-in-depth backstop behind the
|
|
587
|
+
* keyword classifier.
|
|
588
|
+
*
|
|
589
|
+
* Dangerous constructs are rejected before the transaction opens; see
|
|
590
|
+
* assertNoReadOnlyEscapes.
|
|
591
|
+
*/
|
|
592
|
+
async executeReadOnly(processedSQL, parameters) {
|
|
593
|
+
this.assertNoReadOnlyEscapes(processedSQL, { transactionControl: true });
|
|
556
594
|
const transaction = new sql.Transaction(this.connection);
|
|
557
595
|
await transaction.begin();
|
|
558
596
|
const request = new sql.Request(transaction);
|
|
@@ -633,11 +671,14 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
633
671
|
* off the shared pool, so a concurrent query can never land on a connection
|
|
634
672
|
* with SHOWPLAN enabled (which would return a plan instead of its results).
|
|
635
673
|
*/
|
|
636
|
-
async explainQuery(innerQuery) {
|
|
674
|
+
async explainQuery(innerQuery, readonly) {
|
|
637
675
|
const cleaned = stripCommentsAndStrings(innerQuery, "sqlserver").trim();
|
|
638
676
|
if (!cleaned) {
|
|
639
677
|
throw new Error("EXPLAIN requires a statement to analyze");
|
|
640
678
|
}
|
|
679
|
+
if (readonly) {
|
|
680
|
+
this.assertNoReadOnlyEscapes(innerQuery);
|
|
681
|
+
}
|
|
641
682
|
if (/\bset\s+showplan/i.test(cleaned)) {
|
|
642
683
|
throw new Error("EXPLAIN does not support SET SHOWPLAN statements");
|
|
643
684
|
}
|
package/package.json
CHANGED