@kaelio/ktx 0.13.0 → 0.13.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/assets/python/{kaelio_ktx-0.13.0-py3-none-any.whl → kaelio_ktx-0.13.1-py3-none-any.whl} +0 -0
- package/assets/python/manifest.json +4 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/connectors/sqlserver/connector.js +3 -2
- package/dist/context/connections/read-only-sql.d.ts +5 -0
- package/dist/context/connections/read-only-sql.js +139 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertReadOnlySql, stripTrailingSqlNoise } from '../../context/connections/read-only-sql.js';
|
|
1
|
+
import { assertReadOnlySql, hoistLeadingCte, stripTrailingSqlNoise } from '../../context/connections/read-only-sql.js';
|
|
2
2
|
import { getDialectForDriver } from '../../context/connections/dialects.js';
|
|
3
3
|
import { tryConstraintQuery } from '../../context/scan/constraint-discovery.js';
|
|
4
4
|
import { scopedTableNames } from '../../context/scan/table-ref.js';
|
|
@@ -136,7 +136,8 @@ function limitSqlForSqlServerExecution(sqlText, maxRows) {
|
|
|
136
136
|
if (!Number.isInteger(maxRows) || maxRows <= 0) {
|
|
137
137
|
throw new Error('maxRows must be a positive integer.');
|
|
138
138
|
}
|
|
139
|
-
|
|
139
|
+
const { withPrefix, body } = hoistLeadingCte(trimmed);
|
|
140
|
+
return `${withPrefix}SELECT TOP ${maxRows} * FROM (${body}) AS ktx_query_result`;
|
|
140
141
|
}
|
|
141
142
|
export function isKtxSqlServerConnectionConfig(connection) {
|
|
142
143
|
return String(connection?.driver ?? '').toLowerCase() === 'sqlserver';
|
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
export declare function assertReadOnlySql(sql: string): string;
|
|
2
|
+
/** @internal */
|
|
3
|
+
export declare function hoistLeadingCte(sql: string): {
|
|
4
|
+
withPrefix: string;
|
|
5
|
+
body: string;
|
|
6
|
+
};
|
|
2
7
|
export declare function stripTrailingSqlNoise(sql: string): string;
|
|
3
8
|
export declare function limitSqlForExecution(sql: string, maxRows: number | undefined): string;
|
|
@@ -91,6 +91,143 @@ export function assertReadOnlySql(sql) {
|
|
|
91
91
|
assertSingleSqlStatement(trimmed);
|
|
92
92
|
return trimmed;
|
|
93
93
|
}
|
|
94
|
+
function isSqlIdentifierPart(char) {
|
|
95
|
+
return char !== undefined && /[A-Za-z0-9_$]/.test(char);
|
|
96
|
+
}
|
|
97
|
+
function keywordAt(sql, index, keyword) {
|
|
98
|
+
if (sql.slice(index, index + keyword.length).toLowerCase() !== keyword.toLowerCase()) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return !isSqlIdentifierPart(sql[index - 1]) && !isSqlIdentifierPart(sql[index + keyword.length]);
|
|
102
|
+
}
|
|
103
|
+
function skipWhitespaceAndComments(sql, index) {
|
|
104
|
+
let current = index;
|
|
105
|
+
while (current < sql.length) {
|
|
106
|
+
while (/\s/.test(sql[current] ?? '')) {
|
|
107
|
+
current += 1;
|
|
108
|
+
}
|
|
109
|
+
if (sql.startsWith('--', current) || sql.startsWith('/*', current)) {
|
|
110
|
+
current = skipQuotedOrComment(sql, current);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
return current;
|
|
114
|
+
}
|
|
115
|
+
return current;
|
|
116
|
+
}
|
|
117
|
+
function skipBracketIdentifier(sql, index) {
|
|
118
|
+
let current = index + 1;
|
|
119
|
+
while (current < sql.length) {
|
|
120
|
+
if (sql[current] === ']') {
|
|
121
|
+
if (sql[current + 1] === ']') {
|
|
122
|
+
current += 2;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
return current + 1;
|
|
126
|
+
}
|
|
127
|
+
current += 1;
|
|
128
|
+
}
|
|
129
|
+
return -1;
|
|
130
|
+
}
|
|
131
|
+
function skipBacktickIdentifier(sql, index) {
|
|
132
|
+
let current = index + 1;
|
|
133
|
+
while (current < sql.length) {
|
|
134
|
+
if (sql[current] === '`') {
|
|
135
|
+
if (sql[current + 1] === '`') {
|
|
136
|
+
current += 2;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
return current + 1;
|
|
140
|
+
}
|
|
141
|
+
current += 1;
|
|
142
|
+
}
|
|
143
|
+
return -1;
|
|
144
|
+
}
|
|
145
|
+
function skipIdentifier(sql, index) {
|
|
146
|
+
if (sql[index] === '"') {
|
|
147
|
+
const skipped = skipQuotedOrComment(sql, index);
|
|
148
|
+
return skipped > index ? skipped : -1;
|
|
149
|
+
}
|
|
150
|
+
if (sql[index] === '[') {
|
|
151
|
+
return skipBracketIdentifier(sql, index);
|
|
152
|
+
}
|
|
153
|
+
if (sql[index] === '`') {
|
|
154
|
+
return skipBacktickIdentifier(sql, index);
|
|
155
|
+
}
|
|
156
|
+
let current = index;
|
|
157
|
+
while (isSqlIdentifierPart(sql[current])) {
|
|
158
|
+
current += 1;
|
|
159
|
+
}
|
|
160
|
+
return current > index ? current : -1;
|
|
161
|
+
}
|
|
162
|
+
function skipBalancedParentheses(sql, index) {
|
|
163
|
+
if (sql[index] !== '(') {
|
|
164
|
+
return -1;
|
|
165
|
+
}
|
|
166
|
+
let current = index;
|
|
167
|
+
let depth = 0;
|
|
168
|
+
while (current < sql.length) {
|
|
169
|
+
const skipped = skipQuotedOrComment(sql, current);
|
|
170
|
+
if (skipped > current) {
|
|
171
|
+
current = skipped;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (sql[current] === '(') {
|
|
175
|
+
depth += 1;
|
|
176
|
+
}
|
|
177
|
+
else if (sql[current] === ')') {
|
|
178
|
+
depth -= 1;
|
|
179
|
+
if (depth === 0) {
|
|
180
|
+
return current + 1;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
current += 1;
|
|
184
|
+
}
|
|
185
|
+
return -1;
|
|
186
|
+
}
|
|
187
|
+
/** @internal */
|
|
188
|
+
export function hoistLeadingCte(sql) {
|
|
189
|
+
const trimmed = sql.trim();
|
|
190
|
+
if (!keywordAt(trimmed, 0, 'with')) {
|
|
191
|
+
return { withPrefix: '', body: sql };
|
|
192
|
+
}
|
|
193
|
+
let current = skipWhitespaceAndComments(trimmed, 4);
|
|
194
|
+
if (keywordAt(trimmed, current, 'recursive')) {
|
|
195
|
+
current = skipWhitespaceAndComments(trimmed, current + 'recursive'.length);
|
|
196
|
+
}
|
|
197
|
+
while (current < trimmed.length) {
|
|
198
|
+
current = skipIdentifier(trimmed, current);
|
|
199
|
+
if (current < 0) {
|
|
200
|
+
return { withPrefix: '', body: trimmed };
|
|
201
|
+
}
|
|
202
|
+
current = skipWhitespaceAndComments(trimmed, current);
|
|
203
|
+
if (trimmed[current] === '(') {
|
|
204
|
+
current = skipBalancedParentheses(trimmed, current);
|
|
205
|
+
if (current < 0) {
|
|
206
|
+
return { withPrefix: '', body: trimmed };
|
|
207
|
+
}
|
|
208
|
+
current = skipWhitespaceAndComments(trimmed, current);
|
|
209
|
+
}
|
|
210
|
+
if (!keywordAt(trimmed, current, 'as')) {
|
|
211
|
+
return { withPrefix: '', body: trimmed };
|
|
212
|
+
}
|
|
213
|
+
current = skipWhitespaceAndComments(trimmed, current + 2);
|
|
214
|
+
current = skipBalancedParentheses(trimmed, current);
|
|
215
|
+
if (current < 0) {
|
|
216
|
+
return { withPrefix: '', body: trimmed };
|
|
217
|
+
}
|
|
218
|
+
current = skipWhitespaceAndComments(trimmed, current);
|
|
219
|
+
if (trimmed[current] === ',') {
|
|
220
|
+
current = skipWhitespaceAndComments(trimmed, current + 1);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const body = trimmed.slice(current).trimStart();
|
|
224
|
+
if (!body) {
|
|
225
|
+
return { withPrefix: '', body: trimmed };
|
|
226
|
+
}
|
|
227
|
+
return { withPrefix: `${trimmed.slice(0, current).trimEnd()} `, body };
|
|
228
|
+
}
|
|
229
|
+
return { withPrefix: '', body: trimmed };
|
|
230
|
+
}
|
|
94
231
|
// `assertReadOnlySql` deliberately keeps trailing semicolons, comments, and
|
|
95
232
|
// whitespace (e.g. `select 1; -- done`) — harmless for direct single-statement
|
|
96
233
|
// execution. A row-limit subquery wrapper needs a bare expression instead: a
|
|
@@ -130,5 +267,6 @@ export function limitSqlForExecution(sql, maxRows) {
|
|
|
130
267
|
if (!Number.isInteger(maxRows) || maxRows <= 0) {
|
|
131
268
|
throw new KtxQueryError('maxRows must be a positive integer.');
|
|
132
269
|
}
|
|
133
|
-
|
|
270
|
+
const { withPrefix, body } = hoistLeadingCte(trimmed);
|
|
271
|
+
return `${withPrefix}select * from (${body}) as ktx_query_result limit ${maxRows}`;
|
|
134
272
|
}
|