@onreza/sqlx-js 0.4.0 → 0.6.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.
Files changed (38) hide show
  1. package/README.md +139 -48
  2. package/ROADMAP.md +1 -3
  3. package/dist/bin/sqlx-js-diagnostics.d.ts +2 -0
  4. package/dist/bin/sqlx-js-diagnostics.js +96 -0
  5. package/dist/bin/sqlx-js.js +337 -89
  6. package/dist/src/artifacts.d.ts +9 -0
  7. package/dist/src/artifacts.js +26 -0
  8. package/dist/src/cache.d.ts +17 -0
  9. package/dist/src/cache.js +83 -1
  10. package/dist/src/codegen.js +14 -2
  11. package/dist/src/commands/doctor.d.ts +16 -0
  12. package/dist/src/commands/doctor.js +196 -0
  13. package/dist/src/commands/init.js +93 -13
  14. package/dist/src/commands/migrate.d.ts +2 -101
  15. package/dist/src/commands/migrate.js +11 -566
  16. package/dist/src/commands/pgschema.d.ts +6 -0
  17. package/dist/src/commands/pgschema.js +30 -0
  18. package/dist/src/commands/prepare.d.ts +41 -6
  19. package/dist/src/commands/prepare.js +440 -63
  20. package/dist/src/commands/schema.js +1 -1
  21. package/dist/src/commands/watch.d.ts +1 -0
  22. package/dist/src/commands/watch.js +23 -9
  23. package/dist/src/config.d.ts +22 -0
  24. package/dist/src/config.js +149 -11
  25. package/dist/src/index.d.ts +4 -2
  26. package/dist/src/index.js +2 -1
  27. package/dist/src/migration-core.d.ts +157 -0
  28. package/dist/src/migration-core.js +578 -0
  29. package/dist/src/postgres-runtime.d.ts +3 -0
  30. package/dist/src/postgres-runtime.js +66 -29
  31. package/dist/src/runtime.d.ts +36 -3
  32. package/dist/src/runtime.js +92 -23
  33. package/dist/src/scan/scanner.d.ts +10 -3
  34. package/dist/src/scan/scanner.js +83 -32
  35. package/dist/src/type-inspection.d.ts +1 -0
  36. package/dist/src/type-inspection.js +20 -0
  37. package/dist/src/typed.d.ts +10 -0
  38. package/package.json +11 -6
package/README.md CHANGED
@@ -4,7 +4,7 @@ Compile-time-checked raw SQL for TypeScript + PostgreSQL. Inspired by Rust's [sq
4
4
 
5
5
  You write plain SQL strings. A `prepare` step validates them against your database via the PostgreSQL wire protocol and generates a TypeScript declaration file. Wrong column names and stale queries fail during `prepare`; mismatched parameter types and row usage become TypeScript errors.
6
6
 
7
- The runtime uses [Postgres.js](https://github.com/porsager/postgres) through a single adapter instead of a Bun-specific client. The published CLI is a **Node ≥ 18** binary (`#!/usr/bin/env node`) and can also be run through Bun's npm tooling.
7
+ The runtime uses [Postgres.js](https://github.com/porsager/postgres) through a single adapter instead of a Bun-specific client. The published CLI requires **Node ≥ 24** (`#!/usr/bin/env node`) and can also run through **Bun 1.3**.
8
8
 
9
9
  ```ts
10
10
  import { sql } from "@onreza/sqlx-js";
@@ -30,21 +30,24 @@ const rows = await sql(
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
- - **Array params** for `text[]`, `int[]`, etc. are auto-serialised to PostgreSQL array literals (`{a,b,c}`) at runtime no more `string_to_array` workaround.
33
+ - **Unambiguous JSON and PostgreSQL array params** through `sql.json(...)` and `sql.array(...)`. Primitive JSON arrays cannot be silently encoded as PostgreSQL array literals.
34
34
  - **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
35
  - **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.
36
36
  - **Linear migrations** with hash tampering detection.
37
37
  - **Migration squash baselines** via `migrate squash`: generate a schema-only baseline from a shadow database, then hash-adopt it on already-migrated databases.
38
38
  - **Runtime `migrate()`** with PostgreSQL advisory lock, safe for multi-replica startup.
39
39
  - **Optional pgschema workflow** via `init --schema-provider pgschema` and `sqlx-js db install|check|plan|apply` for PostgreSQL schema-as-code projects.
40
- - **Offline cache** committed to your repo. CI verifies via `prepare --check` without a database.
40
+ - **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
41
  - **Schema snapshot + LLM manifest** via `schema dump` / `schema check`: tables, columns, constraints, indexes, types, and function/procedure metadata are introspected from PostgreSQL.
42
42
  - **Generated function catalog** via `KnownFunctions`: `prepare` records user-schema PostgreSQL functions/procedures from `pg_proc` with approximate parameter and return TypeScript types.
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 `.ts` / `.tsx` / `.mts` / `.cts` / `.sql` changes.
46
+ - **Watch mode**: debounced re-prepare with a warm `PgClient` + `SchemaCache` on source/SQL changes and scanner/config graph updates.
47
47
  - **Cache pruning** removes orphaned entries automatically (toggleable with `--no-prune`).
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.
48
51
 
49
52
  ## Install
50
53
 
@@ -54,7 +57,9 @@ npm install @onreza/sqlx-js
54
57
  bun add @onreza/sqlx-js
55
58
  ```
56
59
 
57
- 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.
60
+ Node.js 24 or newer is required. Bun users need Bun 1.3 or newer.
61
+
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.
58
63
 
59
64
  ## Setup
60
65
 
@@ -64,7 +69,7 @@ The package installs a `sqlx-js` binary. The CLI examples below use `npx @onreza
64
69
  npx @onreza/sqlx-js init
65
70
  ```
66
71
 
67
- 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.
68
73
 
69
74
  For declarative PostgreSQL schema management, scaffold the pgschema workflow instead:
70
75
 
@@ -87,6 +92,8 @@ DATABASE_URL=postgres://user:password@localhost:5432/your_db
87
92
 
88
93
  Supported `sslmode` values: `disable`, `prefer` (default — try TLS, fall back to plaintext), `require` (TLS or fail), `verify-ca`, `verify-full`. For a private/self-signed CA, point `sslrootcert` (and optionally `sslcert` / `sslkey` for client certs) at PEM files: `?sslmode=verify-full&sslrootcert=/etc/ssl/ca.pem`. `application_name`, `connect_timeout` (seconds), and `statement_timeout` (milliseconds) are also honored when provided as URL parameters.
89
94
 
95
+ CLI commands load `<root>/.env` before reading connection settings. Variables already present in the process environment take precedence. Application runtime configuration remains owned by your application/framework.
96
+
90
97
  ### 2. Create a migration
91
98
 
92
99
  ```bash
@@ -139,7 +146,7 @@ const users = await sql(
139
146
  npx @onreza/sqlx-js prepare
140
147
  ```
141
148
 
142
- 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`.
143
150
 
144
151
  ### 5. Dev loop with watch
145
152
 
@@ -164,7 +171,7 @@ Unknown queries, wrong parameter types, and dynamic strings are compile errors.
164
171
 
165
172
  ### `sql.file(path, ...params)`
166
173
 
167
- Load SQL from an external file. At prepare time the scanner reads the path relative to the source file. The generated `KnownFileQueries` key is the resolved SQL file path relative to `--root`; at runtime `sql.file(...)` reads the string argument relative to `process.cwd()`.
174
+ 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.
168
175
 
169
176
  ```ts
170
177
  // queries/top_admins.sql
@@ -177,7 +184,7 @@ const admins = await sql.file("queries/top_admins.sql", "admin", 5);
177
184
  // admins: { id: bigint; name: string }[]
178
185
  ```
179
186
 
180
- File-backed queries are emitted into a separate `KnownFileQueries` interface. Because the type key is the root-relative resolved SQL file path, keep file-backed call sites under a convention where that key matches the runtime string literal; the example keeps those call sites at the project root. Nested source-relative file paths are a current limitation.
187
+ 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.
181
188
 
182
189
  ### `sql.one(query, ...params)` and `sql.optional(query, ...params)`
183
190
 
@@ -193,18 +200,46 @@ const maybe = await sql.optional(`SELECT id FROM users WHERE email = $1`, "x@y")
193
200
 
194
201
  Both forms also exist on `sql.file` (`sql.file.one("queries/by_id.sql", ...)`) and inside transactions (`tx.one(...)`, `tx.optional(...)`, `tx.file.one(...)`, `tx.file.optional(...)`). The scanner recognizes every chain — these call sites are added to `KnownQueries` / `KnownFileQueries` just like a plain `sql(...)`.
195
202
 
196
- ### Array parameters
203
+ ### `sql.execute(query, ...params)`
204
+
205
+ Execute a typed statement when rows are not the result contract. It preserves parameter checking and returns Postgres command metadata:
206
+
207
+ ```ts
208
+ const result = await sql.execute(
209
+ `UPDATE jobs SET claimed_at = now() WHERE id = $1 AND claimed_at IS NULL`,
210
+ jobId,
211
+ );
212
+
213
+ if (result.rowCount !== 1) throw new Error("job was already claimed");
214
+ // result: { rowCount: number; command: string }
215
+ ```
216
+
217
+ `sql.file.execute(...)` and `tx.execute(...)` use the same contract. Query hooks receive the affected-row count rather than `0` for DML without `RETURNING`.
218
+
219
+ ### JSON and PostgreSQL array parameters
197
220
 
198
- JavaScript arrays passed to `text[]`, `int[]`, `uuid[]`, etc. are auto-encoded as PostgreSQL array literals before being sent. Strings containing commas, braces, quotes, or backslashes are escaped; `null` elements emit SQL `NULL`.
221
+ Parameter wrappers make the wire representation explicit. Use `sql.array(...)` for PostgreSQL arrays and `sql.json(...)` for `json`/`jsonb` values:
199
222
 
200
223
  ```ts
201
- await sql("SELECT $1::text[] AS tags", ["alpha", "beta,gamma", "with \"quote\""]);
202
- // $1 sent as {alpha,"beta,gamma","with \"quote\""}
224
+ await sql(
225
+ "SELECT $1::text[] AS tags",
226
+ sql.array(["alpha", "beta,gamma", "with \"quote\""]),
227
+ );
228
+
229
+ await sql(
230
+ "INSERT INTO events (payload) VALUES ($1)",
231
+ sql.json([1, 2, 3]),
232
+ );
233
+
234
+ await sql(
235
+ "SELECT $1::jsonb[] AS payloads",
236
+ sql.array([sql.json({ kind: "created" }), sql.json([1, 2, 3]), null]),
237
+ );
203
238
  ```
204
239
 
205
- Encoding only kicks in when every element is a primitive (`string` / `number` / `bigint` / `boolean` / `null`). Arrays containing objects pass through unchanged — that's the path for `jsonb` columns whose value is a JSON array (`attachments: SqlxJsJson.Attachment[]`). If you need to store a primitive JS array as `jsonb` (rare), pass `JSON.stringify(arr)` explicitly. `encodePgArrayLiteral(arr)` is exported if you need the literal yourself for `unsafe(...)`.
240
+ 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.
206
241
 
207
- Empty arrays (`[]`) are passed straight through to the active driver. If you need the literal `"{}"` instead (e.g. when concatenating into raw SQL), call `encodePgArrayLiteral([])`.
242
+ Both helpers also work with `unsafe(...)`. `encodePgArrayLiteral(arr)` remains exported for code that explicitly needs a PostgreSQL array literal string.
208
243
 
209
244
  ### Parameter nullability
210
245
 
@@ -289,13 +324,18 @@ import { createClient, setClient } from "@onreza/sqlx-js";
289
324
  setClient(createClient(process.env.DATABASE_URL));
290
325
  ```
291
326
 
292
- `createClient(url, options)` accepts every Postgres.js option plus two sqlx-js extras for observability and reliability:
327
+ `createClient(url, options)` accepts every Postgres.js option plus sqlx-js runtime options:
293
328
 
294
329
  ```ts
295
330
  setClient(createClient(process.env.DATABASE_URL, {
296
331
  // Server-side per-connection statement timeout (ms). Also settable via
297
332
  // ?statement_timeout=5000 in DATABASE_URL.
298
333
  statementTimeoutMs: 5000,
334
+ // Base directory for root-relative sql.file(...) calls.
335
+ fileRoot: import.meta.dirname,
336
+ // Honored for every unsafe call. Set false for PgBouncer transaction mode
337
+ // unless protocol-level prepared statements are configured there.
338
+ prepare: false,
299
339
  // Fires after every query/transaction statement, success or failure.
300
340
  onQuery: ({ query, params, durationMs, rowCount, error }) => {
301
341
  if (error) logger.error({ query, error }); // database errors are PgError
@@ -347,10 +387,13 @@ In addition to `import { sql } from "@onreza/sqlx-js"`, the scanner recognises `
347
387
 
348
388
  ```
349
389
  sqlx-js init [--root <dir>] [--schema-provider builtin|pgschema]
350
- sqlx-js db install | check | plan | apply [--root <dir>] [-- <pgschema args>]
351
- sqlx-js prepare [--check | --watch] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]
352
- 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]
353
- sqlx-js schema dump | check [--schema <path>] [--manifest <path>] [--no-manifest] [--shadow-url <url>]
390
+ sqlx-js doctor [--root <dir>] [--dts <path>] [--json]
391
+ sqlx-js db install | check [--root <dir>]
392
+ 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]
395
+ sqlx-js schema dump [--schema <path>] [--manifest <path>] [--no-manifest] [--shadow-url <url>]
396
+ sqlx-js schema check [--schema <path>] [--shadow-url <url>]
354
397
  sqlx-js --version | --help
355
398
  ```
356
399
 
@@ -359,26 +402,30 @@ Regular `prepare` describes queries across a small connection pool (default 8, o
359
402
  | Flag | Meaning |
360
403
  |-----------------------|--------------------------------------------------------------------------------------|
361
404
  | `--check` | Offline: verify every scanned query is present in cache, no database required. |
405
+ | `--verify` | Prepare against the live/shadow schema and compare generated artifacts without writing. |
362
406
  | `--watch` | Persistent connection, re-prepare on file change. |
363
407
  | `--root <dir>` | Source/cache/migrations root (default: cwd). |
364
- | `--dts <path>` | Declarations output (default: `<root>/sqlx-js-env.d.ts`). |
408
+ | `--dts <path>` | Root-relative declarations output (default: `<root>/sqlx-js-env.d.ts`). |
365
409
  | `--no-prune` | Keep orphaned cache entries instead of removing them. |
366
- | `--migrations <dir>` | Migrations directory (default: `<root>/migrations`). |
410
+ | `--migrations <dir>` | Root-relative migrations directory (default: `<root>/migrations`). |
367
411
  | `--dry-run` | For `migrate run` / `migrate revert`: validate without applying to the target DB. |
368
- | `--json` | Machine-readable output for `migrate info/check` and migration dry-runs. |
412
+ | `--json` | Machine-readable prepare diagnostics, doctor output, migration inspection and dry-runs. |
413
+ | `--strict-inference` | Fail prepare/dev/verify when nullability degrades or a generated query type contains `unknown`. |
369
414
  | `--force` | For `migrate archive restore`: allow overwriting existing migration files. |
370
415
  | `--lock-timeout <ms>` | Advisory-lock acquisition timeout for `migrate run` / `revert` / `dev` / `verify` / `squash`. |
371
416
  | `--shadow-url <url>` | Use an existing disposable shadow DB instead of auto-creating one. |
372
417
  | `--shadow-admin-url <url>` | Admin/maintenance DB URL used to auto-create shadow DBs. |
373
418
  | `--replace` | For `migrate squash`: archive replaced migration files after writing the baseline. |
374
419
  | `--pg-dump <path>` | For `migrate squash`: `pg_dump` executable path (default: `pg_dump`). |
375
- | `--schema <path>` | Schema snapshot path (default: `<root>/.sqlx-js/schema/schema.json`). |
376
- | `--manifest <path>` | LLM schema manifest path (default: `<root>/.sqlx-js/schema/schema.md`). |
420
+ | `--schema <path>` | Root-relative schema snapshot path (default: `<root>/.sqlx-js/schema/schema.json`). |
421
+ | `--manifest <path>` | Root-relative LLM schema manifest path (default: `<root>/.sqlx-js/schema/schema.md`). |
377
422
  | `--no-manifest` | Skip writing the LLM schema manifest during `schema dump`. |
378
423
  | `--schema-provider <name>` | For `init`: `builtin` (default) or `pgschema`. |
379
424
 
380
425
  Flags that take a value accept both `--flag value` and `--flag=value` forms.
381
426
 
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.
428
+
382
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).
383
430
 
384
431
  ### Development and deployment flows
@@ -412,12 +459,13 @@ The built-in `migrate` workflow is kept for simple projects and embedded applica
412
459
  Use `migrate verify` in PR/CI before merge:
413
460
 
414
461
  ```bash
415
- sqlx-js migrate verify
462
+ sqlx-js migrate verify --strict-inference
416
463
  sqlx-js prepare --check
464
+ sqlx-js doctor --json
417
465
  tsc --noEmit
418
466
  ```
419
467
 
420
- `migrate verify` runs the same shadow-database migration/down/SQL validation as `migrate dev`, but writes prepare output to temporary files instead of modifying `.sqlx-js/` or `sqlx-js-env.d.ts`.
468
+ `migrate verify` runs the same shadow-database migration/down/SQL validation as `migrate dev`, generates prepare output in a temporary directory, and fails when the committed `.sqlx-js/` or `sqlx-js-env.d.ts` differs. It never modifies those artifacts.
421
469
 
422
470
  Use `migrate run` in production/staging:
423
471
 
@@ -471,14 +519,54 @@ When `prepare` fails, every diagnostic points back to the originating call site:
471
519
 
472
520
  Phases reported separately: `describe failed`, `analyze failed`, `paramMap failed`. PostgreSQL `position`, `code`, and `hint` are surfaced when present.
473
521
 
522
+ ### GitHub and editor diagnostics
523
+
524
+ `sqlx-js-diagnostics` converts the versioned prepare JSON document into GitHub workflow commands or a standard Unix problem-matcher stream:
525
+
526
+ ```bash
527
+ set -o pipefail
528
+ sqlx-js prepare --verify --json | sqlx-js-diagnostics github
529
+ sqlx-js prepare --check --json | sqlx-js-diagnostics unix
530
+ ```
531
+
532
+ 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:
533
+
534
+ ```json
535
+ {
536
+ "label": "sqlx-js: check",
537
+ "type": "shell",
538
+ "command": "sqlx-js prepare --check --json | sqlx-js-diagnostics unix",
539
+ "problemMatcher": {
540
+ "owner": "sqlx-js",
541
+ "fileLocation": ["relative", "${workspaceFolder}"],
542
+ "pattern": {
543
+ "regexp": "^(.+):(\\d+):(\\d+): (error|warning): \\[([^\\]]+)\\] (.*)$",
544
+ "file": 1,
545
+ "line": 2,
546
+ "column": 3,
547
+ "severity": 4,
548
+ "code": 5,
549
+ "message": 6
550
+ }
551
+ }
552
+ }
553
+ ```
554
+
474
555
  ## Configuration
475
556
 
476
557
  `sqlx-js.config.ts` at the project root is optional.
477
558
 
559
+ Under Node.js, TypeScript config is loaded through Node 24's native type stripping, so keep it to erasable TypeScript syntax. The generated `defineConfig(...)` form works on both Node and Bun; use `.mjs` if the config needs runtime constructs that Node cannot strip.
560
+
478
561
  ```ts
479
- import type { SqlxJsConfig } from "@onreza/sqlx-js";
562
+ import { defineConfig } from "@onreza/sqlx-js";
480
563
 
481
- const config: SqlxJsConfig = {
564
+ export default defineConfig({
565
+ scan: {
566
+ include: ["apps/*/src/**/*", "packages/*/src/**/*"],
567
+ exclude: ["**/*.generated.ts"],
568
+ modules: ["@onreza/sqlx-js", "@app/database"],
569
+ },
482
570
  schema: {
483
571
  provider: "pgschema",
484
572
  file: "schema.sql",
@@ -489,11 +577,11 @@ const config: SqlxJsConfig = {
489
577
  "posts.meta": "SqlxJsJson.PostMeta",
490
578
  "posts.attachments": "SqlxJsJson.Attachment",
491
579
  },
492
- };
493
-
494
- export default config;
580
+ });
495
581
  ```
496
582
 
583
+ 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.
584
+
497
585
  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.
498
586
 
499
587
  Declare the referenced types anywhere in your project (`.d.ts` file is conventional):
@@ -514,7 +602,7 @@ declare global {
514
602
  export {};
515
603
  ```
516
604
 
517
- 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` for parameters, both exported by `@onreza/sqlx-js`, so 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. Top-level SQL `null` is added separately as `| null` only when the mapped database parameter is nullable.
605
+ 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`.
518
606
 
519
607
  ### Extension types and `customTypes`
520
608
 
@@ -534,16 +622,15 @@ sqlx-js ships with a built-in registry that resolves popular PostgreSQL extensio
534
622
  Add or override mappings via `customTypes` in `sqlx-js.config.ts`. Keys are `pg_type.typname` values (the bare type name). The registry is global by type name, so two schemas with the same `typname` cannot be mapped differently:
535
623
 
536
624
  ```ts
537
- import type { SqlxJsConfig } from "@onreza/sqlx-js";
625
+ import { defineConfig } from "@onreza/sqlx-js";
538
626
 
539
- const config: SqlxJsConfig = {
627
+ export default defineConfig({
540
628
  customTypes: {
541
629
  vector: "Float32Array", // override pgvector default
542
630
  geometry: "GeoJSON.Geometry", // postgis (not built-in by design)
543
631
  myapp_color: "`#${string}`", // your own CREATE TYPE base/domain
544
632
  },
545
- };
546
- export default config;
633
+ });
547
634
  ```
548
635
 
549
636
  Domains resolve to their base type through `pg_type.typbasetype`. `CREATE DOMAIN positive_int AS integer CHECK (VALUE > 0)` → `number`, `CREATE DOMAIN tagged AS hstore` → `Record<string, string | null>`. Array variants of any registered scalar are also wired up automatically — `vector[]` → `(number[])[]`.
@@ -572,18 +659,20 @@ Commit the generated `sqlx-js-env.d.ts` and the `.sqlx-js/` cache directory to y
572
659
 
573
660
  ```yaml
574
661
  - run: bun install
575
- - run: sqlx-js migrate verify # built-in migration workflow
662
+ - run: sqlx-js migrate verify --strict-inference # built-in migration workflow
576
663
  # or, when schema.provider is "pgschema":
577
664
  - run: sqlx-js db install
578
665
  - run: sqlx-js db plan -- --output-json plan.json
579
- - run: sqlx-js prepare --check # fails if any query is missing from the committed cache
666
+ - run: sqlx-js prepare --verify --strict-inference # live/shadow comparison with complete inference
667
+ - run: sqlx-js prepare --check # offline cache/version/config consistency
668
+ - run: sqlx-js doctor --json # runtime/config/DB/cache/tsconfig preflight
580
669
  - run: sqlx-js schema check # fails if the committed schema snapshot is stale
581
670
  - run: tsc --noEmit # fails if types are stale
582
- - run: bun test
671
+ - run: bun test --timeout 120000
583
672
  - run: bun run build # emits publishable JS + declarations under dist/
584
673
  ```
585
674
 
586
- 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. `schema check` intentionally uses a live database because it verifies the committed schema contract against PostgreSQL.
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.
587
676
 
588
677
  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.
589
678
 
@@ -596,31 +685,33 @@ bun install # installs lefthook + wires git hooks
596
685
  cargo install cocogitto # or: brew install cocogitto
597
686
  ```
598
687
 
599
- 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.
688
+ 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.
600
689
 
601
690
  ## Limitations
602
691
 
603
692
  `sqlx-js` is a young library. Known gaps:
604
693
 
605
694
  - PostgreSQL only (no MySQL or SQLite).
606
- - 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.
607
- - `INSERT INTO t VALUES (...)` without an explicit column list isn't parameter-mapped.
695
+ - 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.
608
696
  - `SELECT *` falls back to conservative nullability.
609
- - Statements without a row description, such as `UPDATE ...` without `RETURNING`, are emitted with `row: never`, so the public return type is `Promise<never[]>`.
697
+ - 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.
610
698
  - 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.
611
699
  - 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
+ - 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.
612
701
  - 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.
613
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`).
614
703
  - `connect_timeout` bounds the entire internal-client connect, including the TLS handshake and SCRAM authentication.
615
- - `sql.file(path)` has a path-key mismatch to be aware of: prepare resolves the file relative to the source file, codegen keys it by root-relative resolved path, and runtime reads the literal path relative to `process.cwd()`. Keep a project convention and verify with `tsc` after `prepare`.
704
+ - 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.
616
705
 
617
706
  See [ROADMAP.md](./ROADMAP.md) for what's planned.
618
707
 
619
708
  ## Upgrading
620
709
 
621
- ### Cache schema change (pre-1.0)
710
+ ### Cache and parameter contract change (pre-1.0)
711
+
712
+ 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.
622
713
 
623
- The `.sqlx-js/<fingerprint>.json` entries dropped `forceNonNull`/`forceNullable` in favour of a single `override?: "non-null" | "nullable"` field. Cache files from the previous schema are rejected with a clear error pointing at the offending file. Delete `.sqlx-js/` and re-run `sqlx-js prepare` against your database there's no data loss, the cache is regenerated.
714
+ 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.
624
715
 
625
716
  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`.
626
717
 
package/ROADMAP.md CHANGED
@@ -10,15 +10,13 @@ Items already shipped live in the [README](./README.md) feature list; this file
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
- | `INSERT INTO t VALUES (...)` without column list | 3 | Map params by `pg_attribute attnum` ordering. Rare in practice — most teams use explicit column lists. |
14
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. |
15
- | LSP server | 6 | Realtime diagnostics, hover with column types, autocomplete on schema names. Two-to-four weeks for beta, separate VS Code / Neovim extensions. Watch mode covers ~85% of the value today. |
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. |
16
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. |
17
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. |
18
17
  | SQLite backend | 4 | SQLite's column types are dynamic. Would require running `EXPLAIN` and a heuristic mapper, or schema-driven inference per-statement. |
19
18
  | `EXPLAIN`-based performance hints | 6 | `prepare` could optionally run `EXPLAIN` per query and surface seq-scan / missing-index warnings. Independent feature; pairs well with CI. |
20
19
  | Multi-statement queries | 2 | One SQL string with multiple statements separated by `;`. PG's `Parse` is single-statement; this would require client-side splitting. |
21
- | Stored procedure / function typing | 3 | `CALL proc(...)` and `SELECT func(...)` with parameter and return-type binding from `pg_proc`. |
22
20
  | Streaming / cursor / COPY typing | 3 | Surface Postgres.js cursor / COPY APIs with proper row types. |
23
21
 
24
22
  ## Long-term
@@ -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 {};