@onreza/sqlx-js 0.5.0 → 0.7.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
@@ -43,9 +43,11 @@ const rows = await sql(
43
43
  - **Shadow database validation** via `migrate dev` / `migrate verify`: auto-create a disposable shadow DB, apply migrations, validate SQL, and drop it afterwards.
44
44
  - **Safe identifier quoting** via `sql.id(...)`, backed by the committed schema snapshot whitelist.
45
45
  - **Single runtime adapter**: Postgres.js backs the runtime on Node/Bun-compatible environments — no Bun.SQL-specific adapter to choose.
46
- - **Watch mode**: debounced re-prepare with a warm `PgClient` + `SchemaCache` on source/SQL changes and scanner/config graph updates.
46
+ - **Incremental watch mode**: debounced re-prepare with a warm `PgClient` + `SchemaCache`; source/SQL edits only rescan affected files and re-describe changed fingerprints, while config/tsconfig/schema changes trigger a full rebuild.
47
47
  - **Cache pruning** removes orphaned entries automatically (toggleable with `--no-prune`).
48
48
  - **Environment doctor** checks runtime versions, config loading, `.env`, database connectivity/permissions, cache metadata, tsconfig inclusion, and pgschema availability.
49
+ - **Strict inference gate** promotes degraded nullability analysis and generated `unknown` query types to CI errors.
50
+ - **GitHub/editor diagnostics adapter** converts versioned prepare JSON into workflow annotations or Unix problem-matcher output.
49
51
 
50
52
  ## Install
51
53
 
@@ -57,7 +59,7 @@ bun add @onreza/sqlx-js
57
59
 
58
60
  Node.js 24 or newer is required. Bun users need Bun 1.3 or newer.
59
61
 
60
- The package installs a `sqlx-js` binary. The CLI examples below use `npx @onreza/sqlx-js`; `bunx @onreza/sqlx-js ...` works the same if your project uses Bun.
62
+ 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.
61
63
 
62
64
  ## Setup
63
65
 
@@ -67,7 +69,7 @@ The package installs a `sqlx-js` binary. The CLI examples below use `npx @onreza
67
69
  npx @onreza/sqlx-js init
68
70
  ```
69
71
 
70
- Creates `sqlx-js.config.ts`, a `migrations/` directory, and `.env.example` if they don't already exist (it never overwrites existing files), then prints the next steps. Skip it if you prefer to wire things up manually.
72
+ Creates `sqlx-js.config.ts`, `sqlx-js-env.d.ts`, a `migrations/` directory, and `.env.example` if they don't already exist. For strict-JSON files it adds missing `sqlx:*` scripts to `package.json` and appends the declaration to an existing `files` or `include` array in `tsconfig.json`, without replacing existing values; JSONC files are left unchanged with a manual-update hint. Skip it if you prefer to wire things up manually.
71
73
 
72
74
  For declarative PostgreSQL schema management, scaffold the pgschema workflow instead:
73
75
 
@@ -144,7 +146,7 @@ const users = await sql(
144
146
  npx @onreza/sqlx-js prepare
145
147
  ```
146
148
 
147
- This generates `sqlx-js-env.d.ts` next to your code. Add it to your `tsconfig.json` `include` if it isn't picked up automatically. Use `--dts <path>` to override the destination.
149
+ This generates `sqlx-js-env.d.ts` next to your code. Add it to your `tsconfig.json` `include` if it isn't picked up automatically. Use `--dts <path>` to override the destination relative to `--root`.
148
150
 
149
151
  ### 5. Dev loop with watch
150
152
 
@@ -322,6 +324,25 @@ import { createClient, setClient } from "@onreza/sqlx-js";
322
324
  setClient(createClient(process.env.DATABASE_URL));
323
325
  ```
324
326
 
327
+ For dependency injection, read replicas, tests, or several independent pools in one process, create a scoped runtime instead of replacing the global client:
328
+
329
+ ```ts
330
+ import { createSqlClient } from "@onreza/sqlx-js";
331
+ import type { SqlxJsGeneratedRegistry } from "./sqlx-js-env";
332
+
333
+ const primary = createSqlClient<SqlxJsGeneratedRegistry>(process.env.DATABASE_URL);
334
+ const replica = createSqlClient<SqlxJsGeneratedRegistry>(process.env.REPLICA_DATABASE_URL);
335
+
336
+ await primary.sql(`INSERT INTO audit_log (message) VALUES ($1)`, "created");
337
+ const rows = await replica.sql(`SELECT id, message FROM audit_log ORDER BY id DESC`);
338
+
339
+ await Promise.all([primary.close(), replica.close()]);
340
+ ```
341
+
342
+ 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.
343
+
344
+ 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.
345
+
325
346
  `createClient(url, options)` accepts every Postgres.js option plus sqlx-js runtime options:
326
347
 
327
348
  ```ts
@@ -331,6 +352,9 @@ setClient(createClient(process.env.DATABASE_URL, {
331
352
  statementTimeoutMs: 5000,
332
353
  // Base directory for root-relative sql.file(...) calls.
333
354
  fileRoot: import.meta.dirname,
355
+ // Development-only: re-stat sql.file() files on every call. The default
356
+ // immutable cache avoids synchronous filesystem work in the query hot path.
357
+ reloadSqlFiles: true,
334
358
  // Honored for every unsafe call. Set false for PgBouncer transaction mode
335
359
  // unless protocol-level prepared statements are configured there.
336
360
  prepare: false,
@@ -339,14 +363,15 @@ setClient(createClient(process.env.DATABASE_URL, {
339
363
  if (error) logger.error({ query, error }); // database errors are PgError
340
364
  else if (durationMs > 200) logger.warn({ slow: query, durationMs, rowCount });
341
365
  },
366
+ onQueryHookError: (error) => logger.error({ error }, "query observer failed"),
342
367
  }));
343
368
  ```
344
369
 
345
- The `onQuery` hook is the integration point for metrics, tracing, and slow-query logging — sqlx-js does not log queries itself. 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.
370
+ 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.
346
371
 
347
372
  ### `clearSqlFileCache()`
348
373
 
349
- Drops the in-memory cache used by `sql.file(...)`. The cache invalidates automatically on file mtime change, so this is rarely needed manually.
374
+ Drops the in-memory cache used by `sql.file(...)`. Files are immutable after their first read by default, avoiding a synchronous `stat` call for every query. Call this after a development-time file change or set `reloadSqlFiles: true` on the client to restore mtime-based reloading.
350
375
 
351
376
  ### Typed errors
352
377
 
@@ -385,43 +410,48 @@ In addition to `import { sql } from "@onreza/sqlx-js"`, the scanner recognises `
385
410
 
386
411
  ```
387
412
  sqlx-js init [--root <dir>] [--schema-provider builtin|pgschema]
388
- sqlx-js doctor [--root <dir>] [--json]
389
- sqlx-js db install | check | plan | apply [--root <dir>] [-- <pgschema args>]
390
- sqlx-js prepare [--check | --verify | --watch] [--json] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]
391
- sqlx-js migrate dev [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] | verify [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] | 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]
392
- sqlx-js schema dump | check [--schema <path>] [--manifest <path>] [--no-manifest] [--shadow-url <url>]
413
+ sqlx-js doctor [--root <dir>] [--dts <path>] [--json]
414
+ sqlx-js db install | check [--root <dir>]
415
+ sqlx-js db plan | apply [--root <dir>] [-- <pgschema args>]
416
+ sqlx-js prepare [--check | --offline | --verify | --watch] [--json | --jsonl] [--strict-inference] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]
417
+ sqlx-js migrate dev [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] [--strict-inference] | verify [--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]
418
+ sqlx-js schema dump [--schema <path>] [--manifest <path>] [--no-manifest] [--shadow-url <url>]
419
+ sqlx-js schema check [--schema <path>] [--shadow-url <url>]
393
420
  sqlx-js --version | --help
394
421
  ```
395
422
 
396
- Regular `prepare` describes queries across a small connection pool (default 8, override with `SQLX_JS_PREPARE_CONCURRENCY`) for faster cold runs on large projects. Watch mode keeps one session warm and reuses it between debounced changes.
423
+ Regular `prepare` describes queries across a small connection pool (default 8, override with `SQLX_JS_PREPARE_CONCURRENCY`) for faster cold runs on large projects. Watch mode keeps one session warm, rescans only affected source files, and reuses cached metadata for unchanged fingerprints. Config, tsconfig, and applied shadow-migration changes invalidate the incremental state and perform a full prepare.
397
424
 
398
425
  | Flag | Meaning |
399
426
  |-----------------------|--------------------------------------------------------------------------------------|
400
- | `--check` | Offline: verify every scanned query is present in cache, no database required. |
427
+ | `--check` | Read-only offline verification of the active query cache, function catalog, and generated declaration. |
428
+ | `--offline` | Regenerate `sqlx-js-env.d.ts` from committed cache without a database. |
401
429
  | `--verify` | Prepare against the live/shadow schema and compare generated artifacts without writing. |
402
430
  | `--watch` | Persistent connection, re-prepare on file change. |
403
431
  | `--root <dir>` | Source/cache/migrations root (default: cwd). |
404
- | `--dts <path>` | Declarations output (default: `<root>/sqlx-js-env.d.ts`). |
405
- | `--no-prune` | Keep orphaned cache entries instead of removing them. |
406
- | `--migrations <dir>` | Migrations directory (default: `<root>/migrations`). |
432
+ | `--dts <path>` | Root-relative declarations output (default: `<root>/sqlx-js-env.d.ts`). |
433
+ | `--no-prune` | Keep orphaned cache entries; they do not invalidate a later `--check`. |
434
+ | `--migrations <dir>` | Root-relative migrations directory (default: `<root>/migrations`). |
407
435
  | `--dry-run` | For `migrate run` / `migrate revert`: validate without applying to the target DB. |
408
436
  | `--json` | Machine-readable prepare diagnostics, doctor output, migration inspection and dry-runs. |
437
+ | `--jsonl` | Versioned streaming events for `prepare --watch`. |
438
+ | `--strict-inference` | Fail prepare/dev/verify when nullability degrades or a generated query type contains `unknown`. |
409
439
  | `--force` | For `migrate archive restore`: allow overwriting existing migration files. |
410
440
  | `--lock-timeout <ms>` | Advisory-lock acquisition timeout for `migrate run` / `revert` / `dev` / `verify` / `squash`. |
411
441
  | `--shadow-url <url>` | Use an existing disposable shadow DB instead of auto-creating one. |
412
442
  | `--shadow-admin-url <url>` | Admin/maintenance DB URL used to auto-create shadow DBs. |
413
443
  | `--replace` | For `migrate squash`: archive replaced migration files after writing the baseline. |
414
444
  | `--pg-dump <path>` | For `migrate squash`: `pg_dump` executable path (default: `pg_dump`). |
415
- | `--schema <path>` | Schema snapshot path (default: `<root>/.sqlx-js/schema/schema.json`). |
416
- | `--manifest <path>` | LLM schema manifest path (default: `<root>/.sqlx-js/schema/schema.md`). |
445
+ | `--schema <path>` | Root-relative schema snapshot path (default: `<root>/.sqlx-js/schema/schema.json`). |
446
+ | `--manifest <path>` | Root-relative LLM schema manifest path (default: `<root>/.sqlx-js/schema/schema.md`). |
417
447
  | `--no-manifest` | Skip writing the LLM schema manifest during `schema dump`. |
418
448
  | `--schema-provider <name>` | For `init`: `builtin` (default) or `pgschema`. |
419
449
 
420
450
  Flags that take a value accept both `--flag value` and `--flag=value` forms.
421
451
 
422
- 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. This is intended for CI annotations and editor integrations; stdout contains one JSON document and human progress is suppressed.
452
+ 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.
423
453
 
424
- `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`, `connect_timeout` (seconds), and `statement_timeout` (milliseconds).
454
+ `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`.
425
455
 
426
456
  ### Development and deployment flows
427
457
 
@@ -454,7 +484,7 @@ The built-in `migrate` workflow is kept for simple projects and embedded applica
454
484
  Use `migrate verify` in PR/CI before merge:
455
485
 
456
486
  ```bash
457
- sqlx-js migrate verify
487
+ sqlx-js migrate verify --strict-inference
458
488
  sqlx-js prepare --check
459
489
  sqlx-js doctor --json
460
490
  tsc --noEmit
@@ -514,6 +544,39 @@ When `prepare` fails, every diagnostic points back to the originating call site:
514
544
 
515
545
  Phases reported separately: `describe failed`, `analyze failed`, `paramMap failed`. PostgreSQL `position`, `code`, and `hint` are surfaced when present.
516
546
 
547
+ ### GitHub and editor diagnostics
548
+
549
+ `sqlx-js-diagnostics` converts the versioned prepare JSON document into GitHub workflow commands or a standard Unix problem-matcher stream:
550
+
551
+ ```bash
552
+ set -o pipefail
553
+ sqlx-js prepare --verify --json | sqlx-js-diagnostics github
554
+ sqlx-js prepare --check --json | sqlx-js-diagnostics unix
555
+ ```
556
+
557
+ The `github` format creates inline workflow annotations. The `unix` format emits `file:line:column: severity: [phase] message`, which can be consumed by VS Code tasks and other editors without a dedicated extension. A minimal VS Code task uses a custom problem matcher:
558
+
559
+ ```json
560
+ {
561
+ "label": "sqlx-js: check",
562
+ "type": "shell",
563
+ "command": "sqlx-js prepare --check --json | sqlx-js-diagnostics unix",
564
+ "problemMatcher": {
565
+ "owner": "sqlx-js",
566
+ "fileLocation": ["relative", "${workspaceFolder}"],
567
+ "pattern": {
568
+ "regexp": "^(.+):(\\d+):(\\d+): (error|warning): \\[([^\\]]+)\\] (.*)$",
569
+ "file": 1,
570
+ "line": 2,
571
+ "column": 3,
572
+ "severity": 4,
573
+ "code": 5,
574
+ "message": 6
575
+ }
576
+ }
577
+ }
578
+ ```
579
+
517
580
  ## Configuration
518
581
 
519
582
  `sqlx-js.config.ts` at the project root is optional.
@@ -527,6 +590,7 @@ export default defineConfig({
527
590
  scan: {
528
591
  include: ["apps/*/src/**/*", "packages/*/src/**/*"],
529
592
  exclude: ["**/*.generated.ts"],
593
+ modules: ["@onreza/sqlx-js", "@app/database"],
530
594
  },
531
595
  schema: {
532
596
  provider: "pgschema",
@@ -541,7 +605,7 @@ export default defineConfig({
541
605
  });
542
606
  ```
543
607
 
544
- By default the scanner uses the root `tsconfig.json` file list and follows TypeScript project references, so a referenced monorepo is scanned without walking unrelated folders. `scan.include` replaces that source-file universe with TypeScript glob patterns; `scan.exclude` is added to the built-in dependency/build exclusions. If there is no root `tsconfig.json`, the fallback is a recursive TypeScript scan.
608
+ By default the scanner uses the root `tsconfig.json` file list and follows TypeScript project references, so a referenced monorepo is scanned without walking unrelated folders. `scan.include` replaces that source-file universe with TypeScript glob patterns; `scan.exclude` is added to the built-in dependency/build exclusions. `scan.modules` replaces the default `@onreza/sqlx-js` import source list, which lets an application re-export `sql` through a shared database module without requiring arbitrary re-export graph analysis. Include `@onreza/sqlx-js` explicitly when direct imports and application-module imports are both used. If there is no root `tsconfig.json`, the fallback is a recursive TypeScript scan.
545
609
 
546
610
  The `schema` block is optional. Use `provider: "pgschema"` when sqlx-js should delegate schema planning/apply commands to pgschema. `command` can override the managed binary lookup and point at another executable. With the pinned pgschema 1.12.0 CLI, `schemas` must contain exactly one schema name.
547
611
 
@@ -620,12 +684,12 @@ Commit the generated `sqlx-js-env.d.ts` and the `.sqlx-js/` cache directory to y
620
684
 
621
685
  ```yaml
622
686
  - run: bun install
623
- - run: sqlx-js migrate verify # built-in migration workflow
687
+ - run: sqlx-js migrate verify --strict-inference # built-in migration workflow
624
688
  # or, when schema.provider is "pgschema":
625
689
  - run: sqlx-js db install
626
690
  - run: sqlx-js db plan -- --output-json plan.json
627
- - run: sqlx-js prepare --verify # live/shadow comparison of every generated artifact
628
- - run: sqlx-js prepare --check # offline cache/version/config consistency
691
+ - run: sqlx-js prepare --verify --strict-inference # live/shadow comparison with complete inference
692
+ - run: sqlx-js prepare --check # read-only offline cache/declaration consistency
629
693
  - run: sqlx-js doctor --json # runtime/config/DB/cache/tsconfig preflight
630
694
  - run: sqlx-js schema check # fails if the committed schema snapshot is stale
631
695
  - run: tsc --noEmit # fails if types are stale
@@ -633,7 +697,7 @@ Commit the generated `sqlx-js-env.d.ts` and the `.sqlx-js/` cache directory to y
633
697
  - run: bun run build # emits publishable JS + declarations under dist/
634
698
  ```
635
699
 
636
- The `migrate verify` step needs `DATABASE_URL` credentials that can either create a temporary database or use `--shadow-admin-url` / `--shadow-url`. It does not write `.sqlx-js/` or `sqlx-js-env.d.ts`. For pgschema projects, `sqlx-js db plan` checks the desired `schema.sql` against the target database and leaves application query typing to `prepare`. The `prepare --check` step then runs without a database; your committed offline cache is the source of truth. Add `prepare --verify` when CI has a canonical database/shadow schema and must prove byte-for-byte artifact freshness. `schema check` intentionally uses a live database because it verifies the committed schema contract against PostgreSQL.
700
+ The `migrate verify` step needs `DATABASE_URL` credentials that can either create a temporary database or use `--shadow-admin-url` / `--shadow-url`. It does not write `.sqlx-js/` or `sqlx-js-env.d.ts`. For pgschema projects, `sqlx-js db plan` checks the desired `schema.sql` against the target database and leaves application query typing to `prepare`. `prepare --check` is read-only and fails when either the committed cache or declaration is stale. Use `prepare --offline` when a developer intentionally needs to restore the declaration from a valid committed cache. Add `prepare --verify` when CI has a canonical database/shadow schema and must prove byte-for-byte artifact freshness. `schema check` intentionally uses a live database because it verifies the committed schema contract against PostgreSQL.
637
701
 
638
702
  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.
639
703
 
@@ -646,20 +710,21 @@ bun install # installs lefthook + wires git hooks
646
710
  cargo install cocogitto # or: brew install cocogitto
647
711
  ```
648
712
 
649
- Releases are automated via `release-please`: pushes to `main` accumulate into a release PR that bumps `package.json`, writes `CHANGELOG.md`, and on merge tags the commit. The tag push fires the npm publish workflow, which builds `dist/`, smoke-tests the package entrypoints, checks the tarball contents, and publishes to npm.
713
+ Releases are automated via `release-please`: pushes to `main` accumulate into a release PR that bumps `package.json` and writes `CHANGELOG.md`. Merging that PR creates the tag and release, then the same workflow builds `dist/`, smoke-tests the package entrypoints, checks the tarball contents, and publishes to npm through Trusted Publishing.
650
714
 
651
715
  ## Limitations
652
716
 
653
717
  `sqlx-js` is a young library. Known gaps:
654
718
 
655
719
  - PostgreSQL only (no MySQL or SQLite).
656
- - The scanner only follows direct named imports and namespace imports from `@onreza/sqlx-js`; it does not follow re-exports, dynamic aliases, or tagged-template calls.
720
+ - The scanner only follows direct named imports and namespace imports from configured `scan.modules` (default: `@onreza/sqlx-js`); it does not discover re-export graphs, dynamic aliases, or tagged-template calls automatically.
657
721
  - `SELECT *` falls back to conservative nullability.
658
722
  - Plain `sql(...)` keeps returning rows, so statements without `RETURNING` produce an empty typed array. Use `sql.execute(...)` when affected-row count and command metadata matter.
659
723
  - Nested CTE references (CTE-`b` referencing CTE-`a` in the same `WITH`) and `WITH RECURSIVE` are not analysed transitively — at worst this produces extra `T | null`. Use `AS "id!"` overrides if needed.
660
724
  - Column names whose **real** name (not an alias) ends with `!` or `?` are not supported — the runtime strips those suffixes assuming an override. Use `AS "alias"` if you have such a column.
725
+ - Result columns must have unique names because Postgres.js returns object rows. Alias join projections such as `users.id AS user_id, posts.id AS post_id`; `prepare` rejects duplicate output names before generating declarations.
661
726
  - 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.
662
- - The **internal** wire client (used by `migrate run`, `prepare`, and the runtime `migrate()` helper) reads `sslmode`, `sslrootcert`/`sslcert`/`sslkey`, `application_name`, `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`).
727
+ - 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`).
663
728
  - `connect_timeout` bounds the entire internal-client connect, including the TLS handshake and SCRAM authentication.
664
729
  - 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.
665
730
 
@@ -667,7 +732,7 @@ See [ROADMAP.md](./ROADMAP.md) for what's planned.
667
732
 
668
733
  ## Upgrading
669
734
 
670
- ### Cache and parameter contract change (pre-1.0)
735
+ ### Cache, codegen, and parameter contract changes (pre-1.0)
671
736
 
672
737
  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.
673
738
 
@@ -675,6 +740,10 @@ Generated JSON and PostgreSQL array parameters now require `sql.json(...)` and `
675
740
 
676
741
  CI (`prepare --check`) will also fail loudly until the cache is regenerated; this is intentional so a stale schema can't silently emit incorrect `.d.ts`.
677
742
 
743
+ 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.
744
+
745
+ 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.
746
+
678
747
  ## License
679
748
 
680
749
  MIT.
package/ROADMAP.md CHANGED
@@ -6,12 +6,12 @@ Items already shipped live in the [README](./README.md) feature list; this file
6
6
 
7
7
  | Feature | ROI | Notes |
8
8
  |---------|-----|-------|
9
- | pgschema integration hardening | 8 | Basic `init --schema-provider pgschema` plus managed `sqlx-js db install/check/plan/apply` exists. Continue with better docs, CI examples, schema snapshot handoff, and migration guidance for projects that outgrow built-in migrations. |
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
  | 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. |
11
11
  | 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
12
  | 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. |
13
13
  | 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. |
14
- | LSP server | 6 | Realtime diagnostics, hover with column types, autocomplete on schema names. Versioned `prepare --json` diagnostics now provide the cheaper CI/editor integration foundation; a full server still needs separate VS Code / Neovim clients. |
14
+ | LSP server | 5 | Realtime hover and schema autocomplete. Versioned batch JSON, incremental `prepare --watch --jsonl`, and `sqlx-js-diagnostics` already provide the transport for editor problems; a full server still needs separate VS Code / Neovim clients. |
15
15
  | Schema-aware `jsonb` runtime validation | 5 | Optional opt-in: pass a Zod / Valibot / ArkType schema, validate rows on read. Currently we are compile-time-only by design. |
16
16
  | MySQL backend | 5 | Some runtime clients support it, but MySQL has no `Describe Statement` equivalent. Would need a real SQL parser pass + `INFORMATION_SCHEMA` introspection. |
17
17
  | SQLite backend | 4 | SQLite's column types are dynamic. Would require running `EXPLAIN` and a heuristic mapper, or schema-driven inference per-statement. |
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ function isRecord(value) {
3
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4
+ }
5
+ function optionalString(value, name) {
6
+ if (value !== undefined && typeof value !== "string")
7
+ throw new Error(`${name} must be a string`);
8
+ }
9
+ function optionalPosition(value, name) {
10
+ if (value !== undefined && (!Number.isInteger(value) || value < 1)) {
11
+ throw new Error(`${name} must be a positive integer`);
12
+ }
13
+ }
14
+ function parsePayload(value) {
15
+ if (!isRecord(value) || value.formatVersion !== 1 || typeof value.ok !== "boolean" || !Array.isArray(value.diagnostics)) {
16
+ throw new Error("expected sqlx-js diagnostic formatVersion 1");
17
+ }
18
+ const diagnostics = value.diagnostics.map((item, index) => {
19
+ if (!isRecord(item))
20
+ throw new Error(`diagnostics[${index}] must be an object`);
21
+ if (item.severity !== "error" && item.severity !== "warning") {
22
+ throw new Error(`diagnostics[${index}].severity must be error or warning`);
23
+ }
24
+ if (typeof item.phase !== "string")
25
+ throw new Error(`diagnostics[${index}].phase must be a string`);
26
+ if (typeof item.message !== "string")
27
+ throw new Error(`diagnostics[${index}].message must be a string`);
28
+ optionalString(item.file, `diagnostics[${index}].file`);
29
+ optionalString(item.code, `diagnostics[${index}].code`);
30
+ optionalPosition(item.line, `diagnostics[${index}].line`);
31
+ optionalPosition(item.column, `diagnostics[${index}].column`);
32
+ return {
33
+ severity: item.severity,
34
+ phase: item.phase,
35
+ message: item.message,
36
+ ...(item.file !== undefined ? { file: item.file } : {}),
37
+ ...(item.line !== undefined ? { line: item.line } : {}),
38
+ ...(item.column !== undefined ? { column: item.column } : {}),
39
+ ...(item.code !== undefined ? { code: item.code } : {}),
40
+ };
41
+ });
42
+ return { formatVersion: 1, ok: value.ok, diagnostics };
43
+ }
44
+ function githubData(value) {
45
+ return value.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
46
+ }
47
+ function githubProperty(value) {
48
+ return githubData(value).replaceAll(":", "%3A").replaceAll(",", "%2C");
49
+ }
50
+ function renderGithub(diagnostic) {
51
+ const properties = [];
52
+ if (diagnostic.file)
53
+ properties.push(`file=${githubProperty(diagnostic.file)}`);
54
+ if (diagnostic.line !== undefined)
55
+ properties.push(`line=${diagnostic.line}`);
56
+ if (diagnostic.column !== undefined)
57
+ properties.push(`col=${diagnostic.column}`);
58
+ const propertyText = properties.length > 0 ? ` ${properties.join(",")}` : "";
59
+ const code = diagnostic.code ? ` ${diagnostic.code}` : "";
60
+ return `::${diagnostic.severity}${propertyText}::${githubData(`[${diagnostic.phase}${code}] ${diagnostic.message}`)}`;
61
+ }
62
+ function renderUnix(diagnostic) {
63
+ const clean = (value) => value.replace(/[\r\n]+/g, " ");
64
+ const file = clean(diagnostic.file ?? "<project>");
65
+ const line = diagnostic.line ?? 1;
66
+ const column = diagnostic.column ?? 1;
67
+ const code = diagnostic.code ? ` ${clean(diagnostic.code)}` : "";
68
+ return `${file}:${line}:${column}: ${diagnostic.severity}: [${clean(diagnostic.phase)}${code}] ${clean(diagnostic.message)}`;
69
+ }
70
+ function usage() {
71
+ console.log("usage: sqlx-js-diagnostics github|unix < prepare-diagnostics.json");
72
+ process.exit(0);
73
+ }
74
+ const format = process.argv[2];
75
+ if (format === "--help" || format === "-h")
76
+ usage();
77
+ if (format !== "github" && format !== "unix") {
78
+ console.error("sqlx-js-diagnostics: expected output format github or unix");
79
+ process.exit(2);
80
+ }
81
+ let input = "";
82
+ for await (const chunk of process.stdin)
83
+ input += chunk;
84
+ let payload;
85
+ try {
86
+ payload = parsePayload(JSON.parse(input));
87
+ }
88
+ catch (error) {
89
+ console.error(`sqlx-js-diagnostics: ${error.message}`);
90
+ process.exit(2);
91
+ }
92
+ for (const diagnostic of payload.diagnostics) {
93
+ console.log(format === "github" ? renderGithub(diagnostic) : renderUnix(diagnostic));
94
+ }
95
+ process.exit(payload.ok ? 0 : 1);
96
+ export {};