@bytebase/dbhub 0.19.1 → 0.20.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-LUNM7TUY.js → chunk-25VMLRAQ.js} +25 -477
- package/dist/chunk-B6JS6INF.js +3644 -0
- package/dist/chunk-BRXZ5ZQB.js +127 -0
- package/dist/chunk-C7WEAPX4.js +485 -0
- package/dist/chunk-JFWX35TB.js +34 -0
- package/dist/chunk-OKXJNFBS.js +380 -0
- package/dist/chunk-WWAWV7DQ.js +72 -0
- package/dist/{demo-loader-PSMTLZ2T.js → demo-loader-FM5OJVDA.js} +2 -0
- package/dist/index.js +80 -2377
- package/dist/mariadb-L3YMONWJ.js +17727 -0
- package/dist/mysql-FOCVUTPX.js +15872 -0
- package/dist/postgres-JB3LPXGR.js +5559 -0
- package/dist/{registry-6VNMKD6G.js → registry-FOASCI6Y.js} +3 -1
- package/dist/sqlite-5LT56F5B.js +1099 -0
- package/dist/sqlserver-LGFLHJHL.js +38500 -0
- package/package.json +8 -6
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
stripCommentsAndStrings
|
|
3
|
+
} from "./chunk-C7WEAPX4.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} LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Add or modify TOP clause in a SQL statement (SQL Server)
|
|
74
|
+
*/
|
|
75
|
+
static applyTopToQuery(sql, maxRows) {
|
|
76
|
+
const existingTop = this.extractTopValue(sql);
|
|
77
|
+
if (existingTop !== null) {
|
|
78
|
+
const effectiveTop = Math.min(existingTop, maxRows);
|
|
79
|
+
return sql.replace(/\bselect\s+top\s+\d+/i, `SELECT TOP ${effectiveTop}`);
|
|
80
|
+
} else {
|
|
81
|
+
return sql.replace(/\bselect\s+/i, `SELECT TOP ${maxRows} `);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Check if a LIMIT clause uses a parameter placeholder (not a literal number).
|
|
86
|
+
* Strips comments and string literals first to avoid false positives.
|
|
87
|
+
*/
|
|
88
|
+
static hasParameterizedLimit(sql) {
|
|
89
|
+
const cleanedSQL = stripCommentsAndStrings(sql);
|
|
90
|
+
const parameterizedLimitRegex = /\blimit\s+(?:\$\d+|\?|@p\d+)/i;
|
|
91
|
+
return parameterizedLimitRegex.test(cleanedSQL);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Apply maxRows limit to a SELECT query only
|
|
95
|
+
*
|
|
96
|
+
* This method is used by PostgreSQL, MySQL, MariaDB, and SQLite connectors which all support
|
|
97
|
+
* the LIMIT clause syntax. SQL Server uses applyMaxRowsForSQLServer() instead with TOP syntax.
|
|
98
|
+
*
|
|
99
|
+
* For parameterized LIMIT clauses (e.g., LIMIT $1 or LIMIT ?), we wrap the query in a subquery
|
|
100
|
+
* to enforce max_rows as a hard cap, since the parameter value is not known until runtime.
|
|
101
|
+
*/
|
|
102
|
+
static applyMaxRows(sql, maxRows) {
|
|
103
|
+
if (!maxRows || !this.isSelectQuery(sql)) {
|
|
104
|
+
return sql;
|
|
105
|
+
}
|
|
106
|
+
if (this.hasParameterizedLimit(sql)) {
|
|
107
|
+
const trimmed = sql.trim();
|
|
108
|
+
const hasSemicolon = trimmed.endsWith(";");
|
|
109
|
+
const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
|
|
110
|
+
return `SELECT * FROM (${sqlWithoutSemicolon}) AS subq LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
|
|
111
|
+
}
|
|
112
|
+
return this.applyLimitToQuery(sql, maxRows);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Apply maxRows limit to a SELECT query using SQL Server TOP syntax
|
|
116
|
+
*/
|
|
117
|
+
static applyMaxRowsForSQLServer(sql, maxRows) {
|
|
118
|
+
if (!maxRows || !this.isSelectQuery(sql)) {
|
|
119
|
+
return sql;
|
|
120
|
+
}
|
|
121
|
+
return this.applyTopToQuery(sql, maxRows);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export {
|
|
126
|
+
SQLRowLimiter
|
|
127
|
+
};
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
// src/connectors/interface.ts
|
|
2
|
+
var _ConnectorRegistry = class _ConnectorRegistry {
|
|
3
|
+
/**
|
|
4
|
+
* Register a new connector
|
|
5
|
+
*/
|
|
6
|
+
static register(connector) {
|
|
7
|
+
_ConnectorRegistry.connectors.set(connector.id, connector);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Get a connector by ID
|
|
11
|
+
*/
|
|
12
|
+
static getConnector(id) {
|
|
13
|
+
return _ConnectorRegistry.connectors.get(id) || null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Get connector for a DSN string
|
|
17
|
+
* Tries to find a connector that can handle the given DSN format
|
|
18
|
+
*/
|
|
19
|
+
static getConnectorForDSN(dsn) {
|
|
20
|
+
for (const connector of _ConnectorRegistry.connectors.values()) {
|
|
21
|
+
if (connector.dsnParser.isValidDSN(dsn)) {
|
|
22
|
+
return connector;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Get all available connector IDs
|
|
29
|
+
*/
|
|
30
|
+
static getAvailableConnectors() {
|
|
31
|
+
return Array.from(_ConnectorRegistry.connectors.keys());
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get sample DSN for a specific connector
|
|
35
|
+
*/
|
|
36
|
+
static getSampleDSN(connectorType) {
|
|
37
|
+
const connector = _ConnectorRegistry.getConnector(connectorType);
|
|
38
|
+
if (!connector) return null;
|
|
39
|
+
return connector.dsnParser.getSampleDSN();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get all available sample DSNs
|
|
43
|
+
*/
|
|
44
|
+
static getAllSampleDSNs() {
|
|
45
|
+
const samples = {};
|
|
46
|
+
for (const [id, connector] of _ConnectorRegistry.connectors.entries()) {
|
|
47
|
+
samples[id] = connector.dsnParser.getSampleDSN();
|
|
48
|
+
}
|
|
49
|
+
return samples;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
_ConnectorRegistry.connectors = /* @__PURE__ */ new Map();
|
|
53
|
+
var ConnectorRegistry = _ConnectorRegistry;
|
|
54
|
+
|
|
55
|
+
// src/utils/safe-url.ts
|
|
56
|
+
var SafeURL = class {
|
|
57
|
+
/**
|
|
58
|
+
* Parse a URL and handle special characters in passwords
|
|
59
|
+
* This is a safe alternative to the URL constructor
|
|
60
|
+
*
|
|
61
|
+
* @param urlString - The DSN string to parse
|
|
62
|
+
*/
|
|
63
|
+
constructor(urlString) {
|
|
64
|
+
this.protocol = "";
|
|
65
|
+
this.hostname = "";
|
|
66
|
+
this.port = "";
|
|
67
|
+
this.pathname = "";
|
|
68
|
+
this.username = "";
|
|
69
|
+
this.password = "";
|
|
70
|
+
this.searchParams = /* @__PURE__ */ new Map();
|
|
71
|
+
if (!urlString || urlString.trim() === "") {
|
|
72
|
+
throw new Error("URL string cannot be empty");
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const protocolSeparator = urlString.indexOf("://");
|
|
76
|
+
if (protocolSeparator !== -1) {
|
|
77
|
+
this.protocol = urlString.substring(0, protocolSeparator + 1);
|
|
78
|
+
urlString = urlString.substring(protocolSeparator + 3);
|
|
79
|
+
} else {
|
|
80
|
+
throw new Error('Invalid URL format: missing protocol (e.g., "mysql://")');
|
|
81
|
+
}
|
|
82
|
+
const questionMarkIndex = urlString.indexOf("?");
|
|
83
|
+
let queryParams = "";
|
|
84
|
+
if (questionMarkIndex !== -1) {
|
|
85
|
+
queryParams = urlString.substring(questionMarkIndex + 1);
|
|
86
|
+
urlString = urlString.substring(0, questionMarkIndex);
|
|
87
|
+
queryParams.split("&").forEach((pair) => {
|
|
88
|
+
const parts = pair.split("=");
|
|
89
|
+
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
90
|
+
this.searchParams.set(parts[0], decodeURIComponent(parts[1]));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const atIndex = urlString.indexOf("@");
|
|
95
|
+
if (atIndex !== -1) {
|
|
96
|
+
const auth = urlString.substring(0, atIndex);
|
|
97
|
+
urlString = urlString.substring(atIndex + 1);
|
|
98
|
+
const colonIndex2 = auth.indexOf(":");
|
|
99
|
+
if (colonIndex2 !== -1) {
|
|
100
|
+
this.username = auth.substring(0, colonIndex2);
|
|
101
|
+
this.password = auth.substring(colonIndex2 + 1);
|
|
102
|
+
this.username = decodeURIComponent(this.username);
|
|
103
|
+
this.password = decodeURIComponent(this.password);
|
|
104
|
+
} else {
|
|
105
|
+
this.username = auth;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const pathSeparatorIndex = urlString.indexOf("/");
|
|
109
|
+
if (pathSeparatorIndex !== -1) {
|
|
110
|
+
this.pathname = urlString.substring(pathSeparatorIndex);
|
|
111
|
+
urlString = urlString.substring(0, pathSeparatorIndex);
|
|
112
|
+
}
|
|
113
|
+
const colonIndex = urlString.indexOf(":");
|
|
114
|
+
if (colonIndex !== -1) {
|
|
115
|
+
this.hostname = urlString.substring(0, colonIndex);
|
|
116
|
+
this.port = urlString.substring(colonIndex + 1);
|
|
117
|
+
} else {
|
|
118
|
+
this.hostname = urlString;
|
|
119
|
+
}
|
|
120
|
+
if (this.protocol === "") {
|
|
121
|
+
throw new Error("Invalid URL: protocol is required");
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
throw new Error(`Failed to parse URL: ${error instanceof Error ? error.message : String(error)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Helper method to safely get a parameter from query string
|
|
129
|
+
*
|
|
130
|
+
* @param name - The parameter name to retrieve
|
|
131
|
+
* @returns The parameter value or null if not found
|
|
132
|
+
*/
|
|
133
|
+
getSearchParam(name) {
|
|
134
|
+
return this.searchParams.has(name) ? this.searchParams.get(name) : null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Helper method to iterate over all parameters
|
|
138
|
+
*
|
|
139
|
+
* @param callback - Function to call for each parameter
|
|
140
|
+
*/
|
|
141
|
+
forEachSearchParam(callback) {
|
|
142
|
+
this.searchParams.forEach((value, key) => callback(value, key));
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// src/utils/dsn-obfuscate.ts
|
|
147
|
+
function parseConnectionInfoFromDSN(dsn) {
|
|
148
|
+
if (!dsn) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const type = getDatabaseTypeFromDSN(dsn);
|
|
153
|
+
if (typeof type === "undefined") {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
if (type === "sqlite") {
|
|
157
|
+
const prefix = "sqlite:///";
|
|
158
|
+
if (dsn.length > prefix.length) {
|
|
159
|
+
const rawPath = dsn.substring(prefix.length);
|
|
160
|
+
const firstChar = rawPath[0];
|
|
161
|
+
const isWindowsDrive = rawPath.length > 1 && rawPath[1] === ":";
|
|
162
|
+
const isSpecialPath = firstChar === ":" || firstChar === "." || firstChar === "~" || isWindowsDrive;
|
|
163
|
+
return {
|
|
164
|
+
type,
|
|
165
|
+
database: isSpecialPath ? rawPath : "/" + rawPath
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return { type };
|
|
169
|
+
}
|
|
170
|
+
const url = new SafeURL(dsn);
|
|
171
|
+
const info = { type };
|
|
172
|
+
if (url.hostname) {
|
|
173
|
+
info.host = url.hostname;
|
|
174
|
+
}
|
|
175
|
+
if (url.port) {
|
|
176
|
+
info.port = parseInt(url.port, 10);
|
|
177
|
+
}
|
|
178
|
+
if (url.pathname && url.pathname.length > 1) {
|
|
179
|
+
info.database = url.pathname.substring(1);
|
|
180
|
+
}
|
|
181
|
+
if (url.username) {
|
|
182
|
+
info.user = url.username;
|
|
183
|
+
}
|
|
184
|
+
return info;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function obfuscateDSNPassword(dsn) {
|
|
190
|
+
if (!dsn) {
|
|
191
|
+
return dsn;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const type = getDatabaseTypeFromDSN(dsn);
|
|
195
|
+
if (type === "sqlite") {
|
|
196
|
+
return dsn;
|
|
197
|
+
}
|
|
198
|
+
const url = new SafeURL(dsn);
|
|
199
|
+
if (!url.password) {
|
|
200
|
+
return dsn;
|
|
201
|
+
}
|
|
202
|
+
const obfuscatedPassword = "*".repeat(Math.min(url.password.length, 8));
|
|
203
|
+
const protocol = dsn.split(":")[0];
|
|
204
|
+
let result;
|
|
205
|
+
if (url.username) {
|
|
206
|
+
result = `${protocol}://${url.username}:${obfuscatedPassword}@${url.hostname}`;
|
|
207
|
+
} else {
|
|
208
|
+
result = `${protocol}://${obfuscatedPassword}@${url.hostname}`;
|
|
209
|
+
}
|
|
210
|
+
if (url.port) {
|
|
211
|
+
result += `:${url.port}`;
|
|
212
|
+
}
|
|
213
|
+
result += url.pathname;
|
|
214
|
+
if (url.searchParams.size > 0) {
|
|
215
|
+
const params = [];
|
|
216
|
+
url.forEachSearchParam((value, key) => {
|
|
217
|
+
params.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
|
218
|
+
});
|
|
219
|
+
result += `?${params.join("&")}`;
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
} catch {
|
|
223
|
+
return dsn;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function getDatabaseTypeFromDSN(dsn) {
|
|
227
|
+
if (!dsn) {
|
|
228
|
+
return void 0;
|
|
229
|
+
}
|
|
230
|
+
const protocol = dsn.split(":")[0];
|
|
231
|
+
return protocolToConnectorType(protocol);
|
|
232
|
+
}
|
|
233
|
+
function protocolToConnectorType(protocol) {
|
|
234
|
+
const mapping = {
|
|
235
|
+
"postgres": "postgres",
|
|
236
|
+
"postgresql": "postgres",
|
|
237
|
+
"mysql": "mysql",
|
|
238
|
+
"mariadb": "mariadb",
|
|
239
|
+
"sqlserver": "sqlserver",
|
|
240
|
+
"sqlite": "sqlite"
|
|
241
|
+
};
|
|
242
|
+
return mapping[protocol];
|
|
243
|
+
}
|
|
244
|
+
function getDefaultPortForType(type) {
|
|
245
|
+
const ports = {
|
|
246
|
+
"postgres": 5432,
|
|
247
|
+
"mysql": 3306,
|
|
248
|
+
"mariadb": 3306,
|
|
249
|
+
"sqlserver": 1433,
|
|
250
|
+
"sqlite": void 0
|
|
251
|
+
};
|
|
252
|
+
return ports[type];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/utils/sql-parser.ts
|
|
256
|
+
var TokenType = { Plain: 0, Comment: 1, QuotedBlock: 2 };
|
|
257
|
+
function plainToken(i) {
|
|
258
|
+
return { type: TokenType.Plain, end: i + 1 };
|
|
259
|
+
}
|
|
260
|
+
function scanSingleLineComment(sql, i) {
|
|
261
|
+
if (sql[i] !== "-" || sql[i + 1] !== "-") {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
let j = i;
|
|
265
|
+
while (j < sql.length && sql[j] !== "\n") {
|
|
266
|
+
j++;
|
|
267
|
+
}
|
|
268
|
+
return { type: TokenType.Comment, end: j };
|
|
269
|
+
}
|
|
270
|
+
function scanMultiLineComment(sql, i) {
|
|
271
|
+
if (sql[i] !== "/" || sql[i + 1] !== "*") {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
let j = i + 2;
|
|
275
|
+
while (j < sql.length && !(sql[j] === "*" && sql[j + 1] === "/")) {
|
|
276
|
+
j++;
|
|
277
|
+
}
|
|
278
|
+
if (j < sql.length) {
|
|
279
|
+
j += 2;
|
|
280
|
+
}
|
|
281
|
+
return { type: TokenType.Comment, end: j };
|
|
282
|
+
}
|
|
283
|
+
function scanMultiLineCommentMySQL(sql, i) {
|
|
284
|
+
if (sql[i] !== "/" || sql[i + 1] !== "*") {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
const next = sql[i + 2];
|
|
288
|
+
const nextNext = sql[i + 3];
|
|
289
|
+
if (next === "!" || next === "M" && nextNext === "!") {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
return scanMultiLineComment(sql, i);
|
|
293
|
+
}
|
|
294
|
+
function scanNestedMultiLineComment(sql, i) {
|
|
295
|
+
if (sql[i] !== "/" || sql[i + 1] !== "*") {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
let j = i + 2;
|
|
299
|
+
let depth = 1;
|
|
300
|
+
while (j < sql.length && depth > 0) {
|
|
301
|
+
if (sql[j] === "/" && sql[j + 1] === "*") {
|
|
302
|
+
depth++;
|
|
303
|
+
j += 2;
|
|
304
|
+
} else if (sql[j] === "*" && sql[j + 1] === "/") {
|
|
305
|
+
depth--;
|
|
306
|
+
j += 2;
|
|
307
|
+
} else {
|
|
308
|
+
j++;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return { type: TokenType.Comment, end: j };
|
|
312
|
+
}
|
|
313
|
+
function scanSingleQuotedString(sql, i) {
|
|
314
|
+
if (sql[i] !== "'") {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
let j = i + 1;
|
|
318
|
+
while (j < sql.length) {
|
|
319
|
+
if (sql[j] === "'" && sql[j + 1] === "'") {
|
|
320
|
+
j += 2;
|
|
321
|
+
} else if (sql[j] === "'") {
|
|
322
|
+
j++;
|
|
323
|
+
break;
|
|
324
|
+
} else {
|
|
325
|
+
j++;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return { type: TokenType.QuotedBlock, end: j };
|
|
329
|
+
}
|
|
330
|
+
function scanDoubleQuotedString(sql, i) {
|
|
331
|
+
if (sql[i] !== '"') {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
let j = i + 1;
|
|
335
|
+
while (j < sql.length) {
|
|
336
|
+
if (sql[j] === '"' && sql[j + 1] === '"') {
|
|
337
|
+
j += 2;
|
|
338
|
+
} else if (sql[j] === '"') {
|
|
339
|
+
j++;
|
|
340
|
+
break;
|
|
341
|
+
} else {
|
|
342
|
+
j++;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return { type: TokenType.QuotedBlock, end: j };
|
|
346
|
+
}
|
|
347
|
+
var dollarQuoteOpenRegex = /^\$([a-zA-Z_]\w*)?\$/;
|
|
348
|
+
function scanDollarQuotedBlock(sql, i) {
|
|
349
|
+
if (sql[i] !== "$") {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
const next = sql[i + 1];
|
|
353
|
+
if (next >= "0" && next <= "9") {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
const remaining = sql.substring(i);
|
|
357
|
+
const m = dollarQuoteOpenRegex.exec(remaining);
|
|
358
|
+
if (!m) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
const tag = m[0];
|
|
362
|
+
const bodyStart = i + tag.length;
|
|
363
|
+
const closeIdx = sql.indexOf(tag, bodyStart);
|
|
364
|
+
const end = closeIdx !== -1 ? closeIdx + tag.length : sql.length;
|
|
365
|
+
return { type: TokenType.QuotedBlock, end };
|
|
366
|
+
}
|
|
367
|
+
function scanBacktickQuotedIdentifier(sql, i) {
|
|
368
|
+
if (sql[i] !== "`") {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
let j = i + 1;
|
|
372
|
+
while (j < sql.length) {
|
|
373
|
+
if (sql[j] === "`" && sql[j + 1] === "`") {
|
|
374
|
+
j += 2;
|
|
375
|
+
} else if (sql[j] === "`") {
|
|
376
|
+
j++;
|
|
377
|
+
break;
|
|
378
|
+
} else {
|
|
379
|
+
j++;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return { type: TokenType.QuotedBlock, end: j };
|
|
383
|
+
}
|
|
384
|
+
function scanBracketQuotedIdentifier(sql, i) {
|
|
385
|
+
if (sql[i] !== "[") {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
let j = i + 1;
|
|
389
|
+
while (j < sql.length) {
|
|
390
|
+
if (sql[j] === "]" && sql[j + 1] === "]") {
|
|
391
|
+
j += 2;
|
|
392
|
+
} else if (sql[j] === "]") {
|
|
393
|
+
j++;
|
|
394
|
+
break;
|
|
395
|
+
} else {
|
|
396
|
+
j++;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return { type: TokenType.QuotedBlock, end: j };
|
|
400
|
+
}
|
|
401
|
+
function scanTokenAnsi(sql, i) {
|
|
402
|
+
return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? plainToken(i);
|
|
403
|
+
}
|
|
404
|
+
function scanTokenPostgres(sql, i) {
|
|
405
|
+
return scanSingleLineComment(sql, i) ?? scanNestedMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanDollarQuotedBlock(sql, i) ?? plainToken(i);
|
|
406
|
+
}
|
|
407
|
+
function scanTokenMySQL(sql, i) {
|
|
408
|
+
return scanSingleLineComment(sql, i) ?? scanMultiLineCommentMySQL(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBacktickQuotedIdentifier(sql, i) ?? plainToken(i);
|
|
409
|
+
}
|
|
410
|
+
function scanTokenSQLite(sql, i) {
|
|
411
|
+
return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBacktickQuotedIdentifier(sql, i) ?? scanBracketQuotedIdentifier(sql, i) ?? plainToken(i);
|
|
412
|
+
}
|
|
413
|
+
function scanTokenSQLServer(sql, i) {
|
|
414
|
+
return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBracketQuotedIdentifier(sql, i) ?? plainToken(i);
|
|
415
|
+
}
|
|
416
|
+
var dialectScanners = {
|
|
417
|
+
postgres: scanTokenPostgres,
|
|
418
|
+
mysql: scanTokenMySQL,
|
|
419
|
+
mariadb: scanTokenMySQL,
|
|
420
|
+
sqlite: scanTokenSQLite,
|
|
421
|
+
sqlserver: scanTokenSQLServer
|
|
422
|
+
};
|
|
423
|
+
function getScanner(dialect) {
|
|
424
|
+
return dialect ? dialectScanners[dialect] ?? scanTokenAnsi : scanTokenAnsi;
|
|
425
|
+
}
|
|
426
|
+
function stripCommentsAndStrings(sql, dialect) {
|
|
427
|
+
const scanToken = getScanner(dialect);
|
|
428
|
+
const parts = [];
|
|
429
|
+
let plainStart = -1;
|
|
430
|
+
let i = 0;
|
|
431
|
+
while (i < sql.length) {
|
|
432
|
+
const token = scanToken(sql, i);
|
|
433
|
+
if (token.type === TokenType.Plain) {
|
|
434
|
+
if (plainStart === -1) {
|
|
435
|
+
plainStart = i;
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
if (plainStart !== -1) {
|
|
439
|
+
parts.push(sql.substring(plainStart, i));
|
|
440
|
+
plainStart = -1;
|
|
441
|
+
}
|
|
442
|
+
parts.push(" ");
|
|
443
|
+
}
|
|
444
|
+
i = token.end;
|
|
445
|
+
}
|
|
446
|
+
if (plainStart !== -1) {
|
|
447
|
+
parts.push(sql.substring(plainStart));
|
|
448
|
+
}
|
|
449
|
+
return parts.join("");
|
|
450
|
+
}
|
|
451
|
+
function splitSQLStatements(sql, dialect) {
|
|
452
|
+
const scanToken = getScanner(dialect);
|
|
453
|
+
const statements = [];
|
|
454
|
+
let stmtStart = 0;
|
|
455
|
+
let i = 0;
|
|
456
|
+
while (i < sql.length) {
|
|
457
|
+
if (sql[i] === ";") {
|
|
458
|
+
const trimmed2 = sql.substring(stmtStart, i).trim();
|
|
459
|
+
if (trimmed2.length > 0) {
|
|
460
|
+
statements.push(trimmed2);
|
|
461
|
+
}
|
|
462
|
+
stmtStart = i + 1;
|
|
463
|
+
i++;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
const token = scanToken(sql, i);
|
|
467
|
+
i = token.end;
|
|
468
|
+
}
|
|
469
|
+
const trimmed = sql.substring(stmtStart).trim();
|
|
470
|
+
if (trimmed.length > 0) {
|
|
471
|
+
statements.push(trimmed);
|
|
472
|
+
}
|
|
473
|
+
return statements;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export {
|
|
477
|
+
ConnectorRegistry,
|
|
478
|
+
SafeURL,
|
|
479
|
+
parseConnectionInfoFromDSN,
|
|
480
|
+
obfuscateDSNPassword,
|
|
481
|
+
getDatabaseTypeFromDSN,
|
|
482
|
+
getDefaultPortForType,
|
|
483
|
+
stripCommentsAndStrings,
|
|
484
|
+
splitSQLStatements
|
|
485
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/utils/identifier-quoter.ts
|
|
2
|
+
function quoteIdentifier(identifier, dbType) {
|
|
3
|
+
if (/[\0\x08\x09\x1a\n\r]/.test(identifier)) {
|
|
4
|
+
throw new Error(`Invalid identifier: contains control characters: ${identifier}`);
|
|
5
|
+
}
|
|
6
|
+
if (!identifier) {
|
|
7
|
+
throw new Error("Identifier cannot be empty");
|
|
8
|
+
}
|
|
9
|
+
switch (dbType) {
|
|
10
|
+
case "postgres":
|
|
11
|
+
case "sqlite":
|
|
12
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
13
|
+
case "mysql":
|
|
14
|
+
case "mariadb":
|
|
15
|
+
return `\`${identifier.replace(/`/g, "``")}\``;
|
|
16
|
+
case "sqlserver":
|
|
17
|
+
return `[${identifier.replace(/]/g, "]]")}]`;
|
|
18
|
+
default:
|
|
19
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function quoteQualifiedIdentifier(tableName, schemaName, dbType) {
|
|
23
|
+
const quotedTable = quoteIdentifier(tableName, dbType);
|
|
24
|
+
if (schemaName) {
|
|
25
|
+
const quotedSchema = quoteIdentifier(schemaName, dbType);
|
|
26
|
+
return `${quotedSchema}.${quotedTable}`;
|
|
27
|
+
}
|
|
28
|
+
return quotedTable;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
quoteIdentifier,
|
|
33
|
+
quoteQualifiedIdentifier
|
|
34
|
+
};
|