@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.
@@ -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 SQLSTATE = /^[0-9A-Z]{5}$/;
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 && SQLSTATE.test(code) && severity !== 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
- async function runRawQuery(client, query, params) {
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
- query,
262
- params,
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 = {};
@@ -6,6 +6,8 @@ export type QueryCallSite = {
6
6
  query: string;
7
7
  paramCount: number;
8
8
  kind: "inline" | "file";
9
+ cardinality?: "many" | "one" | "optional" | "execute";
10
+ queryName?: string;
9
11
  sqlFilePath?: string;
10
12
  };
11
13
  export declare class ScanError extends Error {
@@ -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 && importedNamespaces.size === 0 && importedClientFactories.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
- ? { sqlAliases: nextSql, namespaces: nextNs, clientFactories: nextFactories, clients: nextClients }
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;