@onreza/sqlx-js 0.7.0 → 0.9.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 +124 -15
- package/ROADMAP.md +4 -3
- package/dist/bin/sqlx-js.js +117 -7
- package/dist/src/cache.d.ts +6 -2
- package/dist/src/cache.js +35 -134
- package/dist/src/codegen.js +3 -1
- package/dist/src/commands/ci.d.ts +28 -0
- package/dist/src/commands/ci.js +103 -0
- package/dist/src/commands/init.js +9 -0
- package/dist/src/commands/prepare.d.ts +2 -0
- package/dist/src/commands/prepare.js +63 -47
- package/dist/src/commands/queries.d.ts +38 -0
- package/dist/src/commands/queries.js +143 -0
- package/dist/src/config.d.ts +5 -0
- package/dist/src/config.js +29 -0
- package/dist/src/index.d.ts +10 -2
- package/dist/src/index.js +3 -1
- package/dist/src/pg/functions.d.ts +3 -1
- package/dist/src/pg/functions.js +11 -3
- package/dist/src/postgres-runtime.d.ts +2 -0
- package/dist/src/postgres-runtime.js +9 -6
- package/dist/src/query-id.d.ts +1 -0
- package/dist/src/query-id.js +61 -0
- package/dist/src/query.d.ts +53 -0
- package/dist/src/query.js +38 -0
- package/dist/src/runtime.d.ts +27 -3
- package/dist/src/runtime.js +75 -23
- package/dist/src/scan/scanner.d.ts +2 -0
- package/dist/src/scan/scanner.js +123 -17
- 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 +6 -4
- package/package.json +6 -1
package/dist/src/runtime.js
CHANGED
|
@@ -2,6 +2,9 @@ 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";
|
|
6
|
+
import { queryId } from "./query-id.js";
|
|
7
|
+
import { QUERY_EXECUTOR, } from "./query.js";
|
|
5
8
|
const PARAMETER_KIND = Symbol("sqlx-js.parameter");
|
|
6
9
|
export function json(value) {
|
|
7
10
|
return { [PARAMETER_KIND]: "json", value };
|
|
@@ -175,7 +178,7 @@ export class TooManyRowsError extends Error {
|
|
|
175
178
|
}
|
|
176
179
|
// SQLSTATE is exactly five characters from [0-9A-Z]; lowercase or other shapes
|
|
177
180
|
// are never valid, so transport codes like "EPIPE" must not match on shape alone.
|
|
178
|
-
const
|
|
181
|
+
const SQLSTATE_PATTERN = /^[0-9A-Z]{5}$/;
|
|
179
182
|
function firstString(...candidates) {
|
|
180
183
|
for (const value of candidates) {
|
|
181
184
|
if (typeof value === "string" && value.length > 0)
|
|
@@ -199,7 +202,7 @@ export function toPgError(e) {
|
|
|
199
202
|
// and system errors (EPIPE, ECONNREFUSED, CONNECTION_ENDED) carry neither, so
|
|
200
203
|
// they pass through untouched instead of masquerading as a PgError.
|
|
201
204
|
const isDatabaseError = o.name === "PostgresError" ||
|
|
202
|
-
(code !== undefined &&
|
|
205
|
+
(code !== undefined && SQLSTATE_PATTERN.test(code) && severity !== undefined);
|
|
203
206
|
if (!isDatabaseError)
|
|
204
207
|
return null;
|
|
205
208
|
const fields = {};
|
|
@@ -230,6 +233,17 @@ export function toPgError(e) {
|
|
|
230
233
|
fields.s = schema;
|
|
231
234
|
return new PgError(fields, { cause: e });
|
|
232
235
|
}
|
|
236
|
+
export const SQLSTATE = {
|
|
237
|
+
notNullViolation: "23502",
|
|
238
|
+
foreignKeyViolation: "23503",
|
|
239
|
+
uniqueViolation: "23505",
|
|
240
|
+
checkViolation: "23514",
|
|
241
|
+
serializationFailure: "40001",
|
|
242
|
+
deadlockDetected: "40P01",
|
|
243
|
+
};
|
|
244
|
+
export function isPgError(error, code) {
|
|
245
|
+
return error instanceof PgError && (code === undefined || error.code === code);
|
|
246
|
+
}
|
|
233
247
|
export const _internal = {
|
|
234
248
|
renameRows,
|
|
235
249
|
encodeParam,
|
|
@@ -241,7 +255,26 @@ export const _internal = {
|
|
|
241
255
|
parameterKind,
|
|
242
256
|
toPgError,
|
|
243
257
|
};
|
|
244
|
-
|
|
258
|
+
const runtimeQueryIds = new Map();
|
|
259
|
+
const MAX_RUNTIME_QUERY_IDS = 1024;
|
|
260
|
+
function observedMetadata(query, metadata) {
|
|
261
|
+
if (metadata)
|
|
262
|
+
return metadata;
|
|
263
|
+
let id = runtimeQueryIds.get(query);
|
|
264
|
+
if (!id) {
|
|
265
|
+
id = queryId(query);
|
|
266
|
+
if (runtimeQueryIds.size >= MAX_RUNTIME_QUERY_IDS)
|
|
267
|
+
runtimeQueryIds.clear();
|
|
268
|
+
runtimeQueryIds.set(query, id);
|
|
269
|
+
}
|
|
270
|
+
return { queryId: id };
|
|
271
|
+
}
|
|
272
|
+
async function runRawQuery(client, query, params, metadata) {
|
|
273
|
+
const observedQuery = query;
|
|
274
|
+
const observedParams = params;
|
|
275
|
+
const bound = bindNamedParameters(rewriteNamedParameters(query), params);
|
|
276
|
+
query = bound.query;
|
|
277
|
+
params = bound.params;
|
|
245
278
|
const encoded = params.length === 0
|
|
246
279
|
? params
|
|
247
280
|
: params.map((p) => client.transformParam ? client.transformParam(p) : encodeParam(p));
|
|
@@ -254,12 +287,14 @@ async function runRawQuery(client, query, params) {
|
|
|
254
287
|
throw toPgError(e) ?? e;
|
|
255
288
|
}
|
|
256
289
|
}
|
|
290
|
+
const observed = observedMetadata(observedQuery, metadata);
|
|
257
291
|
const start = performance.now();
|
|
258
292
|
try {
|
|
259
293
|
const result = await client.query(query, encoded);
|
|
260
294
|
notifyQuery(client, {
|
|
261
|
-
|
|
262
|
-
|
|
295
|
+
...observed,
|
|
296
|
+
query: observedQuery,
|
|
297
|
+
params: observedParams,
|
|
263
298
|
durationMs: performance.now() - start,
|
|
264
299
|
rowCount: result.count ?? result.length,
|
|
265
300
|
});
|
|
@@ -267,7 +302,7 @@ async function runRawQuery(client, query, params) {
|
|
|
267
302
|
}
|
|
268
303
|
catch (e) {
|
|
269
304
|
const error = toPgError(e) ?? e;
|
|
270
|
-
notifyQuery(client, { query, params, durationMs: performance.now() - start, error });
|
|
305
|
+
notifyQuery(client, { ...observed, query: observedQuery, params: observedParams, durationMs: performance.now() - start, error });
|
|
271
306
|
throw error;
|
|
272
307
|
}
|
|
273
308
|
}
|
|
@@ -290,26 +325,26 @@ function notifyQueryHookError(client, error, event) {
|
|
|
290
325
|
catch {
|
|
291
326
|
}
|
|
292
327
|
}
|
|
293
|
-
async function runQuery(client, query, params) {
|
|
294
|
-
return renameRows(await runRawQuery(client, query, params));
|
|
328
|
+
async function runQuery(client, query, params, metadata) {
|
|
329
|
+
return renameRows(await runRawQuery(client, query, params, metadata));
|
|
295
330
|
}
|
|
296
|
-
async function runExecute(client, query, params) {
|
|
297
|
-
const result = await runRawQuery(client, query, params);
|
|
331
|
+
async function runExecute(client, query, params, metadata) {
|
|
332
|
+
const result = await runRawQuery(client, query, params, metadata);
|
|
298
333
|
return {
|
|
299
334
|
rowCount: result.count ?? result.length,
|
|
300
335
|
command: result.command ?? "",
|
|
301
336
|
};
|
|
302
337
|
}
|
|
303
|
-
async function runOne(client, query, params) {
|
|
304
|
-
const rows = await runQuery(client, query, params);
|
|
338
|
+
async function runOne(client, query, params, metadata) {
|
|
339
|
+
const rows = await runQuery(client, query, params, metadata);
|
|
305
340
|
if (rows.length === 1)
|
|
306
341
|
return rows[0];
|
|
307
342
|
if (rows.length === 0)
|
|
308
343
|
throw new NoRowsError();
|
|
309
344
|
throw new TooManyRowsError(rows.length, "1");
|
|
310
345
|
}
|
|
311
|
-
async function runOptional(client, query, params) {
|
|
312
|
-
const rows = await runQuery(client, query, params);
|
|
346
|
+
async function runOptional(client, query, params, metadata) {
|
|
347
|
+
const rows = await runQuery(client, query, params, metadata);
|
|
313
348
|
if (rows.length === 0)
|
|
314
349
|
return null;
|
|
315
350
|
if (rows.length === 1)
|
|
@@ -317,7 +352,7 @@ async function runOptional(client, query, params) {
|
|
|
317
352
|
throw new TooManyRowsError(rows.length, "0 or 1");
|
|
318
353
|
}
|
|
319
354
|
const sqlFileCache = new Map();
|
|
320
|
-
function loadSqlFile(path, fileRoot = process.env.SQLX_JS_FILE_ROOT ?? process.cwd(), reload = false) {
|
|
355
|
+
function loadSqlFile(path, fileRoot = process.env.SQLX_JS_FILE_ROOT ?? process.cwd(), reload = false, embedded) {
|
|
321
356
|
const root = resolve(fileRoot);
|
|
322
357
|
if (isAbsolute(path)) {
|
|
323
358
|
throw new Error(`sqlx-js.sql.file: path must be relative to fileRoot: ${path}`);
|
|
@@ -327,6 +362,8 @@ function loadSqlFile(path, fileRoot = process.env.SQLX_JS_FILE_ROOT ?? process.c
|
|
|
327
362
|
if (rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(rel)) {
|
|
328
363
|
throw new Error(`sqlx-js.sql.file: path escapes fileRoot: ${path}`);
|
|
329
364
|
}
|
|
365
|
+
if (embedded && Object.hasOwn(embedded, path))
|
|
366
|
+
return embedded[path];
|
|
330
367
|
try {
|
|
331
368
|
const cached = sqlFileCache.get(full);
|
|
332
369
|
if (cached && !reload)
|
|
@@ -458,21 +495,30 @@ export function id(...parts) {
|
|
|
458
495
|
}
|
|
459
496
|
return parts.map(quoteIdentifier).join(".");
|
|
460
497
|
}
|
|
498
|
+
function executeDefinedQuery(client, mode, query, params, metadata) {
|
|
499
|
+
if (mode === "one")
|
|
500
|
+
return runOne(client, query, params, metadata);
|
|
501
|
+
if (mode === "optional")
|
|
502
|
+
return runOptional(client, query, params, metadata);
|
|
503
|
+
if (mode === "execute")
|
|
504
|
+
return runExecute(client, query, params, metadata);
|
|
505
|
+
return runQuery(client, query, params, metadata);
|
|
506
|
+
}
|
|
461
507
|
function makeBoundCallable(client) {
|
|
462
508
|
const fn = (async (query, ...params) => {
|
|
463
509
|
return runQuery(client, query, params);
|
|
464
510
|
});
|
|
465
511
|
const file = (async (path, ...params) => {
|
|
466
|
-
return runQuery(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
|
|
512
|
+
return runQuery(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles, client.sqlFiles), params);
|
|
467
513
|
});
|
|
468
514
|
file.one = (async (path, ...params) => {
|
|
469
|
-
return runOne(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
|
|
515
|
+
return runOne(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles, client.sqlFiles), params);
|
|
470
516
|
});
|
|
471
517
|
file.optional = (async (path, ...params) => {
|
|
472
|
-
return runOptional(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
|
|
518
|
+
return runOptional(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles, client.sqlFiles), params);
|
|
473
519
|
});
|
|
474
520
|
file.execute = (async (path, ...params) => {
|
|
475
|
-
return runExecute(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
|
|
521
|
+
return runExecute(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles, client.sqlFiles), params);
|
|
476
522
|
});
|
|
477
523
|
fn.file = file;
|
|
478
524
|
fn.one = (async (query, ...params) => {
|
|
@@ -487,6 +533,9 @@ function makeBoundCallable(client) {
|
|
|
487
533
|
fn.id = id;
|
|
488
534
|
fn.json = json;
|
|
489
535
|
fn.array = array;
|
|
536
|
+
fn[QUERY_EXECUTOR] = (mode, query, params, metadata) => {
|
|
537
|
+
return executeDefinedQuery(client, mode, query, params, metadata);
|
|
538
|
+
};
|
|
490
539
|
return fn;
|
|
491
540
|
}
|
|
492
541
|
function buildSetTransaction(opts) {
|
|
@@ -507,19 +556,19 @@ export function createSqlRuntime(getClient) {
|
|
|
507
556
|
});
|
|
508
557
|
const rootFile = (async (path, ...params) => {
|
|
509
558
|
const client = getClient();
|
|
510
|
-
return runQuery(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
|
|
559
|
+
return runQuery(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles, client.sqlFiles), params);
|
|
511
560
|
});
|
|
512
561
|
rootFile.one = (async (path, ...params) => {
|
|
513
562
|
const client = getClient();
|
|
514
|
-
return runOne(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
|
|
563
|
+
return runOne(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles, client.sqlFiles), params);
|
|
515
564
|
});
|
|
516
565
|
rootFile.optional = (async (path, ...params) => {
|
|
517
566
|
const client = getClient();
|
|
518
|
-
return runOptional(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
|
|
567
|
+
return runOptional(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles, client.sqlFiles), params);
|
|
519
568
|
});
|
|
520
569
|
rootFile.execute = (async (path, ...params) => {
|
|
521
570
|
const client = getClient();
|
|
522
|
-
return runExecute(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
|
|
571
|
+
return runExecute(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles, client.sqlFiles), params);
|
|
523
572
|
});
|
|
524
573
|
root.file = rootFile;
|
|
525
574
|
root.one = (async (query, ...params) => {
|
|
@@ -534,6 +583,9 @@ export function createSqlRuntime(getClient) {
|
|
|
534
583
|
root.id = id;
|
|
535
584
|
root.json = json;
|
|
536
585
|
root.array = array;
|
|
586
|
+
root[QUERY_EXECUTOR] = (mode, query, params, metadata) => {
|
|
587
|
+
return executeDefinedQuery(getClient(), mode, query, params, metadata);
|
|
588
|
+
};
|
|
537
589
|
root.transaction = (async (fnOrOpts, maybeFn) => {
|
|
538
590
|
const c = getClient();
|
|
539
591
|
let opts = {};
|
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
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;
|
|
@@ -67,7 +68,7 @@ function classifyCallee(callee, scope) {
|
|
|
67
68
|
if (ts.isIdentifier(callee)) {
|
|
68
69
|
if (!scope.sqlAliases.has(callee.text))
|
|
69
70
|
return null;
|
|
70
|
-
return { kind: "inline" };
|
|
71
|
+
return { kind: "inline", cardinality: "many" };
|
|
71
72
|
}
|
|
72
73
|
if (!ts.isPropertyAccessExpression(callee))
|
|
73
74
|
return null;
|
|
@@ -78,7 +79,7 @@ function classifyCallee(callee, scope) {
|
|
|
78
79
|
const id = callee.expression.text;
|
|
79
80
|
if (scope.namespaces.has(id) || scope.clients.has(id)) {
|
|
80
81
|
if (methodName === "sql")
|
|
81
|
-
return { kind: "inline" };
|
|
82
|
+
return { kind: "inline", cardinality: "many" };
|
|
82
83
|
return null;
|
|
83
84
|
}
|
|
84
85
|
if (!scope.sqlAliases.has(id))
|
|
@@ -86,9 +87,10 @@ function classifyCallee(callee, scope) {
|
|
|
86
87
|
if (methodName === "transaction")
|
|
87
88
|
return { kind: "transaction" };
|
|
88
89
|
if (methodName === "file")
|
|
89
|
-
return { kind: "file" };
|
|
90
|
-
if (methodName === "one" || methodName === "optional" || methodName === "execute")
|
|
91
|
-
return { kind: "inline" };
|
|
90
|
+
return { kind: "file", cardinality: "many" };
|
|
91
|
+
if (methodName === "one" || methodName === "optional" || methodName === "execute") {
|
|
92
|
+
return { kind: "inline", cardinality: methodName };
|
|
93
|
+
}
|
|
92
94
|
return null;
|
|
93
95
|
}
|
|
94
96
|
if (ts.isPropertyAccessExpression(callee.expression)) {
|
|
@@ -100,10 +102,11 @@ function classifyCallee(callee, scope) {
|
|
|
100
102
|
(scope.namespaces.has(mid.expression.text) || scope.clients.has(mid.expression.text))) {
|
|
101
103
|
if (mid.name.text !== "sql")
|
|
102
104
|
return null;
|
|
103
|
-
if (methodName === "one" || methodName === "optional" || methodName === "execute")
|
|
104
|
-
return { kind: "inline" };
|
|
105
|
+
if (methodName === "one" || methodName === "optional" || methodName === "execute") {
|
|
106
|
+
return { kind: "inline", cardinality: methodName };
|
|
107
|
+
}
|
|
105
108
|
if (methodName === "file")
|
|
106
|
-
return { kind: "file" };
|
|
109
|
+
return { kind: "file", cardinality: "many" };
|
|
107
110
|
if (methodName === "transaction")
|
|
108
111
|
return { kind: "transaction" };
|
|
109
112
|
return null;
|
|
@@ -115,8 +118,9 @@ function classifyCallee(callee, scope) {
|
|
|
115
118
|
return null;
|
|
116
119
|
if (mid.name.text !== "file")
|
|
117
120
|
return null;
|
|
118
|
-
if (methodName === "one" || methodName === "optional" || methodName === "execute")
|
|
119
|
-
return { kind: "file" };
|
|
121
|
+
if (methodName === "one" || methodName === "optional" || methodName === "execute") {
|
|
122
|
+
return { kind: "file", cardinality: methodName };
|
|
123
|
+
}
|
|
120
124
|
return null;
|
|
121
125
|
}
|
|
122
126
|
// ns.sql.file.X(...) and client.sql.file.X(...) chains
|
|
@@ -127,11 +131,32 @@ function classifyCallee(callee, scope) {
|
|
|
127
131
|
mid.expression.name.text === "sql" &&
|
|
128
132
|
mid.name.text === "file" &&
|
|
129
133
|
(methodName === "one" || methodName === "optional" || methodName === "execute")) {
|
|
130
|
-
return { kind: "file" };
|
|
134
|
+
return { kind: "file", cardinality: methodName };
|
|
131
135
|
}
|
|
132
136
|
}
|
|
133
137
|
return null;
|
|
134
138
|
}
|
|
139
|
+
function classifyDefinitionCallee(callee, scope) {
|
|
140
|
+
if (ts.isIdentifier(callee))
|
|
141
|
+
return scope.queryFactories.has(callee.text) ? "many" : null;
|
|
142
|
+
if (!ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.name))
|
|
143
|
+
return null;
|
|
144
|
+
const method = callee.name.text;
|
|
145
|
+
if (ts.isIdentifier(callee.expression) && scope.queryFactories.has(callee.expression.text)) {
|
|
146
|
+
return method === "one" || method === "optional" || method === "execute" ? method : null;
|
|
147
|
+
}
|
|
148
|
+
if (method === "defineQuery" &&
|
|
149
|
+
ts.isIdentifier(callee.expression) &&
|
|
150
|
+
scope.namespaces.has(callee.expression.text))
|
|
151
|
+
return "many";
|
|
152
|
+
if ((method === "one" || method === "optional" || method === "execute") &&
|
|
153
|
+
ts.isPropertyAccessExpression(callee.expression) &&
|
|
154
|
+
ts.isIdentifier(callee.expression.expression) &&
|
|
155
|
+
scope.namespaces.has(callee.expression.expression.text) &&
|
|
156
|
+
callee.expression.name.text === "defineQuery")
|
|
157
|
+
return method;
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
135
160
|
export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
136
161
|
const text = readFileSync(absPath, "utf8");
|
|
137
162
|
const source = ts.createSourceFile(absPath, text, ts.ScriptTarget.ESNext, false, scriptKind(absPath));
|
|
@@ -146,6 +171,7 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
|
146
171
|
const importedAliases = new Set();
|
|
147
172
|
const importedNamespaces = new Set();
|
|
148
173
|
const importedClientFactories = new Set();
|
|
174
|
+
const importedQueryFactories = new Set();
|
|
149
175
|
for (const stmt of source.statements) {
|
|
150
176
|
if (!ts.isImportDeclaration(stmt))
|
|
151
177
|
continue;
|
|
@@ -170,10 +196,15 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
|
170
196
|
importedAliases.add(elem.name.text);
|
|
171
197
|
if (orig === "createSqlClient")
|
|
172
198
|
importedClientFactories.add(elem.name.text);
|
|
199
|
+
if (orig === "defineQuery")
|
|
200
|
+
importedQueryFactories.add(elem.name.text);
|
|
173
201
|
}
|
|
174
202
|
}
|
|
175
203
|
}
|
|
176
|
-
if (importedAliases.size === 0 &&
|
|
204
|
+
if (importedAliases.size === 0 &&
|
|
205
|
+
importedNamespaces.size === 0 &&
|
|
206
|
+
importedClientFactories.size === 0 &&
|
|
207
|
+
importedQueryFactories.size === 0)
|
|
177
208
|
return [];
|
|
178
209
|
const out = [];
|
|
179
210
|
const here = (node) => {
|
|
@@ -181,12 +212,22 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
|
181
212
|
return { line: line + 1, column: character + 1 };
|
|
182
213
|
};
|
|
183
214
|
const fileRel = relative(root, absPath).replace(/\\/g, "/");
|
|
184
|
-
const recordInline = (first, args) => {
|
|
215
|
+
const recordInline = (first, args, cardinality) => {
|
|
185
216
|
if (!ts.isStringLiteralLike(first)) {
|
|
186
217
|
const pos = here(first);
|
|
187
218
|
throw new ScanError(fileRel, pos.line, pos.column, "sql() requires a string literal as first argument");
|
|
188
219
|
}
|
|
189
220
|
const pos = here(first);
|
|
221
|
+
let named;
|
|
222
|
+
try {
|
|
223
|
+
named = rewriteNamedParameters(first.text).names;
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
throw new ScanError(fileRel, pos.line, pos.column, error.message.replace(/^sqlx-js: /, ""));
|
|
227
|
+
}
|
|
228
|
+
if (named.length > 0 && args.length !== 2) {
|
|
229
|
+
throw new ScanError(fileRel, pos.line, pos.column, "a query with named parameters requires exactly one parameter object");
|
|
230
|
+
}
|
|
190
231
|
out.push({
|
|
191
232
|
file: fileRel,
|
|
192
233
|
line: pos.line,
|
|
@@ -194,10 +235,46 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
|
194
235
|
query: first.text,
|
|
195
236
|
paramCount: args.length - 1,
|
|
196
237
|
kind: "inline",
|
|
238
|
+
cardinality,
|
|
239
|
+
});
|
|
240
|
+
return true;
|
|
241
|
+
};
|
|
242
|
+
const recordDefinition = (args, callee, cardinality) => {
|
|
243
|
+
if (args.length < 1 || args.length > 2) {
|
|
244
|
+
const pos = here(callee);
|
|
245
|
+
throw new ScanError(fileRel, pos.line, pos.column, "defineQuery() requires a SQL literal and optional name");
|
|
246
|
+
}
|
|
247
|
+
const queryNode = args.length === 2 ? args[1] : args[0];
|
|
248
|
+
const nameNode = args.length === 2 ? args[0] : undefined;
|
|
249
|
+
if (!ts.isStringLiteralLike(queryNode) || (nameNode && !ts.isStringLiteralLike(nameNode))) {
|
|
250
|
+
const pos = here(queryNode);
|
|
251
|
+
throw new ScanError(fileRel, pos.line, pos.column, "defineQuery() requires string literals for its name and SQL");
|
|
252
|
+
}
|
|
253
|
+
if (nameNode && nameNode.text.trim() === "") {
|
|
254
|
+
const pos = here(nameNode);
|
|
255
|
+
throw new ScanError(fileRel, pos.line, pos.column, "defineQuery() name must not be empty");
|
|
256
|
+
}
|
|
257
|
+
const pos = here(queryNode);
|
|
258
|
+
let paramNames;
|
|
259
|
+
try {
|
|
260
|
+
paramNames = rewriteNamedParameters(queryNode.text).names;
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
throw new ScanError(fileRel, pos.line, pos.column, error.message.replace(/^sqlx-js: /, ""));
|
|
264
|
+
}
|
|
265
|
+
out.push({
|
|
266
|
+
file: fileRel,
|
|
267
|
+
line: pos.line,
|
|
268
|
+
column: pos.column,
|
|
269
|
+
query: queryNode.text,
|
|
270
|
+
paramCount: paramNames.length,
|
|
271
|
+
kind: "inline",
|
|
272
|
+
cardinality,
|
|
273
|
+
...(nameNode ? { queryName: nameNode.text } : {}),
|
|
197
274
|
});
|
|
198
275
|
return true;
|
|
199
276
|
};
|
|
200
|
-
const recordFile = (first, args, callee) => {
|
|
277
|
+
const recordFile = (first, args, callee, cardinality) => {
|
|
201
278
|
if (!ts.isStringLiteralLike(first)) {
|
|
202
279
|
const pos = first ? here(first) : here(callee);
|
|
203
280
|
throw new ScanError(fileRel, pos.line, pos.column, "sql.file() requires a string literal path");
|
|
@@ -219,6 +296,16 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
|
219
296
|
}
|
|
220
297
|
const query = readFileSync(abs, "utf8");
|
|
221
298
|
const pos = here(first);
|
|
299
|
+
let named;
|
|
300
|
+
try {
|
|
301
|
+
named = rewriteNamedParameters(query).names;
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
throw new ScanError(fileRel, pos.line, pos.column, error.message.replace(/^sqlx-js: /, ""));
|
|
305
|
+
}
|
|
306
|
+
if (named.length > 0 && args.length !== 2) {
|
|
307
|
+
throw new ScanError(fileRel, pos.line, pos.column, "a SQL file with named parameters requires exactly one parameter object");
|
|
308
|
+
}
|
|
222
309
|
out.push({
|
|
223
310
|
file: fileRel,
|
|
224
311
|
line: pos.line,
|
|
@@ -226,6 +313,7 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
|
226
313
|
query,
|
|
227
314
|
paramCount: args.length - 1,
|
|
228
315
|
kind: "file",
|
|
316
|
+
cardinality,
|
|
229
317
|
sqlFilePath: sqlPath,
|
|
230
318
|
});
|
|
231
319
|
return true;
|
|
@@ -246,6 +334,7 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
|
246
334
|
const nextSql = new Set(scope.sqlAliases);
|
|
247
335
|
const nextNs = new Set(scope.namespaces);
|
|
248
336
|
const nextFactories = new Set(scope.clientFactories);
|
|
337
|
+
const nextQueryFactories = new Set(scope.queryFactories);
|
|
249
338
|
const nextClients = new Set(scope.clients);
|
|
250
339
|
for (const binding of bindings) {
|
|
251
340
|
for (const a of scope.sqlAliases) {
|
|
@@ -266,6 +355,12 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
|
266
355
|
changed = true;
|
|
267
356
|
}
|
|
268
357
|
}
|
|
358
|
+
for (const a of scope.queryFactories) {
|
|
359
|
+
if (bindingDeclares(binding, a)) {
|
|
360
|
+
nextQueryFactories.delete(a);
|
|
361
|
+
changed = true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
269
364
|
for (const a of scope.clients) {
|
|
270
365
|
if (bindingDeclares(binding, a)) {
|
|
271
366
|
nextClients.delete(a);
|
|
@@ -274,7 +369,13 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
|
274
369
|
}
|
|
275
370
|
}
|
|
276
371
|
return changed
|
|
277
|
-
? {
|
|
372
|
+
? {
|
|
373
|
+
sqlAliases: nextSql,
|
|
374
|
+
namespaces: nextNs,
|
|
375
|
+
clientFactories: nextFactories,
|
|
376
|
+
queryFactories: nextQueryFactories,
|
|
377
|
+
clients: nextClients,
|
|
378
|
+
}
|
|
278
379
|
: scope;
|
|
279
380
|
};
|
|
280
381
|
const isClientFactoryCall = (initializer, scope) => {
|
|
@@ -301,6 +402,10 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
|
301
402
|
};
|
|
302
403
|
const visit = (node, scope) => {
|
|
303
404
|
if (ts.isCallExpression(node)) {
|
|
405
|
+
const definitionCardinality = classifyDefinitionCallee(node.expression, scope);
|
|
406
|
+
if (definitionCardinality) {
|
|
407
|
+
recordDefinition(node.arguments, node.expression, definitionCardinality);
|
|
408
|
+
}
|
|
304
409
|
const classified = classifyCallee(node.expression, scope);
|
|
305
410
|
if (classified) {
|
|
306
411
|
if (classified.kind === "transaction") {
|
|
@@ -319,12 +424,12 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
|
319
424
|
else if (classified.kind === "file") {
|
|
320
425
|
const first = node.arguments[0];
|
|
321
426
|
if (first)
|
|
322
|
-
recordFile(first, node.arguments, node.expression);
|
|
427
|
+
recordFile(first, node.arguments, node.expression, classified.cardinality ?? "many");
|
|
323
428
|
}
|
|
324
429
|
else if (classified.kind === "inline") {
|
|
325
430
|
const first = node.arguments[0];
|
|
326
431
|
if (first)
|
|
327
|
-
recordInline(first, node.arguments);
|
|
432
|
+
recordInline(first, node.arguments, classified.cardinality ?? "many");
|
|
328
433
|
}
|
|
329
434
|
}
|
|
330
435
|
}
|
|
@@ -378,6 +483,7 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
|
378
483
|
sqlAliases: importedAliases,
|
|
379
484
|
namespaces: importedNamespaces,
|
|
380
485
|
clientFactories: importedClientFactories,
|
|
486
|
+
queryFactories: importedQueryFactories,
|
|
381
487
|
clients: new Set(),
|
|
382
488
|
});
|
|
383
489
|
return 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;
|