@onreza/sqlx-js 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -15
- package/ROADMAP.md +4 -3
- package/dist/bin/sqlx-js.js +118 -23
- package/dist/src/cache.d.ts +3 -2
- package/dist/src/cache.js +26 -74
- package/dist/src/codegen.js +30 -15
- package/dist/src/commands/ci.d.ts +28 -0
- package/dist/src/commands/ci.js +103 -0
- package/dist/src/commands/init.js +14 -3
- package/dist/src/commands/prepare.d.ts +8 -1
- package/dist/src/commands/prepare.js +122 -48
- package/dist/src/commands/watch.d.ts +14 -6
- package/dist/src/commands/watch.js +184 -21
- package/dist/src/function-cache.d.ts +2 -0
- package/dist/src/function-cache.js +6 -3
- package/dist/src/index.d.ts +17 -1
- package/dist/src/index.js +3 -0
- package/dist/src/pg/schema.d.ts +1 -0
- package/dist/src/pg/schema.js +42 -31
- package/dist/src/pg/wire.d.ts +1 -0
- package/dist/src/pg/wire.js +6 -0
- package/dist/src/postgres-runtime.d.ts +11 -1
- package/dist/src/postgres-runtime.js +22 -5
- package/dist/src/runtime.d.ts +5 -2
- package/dist/src/runtime.js +41 -14
- package/dist/src/scan/scanner.js +101 -13
- package/dist/src/sql-lex.d.ts +7 -0
- package/dist/src/sql-lex.js +76 -0
- package/dist/src/sql-params.d.ts +11 -0
- package/dist/src/sql-params.js +101 -0
- package/dist/src/typed.d.ts +2 -2
- package/package.json +6 -1
package/dist/src/runtime.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import { PgClient, parseDatabaseUrl, PgError } from "./pg/wire.js";
|
|
4
4
|
import { applyPending, acquireMigrateLock, releaseMigrateLock, DEFAULT_MIGRATE_LOCK_KEY } from "./migration-core.js";
|
|
5
|
+
import { bindNamedParameters, rewriteNamedParameters } from "./sql-params.js";
|
|
5
6
|
const PARAMETER_KIND = Symbol("sqlx-js.parameter");
|
|
6
7
|
export function json(value) {
|
|
7
8
|
return { [PARAMETER_KIND]: "json", value };
|
|
@@ -242,6 +243,11 @@ export const _internal = {
|
|
|
242
243
|
toPgError,
|
|
243
244
|
};
|
|
244
245
|
async function runRawQuery(client, query, params) {
|
|
246
|
+
const observedQuery = query;
|
|
247
|
+
const observedParams = params;
|
|
248
|
+
const bound = bindNamedParameters(rewriteNamedParameters(query), params);
|
|
249
|
+
query = bound.query;
|
|
250
|
+
params = bound.params;
|
|
245
251
|
const encoded = params.length === 0
|
|
246
252
|
? params
|
|
247
253
|
: params.map((p) => client.transformParam ? client.transformParam(p) : encodeParam(p));
|
|
@@ -257,9 +263,9 @@ async function runRawQuery(client, query, params) {
|
|
|
257
263
|
const start = performance.now();
|
|
258
264
|
try {
|
|
259
265
|
const result = await client.query(query, encoded);
|
|
260
|
-
|
|
261
|
-
query,
|
|
262
|
-
params,
|
|
266
|
+
notifyQuery(client, {
|
|
267
|
+
query: observedQuery,
|
|
268
|
+
params: observedParams,
|
|
263
269
|
durationMs: performance.now() - start,
|
|
264
270
|
rowCount: result.count ?? result.length,
|
|
265
271
|
});
|
|
@@ -267,10 +273,29 @@ async function runRawQuery(client, query, params) {
|
|
|
267
273
|
}
|
|
268
274
|
catch (e) {
|
|
269
275
|
const error = toPgError(e) ?? e;
|
|
270
|
-
|
|
276
|
+
notifyQuery(client, { query: observedQuery, params: observedParams, durationMs: performance.now() - start, error });
|
|
271
277
|
throw error;
|
|
272
278
|
}
|
|
273
279
|
}
|
|
280
|
+
function notifyQuery(client, event) {
|
|
281
|
+
try {
|
|
282
|
+
const pending = client.onQuery?.(event);
|
|
283
|
+
if (pending)
|
|
284
|
+
void pending.catch((error) => notifyQueryHookError(client, error, event));
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
notifyQueryHookError(client, error, event);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function notifyQueryHookError(client, error, event) {
|
|
291
|
+
try {
|
|
292
|
+
const pending = client.onQueryHookError?.(error, event);
|
|
293
|
+
if (pending)
|
|
294
|
+
void pending.catch(() => { });
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
}
|
|
298
|
+
}
|
|
274
299
|
async function runQuery(client, query, params) {
|
|
275
300
|
return renameRows(await runRawQuery(client, query, params));
|
|
276
301
|
}
|
|
@@ -298,7 +323,7 @@ async function runOptional(client, query, params) {
|
|
|
298
323
|
throw new TooManyRowsError(rows.length, "0 or 1");
|
|
299
324
|
}
|
|
300
325
|
const sqlFileCache = new Map();
|
|
301
|
-
function loadSqlFile(path, fileRoot = process.env.SQLX_JS_FILE_ROOT ?? process.cwd()) {
|
|
326
|
+
function loadSqlFile(path, fileRoot = process.env.SQLX_JS_FILE_ROOT ?? process.cwd(), reload = false) {
|
|
302
327
|
const root = resolve(fileRoot);
|
|
303
328
|
if (isAbsolute(path)) {
|
|
304
329
|
throw new Error(`sqlx-js.sql.file: path must be relative to fileRoot: ${path}`);
|
|
@@ -309,8 +334,10 @@ function loadSqlFile(path, fileRoot = process.env.SQLX_JS_FILE_ROOT ?? process.c
|
|
|
309
334
|
throw new Error(`sqlx-js.sql.file: path escapes fileRoot: ${path}`);
|
|
310
335
|
}
|
|
311
336
|
try {
|
|
312
|
-
const st = statSync(full);
|
|
313
337
|
const cached = sqlFileCache.get(full);
|
|
338
|
+
if (cached && !reload)
|
|
339
|
+
return cached.content;
|
|
340
|
+
const st = statSync(full);
|
|
314
341
|
if (cached && cached.mtimeMs === st.mtimeMs && cached.size === st.size) {
|
|
315
342
|
return cached.content;
|
|
316
343
|
}
|
|
@@ -442,16 +469,16 @@ function makeBoundCallable(client) {
|
|
|
442
469
|
return runQuery(client, query, params);
|
|
443
470
|
});
|
|
444
471
|
const file = (async (path, ...params) => {
|
|
445
|
-
return runQuery(client, loadSqlFile(path, client.fileRoot), params);
|
|
472
|
+
return runQuery(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
|
|
446
473
|
});
|
|
447
474
|
file.one = (async (path, ...params) => {
|
|
448
|
-
return runOne(client, loadSqlFile(path, client.fileRoot), params);
|
|
475
|
+
return runOne(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
|
|
449
476
|
});
|
|
450
477
|
file.optional = (async (path, ...params) => {
|
|
451
|
-
return runOptional(client, loadSqlFile(path, client.fileRoot), params);
|
|
478
|
+
return runOptional(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
|
|
452
479
|
});
|
|
453
480
|
file.execute = (async (path, ...params) => {
|
|
454
|
-
return runExecute(client, loadSqlFile(path, client.fileRoot), params);
|
|
481
|
+
return runExecute(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
|
|
455
482
|
});
|
|
456
483
|
fn.file = file;
|
|
457
484
|
fn.one = (async (query, ...params) => {
|
|
@@ -486,19 +513,19 @@ export function createSqlRuntime(getClient) {
|
|
|
486
513
|
});
|
|
487
514
|
const rootFile = (async (path, ...params) => {
|
|
488
515
|
const client = getClient();
|
|
489
|
-
return runQuery(client, loadSqlFile(path, client.fileRoot), params);
|
|
516
|
+
return runQuery(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
|
|
490
517
|
});
|
|
491
518
|
rootFile.one = (async (path, ...params) => {
|
|
492
519
|
const client = getClient();
|
|
493
|
-
return runOne(client, loadSqlFile(path, client.fileRoot), params);
|
|
520
|
+
return runOne(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
|
|
494
521
|
});
|
|
495
522
|
rootFile.optional = (async (path, ...params) => {
|
|
496
523
|
const client = getClient();
|
|
497
|
-
return runOptional(client, loadSqlFile(path, client.fileRoot), params);
|
|
524
|
+
return runOptional(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
|
|
498
525
|
});
|
|
499
526
|
rootFile.execute = (async (path, ...params) => {
|
|
500
527
|
const client = getClient();
|
|
501
|
-
return runExecute(client, loadSqlFile(path, client.fileRoot), params);
|
|
528
|
+
return runExecute(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
|
|
502
529
|
});
|
|
503
530
|
root.file = rootFile;
|
|
504
531
|
root.one = (async (query, ...params) => {
|
package/dist/src/scan/scanner.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import { extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import { rewriteNamedParameters } from "../sql-params.js";
|
|
4
5
|
export class ScanError extends Error {
|
|
5
6
|
file;
|
|
6
7
|
line;
|
|
@@ -76,7 +77,7 @@ function classifyCallee(callee, scope) {
|
|
|
76
77
|
const methodName = callee.name.text;
|
|
77
78
|
if (ts.isIdentifier(callee.expression)) {
|
|
78
79
|
const id = callee.expression.text;
|
|
79
|
-
if (scope.namespaces.has(id)) {
|
|
80
|
+
if (scope.namespaces.has(id) || scope.clients.has(id)) {
|
|
80
81
|
if (methodName === "sql")
|
|
81
82
|
return { kind: "inline" };
|
|
82
83
|
return null;
|
|
@@ -95,8 +96,9 @@ function classifyCallee(callee, scope) {
|
|
|
95
96
|
const mid = callee.expression;
|
|
96
97
|
if (!ts.isIdentifier(mid.name))
|
|
97
98
|
return null;
|
|
98
|
-
// ns.sql.X(...) chains
|
|
99
|
-
if (ts.isIdentifier(mid.expression) &&
|
|
99
|
+
// ns.sql.X(...) and client.sql.X(...) chains
|
|
100
|
+
if (ts.isIdentifier(mid.expression) &&
|
|
101
|
+
(scope.namespaces.has(mid.expression.text) || scope.clients.has(mid.expression.text))) {
|
|
100
102
|
if (mid.name.text !== "sql")
|
|
101
103
|
return null;
|
|
102
104
|
if (methodName === "one" || methodName === "optional" || methodName === "execute")
|
|
@@ -118,11 +120,11 @@ function classifyCallee(callee, scope) {
|
|
|
118
120
|
return { kind: "file" };
|
|
119
121
|
return null;
|
|
120
122
|
}
|
|
121
|
-
// ns.sql.file.X(...) chains
|
|
123
|
+
// ns.sql.file.X(...) and client.sql.file.X(...) chains
|
|
122
124
|
if (ts.isPropertyAccessExpression(mid.expression) &&
|
|
123
125
|
ts.isIdentifier(mid.expression.expression) &&
|
|
124
126
|
ts.isIdentifier(mid.expression.name) &&
|
|
125
|
-
scope.namespaces.has(mid.expression.expression.text) &&
|
|
127
|
+
(scope.namespaces.has(mid.expression.expression.text) || scope.clients.has(mid.expression.expression.text)) &&
|
|
126
128
|
mid.expression.name.text === "sql" &&
|
|
127
129
|
mid.name.text === "file" &&
|
|
128
130
|
(methodName === "one" || methodName === "optional" || methodName === "execute")) {
|
|
@@ -133,9 +135,18 @@ function classifyCallee(callee, scope) {
|
|
|
133
135
|
}
|
|
134
136
|
export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
135
137
|
const text = readFileSync(absPath, "utf8");
|
|
136
|
-
const source = ts.createSourceFile(absPath, text, ts.ScriptTarget.ESNext, false,
|
|
138
|
+
const source = ts.createSourceFile(absPath, text, ts.ScriptTarget.ESNext, false, scriptKind(absPath));
|
|
139
|
+
const parseDiagnostics = source.parseDiagnostics ?? [];
|
|
140
|
+
const parseError = parseDiagnostics.find((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
|
|
141
|
+
if (parseError) {
|
|
142
|
+
const start = parseError.start ?? 0;
|
|
143
|
+
const { line, character } = source.getLineAndCharacterOfPosition(start);
|
|
144
|
+
const file = relative(root, absPath).replace(/\\/g, "/");
|
|
145
|
+
throw new ScanError(file, line + 1, character + 1, ts.flattenDiagnosticMessageText(parseError.messageText, "\n"));
|
|
146
|
+
}
|
|
137
147
|
const importedAliases = new Set();
|
|
138
148
|
const importedNamespaces = new Set();
|
|
149
|
+
const importedClientFactories = new Set();
|
|
139
150
|
for (const stmt of source.statements) {
|
|
140
151
|
if (!ts.isImportDeclaration(stmt))
|
|
141
152
|
continue;
|
|
@@ -158,23 +169,35 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
|
158
169
|
const orig = (elem.propertyName ?? elem.name).text;
|
|
159
170
|
if (orig === "sql")
|
|
160
171
|
importedAliases.add(elem.name.text);
|
|
172
|
+
if (orig === "createSqlClient")
|
|
173
|
+
importedClientFactories.add(elem.name.text);
|
|
161
174
|
}
|
|
162
175
|
}
|
|
163
176
|
}
|
|
164
|
-
if (importedAliases.size === 0 && importedNamespaces.size === 0)
|
|
177
|
+
if (importedAliases.size === 0 && importedNamespaces.size === 0 && importedClientFactories.size === 0)
|
|
165
178
|
return [];
|
|
166
179
|
const out = [];
|
|
167
180
|
const here = (node) => {
|
|
168
181
|
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
169
182
|
return { line: line + 1, column: character + 1 };
|
|
170
183
|
};
|
|
171
|
-
const fileRel = relative(root, absPath);
|
|
184
|
+
const fileRel = relative(root, absPath).replace(/\\/g, "/");
|
|
172
185
|
const recordInline = (first, args) => {
|
|
173
186
|
if (!ts.isStringLiteralLike(first)) {
|
|
174
187
|
const pos = here(first);
|
|
175
188
|
throw new ScanError(fileRel, pos.line, pos.column, "sql() requires a string literal as first argument");
|
|
176
189
|
}
|
|
177
190
|
const pos = here(first);
|
|
191
|
+
let named;
|
|
192
|
+
try {
|
|
193
|
+
named = rewriteNamedParameters(first.text).names;
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
throw new ScanError(fileRel, pos.line, pos.column, error.message.replace(/^sqlx-js: /, ""));
|
|
197
|
+
}
|
|
198
|
+
if (named.length > 0 && args.length !== 2) {
|
|
199
|
+
throw new ScanError(fileRel, pos.line, pos.column, "a query with named parameters requires exactly one parameter object");
|
|
200
|
+
}
|
|
178
201
|
out.push({
|
|
179
202
|
file: fileRel,
|
|
180
203
|
line: pos.line,
|
|
@@ -207,6 +230,16 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
|
207
230
|
}
|
|
208
231
|
const query = readFileSync(abs, "utf8");
|
|
209
232
|
const pos = here(first);
|
|
233
|
+
let named;
|
|
234
|
+
try {
|
|
235
|
+
named = rewriteNamedParameters(query).names;
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
throw new ScanError(fileRel, pos.line, pos.column, error.message.replace(/^sqlx-js: /, ""));
|
|
239
|
+
}
|
|
240
|
+
if (named.length > 0 && args.length !== 2) {
|
|
241
|
+
throw new ScanError(fileRel, pos.line, pos.column, "a SQL file with named parameters requires exactly one parameter object");
|
|
242
|
+
}
|
|
210
243
|
out.push({
|
|
211
244
|
file: fileRel,
|
|
212
245
|
line: pos.line,
|
|
@@ -233,6 +266,8 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
|
233
266
|
let changed = false;
|
|
234
267
|
const nextSql = new Set(scope.sqlAliases);
|
|
235
268
|
const nextNs = new Set(scope.namespaces);
|
|
269
|
+
const nextFactories = new Set(scope.clientFactories);
|
|
270
|
+
const nextClients = new Set(scope.clients);
|
|
236
271
|
for (const binding of bindings) {
|
|
237
272
|
for (const a of scope.sqlAliases) {
|
|
238
273
|
if (bindingDeclares(binding, a)) {
|
|
@@ -246,8 +281,44 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
|
246
281
|
changed = true;
|
|
247
282
|
}
|
|
248
283
|
}
|
|
284
|
+
for (const a of scope.clientFactories) {
|
|
285
|
+
if (bindingDeclares(binding, a)) {
|
|
286
|
+
nextFactories.delete(a);
|
|
287
|
+
changed = true;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (const a of scope.clients) {
|
|
291
|
+
if (bindingDeclares(binding, a)) {
|
|
292
|
+
nextClients.delete(a);
|
|
293
|
+
changed = true;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
249
296
|
}
|
|
250
|
-
return changed
|
|
297
|
+
return changed
|
|
298
|
+
? { sqlAliases: nextSql, namespaces: nextNs, clientFactories: nextFactories, clients: nextClients }
|
|
299
|
+
: scope;
|
|
300
|
+
};
|
|
301
|
+
const isClientFactoryCall = (initializer, scope) => {
|
|
302
|
+
if (!initializer || !ts.isCallExpression(initializer))
|
|
303
|
+
return false;
|
|
304
|
+
const callee = initializer.expression;
|
|
305
|
+
if (ts.isIdentifier(callee))
|
|
306
|
+
return scope.clientFactories.has(callee.text);
|
|
307
|
+
return ts.isPropertyAccessExpression(callee) &&
|
|
308
|
+
ts.isIdentifier(callee.expression) &&
|
|
309
|
+
scope.namespaces.has(callee.expression.text) &&
|
|
310
|
+
callee.name.text === "createSqlClient";
|
|
311
|
+
};
|
|
312
|
+
const scopeWithClientDeclarations = (scope, declarations) => {
|
|
313
|
+
const nextClients = new Set(scope.clients);
|
|
314
|
+
let changed = false;
|
|
315
|
+
for (const declaration of declarations) {
|
|
316
|
+
if (!ts.isIdentifier(declaration.name) || !isClientFactoryCall(declaration.initializer, scope))
|
|
317
|
+
continue;
|
|
318
|
+
nextClients.add(declaration.name.text);
|
|
319
|
+
changed = true;
|
|
320
|
+
}
|
|
321
|
+
return changed ? { ...scope, clients: nextClients } : scope;
|
|
251
322
|
};
|
|
252
323
|
const visit = (node, scope) => {
|
|
253
324
|
if (ts.isCallExpression(node)) {
|
|
@@ -262,7 +333,7 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
|
262
333
|
if (param && ts.isIdentifier(param.name)) {
|
|
263
334
|
innerSql.add(param.name.text);
|
|
264
335
|
}
|
|
265
|
-
visit(fn.body, { sqlAliases: innerSql
|
|
336
|
+
visit(fn.body, { ...shadowed, sqlAliases: innerSql });
|
|
266
337
|
return;
|
|
267
338
|
}
|
|
268
339
|
}
|
|
@@ -283,7 +354,11 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
|
283
354
|
const stmts = node.statements;
|
|
284
355
|
for (const stmt of stmts) {
|
|
285
356
|
if (ts.isVariableStatement(stmt)) {
|
|
286
|
-
|
|
357
|
+
const declarations = stmt.declarationList.declarations;
|
|
358
|
+
current = scopeWithoutBindingShadows(current, declarations.map((d) => d.name));
|
|
359
|
+
visit(stmt, current);
|
|
360
|
+
current = scopeWithClientDeclarations(current, declarations);
|
|
361
|
+
continue;
|
|
287
362
|
}
|
|
288
363
|
else if (ts.isFunctionDeclaration(stmt) && stmt.name) {
|
|
289
364
|
current = scopeWithoutBindingShadows(current, [stmt.name]);
|
|
@@ -320,9 +395,22 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
|
320
395
|
}
|
|
321
396
|
ts.forEachChild(node, (child) => visit(child, scope));
|
|
322
397
|
};
|
|
323
|
-
visit(source, {
|
|
398
|
+
visit(source, {
|
|
399
|
+
sqlAliases: importedAliases,
|
|
400
|
+
namespaces: importedNamespaces,
|
|
401
|
+
clientFactories: importedClientFactories,
|
|
402
|
+
clients: new Set(),
|
|
403
|
+
});
|
|
324
404
|
return out;
|
|
325
405
|
}
|
|
406
|
+
function scriptKind(path) {
|
|
407
|
+
switch (extname(path).toLowerCase()) {
|
|
408
|
+
case ".tsx": return ts.ScriptKind.TSX;
|
|
409
|
+
case ".mts":
|
|
410
|
+
case ".cts":
|
|
411
|
+
default: return ts.ScriptKind.TS;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
326
414
|
export function scanProject(root, scan = {}) {
|
|
327
415
|
const files = findSourceFiles(root, scan);
|
|
328
416
|
const out = [];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function isIdentifierContinuation(value: string): boolean;
|
|
2
|
+
export declare function isEscapeStringPrefix(query: string, quote: number): boolean;
|
|
3
|
+
export declare function readSingleQuoted(query: string, start: number, escapeBackslash: boolean): number;
|
|
4
|
+
export declare function readQuotedIdentifier(query: string, start: number): number;
|
|
5
|
+
export declare function readDollarQuoted(query: string, start: number): number | null;
|
|
6
|
+
export declare function readLineComment(query: string, start: number): number;
|
|
7
|
+
export declare function readBlockComment(query: string, start: number): number;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export function isIdentifierContinuation(value) {
|
|
2
|
+
return /[A-Za-z0-9_$\u0080-\uFFFF]/.test(value);
|
|
3
|
+
}
|
|
4
|
+
export function isEscapeStringPrefix(query, quote) {
|
|
5
|
+
const prefix = query[quote - 1];
|
|
6
|
+
if (prefix !== "e" && prefix !== "E")
|
|
7
|
+
return false;
|
|
8
|
+
const before = query[quote - 2];
|
|
9
|
+
return before === undefined || !isIdentifierContinuation(before);
|
|
10
|
+
}
|
|
11
|
+
export function readSingleQuoted(query, start, escapeBackslash) {
|
|
12
|
+
let i = start + 1;
|
|
13
|
+
while (i < query.length) {
|
|
14
|
+
if (escapeBackslash && query[i] === "\\") {
|
|
15
|
+
i += 2;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (query[i] === "'") {
|
|
19
|
+
if (query[i + 1] === "'") {
|
|
20
|
+
i += 2;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
return i + 1;
|
|
24
|
+
}
|
|
25
|
+
i++;
|
|
26
|
+
}
|
|
27
|
+
return query.length;
|
|
28
|
+
}
|
|
29
|
+
export function readQuotedIdentifier(query, start) {
|
|
30
|
+
let i = start + 1;
|
|
31
|
+
while (i < query.length) {
|
|
32
|
+
if (query[i] === '"') {
|
|
33
|
+
if (query[i + 1] === '"') {
|
|
34
|
+
i += 2;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
return i + 1;
|
|
38
|
+
}
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
return query.length;
|
|
42
|
+
}
|
|
43
|
+
export function readDollarQuoted(query, start) {
|
|
44
|
+
let end = start + 1;
|
|
45
|
+
if (query[end] !== "$" && !/[A-Za-z_\u0080-\uFFFF]/.test(query[end]))
|
|
46
|
+
return null;
|
|
47
|
+
while (end < query.length && /[A-Za-z0-9_\u0080-\uFFFF]/.test(query[end]))
|
|
48
|
+
end++;
|
|
49
|
+
if (query[end] !== "$")
|
|
50
|
+
return null;
|
|
51
|
+
const tag = query.slice(start, end + 1);
|
|
52
|
+
const close = query.indexOf(tag, end + 1);
|
|
53
|
+
return close === -1 ? query.length : close + tag.length;
|
|
54
|
+
}
|
|
55
|
+
export function readLineComment(query, start) {
|
|
56
|
+
const end = query.indexOf("\n", start + 2);
|
|
57
|
+
return end === -1 ? query.length : end + 1;
|
|
58
|
+
}
|
|
59
|
+
export function readBlockComment(query, start) {
|
|
60
|
+
let depth = 1;
|
|
61
|
+
let i = start + 2;
|
|
62
|
+
while (i < query.length && depth > 0) {
|
|
63
|
+
if (query[i] === "/" && query[i + 1] === "*") {
|
|
64
|
+
depth++;
|
|
65
|
+
i += 2;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (query[i] === "*" && query[i + 1] === "/") {
|
|
69
|
+
depth--;
|
|
70
|
+
i += 2;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
i++;
|
|
74
|
+
}
|
|
75
|
+
return i;
|
|
76
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type RewrittenSql = {
|
|
2
|
+
query: string;
|
|
3
|
+
names: string[];
|
|
4
|
+
positionMap: number[];
|
|
5
|
+
};
|
|
6
|
+
export declare function rewriteNamedParameters(query: string): RewrittenSql;
|
|
7
|
+
export declare function bindNamedParameters(rewritten: RewrittenSql, args: readonly unknown[]): {
|
|
8
|
+
query: string;
|
|
9
|
+
params: unknown[];
|
|
10
|
+
};
|
|
11
|
+
export declare function originalPosition(rewritten: RewrittenSql, oneBasedPosition: number): number;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { isEscapeStringPrefix, isIdentifierContinuation, readBlockComment, readDollarQuoted, readLineComment, readQuotedIdentifier, readSingleQuoted, } from "./sql-lex.js";
|
|
2
|
+
export function rewriteNamedParameters(query) {
|
|
3
|
+
let out = "";
|
|
4
|
+
let i = 0;
|
|
5
|
+
let positional = false;
|
|
6
|
+
const names = [];
|
|
7
|
+
const indexes = new Map();
|
|
8
|
+
const positionMap = [];
|
|
9
|
+
const append = (text, sourceStart) => {
|
|
10
|
+
out += text;
|
|
11
|
+
for (let offset = 0; offset < text.length; offset++)
|
|
12
|
+
positionMap.push(sourceStart + offset);
|
|
13
|
+
};
|
|
14
|
+
while (i < query.length) {
|
|
15
|
+
const start = i;
|
|
16
|
+
const ch = query[i];
|
|
17
|
+
if (ch === "-" && query[i + 1] === "-") {
|
|
18
|
+
i = readLineComment(query, i);
|
|
19
|
+
append(query.slice(start, i), start);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (ch === "/" && query[i + 1] === "*") {
|
|
23
|
+
i = readBlockComment(query, i);
|
|
24
|
+
append(query.slice(start, i), start);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (ch === "'") {
|
|
28
|
+
i = readSingleQuoted(query, i, isEscapeStringPrefix(query, i));
|
|
29
|
+
append(query.slice(start, i), start);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (ch === '"') {
|
|
33
|
+
i = readQuotedIdentifier(query, i);
|
|
34
|
+
append(query.slice(start, i), start);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (ch === "$") {
|
|
38
|
+
if (i > 0 && isIdentifierContinuation(query[i - 1])) {
|
|
39
|
+
append(ch, i);
|
|
40
|
+
i++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const quotedEnd = readDollarQuoted(query, i);
|
|
44
|
+
if (quotedEnd !== null) {
|
|
45
|
+
i = quotedEnd;
|
|
46
|
+
append(query.slice(start, i), start);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const positionalMatch = /^\$[1-9][0-9]*/.exec(query.slice(i));
|
|
50
|
+
if (positionalMatch) {
|
|
51
|
+
positional = true;
|
|
52
|
+
i += positionalMatch[0].length;
|
|
53
|
+
append(positionalMatch[0], start);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const namedMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)/.exec(query.slice(i));
|
|
57
|
+
if (namedMatch) {
|
|
58
|
+
const name = namedMatch[1];
|
|
59
|
+
let index = indexes.get(name);
|
|
60
|
+
if (index === undefined) {
|
|
61
|
+
names.push(name);
|
|
62
|
+
index = names.length;
|
|
63
|
+
indexes.set(name, index);
|
|
64
|
+
}
|
|
65
|
+
const replacement = `$${index}`;
|
|
66
|
+
append(replacement, start);
|
|
67
|
+
i += namedMatch[0].length;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
append(ch, i);
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
if (positional && names.length > 0) {
|
|
75
|
+
throw new Error("sqlx-js: named and positional parameters cannot be mixed in one query");
|
|
76
|
+
}
|
|
77
|
+
positionMap.push(query.length);
|
|
78
|
+
return { query: out, names, positionMap };
|
|
79
|
+
}
|
|
80
|
+
export function bindNamedParameters(rewritten, args) {
|
|
81
|
+
if (rewritten.names.length === 0)
|
|
82
|
+
return { query: rewritten.query, params: [...args] };
|
|
83
|
+
if (args.length !== 1 || args[0] === null || typeof args[0] !== "object" || Array.isArray(args[0])) {
|
|
84
|
+
throw new Error("sqlx-js: a query with named parameters requires exactly one parameter object");
|
|
85
|
+
}
|
|
86
|
+
const values = args[0];
|
|
87
|
+
const missing = rewritten.names.filter((name) => !Object.hasOwn(values, name));
|
|
88
|
+
const expected = new Set(rewritten.names);
|
|
89
|
+
const extra = Object.keys(values).filter((name) => !expected.has(name));
|
|
90
|
+
if (missing.length > 0)
|
|
91
|
+
throw new Error(`sqlx-js: missing named parameter(s): ${missing.join(", ")}`);
|
|
92
|
+
if (extra.length > 0)
|
|
93
|
+
throw new Error(`sqlx-js: unknown named parameter(s): ${extra.join(", ")}`);
|
|
94
|
+
return { query: rewritten.query, params: rewritten.names.map((name) => values[name]) };
|
|
95
|
+
}
|
|
96
|
+
export function originalPosition(rewritten, oneBasedPosition) {
|
|
97
|
+
const position = Number(oneBasedPosition);
|
|
98
|
+
if (!Number.isInteger(position) || position < 1)
|
|
99
|
+
return oneBasedPosition;
|
|
100
|
+
return (rewritten.positionMap[position - 1] ?? position - 1) + 1;
|
|
101
|
+
}
|
package/dist/src/typed.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onreza/sqlx-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Compile-time-checked raw SQL for TypeScript + PostgreSQL. Inspired by sqlx.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -66,6 +66,11 @@
|
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"typescript": ">=5.4"
|
|
68
68
|
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"typescript": {
|
|
71
|
+
"optional": true
|
|
72
|
+
}
|
|
73
|
+
},
|
|
69
74
|
"dependencies": {
|
|
70
75
|
"libpg-query": "^17.7.3",
|
|
71
76
|
"postgres": "^3.4.9"
|