@onreza/sqlx-js 0.8.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 CHANGED
@@ -24,12 +24,13 @@ const rows = await sql(
24
24
  - **Precise nullability inference** through `libpg-query`: `JOIN` direction (LEFT/RIGHT/FULL), inner `JOIN ... ON` predicates, DML `RETURNING`, `COALESCE`, `CASE`, `COUNT`, expression propagation. Parameters become `T | null` when wrapped in `COALESCE`/`NULLIF`/`IS [NOT] NULL`/`IS [NOT] DISTINCT FROM`, or when bound to a nullable column in `INSERT`/`UPDATE`.
25
25
  - **WHERE narrowing**: `IS NOT NULL`, equality chains, `IN`, `LIKE`, `BETWEEN` make columns non-null. Tracks `AND`/`OR` semantics.
26
26
  - **PostgreSQL enums** generated as TypeScript literal unions (read + write side).
27
- - **Schema-aware `jsonb`** via a `SqlxJsJson` global namespace and a config-driven column → type mapping. Works for both result columns and `INSERT`/`UPDATE`/`WHERE` parameters. Unmapped `json`/`jsonb` falls back to `JsonValue` for rows and `JsonInput` for parameters instead of `unknown`.
27
+ - **Schema-aware `jsonb`** via a `SqlxJsJson` global namespace and a config-driven column → type mapping. Works for both result columns and `INSERT`/`UPDATE`/`WHERE` parameters. Unmapped `json`/`jsonb` falls back to `JsonValue` for rows and an explicitly wrapped, structurally checked JSON parameter.
28
28
  - **Extension types out of the box**: `pgvector` (`vector`, `halfvec`, `sparsevec`), `hstore`, `citext`, `ltree`/`lquery`/`ltxtquery`. Add your own through `customTypes` config.
29
29
  - **Domains** resolve to their base TypeScript type (`CREATE DOMAIN email AS text` → `string`), including domains over extension types or other domains.
30
30
  - **Wide built-in type coverage**: numeric, text, date/time, UUID, json/jsonb, network (inet/cidr/macaddr/macaddr8), bit strings, ranges/multiranges, geometric, money, tsvector/tsquery, xml — and the matching array variants.
31
31
  - **External SQL files** via `sql.file("queries/foo.sql", ...)` — prepared and typed through `KnownFileQueries`. Watch mode re-prepares on `.sql` edits too.
32
32
  - **One-row helpers**: `sql.one(...)`, `sql.optional(...)`, `sql.file.one(...)`, `sql.file.optional(...)`, and the same chain on the `tx` callback — friendly with `noUncheckedIndexedAccess: true`. The scanner walks all of them.
33
+ - **Reusable query definitions** via `defineQuery`: declare one typed SQL contract and execute it through either a root client or transaction-scoped executor. `QueryParams`, `QueryRow`, and `QueryResult` expose its generated types without indexing a registry by SQL text.
33
34
  - **Unambiguous JSON and PostgreSQL array params** through `sql.json(...)` and `sql.array(...)`. Primitive JSON arrays cannot be silently encoded as PostgreSQL array literals.
34
35
  - **Typed transactions** via `sql.transaction(async tx => …)` — the `tx` callback parameter is recognized by the scanner, so queries inside the block keep full type checking.
35
36
  - **Sourcemap-accurate error reporting**: every prepare failure points to `file:line:column` of the originating `sql(...)` call site, with PG error code, position, and hint.
@@ -39,7 +40,7 @@ const rows = await sql(
39
40
  - **Optional pgschema workflow** via `init --schema-provider pgschema` and `sqlx-js db install|check|plan|apply` for PostgreSQL schema-as-code projects.
40
41
  - **Versioned offline cache** committed to your repo. `prepare --check` validates fingerprints, generator revision, and type-affecting config without a database; `prepare --verify` compares fresh live/shadow artifacts without writing.
41
42
  - **Schema snapshot + LLM manifest** via `schema dump` / `schema check`: tables, columns, constraints, indexes, types, and function/procedure metadata are introspected from PostgreSQL.
42
- - **Generated function catalog** via `KnownFunctions`: `prepare` records user-schema PostgreSQL functions/procedures from `pg_proc` with approximate parameter and return TypeScript types.
43
+ - **Generated function catalog** via `KnownFunctions`: `prepare` records application-owned PostgreSQL functions/procedures from `pg_proc` with approximate parameter and return TypeScript types while excluding extension-owned internals by default.
43
44
  - **Shadow database validation** via `migrate dev` / `migrate verify`: auto-create a disposable shadow DB, apply migrations, validate SQL, and drop it afterwards.
44
45
  - **Safe identifier quoting** via `sql.id(...)`, backed by the committed schema snapshot whitelist.
45
46
  - **Single runtime adapter**: Postgres.js backs the runtime on Node/Bun-compatible environments — no Bun.SQL-specific adapter to choose.
@@ -48,6 +49,7 @@ const rows = await sql(
48
49
  - **Environment doctor** checks runtime versions, config loading, `.env`, database connectivity/permissions, cache metadata, tsconfig inclusion, and pgschema availability.
49
50
  - **Strict inference gate** promotes degraded nullability analysis and generated `unknown` query types to CI errors.
50
51
  - **GitHub/editor diagnostics adapter** converts versioned prepare JSON into workflow annotations or Unix problem-matcher output.
52
+ - **Versioned query inventory** via `queries --json`, including stable query IDs, definition names, cardinality, call sites, SQL files, and cache state. The same command can emit a deterministic embedded-SQL module for bundled applications.
51
53
 
52
54
  ## Install
53
55
 
@@ -59,7 +61,7 @@ bun add @onreza/sqlx-js
59
61
  bun add --dev typescript
60
62
  ```
61
63
 
62
- Node.js 24 or newer is required. Bun users need Bun 1.3 or newer. TypeScript is an optional peer so production-only installs do not pull the compiler and its platform package into the application image; source scanning commands (`prepare`, `doctor`, `ci`, and migration development/verification) require it in development dependencies.
64
+ Node.js 24 or newer is required. Bun users need Bun 1.3 or newer. TypeScript is an optional peer so production-only installs do not pull the compiler and its platform package into the application image; source scanning commands (`prepare`, `queries`, `doctor`, `ci`, and migration development/verification) require it in development dependencies.
63
65
 
64
66
  The package installs `sqlx-js` and `sqlx-js-diagnostics` binaries. The CLI examples below use `npx @onreza/sqlx-js`; `bunx @onreza/sqlx-js ...` works the same if your project uses Bun.
65
67
 
@@ -128,6 +130,8 @@ const rows = await sql(
128
130
 
129
131
  Named parameters use ASCII identifier names (`$user_id`), are numbered by first appearance before PostgreSQL sees the query, and repeated names reuse the same positional parameter. The generated object contract rejects missing, extra, and incorrectly typed properties. Named `$name` and positional `$1` parameters cannot be mixed in one query. Quoted strings, comments, dollar-quoted bodies, and `$` inside PostgreSQL identifiers are left unchanged. Positional parameters remain supported and are still the shortest form for simple queries.
130
132
 
133
+ PostgreSQL describes an uncast `LIMIT $limit` or `OFFSET $offset` parameter as `int8`, so its TypeScript type is `bigint`. Use `LIMIT $limit::int` / `OFFSET $offset::int` when the application pagination API uses JavaScript `number` values.
134
+
131
135
  During local development, validate the migration and regenerate query artifacts against a disposable shadow database:
132
136
 
133
137
  ```bash
@@ -185,6 +189,39 @@ const rows = await sql(`SELECT id FROM users WHERE name = $1`, "alice");
185
189
 
186
190
  Unknown queries, wrong parameter types, and dynamic strings are compile errors. For genuinely dynamic SQL, use `unsafe`.
187
191
 
192
+ ### `defineQuery`
193
+
194
+ Define a query once without closing over a global client, then run the same generated contract through a root or transaction executor:
195
+
196
+ ```ts
197
+ import {
198
+ defineQuery,
199
+ sql,
200
+ type QueryParams,
201
+ type QueryResult,
202
+ type QueryRow,
203
+ type SqlExecutor,
204
+ } from "@onreza/sqlx-js";
205
+
206
+ export const findUser = defineQuery.optional(
207
+ "users.findById",
208
+ `SELECT id, email FROM users WHERE id = $id`,
209
+ );
210
+
211
+ type FindUserParams = QueryParams<typeof findUser>;
212
+ type FindUserRow = QueryRow<typeof findUser>;
213
+ type FindUserResult = QueryResult<typeof findUser>; // FindUserRow | null
214
+
215
+ await findUser.run(sql, { id: userId });
216
+ await sql.transaction((tx) => findUser.run(tx, { id: userId }));
217
+
218
+ async function loadUser(executor: SqlExecutor, params: FindUserParams) {
219
+ return findUser.run(executor, params);
220
+ }
221
+ ```
222
+
223
+ The optional definition name is included in query observer and inventory metadata. The stable `queryId` is derived from the same lexical SQL fingerprint used by prepare/cache. `defineQuery.one`, `.optional`, and `.execute` mirror the cardinality contracts of the corresponding `sql` helpers.
224
+
188
225
  ### `sql.file(path, ...params)`
189
226
 
190
227
  Load SQL from an external file. The path is root-relative everywhere: prepare resolves it against `--root`, codegen keeps the exact string literal as the `KnownFileQueries` key, and runtime resolves it against `fileRoot` (default: `process.cwd()`). Absolute paths and paths escaping the root are rejected.
@@ -202,6 +239,20 @@ const admins = await sql.file("queries/top_admins.sql", "admin", 5);
202
239
 
203
240
  File-backed queries are emitted into a separate `KnownFileQueries` interface. A call from any nested source directory still uses the same project-root-relative literal.
204
241
 
242
+ For a compiled or bundled application, emit a TypeScript asset module and pass it to the client:
243
+
244
+ ```bash
245
+ sqlx-js queries --embed src/sqlx-js-files.generated.ts
246
+ ```
247
+
248
+ ```ts
249
+ import { sqlxJsEmbeddedSql } from "./sqlx-js-files.generated";
250
+
251
+ const db = createSqlClient(databaseUrl, { sqlFiles: sqlxJsEmbeddedSql });
252
+ ```
253
+
254
+ Embedded entries take precedence over filesystem reads. The module contains only referenced external SQL files; inline SQL remains in application code.
255
+
205
256
  ### `sql.one(query, ...params)` and `sql.optional(query, ...params)`
206
257
 
207
258
  Convenience wrappers for single-row queries. `one` throws if the row count is not exactly 1; `optional` returns `null` for 0 rows and throws on more than 1. They keep working under `noUncheckedIndexedAccess: true` without `rows[0]!` patterns.
@@ -255,6 +306,8 @@ await sql(
255
306
 
256
307
  Generated parameter types require `PgArrayParameter<T>` or `JsonParameter<T>`, so mixing the two representations is a TypeScript error. A PostgreSQL `json[]` / `jsonb[]` composes both wrappers: the outer `sql.array(...)` selects the PostgreSQL array representation and each non-SQL-NULL element uses `sql.json(...)`. `sql.json(null)` represents JSON `null`; a bare `null` remains SQL `NULL` when the database parameter is nullable.
257
308
 
309
+ `sql.json()` accepts ordinary structurally JSON-compatible interfaces and preserves their concrete type in `JsonParameter<T>`. It rejects known non-JSON values such as `Date`, `bigint`, functions, and `undefined` array elements. TypeScript is structurally typed, so it cannot identify every user-defined class solely because it was constructed with `new`; runtime JSON semantics still belong to `JSON.stringify`.
310
+
258
311
  Both helpers also work with `unsafe(...)`. `encodePgArrayLiteral(arr)` remains exported for code that explicitly needs a PostgreSQL array literal string.
259
312
 
260
313
  ### Parameter nullability
@@ -357,6 +410,8 @@ await Promise.all([primary.close(), replica.close()]);
357
410
 
358
411
  Each generated `sqlx-js-env.d.ts` exports its own `SqlxJsGeneratedRegistry`. Passing it to `createSqlClient<...>()` keeps a scoped client on that project's query contract even when a monorepo TypeScript program includes declarations for several databases. The global `sql` export remains available for the single-client convenience path.
359
412
 
413
+ When a workspace package exports database source to other TypeScript programs, bind `SqlxJsGeneratedRegistry` at that package's client boundary. A consumer does not automatically include the database package's ambient `.d.ts`; exporting an unscoped client can therefore collapse its literal parameters to `never` outside the package.
414
+
360
415
  The scanner recognizes clients assigned directly from an imported `createSqlClient(...)` (including aliased and namespace imports), so `client.sql(...)`, its cardinality helpers, file queries, and transactions participate in `prepare` exactly like the global `sql` surface.
361
416
 
362
417
  `createClient(url, options)` accepts every Postgres.js option plus sqlx-js runtime options:
@@ -371,19 +426,22 @@ setClient(createClient(process.env.DATABASE_URL, {
371
426
  // Development-only: re-stat sql.file() files on every call. The default
372
427
  // immutable cache avoids synchronous filesystem work in the query hot path.
373
428
  reloadSqlFiles: true,
429
+ // Optional generated map from `sqlx-js queries --embed ...`. When present,
430
+ // sql.file() needs no runtime filesystem asset for those paths.
431
+ sqlFiles: sqlxJsEmbeddedSql,
374
432
  // Honored for every unsafe call. Set false for PgBouncer transaction mode
375
433
  // unless protocol-level prepared statements are configured there.
376
434
  prepare: false,
377
435
  // Fires after every query/transaction statement, success or failure.
378
- onQuery: ({ query, params, durationMs, rowCount, error }) => {
379
- if (error) logger.error({ query, error }); // database errors are PgError
380
- else if (durationMs > 200) logger.warn({ slow: query, durationMs, rowCount });
436
+ onQuery: ({ queryId, queryName, query, params, durationMs, rowCount, error }) => {
437
+ if (error) logger.error({ queryId, queryName, query, error });
438
+ else if (durationMs > 200) logger.warn({ queryId, queryName, durationMs, rowCount });
381
439
  },
382
440
  onQueryHookError: (error) => logger.error({ error }, "query observer failed"),
383
441
  }));
384
442
  ```
385
443
 
386
- The `onQuery` hook is the integration point for metrics, tracing, and slow-query logging — sqlx-js does not log queries itself. It is a non-blocking observer: synchronous throws and asynchronous rejections preserve the database result/error and are passed to `onQueryHookError` when configured. The event carries the raw `params`, which may contain personal or sensitive data — don't log them blindly; redact or omit `params` in shared sinks. Database errors are normalized to `PgError`; transport and non-database errors pass through unchanged.
444
+ The `onQuery` hook is the integration point for metrics, tracing, and slow-query logging — sqlx-js does not log queries itself. `queryId` is the stable prepare/cache fingerprint and is suitable for metric labels; `queryName` is present for named `defineQuery` calls. The hook is a non-blocking observer: synchronous throws and asynchronous rejections preserve the database result/error and are passed to `onQueryHookError` when configured. The event carries the raw `params`, which may contain personal or sensitive data — don't log them blindly; redact or omit `params` in shared sinks. Database errors are normalized to `PgError`; transport and non-database errors pass through unchanged.
387
445
 
388
446
  ### `clearSqlFileCache()`
389
447
 
@@ -392,19 +450,19 @@ Drops the in-memory cache used by `sql.file(...)`. Files are immutable after the
392
450
  ### Typed errors
393
451
 
394
452
  ```ts
395
- import { NoRowsError, TooManyRowsError, PgError } from "@onreza/sqlx-js";
453
+ import { NoRowsError, TooManyRowsError, SQLSTATE, isPgError } from "@onreza/sqlx-js";
396
454
 
397
455
  try {
398
456
  const u = await sql.one(`SELECT id FROM users WHERE id = $1`, 99);
399
457
  } catch (e) {
400
458
  if (e instanceof NoRowsError) return null;
401
459
  if (e instanceof TooManyRowsError) console.error("ambiguous query, got", e.actual);
402
- if (e instanceof PgError) console.error("pg code:", e.code, "position:", e.position);
460
+ if (isPgError(e, SQLSTATE.uniqueViolation)) console.error("duplicate:", e.constraint);
403
461
  throw e;
404
462
  }
405
463
  ```
406
464
 
407
- `sql.one` throws `NoRowsError` on 0 rows and `TooManyRowsError` (with `.actual`) on >1. Any database error raised by the default runtime is normalized into a `PgError`, so `e instanceof PgError` works the same in `prepare`, `migrate`, and ordinary `sql(...)` calls. `PgError` exposes `.code`, `.position`, `.hint`, `.detail`, `.severity`, `.schema`, `.table`, `.column`, `.constraint`, and the original driver error on `.cause`. Non-database failures (e.g. a dropped connection) are rethrown unchanged.
465
+ `sql.one` throws `NoRowsError` on 0 rows and `TooManyRowsError` (with `.actual`) on >1. Any database error raised by the default runtime is normalized into a `PgError`; `isPgError(error, code?)` is the concise type guard for SQLSTATE handling. `SQLSTATE` contains the common unique, foreign-key, not-null, check, serialization, and deadlock codes. `PgError` exposes `.code`, `.position`, `.hint`, `.detail`, `.severity`, `.schema`, `.table`, `.column`, `.constraint`, and the original driver error on `.cause`. A zero-row update/delete is not a PostgreSQL error: use `sql.execute(...).rowCount`, `sql.optional`, or `sql.one` for that invariant. Non-database failures are rethrown unchanged.
408
466
 
409
467
  ### Transactions with options
410
468
 
@@ -431,6 +489,7 @@ sqlx-js ci [--root <dir>] [--dts <path>] [--schema <path>] [--json] [--shadow-ur
431
489
  sqlx-js db install | check [--root <dir>]
432
490
  sqlx-js db plan | apply [--root <dir>] [-- <pgschema args>]
433
491
  sqlx-js prepare [--check | --offline | --verify | --watch] [--json | --jsonl] [--strict-inference] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]
492
+ sqlx-js queries [--json] [--embed <path>] [--root <dir>]
434
493
  sqlx-js migrate dev [--dts <path>] [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] [--strict-inference] | verify [--dts <path>] [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] [--strict-inference] | run [--dry-run] [--json] [--lock-timeout <ms>] | info [--json] | check [--json] | revert [--dry-run] [--json] [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] | add <name> | squash <name> [--shadow-admin-url <url> | --shadow-url <url>] [--replace] [--pg-dump <path>] [--lock-timeout <ms>] | archive list | archive restore <name> [--force]
435
494
  sqlx-js schema dump [--schema <path>] [--manifest <path>] [--no-manifest] [--shadow-url <url>]
436
495
  sqlx-js schema check [--schema <path>] [--shadow-url <url>]
@@ -451,6 +510,7 @@ Regular `prepare` describes queries across a small connection pool (default 8, o
451
510
  | `--migrations <dir>` | Root-relative migrations directory (default: `<root>/migrations`). |
452
511
  | `--dry-run` | For `migrate run` / `migrate revert`: validate without applying to the target DB. |
453
512
  | `--json` | Machine-readable prepare diagnostics, doctor output, migration inspection and dry-runs. |
513
+ | `--embed <path>` | For `queries`: write a deterministic TypeScript map of referenced external SQL files. |
454
514
  | `--jsonl` | Versioned streaming events for `prepare --watch`. |
455
515
  | `--strict-inference` | Fail prepare/dev/verify when nullability degrades or a generated query type contains `unknown`. |
456
516
  | `--force` | For `migrate archive restore`: allow overwriting existing migration files. |
@@ -466,7 +526,9 @@ Regular `prepare` describes queries across a small connection pool (default 8, o
466
526
 
467
527
  Flags that take a value accept both `--flag value` and `--flag=value` forms.
468
528
 
469
- Prepare and doctor JSON use `formatVersion: 1`. Prepare diagnostics include a stable phase plus root-relative file, 1-based line/column, PostgreSQL code/position/hint when available, and the query text. Degraded inference and generated `unknown` types appear as warnings by default; `--strict-inference` promotes them to errors. This is intended for CI annotations and editor integrations; stdout contains one JSON document and human progress is suppressed. `prepare --watch --jsonl` emits one `start`, `diagnostic`, `prepared`, `error`, `watching`, or `stopping` event per line so an editor can consume diagnostics without waiting for the watch process to exit. Fatal `error` events include the same structured `diagnostic` object as CLI preflight failures, preserving the prepare phase and source location when available.
529
+ Prepare and doctor JSON use `formatVersion: 1`. Prepare diagnostics include a stable phase plus root-relative file, 1-based line/column, query ID/name, PostgreSQL code/position/hint when available, and the query text. Degraded inference and generated `unknown` types appear as warnings by default; `--strict-inference` promotes them to errors. This is intended for CI annotations and editor integrations; stdout contains one JSON document and human progress is suppressed. `prepare --watch --jsonl` emits one `start`, `diagnostic`, `prepared`, `error`, `watching`, or `stopping` event per line so an editor can consume diagnostics without waiting for the watch process to exit. Fatal `error` events include the same structured `diagnostic` object as CLI preflight failures, preserving the prepare phase and source location when available.
530
+
531
+ `queries --json` is database-free and read-only. It emits `formatVersion: 1` inventory entries with `queryId`, optional definition names, cardinalities, root-relative call sites, SQL file paths, and `current`/`stale`/`missing` cache status, plus orphaned cache IDs. Config, scan, cache, and embed failures use versioned structured diagnostics with source location when available. Adding `--embed` writes the external-SQL module only after a successful scan.
470
532
 
471
533
  `DATABASE_URL` must be set for any command that touches the application database or auto-creates a shadow database. `SHADOW_ADMIN_DATABASE_URL` can point at a maintenance/admin database when the application user cannot `CREATE DATABASE`; `SHADOW_DATABASE_URL` can point at a pre-created disposable shadow database. The internal wire client understands `sslmode`, `sslrootcert`, `sslcert`, `sslkey`, `application_name`, `options` (PostgreSQL startup options such as `-c search_path=app,public`), `connect_timeout` (seconds), and `statement_timeout` (milliseconds). Unqualified relations are resolved using the prepare session's real `search_path`; they are not assumed to live in `public`.
472
534
 
@@ -619,6 +681,14 @@ export default defineConfig({
619
681
  "posts.meta": "SqlxJsJson.PostMeta",
620
682
  "posts.attachments": "SqlxJsJson.Attachment",
621
683
  },
684
+ // Explicit application-owned assertions for direct scalar columns only.
685
+ columnTypes: {
686
+ "analytics_event.action": "AnalyticsAction",
687
+ },
688
+ functionCatalog: {
689
+ // Extension-owned functions are excluded by default.
690
+ includeExtensionOwned: false,
691
+ },
622
692
  });
623
693
  ```
624
694
 
@@ -644,7 +714,17 @@ declare global {
644
714
  export {};
645
715
  ```
646
716
 
647
- After re-running `prepare`, every `jsonb` column or parameter declared in `jsonbTypes` is checked against the corresponding TypeScript type. Columns without a custom mapping use `JsonValue` for result rows and `JsonInput` inside `JsonParameter` for parameters, both exported by `@onreza/sqlx-js`. Pass JSON parameters through `sql.json(value)`: non-JSON inputs such as `Date`, functions, and `bigint` are rejected by TypeScript while plain JSON objects, arrays, strings, numbers, booleans, and nested JSON `null` values are accepted. A bare top-level `null` remains SQL `NULL` and is allowed only when the mapped database parameter is nullable; use `sql.json(null)` for JSON `null`.
717
+ After re-running `prepare`, every `jsonb` column or parameter declared in `jsonbTypes` is checked against the corresponding TypeScript type. Columns without a custom mapping use `JsonValue` for result rows and `JsonParameter<unknown>` for parameters: the existential parameter type accepts any wrapper already proven JSON-safe by `sql.json(value)` without requiring domain interfaces to declare a string index signature. Non-JSON inputs such as `Date`, functions, and `bigint` are rejected by TypeScript while plain JSON objects, arrays, strings, numbers, booleans, and nested JSON `null` values are accepted. A bare top-level `null` remains SQL `NULL` and is allowed only when the mapped database parameter is nullable; use `sql.json(null)` for JSON `null`.
718
+
719
+ ### Direct scalar `columnTypes`
720
+
721
+ `columnTypes` is an explicit application-owned type assertion for a direct scalar table column. It affects result fields that PostgreSQL attributes to that exact column and parameters that sqlx-js maps back to it through `INSERT`, `UPDATE`, `WHERE`, or `JOIN` analysis. It never changes arbitrary expressions such as `upper(action)`, and it does not apply to PostgreSQL/JSON array columns. Use a schema-qualified key when table names can collide. Mapping the same logical column through both `jsonbTypes` and `columnTypes` is rejected.
722
+
723
+ This assertion does not validate stored values at runtime. Prefer a PostgreSQL enum/domain when the database truly owns a closed value set; use `columnTypes` when the database deliberately stores a broader scalar such as `text` and the application accepts responsibility for the narrower TypeScript contract.
724
+
725
+ ### Function catalog scope
726
+
727
+ Application-owned functions and procedures from non-system schemas are generated into `KnownFunctions`. Objects owned by installed extensions are excluded through `pg_depend`, preventing extension internals from dominating committed artifacts. Set `functionCatalog.includeExtensionOwned: true` only when those approximate signatures are needed, or set `functionCatalog: false` to disable catalog generation entirely.
648
728
 
649
729
  ### Extension types and `customTypes`
650
730
 
@@ -726,6 +806,8 @@ The `migrate verify` step needs `DATABASE_URL` credentials that can either creat
726
806
 
727
807
  The managed pgschema binary is installed under `node_modules/.cache/sqlx-js/pgschema/`, not `.sqlx-js/`, so it is not part of the committed offline cache.
728
808
 
809
+ Generated declarations and cache files should be excluded from formatters and linters. They remain included in `tsconfig.json` for type checking, but rules such as Biome's empty-interface or confusing-void checks are not meaningful for declaration-merging points and PostgreSQL procedure contracts.
810
+
729
811
  ## Contributing
730
812
 
731
813
  The project uses [conventional commits](https://www.conventionalcommits.org/), validated locally by `cocogitto` through `lefthook` hooks. Install both before contributing:
@@ -751,7 +833,7 @@ Releases are automated via `release-please`: pushes to `main` accumulate into a
751
833
  - Migrations run inside `BEGIN/COMMIT`. DDL that disallows transactions (`CREATE INDEX CONCURRENTLY`, `VACUUM`, `REINDEX CONCURRENTLY`, …) will fail; split such operations into separate migrations executed outside the runner.
752
834
  - The **internal** wire client (used by `migrate run`, `prepare`, and the runtime `migrate()` helper) reads `sslmode`, `sslrootcert`/`sslcert`/`sslkey`, `application_name`, `options`, `connect_timeout`, and `statement_timeout` from `DATABASE_URL`. The default runtime `sql()` path delegates connection handling to Postgres.js; configure TLS, pooling, and timeouts through the `DATABASE_URL` and `createClient(...)` options it understands (`statementTimeoutMs` is a convenience that maps to a per-connection `statement_timeout`).
753
835
  - `connect_timeout` bounds the entire internal-client connect, including the TLS handshake and SCRAM authentication.
754
- - Runtime `sql.file(path)` resolves against `fileRoot` while prepare resolves against `--root`. They are both root-relative, but applications started outside the project root must set `fileRoot` explicitly.
836
+ - Runtime `sql.file(path)` resolves against `fileRoot` while prepare resolves against `--root`. They are both root-relative, but applications started outside the project root must set `fileRoot` explicitly or provide the generated `sqlFiles` map.
755
837
 
756
838
  See [ROADMAP.md](./ROADMAP.md) for what's planned.
757
839
 
@@ -759,7 +841,7 @@ See [ROADMAP.md](./ROADMAP.md) for what's planned.
759
841
 
760
842
  ### Cache, codegen, and parameter contract changes (pre-1.0)
761
843
 
762
- Generated cache now includes `.sqlx-js/cache-manifest.json` with an explicit cache format, generator revision, and hash of `jsonbTypes` / `customTypes`. Cache without this manifest is rejected. Delete `.sqlx-js/` and re-run `sqlx-js prepare` against your database — there is no data loss because the cache is generated.
844
+ Generated cache includes `.sqlx-js/cache-manifest.json` with an explicit cache format, generator revision, and hash of type/function-catalog settings. Cache without this manifest is rejected. Delete `.sqlx-js/` and re-run `sqlx-js prepare` against your database — there is no data loss because the cache is generated.
763
845
 
764
846
  Generated JSON and PostgreSQL array parameters now require `sql.json(...)` and `sql.array(...)`. This removes the ambiguous runtime guess where a JavaScript array could mean either a PostgreSQL array or a JSON array. Replace raw array JSON params with `sql.json(value)` and PostgreSQL arrays with `sql.array(value)` before regenerating declarations.
765
847
 
@@ -767,6 +849,8 @@ CI (`prepare --check`) will also fail loudly until the cache is regenerated; thi
767
849
 
768
850
  Generator revision 4 changes the declaration layout so it exports `SqlxJsGeneratedRegistry` for scoped clients while continuing to augment the global `KnownQueries` convenience API. Re-run live `sqlx-js prepare` after upgrading. `prepare --check` is now strictly read-only; use `prepare --offline` when deliberate cache-to-declaration regeneration is required.
769
851
 
852
+ Generator revision 6 adds `columnTypes` and function-catalog scope to the generated contract. Extension-owned functions are no longer emitted by default. Re-run live `sqlx-js prepare`; set `functionCatalog.includeExtensionOwned: true` only if application code intentionally indexes those signatures.
853
+
770
854
  Runtime observers and SQL-file caching are also stricter production boundaries. An exception from `onQuery` no longer replaces a successful query result; handle it through `onQueryHookError`. `sql.file()` no longer performs an mtime check on every call—use `reloadSqlFiles: true` during development or call `clearSqlFileCache()` explicitly after changing a file.
771
855
 
772
856
  ## License
package/ROADMAP.md CHANGED
@@ -9,7 +9,7 @@ Items already shipped live in the [README](./README.md) feature list; this file
9
9
  | pgschema integration hardening | 6 | Managed install/check/plan/apply now has a real pinned-binary PostgreSQL E2E in CI. Continue with schema snapshot handoff and migration guidance for projects that outgrow built-in migrations. |
10
10
  | Separate runtime package | Deferred | The audited root import already excludes compile-time modules. Making TypeScript an optional peer reduced a clean production install from about 33 MB to 2.4 MB; a second public package and release boundary is not justified for the remaining analyzer dependency unless production consumers demonstrate measurable pressure. |
11
11
  | Built-in migration lifecycle maintenance | 5 | Keep `migrate run/dev/verify/revert/squash/archive` stable for simple projects and application startup, but avoid expanding it into a full PostgreSQL schema-as-code system. |
12
- | Prisma migration assistant | 7 | Import Prisma Migrate SQL history and Prisma TypedSQL/raw SQL into `sqlx-js`; classify Prisma Client CRUD/nested-write sites as assisted/manual instead of promising a fully automatic ORM rewrite. |
12
+ | Prisma migration assistant | 7 | Import Prisma Migrate SQL history and Prisma TypedSQL/raw SQL into `sqlx-js`; classify Prisma Client CRUD/nested-write sites as assisted/manual instead of promising a fully automatic ORM rewrite. The shipped `queries --json` inventory covers sqlx-js definitions/call sites after conversion, not Prisma reference-graph discovery. |
13
13
  | Self-join precision (unqualified ColumnRef) | 4 | `SELECT name FROM users u1 JOIN users u2 ON ...` with unqualified `name` can't be attributed to a specific alias. PG would reject ambiguous unqualified refs anyway, but explicit aliasing currently has no narrowing benefit in self-joins. |
14
14
  | Tagged-template literal API (`` sql`SELECT ${x}` ``) | 8 | Restoring sqlx's inline-SQL aesthetic requires either a TS compiler plugin (`ts-patch`) or a Bun preload-time AST rewriter. TS itself hardcodes the first tag argument as `TemplateStringsArray` and refuses to narrow to literal tuples. Significant effort, large UX win. |
15
15
  | Editor integration / LSP | Deferred | Keep the versioned batch JSON, incremental `prepare --watch --jsonl`, and `sqlx-js-diagnostics` transport stable, but do not build or maintain a VS Code extension or full LSP until real consumer demand justifies the separate editor clients and release lifecycle. |
@@ -26,6 +26,7 @@ usage:
26
26
  sqlx-js db install | check [--root <dir>]
27
27
  sqlx-js db plan | apply [--root <dir>] [-- <pgschema args>]
28
28
  sqlx-js prepare [--check | --offline | --verify | --watch] [--json | --jsonl] [--strict-inference] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]
29
+ sqlx-js queries [--json] [--embed <path>] [--root <dir>]
29
30
  sqlx-js migrate dev [--dts <path>] [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] [--strict-inference] | verify [--dts <path>] [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] [--strict-inference] | run [--dry-run] [--json] [--lock-timeout <ms>] | info [--json] | check [--json] | revert [--dry-run] [--json] [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] | add <name> | squash <name> [--shadow-admin-url <url> | --shadow-url <url>] [--replace] [--pg-dump <path>] [--lock-timeout <ms>] | archive list | archive restore <name> [--force]
30
31
  sqlx-js schema dump [--schema <path>] [--manifest <path>] [--no-manifest] [--shadow-url <url>]
31
32
  sqlx-js schema check [--schema <path>] [--shadow-url <url>]
@@ -49,6 +50,7 @@ flags:
49
50
  --dry-run validate and print migrate run/revert plan without applying migrations
50
51
  --json machine-readable output for prepare and migration inspection/dry-run commands
51
52
  --jsonl streaming machine-readable output for prepare --watch
53
+ --embed <path> emit a deterministic TypeScript map of referenced SQL files
52
54
  --strict-inference fail when query inference degrades or emits unknown types
53
55
  --force allow archive restore to overwrite existing migration files
54
56
  --lock-timeout <ms> advisory-lock acquisition timeout for migrate run/revert/dev/verify/squash
@@ -66,6 +68,7 @@ flags:
66
68
  ci: `usage: sqlx-js ci [--root <dir>] [--dts <path>] [--schema <path>] [--json] [--shadow-url <url>] [--shadow-admin-url <url>] [--migrations <dir>]`,
67
69
  db: `usage: sqlx-js db install | check [--root <dir>] | plan | apply [--root <dir>] [-- <pgschema args>]`,
68
70
  prepare: `usage: sqlx-js prepare [--check | --offline | --verify | --watch] [--json | --jsonl] [--strict-inference] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]`,
71
+ queries: `usage: sqlx-js queries [--json] [--embed <path>] [--root <dir>]`,
69
72
  migrate: `usage: sqlx-js migrate dev [--dts <path>] [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] [--strict-inference] | verify [--dts <path>] [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] [--strict-inference] | run [--dry-run] [--json] [--lock-timeout <ms>] | info [--json] | check [--json] | revert [--dry-run] [--json] [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] | add <name> | squash <name> [--shadow-admin-url <url> | --shadow-url <url>] [--replace] [--pg-dump <path>] [--lock-timeout <ms>] | archive list | archive restore <name> [--force]`,
70
73
  schema: `usage: sqlx-js schema dump [--schema <path>] [--manifest <path>] [--no-manifest] [--shadow-url <url>] | check [--schema <path>] [--shadow-url <url>]`,
71
74
  };
@@ -86,7 +89,7 @@ const passthroughIndex = rawArgv.indexOf("--");
86
89
  const cliArgv = passthroughIndex >= 0 ? rawArgv.slice(0, passthroughIndex) : rawArgv;
87
90
  const passthroughArgs = passthroughIndex >= 0 ? rawArgv.slice(passthroughIndex + 1) : [];
88
91
  const cmd = cliArgv[0];
89
- const scopes = new Set(["init", "doctor", "ci", "db", "prepare", "migrate", "schema"]);
92
+ const scopes = new Set(["init", "doctor", "ci", "db", "prepare", "queries", "migrate", "schema"]);
90
93
  if (cmd === "--version" || cmd === "-v") {
91
94
  console.log(VERSION);
92
95
  process.exit(0);
@@ -137,6 +140,8 @@ function optionsFor(command, subcommand) {
137
140
  "strict-inference": { type: "boolean" },
138
141
  };
139
142
  }
143
+ if (command === "queries")
144
+ return { ...ROOT_OPTIONS, json: { type: "boolean" }, embed: { type: "string" } };
140
145
  if (command === "schema") {
141
146
  const common = {
142
147
  ...ROOT_OPTIONS,
@@ -228,7 +233,7 @@ function requirePositionals(min, max, label) {
228
233
  }
229
234
  }
230
235
  function validateInvocation() {
231
- if (cmd === "init" || cmd === "doctor" || cmd === "ci" || cmd === "prepare") {
236
+ if (cmd === "init" || cmd === "doctor" || cmd === "ci" || cmd === "prepare" || cmd === "queries") {
232
237
  requirePositionals(0, 0, cmd);
233
238
  return;
234
239
  }
@@ -302,6 +307,7 @@ if (cmd !== "doctor") {
302
307
  const needsTypeScript = cmd === "doctor" ||
303
308
  cmd === "ci" ||
304
309
  cmd === "prepare" ||
310
+ cmd === "queries" ||
305
311
  (cmd === "migrate" && (positionals[0] === "dev" || positionals[0] === "verify"));
306
312
  if (needsTypeScript) {
307
313
  try {
@@ -531,6 +537,37 @@ else if (cmd === "prepare") {
531
537
  }
532
538
  }
533
539
  }
540
+ else if (cmd === "queries") {
541
+ const { QueriesError, runQueries } = await import("../src/commands/queries.js");
542
+ const embed = arg("--embed");
543
+ try {
544
+ await runQueries({
545
+ root,
546
+ cacheDir,
547
+ json: flag("--json"),
548
+ embedPath: embed ? resolve(root, embed) : undefined,
549
+ });
550
+ }
551
+ catch (error) {
552
+ if (flag("--json")) {
553
+ const diagnostic = error instanceof QueriesError
554
+ ? {
555
+ severity: "error",
556
+ phase: error.phase,
557
+ message: error.message,
558
+ ...(error.file === undefined ? {} : { file: error.file }),
559
+ ...(error.line === undefined ? {} : { line: error.line }),
560
+ ...(error.column === undefined ? {} : { column: error.column }),
561
+ }
562
+ : { severity: "error", phase: "scan", message: error.message };
563
+ console.log(JSON.stringify({ formatVersion: 1, ok: false, diagnostics: [diagnostic] }, null, 2));
564
+ }
565
+ else {
566
+ console.error(error.message);
567
+ }
568
+ process.exit(2);
569
+ }
570
+ }
534
571
  else if (cmd === "schema") {
535
572
  const { runSchemaCheck, runSchemaDump } = await import("../src/commands/schema.js");
536
573
  const sub = positionals[0];
@@ -1,6 +1,9 @@
1
1
  export declare const CACHE_FORMAT_VERSION = 3;
2
- export declare const GENERATOR_REVISION = 5;
2
+ export declare const GENERATOR_REVISION = 6;
3
3
  export declare const CACHE_MANIFEST_FILE = "cache-manifest.json";
4
+ export declare class CacheManifestStaleError extends Error {
5
+ constructor(path: string);
6
+ }
4
7
  export type CacheManifest = {
5
8
  cacheFormat: typeof CACHE_FORMAT_VERSION;
6
9
  generatorRevision: typeof GENERATOR_REVISION;
package/dist/src/cache.js CHANGED
@@ -1,74 +1,23 @@
1
- import { createHash, randomBytes } from "node:crypto";
1
+ import { randomBytes } from "node:crypto";
2
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, renameSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { isBuiltinOid } from "./pg/oids.js";
5
- import { isEscapeStringPrefix, isIdentifierContinuation, readBlockComment, readDollarQuoted, readLineComment, readQuotedIdentifier, readSingleQuoted, } from "./sql-lex.js";
5
+ import { queryId } from "./query-id.js";
6
6
  import { rewriteNamedParameters } from "./sql-params.js";
7
7
  export const CACHE_FORMAT_VERSION = 3;
8
- export const GENERATOR_REVISION = 5;
8
+ export const GENERATOR_REVISION = 6;
9
9
  export const CACHE_MANIFEST_FILE = "cache-manifest.json";
10
+ export class CacheManifestStaleError extends Error {
11
+ constructor(path) {
12
+ super(`sqlx-js: cache manifest is stale: ${path}. Run \`sqlx-js prepare\`.`);
13
+ this.name = "CacheManifestStaleError";
14
+ }
15
+ }
10
16
  export function portableCacheOid(oid) {
11
17
  return isBuiltinOid(oid) ? oid : 0;
12
18
  }
13
19
  export function fingerprint(query) {
14
- const norm = normalizeForFingerprint(query);
15
- return createHash("sha256").update(norm).digest("hex").slice(0, 16);
16
- }
17
- function normalizeForFingerprint(query) {
18
- let out = "";
19
- let pendingSpace = false;
20
- let i = 0;
21
- const emit = (text) => {
22
- if (pendingSpace && out.length > 0)
23
- out += " ";
24
- out += text;
25
- pendingSpace = false;
26
- };
27
- const markSpace = () => {
28
- if (out.length > 0)
29
- pendingSpace = true;
30
- };
31
- while (i < query.length) {
32
- const ch = query[i];
33
- if (/\s/.test(ch)) {
34
- markSpace();
35
- i++;
36
- continue;
37
- }
38
- if (ch === "-" && query[i + 1] === "-") {
39
- i = readLineComment(query, i);
40
- markSpace();
41
- continue;
42
- }
43
- if (ch === "/" && query[i + 1] === "*") {
44
- i = readBlockComment(query, i);
45
- markSpace();
46
- continue;
47
- }
48
- if (ch === "'") {
49
- const next = readSingleQuoted(query, i, isEscapeStringPrefix(query, i));
50
- emit(query.slice(i, next));
51
- i = next;
52
- continue;
53
- }
54
- if (ch === "\"") {
55
- const next = readQuotedIdentifier(query, i);
56
- emit(query.slice(i, next));
57
- i = next;
58
- continue;
59
- }
60
- if (ch === "$") {
61
- const next = i === 0 || !isIdentifierContinuation(query[i - 1]) ? readDollarQuoted(query, i) : null;
62
- if (next !== null) {
63
- emit(query.slice(i, next));
64
- i = next;
65
- continue;
66
- }
67
- }
68
- emit(ch);
69
- i++;
70
- }
71
- return out;
20
+ return queryId(query);
72
21
  }
73
22
  export function effectiveNullable(c) {
74
23
  if (c.override === "non-null")
@@ -249,7 +198,7 @@ export function readCacheManifest(cacheDir) {
249
198
  if (value.cacheFormat !== CACHE_FORMAT_VERSION ||
250
199
  value.generatorRevision !== GENERATOR_REVISION ||
251
200
  typeof value.configHash !== "string") {
252
- throw new Error(`sqlx-js: cache manifest is stale: ${path}. Run \`sqlx-js prepare\`.`);
201
+ throw new CacheManifestStaleError(path);
253
202
  }
254
203
  return value;
255
204
  }
@@ -259,7 +208,7 @@ export function assertCacheManifest(cacheDir, configHash) {
259
208
  throw new Error(`sqlx-js: cache manifest is missing. Run \`sqlx-js prepare\` to regenerate the cache.`);
260
209
  }
261
210
  if (manifest.configHash !== configHash) {
262
- throw new Error("sqlx-js: cache was generated with a different jsonbTypes/customTypes config. Run `sqlx-js prepare`.");
211
+ throw new Error("sqlx-js: cache was generated with a different jsonbTypes/customTypes config or other type/function catalog settings. Run `sqlx-js prepare`.");
263
212
  }
264
213
  return manifest;
265
214
  }
@@ -6,9 +6,14 @@ export default defineConfig({
6
6
  // Map jsonb columns/params to TypeScript types declared in a .d.ts, e.g.
7
7
  // "users.settings": "SqlxJsJson.UserSettings",
8
8
  jsonbTypes: {},
9
+ // Assert narrower TypeScript types for direct scalar columns.
10
+ columnTypes: {},
9
11
  // Map PostgreSQL type names to TypeScript types, e.g.
10
12
  // geometry: "GeoJSON.Geometry",
11
13
  customTypes: {},
14
+ // Extension-owned functions are excluded by default. Set false to disable
15
+ // the generated function catalog entirely.
16
+ functionCatalog: {},
12
17
  });
13
18
  `;
14
19
  const PGSCHEMA_CONFIG_TEMPLATE = `import { defineConfig } from "@onreza/sqlx-js";
@@ -20,7 +25,9 @@ export default defineConfig({
20
25
  schemas: ["public"],
21
26
  },
22
27
  jsonbTypes: {},
28
+ columnTypes: {},
23
29
  customTypes: {},
30
+ functionCatalog: {},
24
31
  });
25
32
  `;
26
33
  const ENV_TEMPLATE = `# Connection string used by sqlx-js prepare/migrate and the runtime.
@@ -144,6 +151,7 @@ export function runInit(opts) {
144
151
  "sqlx:offline": "sqlx-js prepare --offline",
145
152
  "sqlx:verify": "sqlx-js prepare --verify --strict-inference",
146
153
  "sqlx:ci": "sqlx-js ci",
154
+ "sqlx:queries": "sqlx-js queries --json",
147
155
  };
148
156
  let changed = false;
149
157
  for (const [name, command] of Object.entries(defaults)) {
@@ -34,6 +34,8 @@ export type PrepareDiagnostic = {
34
34
  line?: number;
35
35
  column?: number;
36
36
  query?: string;
37
+ queryId?: string;
38
+ queryName?: string;
37
39
  code?: string;
38
40
  position?: number;
39
41
  hint?: string;