@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.
@@ -0,0 +1,38 @@
1
+ import type { QueryExecutionMode } from "../query.js";
2
+ export type QueriesPhase = "config" | "scan" | "cache" | "embed";
3
+ export declare class QueriesError extends Error {
4
+ readonly phase: QueriesPhase;
5
+ readonly file?: string | undefined;
6
+ readonly line?: number | undefined;
7
+ readonly column?: number | undefined;
8
+ constructor(phase: QueriesPhase, message: string, file?: string | undefined, line?: number | undefined, column?: number | undefined, options?: {
9
+ cause?: unknown;
10
+ });
11
+ }
12
+ export type QueryInventoryItem = {
13
+ queryId: string;
14
+ queryNames: string[];
15
+ query: string;
16
+ cardinalities: QueryExecutionMode[];
17
+ sqlFilePaths: string[];
18
+ callSites: {
19
+ file: string;
20
+ line: number;
21
+ column: number;
22
+ }[];
23
+ cacheStatus: "current" | "stale" | "missing";
24
+ };
25
+ export type QueryInventory = {
26
+ formatVersion: 1;
27
+ ok: true;
28
+ queries: QueryInventoryItem[];
29
+ orphanedCacheIds: string[];
30
+ };
31
+ export declare function buildQueryInventory(root: string, cacheDir: string): Promise<QueryInventory>;
32
+ export declare function emitEmbeddedSqlModule(path: string, inventory: QueryInventory): void;
33
+ export declare function runQueries(options: {
34
+ root: string;
35
+ cacheDir: string;
36
+ json?: boolean;
37
+ embedPath?: string;
38
+ }): Promise<void>;
@@ -0,0 +1,143 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { mkdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { dirname, relative } from "node:path";
4
+ import { Cache, CacheManifestStaleError, readCacheManifest } from "../cache.js";
5
+ import { loadConfig, prepareConfigHash } from "../config.js";
6
+ import { queryId } from "../query-id.js";
7
+ import { ScanError, scanProject } from "../scan/scanner.js";
8
+ export class QueriesError extends Error {
9
+ phase;
10
+ file;
11
+ line;
12
+ column;
13
+ constructor(phase, message, file, line, column, options) {
14
+ super(message, options);
15
+ this.phase = phase;
16
+ this.file = file;
17
+ this.line = line;
18
+ this.column = column;
19
+ this.name = "QueriesError";
20
+ }
21
+ }
22
+ function queriesError(phase, error) {
23
+ if (error instanceof QueriesError)
24
+ return error;
25
+ if (error instanceof ScanError) {
26
+ return new QueriesError(phase, error.message, error.file, error.line, error.column, { cause: error });
27
+ }
28
+ return new QueriesError(phase, error instanceof Error ? error.message : String(error), undefined, undefined, undefined, { cause: error });
29
+ }
30
+ export async function buildQueryInventory(root, cacheDir) {
31
+ let config;
32
+ try {
33
+ config = await loadConfig(root);
34
+ }
35
+ catch (error) {
36
+ throw queriesError("config", error);
37
+ }
38
+ let sites;
39
+ try {
40
+ sites = scanProject(root, config.scan);
41
+ }
42
+ catch (error) {
43
+ throw queriesError("scan", error);
44
+ }
45
+ const cache = new Cache(cacheDir);
46
+ let manifestCurrent = false;
47
+ try {
48
+ const manifest = readCacheManifest(cacheDir);
49
+ manifestCurrent = manifest?.configHash === prepareConfigHash(config);
50
+ }
51
+ catch (error) {
52
+ if (!(error instanceof CacheManifestStaleError)) {
53
+ throw queriesError("cache", error);
54
+ }
55
+ manifestCurrent = false;
56
+ }
57
+ let cacheIds;
58
+ try {
59
+ cacheIds = cache.list().map(({ fp }) => fp);
60
+ }
61
+ catch (error) {
62
+ throw queriesError("cache", error);
63
+ }
64
+ const cached = new Set(cacheIds);
65
+ const grouped = new Map();
66
+ for (const site of sites) {
67
+ const id = queryId(site.query);
68
+ const group = grouped.get(id) ?? [];
69
+ group.push(site);
70
+ grouped.set(id, group);
71
+ }
72
+ const queries = [...grouped.entries()].map(([id, group]) => {
73
+ return {
74
+ queryId: id,
75
+ queryNames: [...new Set(group.flatMap((site) => site.queryName ? [site.queryName] : []))].sort(),
76
+ query: group[0].query,
77
+ cardinalities: [...new Set(group.map((site) => site.cardinality ?? "many"))].sort(),
78
+ sqlFilePaths: [...new Set(group.flatMap((site) => site.sqlFilePath ? [site.sqlFilePath] : []))].sort(),
79
+ callSites: group
80
+ .map(({ file, line, column }) => ({ file, line, column }))
81
+ .sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column),
82
+ cacheStatus: !cached.has(id) ? "missing" : manifestCurrent ? "current" : "stale",
83
+ };
84
+ }).sort((a, b) => a.queryId.localeCompare(b.queryId));
85
+ const active = new Set(queries.map((query) => query.queryId));
86
+ const orphanedCacheIds = cacheIds.filter((fp) => !active.has(fp)).sort();
87
+ return { formatVersion: 1, ok: true, queries, orphanedCacheIds };
88
+ }
89
+ export function emitEmbeddedSqlModule(path, inventory) {
90
+ const sqlFiles = Object.fromEntries(inventory.queries
91
+ .flatMap((query) => query.sqlFilePaths.map((file) => [file, query.query]))
92
+ .sort(([a], [b]) => a.localeCompare(b)));
93
+ const content = [
94
+ "// AUTO-GENERATED by sqlx-js. Do not edit.",
95
+ "// Run `sqlx-js queries --embed <path>` to regenerate.",
96
+ "",
97
+ `export const sqlxJsEmbeddedSql = ${JSON.stringify(sqlFiles, null, 2)} as const;`,
98
+ "",
99
+ ].join("\n");
100
+ mkdirSync(dirname(path), { recursive: true });
101
+ const tmp = `${path}.tmp-${randomBytes(4).toString("hex")}`;
102
+ writeFileSync(tmp, content);
103
+ try {
104
+ renameSync(tmp, path);
105
+ }
106
+ catch (error) {
107
+ try {
108
+ unlinkSync(tmp);
109
+ }
110
+ catch { }
111
+ throw error;
112
+ }
113
+ }
114
+ export async function runQueries(options) {
115
+ const inventory = await buildQueryInventory(options.root, options.cacheDir);
116
+ if (options.embedPath) {
117
+ try {
118
+ emitEmbeddedSqlModule(options.embedPath, inventory);
119
+ }
120
+ catch (error) {
121
+ throw queriesError("embed", error);
122
+ }
123
+ }
124
+ if (options.json) {
125
+ console.log(JSON.stringify({
126
+ ...inventory,
127
+ ...(options.embedPath ? { embeddedModule: relative(options.root, options.embedPath).replace(/\\/g, "/") } : {}),
128
+ }, null, 2));
129
+ return;
130
+ }
131
+ for (const query of inventory.queries) {
132
+ const names = query.queryNames.length > 0 ? ` ${query.queryNames.join(",")}` : "";
133
+ console.log(`${query.queryId}${names} ${query.cardinalities.join(",")} ${query.cacheStatus}`);
134
+ for (const site of query.callSites)
135
+ console.log(` ${site.file}:${site.line}:${site.column}`);
136
+ }
137
+ if (inventory.queries.length === 0)
138
+ console.log("no sqlx-js queries found");
139
+ if (inventory.orphanedCacheIds.length > 0)
140
+ console.log(`orphaned cache: ${inventory.orphanedCacheIds.join(", ")}`);
141
+ if (options.embedPath)
142
+ console.log(`embedded SQL module: ${relative(options.root, options.embedPath).replace(/\\/g, "/")}`);
143
+ }
@@ -5,7 +5,11 @@ export type ScanConfig = {
5
5
  };
6
6
  export type SqlxJsConfig = {
7
7
  jsonbTypes?: Record<string, string>;
8
+ columnTypes?: Record<string, string>;
8
9
  customTypes?: Record<string, string>;
10
+ functionCatalog?: false | {
11
+ includeExtensionOwned?: boolean;
12
+ };
9
13
  scan?: ScanConfig;
10
14
  schema?: {
11
15
  provider?: "builtin" | "pgschema";
@@ -32,3 +36,4 @@ export declare function runtimeVersion(): {
32
36
  export declare function nativeTypeScriptEnabled(): boolean | string;
33
37
  export declare function envFilePath(root: string): string;
34
38
  export declare function lookupJsonbType(cfg: SqlxJsConfig, schema: string, table: string, column: string): string | undefined;
39
+ export declare function lookupColumnType(cfg: SqlxJsConfig, schema: string, table: string, column: string): string | undefined;
@@ -65,8 +65,27 @@ function validateConfig(value, path) {
65
65
  const config = value;
66
66
  if (config.jsonbTypes !== undefined)
67
67
  validateStringRecord(config.jsonbTypes, "jsonbTypes", path);
68
+ if (config.columnTypes !== undefined)
69
+ validateStringRecord(config.columnTypes, "columnTypes", path);
68
70
  if (config.customTypes !== undefined)
69
71
  validateStringRecord(config.customTypes, "customTypes", path);
72
+ if (config.jsonbTypes && config.columnTypes) {
73
+ const jsonKeys = Object.keys(config.jsonbTypes);
74
+ const columnKeys = Object.keys(config.columnTypes);
75
+ const conflicts = jsonKeys.filter((jsonKey) => columnKeys.some((columnKey) => jsonKey === columnKey || jsonKey.endsWith(`.${columnKey}`) || columnKey.endsWith(`.${jsonKey}`)));
76
+ if (conflicts.length > 0) {
77
+ throw new Error(`sqlx-js: ${path} maps the same column in jsonbTypes and columnTypes: ${conflicts.join(", ")}`);
78
+ }
79
+ }
80
+ if (config.functionCatalog !== undefined && config.functionCatalog !== false) {
81
+ if (!config.functionCatalog || typeof config.functionCatalog !== "object" || Array.isArray(config.functionCatalog)) {
82
+ throw new Error(`sqlx-js: ${path} functionCatalog must be false or an object`);
83
+ }
84
+ const functionCatalog = config.functionCatalog;
85
+ if (functionCatalog.includeExtensionOwned !== undefined && typeof functionCatalog.includeExtensionOwned !== "boolean") {
86
+ throw new Error(`sqlx-js: ${path} functionCatalog.includeExtensionOwned must be a boolean`);
87
+ }
88
+ }
70
89
  if (config.scan !== undefined) {
71
90
  if (!config.scan || typeof config.scan !== "object" || Array.isArray(config.scan)) {
72
91
  throw new Error(`sqlx-js: ${path} scan must be an object`);
@@ -100,7 +119,11 @@ function validateConfig(value, path) {
100
119
  export function prepareConfigHash(cfg) {
101
120
  const value = stableValue({
102
121
  jsonbTypes: cfg.jsonbTypes ?? {},
122
+ columnTypes: cfg.columnTypes ?? {},
103
123
  customTypes: cfg.customTypes ?? {},
124
+ functionCatalog: cfg.functionCatalog === false
125
+ ? false
126
+ : { includeExtensionOwned: cfg.functionCatalog?.includeExtensionOwned === true },
104
127
  });
105
128
  return createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
106
129
  }
@@ -159,3 +182,9 @@ export function lookupJsonbType(cfg, schema, table, column) {
159
182
  return (types[`${schema}.${table}.${column}`] ??
160
183
  types[`${table}.${column}`]);
161
184
  }
185
+ export function lookupColumnType(cfg, schema, table, column) {
186
+ const types = cfg.columnTypes;
187
+ if (!types)
188
+ return undefined;
189
+ return types[`${schema}.${table}.${column}`] ?? types[`${table}.${column}`];
190
+ }
@@ -1,5 +1,6 @@
1
1
  import * as rt from "./postgres-runtime.js";
2
2
  import type { Typed as TypedFor, TypedFile as TypedFileFor, TypedSql as TypedSqlFor } from "./typed.js";
3
+ import type { QueryParamsFor, QueryResultFor, QueryRowFor } from "./query.js";
3
4
  export interface KnownQueries {
4
5
  }
5
6
  export interface KnownFileQueries {
@@ -22,9 +23,9 @@ export { defineConfig } from "./config.js";
22
23
  export type { ScanConfig, SqlxJsConfig } from "./config.js";
23
24
  export type { SslMode, ConnConfig } from "./pg/wire.js";
24
25
  export { PgError, ConnectionLostError } from "./pg/wire.js";
25
- export { NoRowsError, TooManyRowsError } from "./runtime.js";
26
+ export { NoRowsError, TooManyRowsError, SQLSTATE, isPgError } from "./runtime.js";
26
27
  export type { TransactionOptions, MigrateOptions, OnQueryEvent, OnQueryHook, OnQueryHookError } from "./runtime.js";
27
- export type { ExecuteResult, JsonParameter, PgArrayParameter } from "./runtime.js";
28
+ export type { ExecuteResult, JsonParameter, PgArrayParameter, JsonCompatible, KnownSqlState } from "./runtime.js";
28
29
  export type { PostgresClient, PostgresOptions, CreateClientOptions } from "./postgres-runtime.js";
29
30
  export type TypedFile = TypedFileFor<KnownFileQueries>;
30
31
  export type TypedSql = TypedSqlFor<KnownQueries, KnownFileQueries>;
@@ -44,6 +45,13 @@ export type SqlClient<Registry extends QueryRegistry = DefaultQueryRegistry> = {
44
45
  client: import("./postgres-runtime.js").PostgresClient;
45
46
  close: () => Promise<void>;
46
47
  };
48
+ export type SqlExecutor<Registry extends QueryRegistry = DefaultQueryRegistry> = TypedSqlFor<Registry["queries"], Registry["fileQueries"]>;
49
+ export type QueryParams<Definition, Registry extends QueryRegistry = DefaultQueryRegistry> = QueryParamsFor<Definition, Registry>;
50
+ export type QueryRow<Definition, Registry extends QueryRegistry = DefaultQueryRegistry> = QueryRowFor<Definition, Registry>;
51
+ export type QueryResult<Definition, Registry extends QueryRegistry = DefaultQueryRegistry> = QueryResultFor<Definition, Registry>;
52
+ export type { QueryDefinition, QueryExecutionMode } from "./query.js";
53
+ export { defineQuery } from "./query.js";
54
+ export { queryId } from "./query-id.js";
47
55
  export declare const sql: Typed;
48
56
  export type Unsafe = (query: string, ...params: unknown[]) => Promise<Record<string, unknown>[]>;
49
57
  export declare const unsafe: Unsafe;
package/dist/src/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import * as rt from "./postgres-runtime.js";
2
2
  export { defineConfig } from "./config.js";
3
3
  export { PgError, ConnectionLostError } from "./pg/wire.js";
4
- export { NoRowsError, TooManyRowsError } from "./runtime.js";
4
+ export { NoRowsError, TooManyRowsError, SQLSTATE, isPgError } from "./runtime.js";
5
+ export { defineQuery } from "./query.js";
6
+ export { queryId } from "./query-id.js";
5
7
  export const sql = rt.sql;
6
8
  export const unsafe = rt.unsafe;
7
9
  export const getClient = rt.getClient;
@@ -1,4 +1,6 @@
1
1
  import type { FunctionEntry } from "../function-cache.js";
2
2
  import { type PgClient } from "./wire.js";
3
3
  import { SchemaCache } from "./schema.js";
4
- export declare function introspectFunctions(client: PgClient, schema: SchemaCache): Promise<FunctionEntry[]>;
4
+ export declare function introspectFunctions(client: PgClient, schema: SchemaCache, options?: {
5
+ includeExtensionOwned?: boolean;
6
+ }): Promise<FunctionEntry[]>;
@@ -2,8 +2,8 @@ import { decodeText } from "./wire.js";
2
2
  const JSON_OIDS = new Set([114, 3802]);
3
3
  const JSON_ARRAY_OIDS = new Set([199, 3807]);
4
4
  const JSON_INPUT = 'import("@onreza/sqlx-js").JsonInput';
5
- export async function introspectFunctions(client, schema) {
6
- const rows = await loadFunctionRows(client);
5
+ export async function introspectFunctions(client, schema, options = {}) {
6
+ const rows = await loadFunctionRows(client, options.includeExtensionOwned === true);
7
7
  const typeOids = new Set();
8
8
  for (const row of rows) {
9
9
  typeOids.add(row.returnOid);
@@ -15,7 +15,7 @@ export async function introspectFunctions(client, schema) {
15
15
  await schema.loadCustomTypes([...typeOids]);
16
16
  return rows.map((row) => toEntry(row, schema)).sort((a, b) => a.signature.localeCompare(b.signature));
17
17
  }
18
- async function loadFunctionRows(client) {
18
+ async function loadFunctionRows(client, includeExtensionOwned) {
19
19
  const result = await client.simpleQueryAll(`
20
20
  SELECT
21
21
  n.nspname,
@@ -36,6 +36,14 @@ async function loadFunctionRows(client) {
36
36
  FROM pg_proc p
37
37
  JOIN pg_namespace n ON n.oid = p.pronamespace
38
38
  WHERE ${userSchemaFilter("n")}
39
+ ${includeExtensionOwned ? "" : `AND NOT EXISTS (
40
+ SELECT 1
41
+ FROM pg_depend d
42
+ WHERE d.classid = 'pg_proc'::regclass
43
+ AND d.objid = p.oid
44
+ AND d.refclassid = 'pg_extension'::regclass
45
+ AND d.deptype = 'e'
46
+ )`}
39
47
  ORDER BY n.nspname, p.proname, pg_get_function_identity_arguments(p.oid)
40
48
  `);
41
49
  return result.rows.map((row) => ({
@@ -10,6 +10,7 @@ export type CreateClientOptions = PostgresOptions & {
10
10
  statementTimeoutMs?: number;
11
11
  fileRoot?: string;
12
12
  reloadSqlFiles?: boolean;
13
+ sqlFiles?: Readonly<Record<string, string>>;
13
14
  };
14
15
  export declare function createClient(url?: string | undefined, options?: CreateClientOptions): PostgresClient;
15
16
  export declare function getClient(): PostgresClient;
@@ -19,6 +20,7 @@ export declare function setClient(client: PostgresClient, options?: {
19
20
  prepare?: boolean;
20
21
  fileRoot?: string;
21
22
  reloadSqlFiles?: boolean;
23
+ sqlFiles?: Readonly<Record<string, string>>;
22
24
  }): void;
23
25
  export declare function close(): Promise<void>;
24
26
  export declare function createSqlClient(url?: string | undefined, options?: CreateClientOptions): {
@@ -13,13 +13,15 @@ class PostgresRuntimeClient {
13
13
  prepare;
14
14
  fileRoot;
15
15
  reloadSqlFiles;
16
- constructor(client, onQuery, onQueryHookError, prepare = true, fileRoot = resolvedFileRoot(), reloadSqlFiles = false) {
16
+ sqlFiles;
17
+ constructor(client, onQuery, onQueryHookError, prepare = true, fileRoot = resolvedFileRoot(), reloadSqlFiles = false, sqlFiles) {
17
18
  this.client = client;
18
19
  this.onQuery = onQuery;
19
20
  this.onQueryHookError = onQueryHookError;
20
21
  this.prepare = prepare;
21
22
  this.fileRoot = fileRoot;
22
23
  this.reloadSqlFiles = reloadSqlFiles;
24
+ this.sqlFiles = sqlFiles;
23
25
  }
24
26
  async query(query, params) {
25
27
  return await this.client.unsafe(query, params, { prepare: this.prepare });
@@ -43,7 +45,7 @@ class PostgresRuntimeClient {
43
45
  if (!("begin" in this.client))
44
46
  throw new Error("sqlx-js.transaction: nested transactions are not supported");
45
47
  return await this.client.begin(async (tx) => {
46
- return await fn(new PostgresRuntimeClient(tx, this.onQuery, this.onQueryHookError, this.prepare, this.fileRoot, this.reloadSqlFiles));
48
+ return await fn(new PostgresRuntimeClient(tx, this.onQuery, this.onQueryHookError, this.prepare, this.fileRoot, this.reloadSqlFiles, this.sqlFiles));
47
49
  });
48
50
  }
49
51
  async close() {
@@ -162,7 +164,7 @@ function installJsonArrayCodecs(client) {
162
164
  export function createClient(url = process.env.DATABASE_URL, options = {}) {
163
165
  if (!url)
164
166
  throw new Error("sqlx-js: DATABASE_URL is not set");
165
- const { onQuery, onQueryHookError, statementTimeoutMs, fileRoot, reloadSqlFiles, ...pgOptions } = options;
167
+ const { onQuery, onQueryHookError, statementTimeoutMs, fileRoot, reloadSqlFiles, sqlFiles, ...pgOptions } = options;
166
168
  const connection = statementTimeoutMs !== undefined
167
169
  ? { ...(pgOptions.connection ?? {}), statement_timeout: statementTimeoutMs }
168
170
  : pgOptions.connection;
@@ -177,13 +179,14 @@ export function createClient(url = process.env.DATABASE_URL, options = {}) {
177
179
  prepare: pgOptions.prepare ?? true,
178
180
  fileRoot: resolvedFileRoot(fileRoot),
179
181
  reloadSqlFiles: reloadSqlFiles ?? false,
182
+ sqlFiles,
180
183
  };
181
184
  return client;
182
185
  }
183
186
  function createDefaultClient() {
184
187
  const client = createClient();
185
188
  const attached = client[HOOKS];
186
- return new PostgresRuntimeClient(client, attached.onQuery, attached.onQueryHookError, attached.prepare, attached.fileRoot, attached.reloadSqlFiles);
189
+ return new PostgresRuntimeClient(client, attached.onQuery, attached.onQueryHookError, attached.prepare, attached.fileRoot, attached.reloadSqlFiles, attached.sqlFiles);
187
190
  }
188
191
  function getRuntimeClient() {
189
192
  defaultClient ??= createDefaultClient();
@@ -195,7 +198,7 @@ export function getClient() {
195
198
  export function setClient(client, options) {
196
199
  installJsonArrayCodecs(client);
197
200
  const attached = client[HOOKS];
198
- defaultClient = new PostgresRuntimeClient(client, options?.onQuery ?? attached?.onQuery, options?.onQueryHookError ?? attached?.onQueryHookError, options?.prepare ?? attached?.prepare ?? client.options?.prepare ?? true, resolvedFileRoot(options?.fileRoot ?? attached?.fileRoot), options?.reloadSqlFiles ?? attached?.reloadSqlFiles ?? false);
201
+ defaultClient = new PostgresRuntimeClient(client, options?.onQuery ?? attached?.onQuery, options?.onQueryHookError ?? attached?.onQueryHookError, options?.prepare ?? attached?.prepare ?? client.options?.prepare ?? true, resolvedFileRoot(options?.fileRoot ?? attached?.fileRoot), options?.reloadSqlFiles ?? attached?.reloadSqlFiles ?? false, options?.sqlFiles ?? attached?.sqlFiles);
199
202
  }
200
203
  export async function close() {
201
204
  if (defaultClient) {
@@ -206,7 +209,7 @@ export async function close() {
206
209
  export function createSqlClient(url = process.env.DATABASE_URL, options = {}) {
207
210
  const client = createClient(url, options);
208
211
  const attached = client[HOOKS];
209
- const runtimeClient = new PostgresRuntimeClient(client, attached.onQuery, attached.onQueryHookError, attached.prepare, attached.fileRoot, attached.reloadSqlFiles);
212
+ const runtimeClient = new PostgresRuntimeClient(client, attached.onQuery, attached.onQueryHookError, attached.prepare, attached.fileRoot, attached.reloadSqlFiles, attached.sqlFiles);
210
213
  const runtime = createSqlRuntime(() => runtimeClient);
211
214
  return {
212
215
  ...runtime,
@@ -0,0 +1 @@
1
+ export declare function queryId(query: string): string;
@@ -0,0 +1,61 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isEscapeStringPrefix, isIdentifierContinuation, readBlockComment, readDollarQuoted, readLineComment, readQuotedIdentifier, readSingleQuoted, } from "./sql-lex.js";
3
+ export function queryId(query) {
4
+ return createHash("sha256").update(normalizeQuery(query)).digest("hex").slice(0, 16);
5
+ }
6
+ function normalizeQuery(query) {
7
+ let out = "";
8
+ let pendingSpace = false;
9
+ let i = 0;
10
+ const emit = (text) => {
11
+ if (pendingSpace && out.length > 0)
12
+ out += " ";
13
+ out += text;
14
+ pendingSpace = false;
15
+ };
16
+ const markSpace = () => {
17
+ if (out.length > 0)
18
+ pendingSpace = true;
19
+ };
20
+ while (i < query.length) {
21
+ const ch = query[i];
22
+ if (/\s/.test(ch)) {
23
+ markSpace();
24
+ i++;
25
+ continue;
26
+ }
27
+ if (ch === "-" && query[i + 1] === "-") {
28
+ i = readLineComment(query, i);
29
+ markSpace();
30
+ continue;
31
+ }
32
+ if (ch === "/" && query[i + 1] === "*") {
33
+ i = readBlockComment(query, i);
34
+ markSpace();
35
+ continue;
36
+ }
37
+ if (ch === "'") {
38
+ const next = readSingleQuoted(query, i, isEscapeStringPrefix(query, i));
39
+ emit(query.slice(i, next));
40
+ i = next;
41
+ continue;
42
+ }
43
+ if (ch === "\"") {
44
+ const next = readQuotedIdentifier(query, i);
45
+ emit(query.slice(i, next));
46
+ i = next;
47
+ continue;
48
+ }
49
+ if (ch === "$") {
50
+ const next = i === 0 || !isIdentifierContinuation(query[i - 1]) ? readDollarQuoted(query, i) : null;
51
+ if (next !== null) {
52
+ emit(query.slice(i, next));
53
+ i = next;
54
+ continue;
55
+ }
56
+ }
57
+ emit(ch);
58
+ i++;
59
+ }
60
+ return out;
61
+ }
@@ -0,0 +1,53 @@
1
+ import type { ExecuteResult } from "./runtime.js";
2
+ import type { TypedSql } from "./typed.js";
3
+ export type QueryExecutionMode = "many" | "one" | "optional" | "execute";
4
+ export type QueryExecutionMetadata = {
5
+ queryId: string;
6
+ queryName?: string;
7
+ };
8
+ export declare const QUERY_EXECUTOR: unique symbol;
9
+ export type QueryExecutorMethod = (mode: QueryExecutionMode, query: string, params: unknown[], metadata: QueryExecutionMetadata) => Promise<unknown>;
10
+ type QueryEntry = {
11
+ params: unknown;
12
+ row: unknown;
13
+ };
14
+ type ParamsOf<T> = T extends {
15
+ params: infer P;
16
+ } ? P extends readonly unknown[] ? P : [P] : never[];
17
+ type RowOf<T> = T extends {
18
+ row: infer R;
19
+ } ? R : never;
20
+ type QueryModeResult<Mode extends QueryExecutionMode, Row> = Mode extends "many" ? Row[] : Mode extends "one" ? Row : Mode extends "optional" ? Row | null : ExecuteResult;
21
+ export type QueryDefinition<Query extends string = string, Mode extends QueryExecutionMode = QueryExecutionMode> = {
22
+ readonly query: Query;
23
+ readonly mode: Mode;
24
+ readonly queryId: string;
25
+ readonly queryName?: string;
26
+ run<Queries extends Record<Query, QueryEntry>, FileQueries>(executor: TypedSql<Queries, FileQueries>, ...params: ParamsOf<Queries[Query]>): Promise<QueryModeResult<Mode, RowOf<Queries[Query]>>>;
27
+ };
28
+ type DefinitionQuery<Definition> = Definition extends QueryDefinition<infer Query, QueryExecutionMode> ? Query : never;
29
+ type DefinitionMode<Definition> = Definition extends QueryDefinition<string, infer Mode> ? Mode : never;
30
+ type RegistryQuery<Definition, Registry extends {
31
+ queries: object;
32
+ }> = DefinitionQuery<Definition> extends keyof Registry["queries"] ? Registry["queries"][DefinitionQuery<Definition>] : never;
33
+ export type QueryParamsFor<Definition, Registry extends {
34
+ queries: object;
35
+ }> = RegistryQuery<Definition, Registry> extends {
36
+ params: infer Params;
37
+ } ? Params : never;
38
+ export type QueryRowFor<Definition, Registry extends {
39
+ queries: object;
40
+ }> = RowOf<RegistryQuery<Definition, Registry>>;
41
+ export type QueryResultFor<Definition, Registry extends {
42
+ queries: object;
43
+ }> = QueryModeResult<DefinitionMode<Definition>, QueryRowFor<Definition, Registry>>;
44
+ type DefineQueryMethod<Mode extends QueryExecutionMode> = {
45
+ <const Query extends string>(query: Query): QueryDefinition<Query, Mode>;
46
+ <const Query extends string>(name: string, query: Query): QueryDefinition<Query, Mode>;
47
+ };
48
+ export declare const defineQuery: DefineQueryMethod<"many"> & {
49
+ one: DefineQueryMethod<"one">;
50
+ optional: DefineQueryMethod<"optional">;
51
+ execute: DefineQueryMethod<"execute">;
52
+ };
53
+ export {};
@@ -0,0 +1,38 @@
1
+ import { queryId } from "./query-id.js";
2
+ export const QUERY_EXECUTOR = Symbol.for("@onreza/sqlx-js.query-executor");
3
+ function definitionMethod(mode) {
4
+ return ((nameOrQuery, maybeQuery) => {
5
+ const query = maybeQuery ?? nameOrQuery;
6
+ const name = maybeQuery === undefined ? undefined : nameOrQuery;
7
+ if (name !== undefined && name.trim() === "") {
8
+ throw new Error("sqlx-js.defineQuery: query name must not be empty");
9
+ }
10
+ const metadata = {
11
+ queryId: queryId(query),
12
+ ...(name ? { queryName: name } : {}),
13
+ };
14
+ return Object.freeze({
15
+ query,
16
+ mode,
17
+ queryId: metadata.queryId,
18
+ ...(name ? { queryName: name } : {}),
19
+ async run(executor, ...params) {
20
+ const execute = executor[QUERY_EXECUTOR];
21
+ if (execute)
22
+ return await execute(mode, query, params, metadata);
23
+ if (mode === "one")
24
+ return await executor.one(query, ...params);
25
+ if (mode === "optional")
26
+ return await executor.optional(query, ...params);
27
+ if (mode === "execute")
28
+ return await executor.execute(query, ...params);
29
+ return await executor(query, ...params);
30
+ },
31
+ });
32
+ });
33
+ }
34
+ export const defineQuery = Object.assign(definitionMethod("many"), {
35
+ one: definitionMethod("one"),
36
+ optional: definitionMethod("optional"),
37
+ execute: definitionMethod("execute"),
38
+ });
@@ -1,5 +1,8 @@
1
1
  import { PgError } from "./pg/wire.js";
2
+ import { QUERY_EXECUTOR, type QueryExecutorMethod } from "./query.js";
2
3
  export type OnQueryEvent = {
4
+ queryId: string;
5
+ queryName?: string;
3
6
  query: string;
4
7
  params: unknown[];
5
8
  durationMs: number;
@@ -21,6 +24,7 @@ export type RuntimeClient = {
21
24
  onQueryHookError?: OnQueryHookError;
22
25
  fileRoot?: string;
23
26
  reloadSqlFiles?: boolean;
27
+ sqlFiles?: Readonly<Record<string, string>>;
24
28
  };
25
29
  type AnyFn = (...args: unknown[]) => Promise<unknown[]>;
26
30
  type AnyOneFn = (...args: unknown[]) => Promise<unknown>;
@@ -28,7 +32,7 @@ type AnyOptionalFn = (...args: unknown[]) => Promise<unknown | null>;
28
32
  type AnyExecuteFn = (...args: unknown[]) => Promise<ExecuteResult>;
29
33
  type IdentifierFn = (...parts: string[]) => string;
30
34
  declare const PARAMETER_KIND: unique symbol;
31
- export type JsonParameter<T = JsonInputValue> = {
35
+ export type JsonParameter<T = unknown> = {
32
36
  readonly [PARAMETER_KIND]: "json";
33
37
  readonly value: T;
34
38
  };
@@ -42,11 +46,17 @@ export type JsonInputObject = {
42
46
  readonly [key: string]: JsonInputValue | undefined;
43
47
  };
44
48
  export type JsonInputArray = readonly JsonInputValue[];
49
+ type NonJsonValue = bigint | symbol | Date | Uint8Array | ((...args: never[]) => unknown);
50
+ export type JsonCompatible<T> = T extends JsonPrimitive ? T : T extends NonJsonValue ? never : T extends readonly unknown[] ? T extends JsonInputArray ? T : {
51
+ readonly [K in keyof T]: JsonCompatible<T[K]>;
52
+ } : T extends object ? Extract<keyof T, symbol> extends never ? T extends JsonInputObject ? T : {
53
+ readonly [K in keyof T]: undefined extends T[K] ? JsonCompatible<Exclude<T[K], undefined>> | undefined : JsonCompatible<T[K]>;
54
+ } : never : never;
45
55
  export type ExecuteResult = {
46
56
  rowCount: number;
47
57
  command: string;
48
58
  };
49
- export declare function json<T extends JsonInputValue>(value: T): JsonParameter<T>;
59
+ export declare function json<T>(value: T & JsonCompatible<T>): JsonParameter<T>;
50
60
  export declare function array<T>(value: readonly (T | null)[]): PgArrayParameter<T>;
51
61
  export declare function parameterKind(value: unknown): "json" | "array" | undefined;
52
62
  declare function renameRows(rows: unknown[]): unknown[];
@@ -63,6 +73,19 @@ export declare class TooManyRowsError extends Error {
63
73
  constructor(actual: number, expected?: "1" | "0 or 1");
64
74
  }
65
75
  export declare function toPgError(e: unknown): PgError | null;
76
+ export declare const SQLSTATE: {
77
+ readonly notNullViolation: "23502";
78
+ readonly foreignKeyViolation: "23503";
79
+ readonly uniqueViolation: "23505";
80
+ readonly checkViolation: "23514";
81
+ readonly serializationFailure: "40001";
82
+ readonly deadlockDetected: "40P01";
83
+ };
84
+ export type KnownSqlState = (typeof SQLSTATE)[keyof typeof SQLSTATE];
85
+ export declare function isPgError(error: unknown): error is PgError;
86
+ export declare function isPgError<const Code extends string>(error: unknown, code: Code): error is PgError & {
87
+ readonly code: Code;
88
+ };
66
89
  export declare const _internal: {
67
90
  renameRows: typeof renameRows;
68
91
  encodeParam: typeof encodeParam;
@@ -74,7 +97,7 @@ export declare const _internal: {
74
97
  parameterKind: typeof parameterKind;
75
98
  toPgError: typeof toPgError;
76
99
  };
77
- declare function loadSqlFile(path: string, fileRoot?: string, reload?: boolean): string;
100
+ declare function loadSqlFile(path: string, fileRoot?: string, reload?: boolean, embedded?: Readonly<Record<string, string>>): string;
78
101
  export declare function clearSqlFileCache(): void;
79
102
  declare function clearIdentifierCache(): void;
80
103
  export declare function id(...parts: string[]): string;
@@ -91,6 +114,7 @@ type SqlCallable = AnyFn & {
91
114
  id: IdentifierFn;
92
115
  json: typeof json;
93
116
  array: typeof array;
117
+ [QUERY_EXECUTOR]: QueryExecutorMethod;
94
118
  };
95
119
  export type TransactionOptions = {
96
120
  isolation?: "read uncommitted" | "read committed" | "repeatable read" | "serializable";