@onreza/sqlx-js 0.6.0 → 0.8.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,7 +43,7 @@ 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
49
  - **Strict inference gate** promotes degraded nullability analysis and generated `unknown` query types to CI errors.
@@ -53,11 +53,13 @@ const rows = await sql(
53
53
 
54
54
  ```bash
55
55
  npm install @onreza/sqlx-js
56
+ npm install --save-dev typescript
56
57
  # or
57
58
  bun add @onreza/sqlx-js
59
+ bun add --dev typescript
58
60
  ```
59
61
 
60
- Node.js 24 or newer is required. Bun users need Bun 1.3 or newer.
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.
61
63
 
62
64
  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.
63
65
 
@@ -112,6 +114,20 @@ CREATE TABLE users (
112
114
  );
113
115
  ```
114
116
 
117
+ For queries with several values, named parameters keep the SQL and arguments aligned:
118
+
119
+ ```ts
120
+ const rows = await sql(
121
+ `SELECT id, name
122
+ FROM users
123
+ WHERE email = $email OR recovery_email = $email
124
+ LIMIT $limit`,
125
+ { email: "user@example.com", limit: 10 },
126
+ );
127
+ ```
128
+
129
+ 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
+
115
131
  During local development, validate the migration and regenerate query artifacts against a disposable shadow database:
116
132
 
117
133
  ```bash
@@ -324,6 +340,25 @@ import { createClient, setClient } from "@onreza/sqlx-js";
324
340
  setClient(createClient(process.env.DATABASE_URL));
325
341
  ```
326
342
 
343
+ For dependency injection, read replicas, tests, or several independent pools in one process, create a scoped runtime instead of replacing the global client:
344
+
345
+ ```ts
346
+ import { createSqlClient } from "@onreza/sqlx-js";
347
+ import type { SqlxJsGeneratedRegistry } from "./sqlx-js-env";
348
+
349
+ const primary = createSqlClient<SqlxJsGeneratedRegistry>(process.env.DATABASE_URL);
350
+ const replica = createSqlClient<SqlxJsGeneratedRegistry>(process.env.REPLICA_DATABASE_URL);
351
+
352
+ await primary.sql(`INSERT INTO audit_log (message) VALUES ($1)`, "created");
353
+ const rows = await replica.sql(`SELECT id, message FROM audit_log ORDER BY id DESC`);
354
+
355
+ await Promise.all([primary.close(), replica.close()]);
356
+ ```
357
+
358
+ 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
+
360
+ 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
+
327
362
  `createClient(url, options)` accepts every Postgres.js option plus sqlx-js runtime options:
328
363
 
329
364
  ```ts
@@ -333,6 +368,9 @@ setClient(createClient(process.env.DATABASE_URL, {
333
368
  statementTimeoutMs: 5000,
334
369
  // Base directory for root-relative sql.file(...) calls.
335
370
  fileRoot: import.meta.dirname,
371
+ // Development-only: re-stat sql.file() files on every call. The default
372
+ // immutable cache avoids synchronous filesystem work in the query hot path.
373
+ reloadSqlFiles: true,
336
374
  // Honored for every unsafe call. Set false for PgBouncer transaction mode
337
375
  // unless protocol-level prepared statements are configured there.
338
376
  prepare: false,
@@ -341,14 +379,15 @@ setClient(createClient(process.env.DATABASE_URL, {
341
379
  if (error) logger.error({ query, error }); // database errors are PgError
342
380
  else if (durationMs > 200) logger.warn({ slow: query, durationMs, rowCount });
343
381
  },
382
+ onQueryHookError: (error) => logger.error({ error }, "query observer failed"),
344
383
  }));
345
384
  ```
346
385
 
347
- 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.
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.
348
387
 
349
388
  ### `clearSqlFileCache()`
350
389
 
351
- Drops the in-memory cache used by `sql.file(...)`. The cache invalidates automatically on file mtime change, so this is rarely needed manually.
390
+ 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.
352
391
 
353
392
  ### Typed errors
354
393
 
@@ -388,28 +427,31 @@ In addition to `import { sql } from "@onreza/sqlx-js"`, the scanner recognises `
388
427
  ```
389
428
  sqlx-js init [--root <dir>] [--schema-provider builtin|pgschema]
390
429
  sqlx-js doctor [--root <dir>] [--dts <path>] [--json]
430
+ sqlx-js ci [--root <dir>] [--dts <path>] [--schema <path>] [--json] [--shadow-url <url>] [--shadow-admin-url <url>]
391
431
  sqlx-js db install | check [--root <dir>]
392
432
  sqlx-js db plan | apply [--root <dir>] [-- <pgschema args>]
393
- sqlx-js prepare [--check | --verify | --watch] [--json] [--strict-inference] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]
394
- 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]
433
+ sqlx-js prepare [--check | --offline | --verify | --watch] [--json | --jsonl] [--strict-inference] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]
434
+ 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]
395
435
  sqlx-js schema dump [--schema <path>] [--manifest <path>] [--no-manifest] [--shadow-url <url>]
396
436
  sqlx-js schema check [--schema <path>] [--shadow-url <url>]
397
437
  sqlx-js --version | --help
398
438
  ```
399
439
 
400
- 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.
440
+ 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.
401
441
 
402
442
  | Flag | Meaning |
403
443
  |-----------------------|--------------------------------------------------------------------------------------|
404
- | `--check` | Offline: verify every scanned query is present in cache, no database required. |
444
+ | `--check` | Read-only offline verification of the active query cache, function catalog, and generated declaration. |
445
+ | `--offline` | Regenerate `sqlx-js-env.d.ts` from committed cache without a database. |
405
446
  | `--verify` | Prepare against the live/shadow schema and compare generated artifacts without writing. |
406
447
  | `--watch` | Persistent connection, re-prepare on file change. |
407
448
  | `--root <dir>` | Source/cache/migrations root (default: cwd). |
408
449
  | `--dts <path>` | Root-relative declarations output (default: `<root>/sqlx-js-env.d.ts`). |
409
- | `--no-prune` | Keep orphaned cache entries instead of removing them. |
450
+ | `--no-prune` | Keep orphaned cache entries; they do not invalidate a later `--check`. |
410
451
  | `--migrations <dir>` | Root-relative migrations directory (default: `<root>/migrations`). |
411
452
  | `--dry-run` | For `migrate run` / `migrate revert`: validate without applying to the target DB. |
412
453
  | `--json` | Machine-readable prepare diagnostics, doctor output, migration inspection and dry-runs. |
454
+ | `--jsonl` | Versioned streaming events for `prepare --watch`. |
413
455
  | `--strict-inference` | Fail prepare/dev/verify when nullability degrades or a generated query type contains `unknown`. |
414
456
  | `--force` | For `migrate archive restore`: allow overwriting existing migration files. |
415
457
  | `--lock-timeout <ms>` | Advisory-lock acquisition timeout for `migrate run` / `revert` / `dev` / `verify` / `squash`. |
@@ -424,9 +466,9 @@ Regular `prepare` describes queries across a small connection pool (default 8, o
424
466
 
425
467
  Flags that take a value accept both `--flag value` and `--flag=value` forms.
426
468
 
427
- 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.
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.
428
470
 
429
- `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).
471
+ `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`.
430
472
 
431
473
  ### Development and deployment flows
432
474
 
@@ -655,6 +697,14 @@ The runtime strips the `!`/`?` suffix from column keys so the row shape stays cl
655
697
 
656
698
  ## CI workflow
657
699
 
700
+ The shortest production gate is provider-aware:
701
+
702
+ ```bash
703
+ sqlx-js ci
704
+ ```
705
+
706
+ For the built-in migration provider it runs shadow migration verification with strict inference, followed by the read-only offline artifact check. For pgschema it checks the configured provider, fails when the desired schema produces an unapplied plan, performs live `prepare --verify --strict-inference`, and then verifies committed artifacts offline. If a committed schema snapshot exists, both flows also run `schema check`. `--json` returns a versioned per-step report suitable for CI systems.
707
+
658
708
  Commit the generated `sqlx-js-env.d.ts` and the `.sqlx-js/` cache directory to your repo. In CI:
659
709
 
660
710
  ```yaml
@@ -664,7 +714,7 @@ Commit the generated `sqlx-js-env.d.ts` and the `.sqlx-js/` cache directory to y
664
714
  - run: sqlx-js db install
665
715
  - run: sqlx-js db plan -- --output-json plan.json
666
716
  - run: sqlx-js prepare --verify --strict-inference # live/shadow comparison with complete inference
667
- - run: sqlx-js prepare --check # offline cache/version/config consistency
717
+ - run: sqlx-js prepare --check # read-only offline cache/declaration consistency
668
718
  - run: sqlx-js doctor --json # runtime/config/DB/cache/tsconfig preflight
669
719
  - run: sqlx-js schema check # fails if the committed schema snapshot is stale
670
720
  - run: tsc --noEmit # fails if types are stale
@@ -672,7 +722,7 @@ Commit the generated `sqlx-js-env.d.ts` and the `.sqlx-js/` cache directory to y
672
722
  - run: bun run build # emits publishable JS + declarations under dist/
673
723
  ```
674
724
 
675
- 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.
725
+ 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.
676
726
 
677
727
  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.
678
728
 
@@ -699,7 +749,7 @@ Releases are automated via `release-please`: pushes to `main` accumulate into a
699
749
  - 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.
700
750
  - 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.
701
751
  - 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.
702
- - 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`).
752
+ - 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`).
703
753
  - `connect_timeout` bounds the entire internal-client connect, including the TLS handshake and SCRAM authentication.
704
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.
705
755
 
@@ -707,7 +757,7 @@ See [ROADMAP.md](./ROADMAP.md) for what's planned.
707
757
 
708
758
  ## Upgrading
709
759
 
710
- ### Cache and parameter contract change (pre-1.0)
760
+ ### Cache, codegen, and parameter contract changes (pre-1.0)
711
761
 
712
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.
713
763
 
@@ -715,6 +765,10 @@ Generated JSON and PostgreSQL array parameters now require `sql.json(...)` and `
715
765
 
716
766
  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`.
717
767
 
768
+ 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
+
770
+ 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
+
718
772
  ## License
719
773
 
720
774
  MIT.
package/ROADMAP.md CHANGED
@@ -6,12 +6,13 @@ 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
+ | 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. |
10
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. |
11
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
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. |
13
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. |
14
- | LSP server | 6 | Realtime diagnostics, hover with column types, autocomplete on schema names. Versioned `prepare --json` plus `sqlx-js-diagnostics` already cover GitHub annotations and editor problem matchers; a full server still needs separate VS Code / Neovim clients. |
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. |
15
16
  | 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
17
  | 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
18
  | SQLite backend | 4 | SQLite's column types are dynamic. Would require running `EXPLAIN` and a heuristic mapper, or schema-driven inference per-statement. |
@@ -21,6 +22,6 @@ Items already shipped live in the [README](./README.md) feature list; this file
21
22
 
22
23
  ## Long-term
23
24
 
24
- - Full LSP with schema-driven autocomplete.
25
+ - Editor clients or a full LSP only after repeated consumer demand demonstrates that the separate maintenance and release lifecycle will pay for itself.
25
26
  - Hooks for ORM-like helpers that build on top of the typed `sql()` primitive (joins, paginated queries, etc.) without becoming an ORM.
26
27
  - Optional binary protocol support in the underlying wire client for measurable perf gain on large result sets.
@@ -22,30 +22,33 @@ const HELP = {
22
22
  usage:
23
23
  sqlx-js init [--root <dir>] [--schema-provider builtin|pgschema]
24
24
  sqlx-js doctor [--root <dir>] [--dts <path>] [--json]
25
+ sqlx-js ci [--root <dir>] [--dts <path>] [--schema <path>] [--json] [--shadow-url <url>] [--shadow-admin-url <url>]
25
26
  sqlx-js db install | check [--root <dir>]
26
27
  sqlx-js db plan | apply [--root <dir>] [-- <pgschema args>]
27
- sqlx-js prepare [--check | --verify | --watch] [--json] [--strict-inference] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]
28
- 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]
28
+ sqlx-js prepare [--check | --offline | --verify | --watch] [--json | --jsonl] [--strict-inference] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]
29
+ 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]
29
30
  sqlx-js schema dump [--schema <path>] [--manifest <path>] [--no-manifest] [--shadow-url <url>]
30
31
  sqlx-js schema check [--schema <path>] [--shadow-url <url>]
31
32
  sqlx-js --version
32
33
  sqlx-js-diagnostics github|unix < prepare-diagnostics.json
33
34
 
34
35
  env:
35
- DATABASE_URL=postgres://... (supports sslmode, cert paths, application_name, connect_timeout, statement_timeout)
36
+ DATABASE_URL=postgres://... (supports sslmode, cert paths, application_name, options, connect_timeout, statement_timeout)
36
37
  SHADOW_DATABASE_URL=postgres://... (optional pre-created disposable shadow DB)
37
38
  SHADOW_ADMIN_DATABASE_URL=postgres://... (optional admin URL for auto-created shadow DBs)
38
39
 
39
40
  flags:
40
41
  --root <dir> scan root (default: cwd)
41
42
  --dts <path> declarations output (default: <root>/sqlx-js-env.d.ts)
42
- --check offline mode: verify scanned queries exist in cache, no DB
43
+ --check read-only offline verification of cache and declarations
44
+ --offline regenerate declarations from committed cache, no DB
43
45
  --verify prepare against DB/shadow and compare committed generated artifacts
44
46
  --watch re-prepare on file change (persistent PG connection)
45
47
  --no-prune keep orphaned cache entries (default: remove)
46
48
  --migrations <dir> migrations directory (default: <root>/migrations)
47
49
  --dry-run validate and print migrate run/revert plan without applying migrations
48
50
  --json machine-readable output for prepare and migration inspection/dry-run commands
51
+ --jsonl streaming machine-readable output for prepare --watch
49
52
  --strict-inference fail when query inference degrades or emits unknown types
50
53
  --force allow archive restore to overwrite existing migration files
51
54
  --lock-timeout <ms> advisory-lock acquisition timeout for migrate run/revert/dev/verify/squash
@@ -60,9 +63,10 @@ flags:
60
63
  `,
61
64
  init: `usage: sqlx-js init [--root <dir>] [--schema-provider builtin|pgschema]`,
62
65
  doctor: `usage: sqlx-js doctor [--root <dir>] [--dts <path>] [--json]`,
66
+ ci: `usage: sqlx-js ci [--root <dir>] [--dts <path>] [--schema <path>] [--json] [--shadow-url <url>] [--shadow-admin-url <url>] [--migrations <dir>]`,
63
67
  db: `usage: sqlx-js db install | check [--root <dir>] | plan | apply [--root <dir>] [-- <pgschema args>]`,
64
- prepare: `usage: sqlx-js prepare [--check | --verify | --watch] [--json] [--strict-inference] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]`,
65
- migrate: `usage: 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]`,
68
+ prepare: `usage: sqlx-js prepare [--check | --offline | --verify | --watch] [--json | --jsonl] [--strict-inference] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]`,
69
+ 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]`,
66
70
  schema: `usage: sqlx-js schema dump [--schema <path>] [--manifest <path>] [--no-manifest] [--shadow-url <url>] | check [--schema <path>] [--shadow-url <url>]`,
67
71
  };
68
72
  function printHelp(scope, error = false) {
@@ -82,7 +86,7 @@ const passthroughIndex = rawArgv.indexOf("--");
82
86
  const cliArgv = passthroughIndex >= 0 ? rawArgv.slice(0, passthroughIndex) : rawArgv;
83
87
  const passthroughArgs = passthroughIndex >= 0 ? rawArgv.slice(passthroughIndex + 1) : [];
84
88
  const cmd = cliArgv[0];
85
- const scopes = new Set(["init", "doctor", "db", "prepare", "migrate", "schema"]);
89
+ const scopes = new Set(["init", "doctor", "ci", "db", "prepare", "migrate", "schema"]);
86
90
  if (cmd === "--version" || cmd === "-v") {
87
91
  console.log(VERSION);
88
92
  process.exit(0);
@@ -105,6 +109,16 @@ function optionsFor(command, subcommand) {
105
109
  return { ...ROOT_OPTIONS, "schema-provider": { type: "string" } };
106
110
  if (command === "doctor")
107
111
  return { ...ROOT_OPTIONS, dts: { type: "string" }, json: { type: "boolean" } };
112
+ if (command === "ci")
113
+ return {
114
+ ...ROOT_OPTIONS,
115
+ json: { type: "boolean" },
116
+ dts: { type: "string" },
117
+ schema: { type: "string" },
118
+ migrations: { type: "string" },
119
+ "shadow-url": { type: "string" },
120
+ "shadow-admin-url": { type: "string" },
121
+ };
108
122
  if (command === "db")
109
123
  return ROOT_OPTIONS;
110
124
  if (command === "prepare") {
@@ -112,9 +126,11 @@ function optionsFor(command, subcommand) {
112
126
  ...ROOT_OPTIONS,
113
127
  dts: { type: "string" },
114
128
  check: { type: "boolean" },
129
+ offline: { type: "boolean" },
115
130
  verify: { type: "boolean" },
116
131
  watch: { type: "boolean" },
117
132
  json: { type: "boolean" },
133
+ jsonl: { type: "boolean" },
118
134
  "no-prune": { type: "boolean" },
119
135
  "shadow-url": { type: "string" },
120
136
  migrations: { type: "string" },
@@ -141,6 +157,7 @@ function optionsFor(command, subcommand) {
141
157
  if (subcommand === "dev") {
142
158
  return {
143
159
  ...common,
160
+ dts: { type: "string" },
144
161
  "shadow-admin-url": { type: "string" },
145
162
  "shadow-url": { type: "string" },
146
163
  "lock-timeout": { type: "string" },
@@ -151,6 +168,7 @@ function optionsFor(command, subcommand) {
151
168
  if (subcommand === "verify") {
152
169
  return {
153
170
  ...common,
171
+ dts: { type: "string" },
154
172
  "shadow-admin-url": { type: "string" },
155
173
  "shadow-url": { type: "string" },
156
174
  "lock-timeout": { type: "string" },
@@ -210,7 +228,7 @@ function requirePositionals(min, max, label) {
210
228
  }
211
229
  }
212
230
  function validateInvocation() {
213
- if (cmd === "init" || cmd === "doctor" || cmd === "prepare") {
231
+ if (cmd === "init" || cmd === "doctor" || cmd === "ci" || cmd === "prepare") {
214
232
  requirePositionals(0, 0, cmd);
215
233
  return;
216
234
  }
@@ -260,20 +278,58 @@ function validateInvocation() {
260
278
  }
261
279
  validateInvocation();
262
280
  const root = resolve(arg("--root", process.cwd()));
281
+ function failCiPreflight(message) {
282
+ if (cmd === "ci" && flag("--json")) {
283
+ console.log(JSON.stringify({
284
+ formatVersion: 1,
285
+ ok: false,
286
+ results: [{ name: "preflight", ok: false, durationMs: 0, exitCode: 2, stderr: message }],
287
+ }, null, 2));
288
+ }
289
+ else {
290
+ console.error(message);
291
+ }
292
+ process.exit(2);
293
+ }
263
294
  if (cmd !== "doctor") {
264
295
  try {
265
296
  assertSupportedRuntime();
266
297
  }
267
298
  catch (e) {
268
- console.error(e.message);
299
+ failCiPreflight(e.message);
300
+ }
301
+ }
302
+ const needsTypeScript = cmd === "doctor" ||
303
+ cmd === "ci" ||
304
+ cmd === "prepare" ||
305
+ (cmd === "migrate" && (positionals[0] === "dev" || positionals[0] === "verify"));
306
+ if (needsTypeScript) {
307
+ try {
308
+ import.meta.resolve("typescript");
309
+ }
310
+ catch {
311
+ const message = "sqlx-js: TypeScript is required for source scanning. Install it with `npm install --save-dev typescript` or `bun add --dev typescript`.";
312
+ if (cmd === "doctor" && flag("--json")) {
313
+ console.log(JSON.stringify({
314
+ formatVersion: 1,
315
+ ok: false,
316
+ checks: [{ name: "typescript", status: "error", message }],
317
+ }, null, 2));
318
+ }
319
+ else {
320
+ if (cmd === "ci")
321
+ failCiPreflight(message);
322
+ console.error(message);
323
+ }
269
324
  process.exit(2);
270
325
  }
271
326
  }
272
327
  let envError;
273
328
  const needsEnvironment = cmd === "doctor" ||
329
+ cmd === "ci" ||
274
330
  cmd === "schema" ||
275
331
  (cmd === "db" && (positionals[0] === "plan" || positionals[0] === "apply")) ||
276
- (cmd === "prepare" && !flag("--check")) ||
332
+ (cmd === "prepare" && !flag("--check") && !flag("--offline")) ||
277
333
  (cmd === "migrate" && !["add", "check", "archive"].includes(positionals[0]));
278
334
  if (needsEnvironment) {
279
335
  try {
@@ -282,8 +338,7 @@ if (needsEnvironment) {
282
338
  catch (e) {
283
339
  envError = e.message;
284
340
  if (cmd !== "doctor") {
285
- console.error(envError);
286
- process.exit(2);
341
+ failCiPreflight(envError);
287
342
  }
288
343
  }
289
344
  }
@@ -312,6 +367,28 @@ else if (cmd === "doctor") {
312
367
  const { runDoctor } = await import("../src/commands/doctor.js");
313
368
  await runDoctor({ root, databaseUrl, cacheDir, dtsPath, json: flag("--json"), envError });
314
369
  }
370
+ else if (cmd === "ci") {
371
+ const { runCi } = await import("../src/commands/ci.js");
372
+ let config;
373
+ try {
374
+ config = await loadConfig(root);
375
+ }
376
+ catch (error) {
377
+ failCiPreflight(error.message);
378
+ }
379
+ runCi({
380
+ executable: process.execPath,
381
+ cliPath: fileURLToPath(import.meta.url),
382
+ root,
383
+ config,
384
+ schemaPath,
385
+ json: flag("--json"),
386
+ shadowUrl,
387
+ shadowAdminUrl,
388
+ migrationsDir: arg("--migrations"),
389
+ dtsPath: dtsArg ? dtsPath : undefined,
390
+ });
391
+ }
315
392
  else if (cmd === "db") {
316
393
  const { runPgschemaCommand, runPgschemaInstall } = await import("../src/commands/pgschema.js");
317
394
  const sub = positionals[0];
@@ -343,12 +420,22 @@ else if (cmd === "db") {
343
420
  else if (cmd === "prepare") {
344
421
  const { PrepareFatalError, runPrepare } = await import("../src/commands/prepare.js");
345
422
  const prepareCheck = flag("--check");
423
+ const prepareOffline = flag("--offline");
346
424
  const prepareVerify = flag("--verify");
347
425
  const prepareWatch = flag("--watch");
348
426
  const prepareJson = flag("--json");
349
- const prepareMode = prepareVerify ? "verify" : prepareCheck ? "check" : "prepare";
427
+ const prepareJsonl = flag("--jsonl");
428
+ const prepareMode = prepareVerify ? "verify" : prepareCheck ? "check" : prepareOffline ? "offline" : "prepare";
350
429
  const failPrepare = (message, phase, exitCode = 2, location = {}) => {
351
- if (prepareJson) {
430
+ if (prepareJsonl) {
431
+ console.log(JSON.stringify({
432
+ formatVersion: 1,
433
+ event: "error",
434
+ timestamp: new Date().toISOString(),
435
+ diagnostic: { severity: "error", phase, message, ...location },
436
+ }));
437
+ }
438
+ else if (prepareJson) {
352
439
  console.log(JSON.stringify({
353
440
  formatVersion: 1,
354
441
  ok: false,
@@ -366,22 +453,28 @@ else if (cmd === "prepare") {
366
453
  }
367
454
  process.exit(exitCode);
368
455
  };
369
- if ([prepareCheck, prepareVerify, prepareWatch].filter(Boolean).length > 1) {
370
- failPrepare("--check, --verify, and --watch are mutually exclusive", "config");
456
+ if ([prepareCheck, prepareOffline, prepareVerify, prepareWatch].filter(Boolean).length > 1) {
457
+ failPrepare("--check, --offline, --verify, and --watch are mutually exclusive", "config");
371
458
  }
372
- if (prepareCheck && shadowUrlArg) {
373
- failPrepare("--shadow-url cannot be used with prepare --check; use live prepare or schema check --shadow-url", "config");
459
+ if ((prepareCheck || prepareOffline) && shadowUrlArg) {
460
+ failPrepare("--shadow-url cannot be used with offline prepare modes; use live prepare or schema check --shadow-url", "config");
374
461
  }
375
462
  if (prepareWatch && prepareJson) {
376
463
  failPrepare("--watch and --json are mutually exclusive", "config");
377
464
  }
378
- if ((prepareCheck || prepareVerify) && flag("--no-prune")) {
379
- failPrepare("--no-prune is only supported by prepare and prepare --watch", "config");
465
+ if (prepareJson && prepareJsonl) {
466
+ failPrepare("--json and --jsonl are mutually exclusive", "config");
467
+ }
468
+ if (prepareJsonl && !prepareWatch) {
469
+ failPrepare("--jsonl is only supported by prepare --watch", "config");
470
+ }
471
+ if ((prepareCheck || prepareOffline || prepareVerify) && flag("--no-prune")) {
472
+ failPrepare("--no-prune is only supported by live prepare and prepare --watch", "config");
380
473
  }
381
- const prepareShadowUrl = prepareCheck ? undefined : shadowUrl;
474
+ const prepareShadowUrl = prepareCheck || prepareOffline ? undefined : shadowUrl;
382
475
  const prepareDatabaseUrl = prepareShadowUrl ?? databaseUrl;
383
- if (!prepareCheck && !prepareDatabaseUrl) {
384
- failPrepare("DATABASE_URL is required for prepare (use --check for offline)", "connect");
476
+ if (!prepareCheck && !prepareOffline && !prepareDatabaseUrl) {
477
+ failPrepare("DATABASE_URL is required for prepare (use --check or --offline without a database)", "connect");
385
478
  }
386
479
  const opts = {
387
480
  root,
@@ -389,6 +482,7 @@ else if (cmd === "prepare") {
389
482
  cacheDir,
390
483
  dtsPath,
391
484
  check: prepareCheck,
485
+ offline: prepareOffline,
392
486
  verify: prepareVerify,
393
487
  json: prepareJson,
394
488
  prune: !flag("--no-prune"),
@@ -401,6 +495,7 @@ else if (cmd === "prepare") {
401
495
  const { runWatch } = await import("../src/commands/watch.js");
402
496
  await runWatch({
403
497
  ...opts,
498
+ jsonl: prepareJsonl,
404
499
  ...(prepareShadowUrl
405
500
  ? {
406
501
  beforePrepare: async () => {
@@ -1,5 +1,5 @@
1
- export declare const CACHE_FORMAT_VERSION = 2;
2
- export declare const GENERATOR_REVISION = 3;
1
+ export declare const CACHE_FORMAT_VERSION = 3;
2
+ export declare const GENERATOR_REVISION = 5;
3
3
  export declare const CACHE_MANIFEST_FILE = "cache-manifest.json";
4
4
  export type CacheManifest = {
5
5
  cacheFormat: typeof CACHE_FORMAT_VERSION;
@@ -19,6 +19,7 @@ export type CacheEntry = {
19
19
  paramOids: number[];
20
20
  paramTsTypes: string[];
21
21
  paramNullable?: boolean[];
22
+ paramNames?: string[];
22
23
  columns: CacheColumn[];
23
24
  hasResultSet: boolean;
24
25
  hasInline?: boolean;