@onreza/sqlx-js 0.4.0 → 0.5.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 +85 -34
- package/ROADMAP.md +1 -3
- package/dist/bin/sqlx-js.js +91 -20
- package/dist/src/artifacts.d.ts +9 -0
- package/dist/src/artifacts.js +26 -0
- package/dist/src/cache.d.ts +17 -0
- package/dist/src/cache.js +83 -1
- package/dist/src/codegen.js +14 -2
- package/dist/src/commands/doctor.d.ts +16 -0
- package/dist/src/commands/doctor.js +196 -0
- package/dist/src/commands/init.js +6 -10
- package/dist/src/commands/migrate.js +7 -22
- package/dist/src/commands/pgschema.d.ts +6 -0
- package/dist/src/commands/pgschema.js +30 -0
- package/dist/src/commands/prepare.d.ts +40 -6
- package/dist/src/commands/prepare.js +341 -63
- package/dist/src/commands/watch.d.ts +1 -0
- package/dist/src/commands/watch.js +23 -9
- package/dist/src/config.d.ts +21 -0
- package/dist/src/config.js +141 -11
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +2 -1
- package/dist/src/postgres-runtime.d.ts +3 -0
- package/dist/src/postgres-runtime.js +66 -29
- package/dist/src/runtime.d.ts +36 -3
- package/dist/src/runtime.js +91 -22
- package/dist/src/scan/scanner.d.ts +9 -2
- package/dist/src/scan/scanner.js +79 -28
- package/dist/src/typed.d.ts +10 -0
- package/package.json +6 -5
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
|
|
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,22 @@ 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
|
-
- **
|
|
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
|
-
- **
|
|
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
|
|
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.
|
|
48
49
|
|
|
49
50
|
## Install
|
|
50
51
|
|
|
@@ -54,6 +55,8 @@ npm install @onreza/sqlx-js
|
|
|
54
55
|
bun add @onreza/sqlx-js
|
|
55
56
|
```
|
|
56
57
|
|
|
58
|
+
Node.js 24 or newer is required. Bun users need Bun 1.3 or newer.
|
|
59
|
+
|
|
57
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.
|
|
58
61
|
|
|
59
62
|
## Setup
|
|
@@ -87,6 +90,8 @@ DATABASE_URL=postgres://user:password@localhost:5432/your_db
|
|
|
87
90
|
|
|
88
91
|
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
92
|
|
|
93
|
+
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.
|
|
94
|
+
|
|
90
95
|
### 2. Create a migration
|
|
91
96
|
|
|
92
97
|
```bash
|
|
@@ -164,7 +169,7 @@ Unknown queries, wrong parameter types, and dynamic strings are compile errors.
|
|
|
164
169
|
|
|
165
170
|
### `sql.file(path, ...params)`
|
|
166
171
|
|
|
167
|
-
Load SQL from an external file.
|
|
172
|
+
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
173
|
|
|
169
174
|
```ts
|
|
170
175
|
// queries/top_admins.sql
|
|
@@ -177,7 +182,7 @@ const admins = await sql.file("queries/top_admins.sql", "admin", 5);
|
|
|
177
182
|
// admins: { id: bigint; name: string }[]
|
|
178
183
|
```
|
|
179
184
|
|
|
180
|
-
File-backed queries are emitted into a separate `KnownFileQueries` interface.
|
|
185
|
+
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
186
|
|
|
182
187
|
### `sql.one(query, ...params)` and `sql.optional(query, ...params)`
|
|
183
188
|
|
|
@@ -193,18 +198,46 @@ const maybe = await sql.optional(`SELECT id FROM users WHERE email = $1`, "x@y")
|
|
|
193
198
|
|
|
194
199
|
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
200
|
|
|
196
|
-
###
|
|
201
|
+
### `sql.execute(query, ...params)`
|
|
197
202
|
|
|
198
|
-
|
|
203
|
+
Execute a typed statement when rows are not the result contract. It preserves parameter checking and returns Postgres command metadata:
|
|
199
204
|
|
|
200
205
|
```ts
|
|
201
|
-
await sql(
|
|
202
|
-
|
|
206
|
+
const result = await sql.execute(
|
|
207
|
+
`UPDATE jobs SET claimed_at = now() WHERE id = $1 AND claimed_at IS NULL`,
|
|
208
|
+
jobId,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (result.rowCount !== 1) throw new Error("job was already claimed");
|
|
212
|
+
// result: { rowCount: number; command: string }
|
|
203
213
|
```
|
|
204
214
|
|
|
205
|
-
|
|
215
|
+
`sql.file.execute(...)` and `tx.execute(...)` use the same contract. Query hooks receive the affected-row count rather than `0` for DML without `RETURNING`.
|
|
206
216
|
|
|
207
|
-
|
|
217
|
+
### JSON and PostgreSQL array parameters
|
|
218
|
+
|
|
219
|
+
Parameter wrappers make the wire representation explicit. Use `sql.array(...)` for PostgreSQL arrays and `sql.json(...)` for `json`/`jsonb` values:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
await sql(
|
|
223
|
+
"SELECT $1::text[] AS tags",
|
|
224
|
+
sql.array(["alpha", "beta,gamma", "with \"quote\""]),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
await sql(
|
|
228
|
+
"INSERT INTO events (payload) VALUES ($1)",
|
|
229
|
+
sql.json([1, 2, 3]),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
await sql(
|
|
233
|
+
"SELECT $1::jsonb[] AS payloads",
|
|
234
|
+
sql.array([sql.json({ kind: "created" }), sql.json([1, 2, 3]), null]),
|
|
235
|
+
);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
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.
|
|
239
|
+
|
|
240
|
+
Both helpers also work with `unsafe(...)`. `encodePgArrayLiteral(arr)` remains exported for code that explicitly needs a PostgreSQL array literal string.
|
|
208
241
|
|
|
209
242
|
### Parameter nullability
|
|
210
243
|
|
|
@@ -289,13 +322,18 @@ import { createClient, setClient } from "@onreza/sqlx-js";
|
|
|
289
322
|
setClient(createClient(process.env.DATABASE_URL));
|
|
290
323
|
```
|
|
291
324
|
|
|
292
|
-
`createClient(url, options)` accepts every Postgres.js option plus
|
|
325
|
+
`createClient(url, options)` accepts every Postgres.js option plus sqlx-js runtime options:
|
|
293
326
|
|
|
294
327
|
```ts
|
|
295
328
|
setClient(createClient(process.env.DATABASE_URL, {
|
|
296
329
|
// Server-side per-connection statement timeout (ms). Also settable via
|
|
297
330
|
// ?statement_timeout=5000 in DATABASE_URL.
|
|
298
331
|
statementTimeoutMs: 5000,
|
|
332
|
+
// Base directory for root-relative sql.file(...) calls.
|
|
333
|
+
fileRoot: import.meta.dirname,
|
|
334
|
+
// Honored for every unsafe call. Set false for PgBouncer transaction mode
|
|
335
|
+
// unless protocol-level prepared statements are configured there.
|
|
336
|
+
prepare: false,
|
|
299
337
|
// Fires after every query/transaction statement, success or failure.
|
|
300
338
|
onQuery: ({ query, params, durationMs, rowCount, error }) => {
|
|
301
339
|
if (error) logger.error({ query, error }); // database errors are PgError
|
|
@@ -347,8 +385,9 @@ In addition to `import { sql } from "@onreza/sqlx-js"`, the scanner recognises `
|
|
|
347
385
|
|
|
348
386
|
```
|
|
349
387
|
sqlx-js init [--root <dir>] [--schema-provider builtin|pgschema]
|
|
388
|
+
sqlx-js doctor [--root <dir>] [--json]
|
|
350
389
|
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>]
|
|
390
|
+
sqlx-js prepare [--check | --verify | --watch] [--json] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]
|
|
352
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]
|
|
353
392
|
sqlx-js schema dump | check [--schema <path>] [--manifest <path>] [--no-manifest] [--shadow-url <url>]
|
|
354
393
|
sqlx-js --version | --help
|
|
@@ -359,13 +398,14 @@ Regular `prepare` describes queries across a small connection pool (default 8, o
|
|
|
359
398
|
| Flag | Meaning |
|
|
360
399
|
|-----------------------|--------------------------------------------------------------------------------------|
|
|
361
400
|
| `--check` | Offline: verify every scanned query is present in cache, no database required. |
|
|
401
|
+
| `--verify` | Prepare against the live/shadow schema and compare generated artifacts without writing. |
|
|
362
402
|
| `--watch` | Persistent connection, re-prepare on file change. |
|
|
363
403
|
| `--root <dir>` | Source/cache/migrations root (default: cwd). |
|
|
364
404
|
| `--dts <path>` | Declarations output (default: `<root>/sqlx-js-env.d.ts`). |
|
|
365
405
|
| `--no-prune` | Keep orphaned cache entries instead of removing them. |
|
|
366
406
|
| `--migrations <dir>` | Migrations directory (default: `<root>/migrations`). |
|
|
367
407
|
| `--dry-run` | For `migrate run` / `migrate revert`: validate without applying to the target DB. |
|
|
368
|
-
| `--json` | Machine-readable output
|
|
408
|
+
| `--json` | Machine-readable prepare diagnostics, doctor output, migration inspection and dry-runs. |
|
|
369
409
|
| `--force` | For `migrate archive restore`: allow overwriting existing migration files. |
|
|
370
410
|
| `--lock-timeout <ms>` | Advisory-lock acquisition timeout for `migrate run` / `revert` / `dev` / `verify` / `squash`. |
|
|
371
411
|
| `--shadow-url <url>` | Use an existing disposable shadow DB instead of auto-creating one. |
|
|
@@ -379,6 +419,8 @@ Regular `prepare` describes queries across a small connection pool (default 8, o
|
|
|
379
419
|
|
|
380
420
|
Flags that take a value accept both `--flag value` and `--flag=value` forms.
|
|
381
421
|
|
|
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.
|
|
423
|
+
|
|
382
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).
|
|
383
425
|
|
|
384
426
|
### Development and deployment flows
|
|
@@ -414,10 +456,11 @@ Use `migrate verify` in PR/CI before merge:
|
|
|
414
456
|
```bash
|
|
415
457
|
sqlx-js migrate verify
|
|
416
458
|
sqlx-js prepare --check
|
|
459
|
+
sqlx-js doctor --json
|
|
417
460
|
tsc --noEmit
|
|
418
461
|
```
|
|
419
462
|
|
|
420
|
-
`migrate verify` runs the same shadow-database migration/down/SQL validation as `migrate dev`,
|
|
463
|
+
`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
464
|
|
|
422
465
|
Use `migrate run` in production/staging:
|
|
423
466
|
|
|
@@ -475,10 +518,16 @@ Phases reported separately: `describe failed`, `analyze failed`, `paramMap faile
|
|
|
475
518
|
|
|
476
519
|
`sqlx-js.config.ts` at the project root is optional.
|
|
477
520
|
|
|
521
|
+
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.
|
|
522
|
+
|
|
478
523
|
```ts
|
|
479
|
-
import
|
|
524
|
+
import { defineConfig } from "@onreza/sqlx-js";
|
|
480
525
|
|
|
481
|
-
|
|
526
|
+
export default defineConfig({
|
|
527
|
+
scan: {
|
|
528
|
+
include: ["apps/*/src/**/*", "packages/*/src/**/*"],
|
|
529
|
+
exclude: ["**/*.generated.ts"],
|
|
530
|
+
},
|
|
482
531
|
schema: {
|
|
483
532
|
provider: "pgschema",
|
|
484
533
|
file: "schema.sql",
|
|
@@ -489,11 +538,11 @@ const config: SqlxJsConfig = {
|
|
|
489
538
|
"posts.meta": "SqlxJsJson.PostMeta",
|
|
490
539
|
"posts.attachments": "SqlxJsJson.Attachment",
|
|
491
540
|
},
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
export default config;
|
|
541
|
+
});
|
|
495
542
|
```
|
|
496
543
|
|
|
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.
|
|
545
|
+
|
|
497
546
|
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
547
|
|
|
499
548
|
Declare the referenced types anywhere in your project (`.d.ts` file is conventional):
|
|
@@ -514,7 +563,7 @@ declare global {
|
|
|
514
563
|
export {};
|
|
515
564
|
```
|
|
516
565
|
|
|
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
|
|
566
|
+
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
567
|
|
|
519
568
|
### Extension types and `customTypes`
|
|
520
569
|
|
|
@@ -534,16 +583,15 @@ sqlx-js ships with a built-in registry that resolves popular PostgreSQL extensio
|
|
|
534
583
|
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
584
|
|
|
536
585
|
```ts
|
|
537
|
-
import
|
|
586
|
+
import { defineConfig } from "@onreza/sqlx-js";
|
|
538
587
|
|
|
539
|
-
|
|
588
|
+
export default defineConfig({
|
|
540
589
|
customTypes: {
|
|
541
590
|
vector: "Float32Array", // override pgvector default
|
|
542
591
|
geometry: "GeoJSON.Geometry", // postgis (not built-in by design)
|
|
543
592
|
myapp_color: "`#${string}`", // your own CREATE TYPE base/domain
|
|
544
593
|
},
|
|
545
|
-
};
|
|
546
|
-
export default config;
|
|
594
|
+
});
|
|
547
595
|
```
|
|
548
596
|
|
|
549
597
|
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[])[]`.
|
|
@@ -576,14 +624,16 @@ Commit the generated `sqlx-js-env.d.ts` and the `.sqlx-js/` cache directory to y
|
|
|
576
624
|
# or, when schema.provider is "pgschema":
|
|
577
625
|
- run: sqlx-js db install
|
|
578
626
|
- run: sqlx-js db plan -- --output-json plan.json
|
|
579
|
-
- run: sqlx-js prepare --
|
|
627
|
+
- run: sqlx-js prepare --verify # live/shadow comparison of every generated artifact
|
|
628
|
+
- run: sqlx-js prepare --check # offline cache/version/config consistency
|
|
629
|
+
- run: sqlx-js doctor --json # runtime/config/DB/cache/tsconfig preflight
|
|
580
630
|
- run: sqlx-js schema check # fails if the committed schema snapshot is stale
|
|
581
631
|
- run: tsc --noEmit # fails if types are stale
|
|
582
|
-
- run: bun test
|
|
632
|
+
- run: bun test --timeout 120000
|
|
583
633
|
- run: bun run build # emits publishable JS + declarations under dist/
|
|
584
634
|
```
|
|
585
635
|
|
|
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.
|
|
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.
|
|
587
637
|
|
|
588
638
|
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
639
|
|
|
@@ -604,23 +654,24 @@ Releases are automated via `release-please`: pushes to `main` accumulate into a
|
|
|
604
654
|
|
|
605
655
|
- PostgreSQL only (no MySQL or SQLite).
|
|
606
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.
|
|
607
|
-
- `INSERT INTO t VALUES (...)` without an explicit column list isn't parameter-mapped.
|
|
608
657
|
- `SELECT *` falls back to conservative nullability.
|
|
609
|
-
-
|
|
658
|
+
- 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
659
|
- 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
660
|
- 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.
|
|
612
661
|
- 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
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`).
|
|
614
663
|
- `connect_timeout` bounds the entire internal-client connect, including the TLS handshake and SCRAM authentication.
|
|
615
|
-
- `sql.file(path)`
|
|
664
|
+
- 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
665
|
|
|
617
666
|
See [ROADMAP.md](./ROADMAP.md) for what's planned.
|
|
618
667
|
|
|
619
668
|
## Upgrading
|
|
620
669
|
|
|
621
|
-
### Cache
|
|
670
|
+
### Cache and parameter contract change (pre-1.0)
|
|
671
|
+
|
|
672
|
+
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
673
|
|
|
623
|
-
|
|
674
|
+
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
675
|
|
|
625
676
|
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
677
|
|
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.
|
|
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. |
|
|
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
|
package/dist/bin/sqlx-js.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { runPrepare } from "../src/commands/prepare.js";
|
|
5
|
+
import { PrepareFatalError, runPrepare, } from "../src/commands/prepare.js";
|
|
6
6
|
import { runWatch } from "../src/commands/watch.js";
|
|
7
7
|
import { migrateArchiveList, migrateArchiveRestore, migrateCheck, migrateDev, migrateRun, migrateInfo, migrateRevert, migrateAdd, migrateSquash, migrateVerify, } from "../src/commands/migrate.js";
|
|
8
8
|
import { applyShadowMigrations, runSchemaCheck, runSchemaDump } from "../src/commands/schema.js";
|
|
9
9
|
import { runInit } from "../src/commands/init.js";
|
|
10
10
|
import { runPgschemaCommand, runPgschemaInstall } from "../src/commands/pgschema.js";
|
|
11
|
-
import {
|
|
11
|
+
import { runDoctor } from "../src/commands/doctor.js";
|
|
12
|
+
import { assertSupportedRuntime, loadConfig, loadRootEnv } from "../src/config.js";
|
|
12
13
|
function packageVersion() {
|
|
13
14
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
for (const path of [join(here, "../package.json"), join(here, "../../package.json")]) {
|
|
@@ -26,8 +27,9 @@ function help() {
|
|
|
26
27
|
|
|
27
28
|
usage:
|
|
28
29
|
sqlx-js init [--root <dir>] [--schema-provider builtin|pgschema]
|
|
30
|
+
sqlx-js doctor [--root <dir>] [--json]
|
|
29
31
|
sqlx-js db install | check | plan | apply [--root <dir>] [-- <pgschema args>]
|
|
30
|
-
sqlx-js prepare [--check | --watch] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]
|
|
32
|
+
sqlx-js prepare [--check | --verify | --watch] [--json] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]
|
|
31
33
|
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]
|
|
32
34
|
sqlx-js schema dump | check [--schema <path>] [--manifest <path>] [--no-manifest] [--shadow-url <url>]
|
|
33
35
|
sqlx-js --version
|
|
@@ -41,11 +43,12 @@ flags:
|
|
|
41
43
|
--root <dir> scan root (default: cwd)
|
|
42
44
|
--dts <path> declarations output (default: <root>/sqlx-js-env.d.ts)
|
|
43
45
|
--check offline mode: verify scanned queries exist in cache, no DB
|
|
46
|
+
--verify prepare against DB/shadow and compare committed generated artifacts
|
|
44
47
|
--watch re-prepare on file change (persistent PG connection)
|
|
45
48
|
--no-prune keep orphaned cache entries (default: remove)
|
|
46
49
|
--migrations <dir> migrations directory (default: <root>/migrations)
|
|
47
50
|
--dry-run validate and print migrate run/revert plan without applying migrations
|
|
48
|
-
--json machine-readable output for
|
|
51
|
+
--json machine-readable output for prepare and migration inspection/dry-run commands
|
|
49
52
|
--force allow archive restore to overwrite existing migration files
|
|
50
53
|
--lock-timeout <ms> advisory-lock acquisition timeout for migrate run/revert/dev/verify/squash
|
|
51
54
|
--shadow-url <url> use an existing disposable shadow DB instead of auto-creating one
|
|
@@ -89,6 +92,26 @@ if (cmd === "--help" || cmd === "-h" || !cmd) {
|
|
|
89
92
|
help();
|
|
90
93
|
}
|
|
91
94
|
const root = resolve(arg("--root", process.cwd()));
|
|
95
|
+
if (cmd !== "doctor") {
|
|
96
|
+
try {
|
|
97
|
+
assertSupportedRuntime();
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
console.error(e.message);
|
|
101
|
+
process.exit(2);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
let envError;
|
|
105
|
+
try {
|
|
106
|
+
loadRootEnv(root);
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
envError = e.message;
|
|
110
|
+
if (cmd !== "doctor") {
|
|
111
|
+
console.error(envError);
|
|
112
|
+
process.exit(2);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
92
115
|
const databaseUrl = process.env.DATABASE_URL ?? "";
|
|
93
116
|
const shadowUrlArg = arg("--shadow-url");
|
|
94
117
|
const shadowUrl = shadowUrlArg ?? process.env.SHADOW_DATABASE_URL;
|
|
@@ -109,6 +132,9 @@ if (cmd === "init") {
|
|
|
109
132
|
}
|
|
110
133
|
runInit({ root, schemaProvider: provider });
|
|
111
134
|
}
|
|
135
|
+
else if (cmd === "doctor") {
|
|
136
|
+
await runDoctor({ root, databaseUrl, cacheDir, dtsPath, json: flag("--json"), envError });
|
|
137
|
+
}
|
|
112
138
|
else if (cmd === "db") {
|
|
113
139
|
const sub = process.argv[3];
|
|
114
140
|
if (sub === "install") {
|
|
@@ -139,29 +165,55 @@ else if (cmd === "db") {
|
|
|
139
165
|
}
|
|
140
166
|
}
|
|
141
167
|
else if (cmd === "prepare") {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
168
|
+
const prepareCheck = flag("--check");
|
|
169
|
+
const prepareVerify = flag("--verify");
|
|
170
|
+
const prepareWatch = flag("--watch");
|
|
171
|
+
const prepareJson = flag("--json");
|
|
172
|
+
const prepareMode = prepareVerify ? "verify" : prepareCheck ? "check" : "prepare";
|
|
173
|
+
const failPrepare = (message, phase, exitCode = 2, location = {}) => {
|
|
174
|
+
if (prepareJson) {
|
|
175
|
+
console.log(JSON.stringify({
|
|
176
|
+
formatVersion: 1,
|
|
177
|
+
ok: false,
|
|
178
|
+
mode: prepareMode,
|
|
179
|
+
sites: 0,
|
|
180
|
+
entries: 0,
|
|
181
|
+
failures: 1,
|
|
182
|
+
pruned: 0,
|
|
183
|
+
functions: 0,
|
|
184
|
+
diagnostics: [{ severity: "error", phase, message, ...location }],
|
|
185
|
+
}, null, 2));
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
console.error(message);
|
|
189
|
+
}
|
|
190
|
+
process.exit(exitCode);
|
|
191
|
+
};
|
|
192
|
+
if ([prepareCheck, prepareVerify, prepareWatch].filter(Boolean).length > 1) {
|
|
193
|
+
failPrepare("--check, --verify, and --watch are mutually exclusive", "config");
|
|
145
194
|
}
|
|
146
|
-
|
|
195
|
+
if (prepareCheck && shadowUrlArg) {
|
|
196
|
+
failPrepare("--shadow-url cannot be used with prepare --check; use live prepare or schema check --shadow-url", "config");
|
|
197
|
+
}
|
|
198
|
+
if (prepareWatch && prepareJson) {
|
|
199
|
+
failPrepare("--watch and --json are mutually exclusive", "config");
|
|
200
|
+
}
|
|
201
|
+
const prepareShadowUrl = prepareCheck ? undefined : shadowUrl;
|
|
147
202
|
const prepareDatabaseUrl = prepareShadowUrl ?? databaseUrl;
|
|
148
|
-
if (!
|
|
149
|
-
|
|
150
|
-
process.exit(2);
|
|
203
|
+
if (!prepareCheck && !prepareDatabaseUrl) {
|
|
204
|
+
failPrepare("DATABASE_URL is required for prepare (use --check for offline)", "connect");
|
|
151
205
|
}
|
|
152
206
|
const opts = {
|
|
153
207
|
root,
|
|
154
208
|
databaseUrl: prepareDatabaseUrl,
|
|
155
209
|
cacheDir,
|
|
156
210
|
dtsPath,
|
|
157
|
-
check:
|
|
211
|
+
check: prepareCheck,
|
|
212
|
+
verify: prepareVerify,
|
|
213
|
+
json: prepareJson,
|
|
158
214
|
prune: !flag("--no-prune"),
|
|
159
215
|
};
|
|
160
|
-
if (
|
|
161
|
-
if (flag("--check")) {
|
|
162
|
-
console.error("--watch and --check are mutually exclusive");
|
|
163
|
-
process.exit(2);
|
|
164
|
-
}
|
|
216
|
+
if (prepareWatch) {
|
|
165
217
|
await runWatch({
|
|
166
218
|
...opts,
|
|
167
219
|
...(prepareShadowUrl
|
|
@@ -175,9 +227,28 @@ else if (cmd === "prepare") {
|
|
|
175
227
|
});
|
|
176
228
|
}
|
|
177
229
|
else {
|
|
178
|
-
if (prepareShadowUrl)
|
|
179
|
-
|
|
180
|
-
|
|
230
|
+
if (prepareShadowUrl) {
|
|
231
|
+
try {
|
|
232
|
+
await applyShadowMigrations(prepareShadowUrl, migrationsDir, prepareJson ? () => { } : console.log);
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
failPrepare(e.message, "shadow", 1);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
await runPrepare(opts);
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
const message = e.message;
|
|
243
|
+
const phase = e instanceof PrepareFatalError
|
|
244
|
+
? e.phase
|
|
245
|
+
: prepareVerify
|
|
246
|
+
? "verify"
|
|
247
|
+
: "scan";
|
|
248
|
+
failPrepare(message, phase, 1, e instanceof PrepareFatalError
|
|
249
|
+
? { file: e.file, line: e.line, column: e.column }
|
|
250
|
+
: {});
|
|
251
|
+
}
|
|
181
252
|
}
|
|
182
253
|
}
|
|
183
254
|
else if (cmd === "schema") {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { CACHE_MANIFEST_FILE } from "./cache.js";
|
|
4
|
+
function readGeneratedFiles(set) {
|
|
5
|
+
const files = new Map();
|
|
6
|
+
if (existsSync(set.cacheDir)) {
|
|
7
|
+
for (const name of readdirSync(set.cacheDir).sort()) {
|
|
8
|
+
if (name !== CACHE_MANIFEST_FILE && !/^[0-9a-f]{16}\.json$/.test(name))
|
|
9
|
+
continue;
|
|
10
|
+
files.set(`cache/${name}`, readFileSync(join(set.cacheDir, name), "utf8"));
|
|
11
|
+
}
|
|
12
|
+
const functions = join(set.cacheDir, "functions/functions.json");
|
|
13
|
+
if (existsSync(functions))
|
|
14
|
+
files.set("cache/functions/functions.json", readFileSync(functions, "utf8"));
|
|
15
|
+
}
|
|
16
|
+
if (existsSync(set.dtsPath))
|
|
17
|
+
files.set("sqlx-js-env.d.ts", readFileSync(set.dtsPath, "utf8"));
|
|
18
|
+
return files;
|
|
19
|
+
}
|
|
20
|
+
export function compareArtifacts(expected, actual) {
|
|
21
|
+
const left = readGeneratedFiles(expected);
|
|
22
|
+
const right = readGeneratedFiles(actual);
|
|
23
|
+
const names = new Set([...left.keys(), ...right.keys()]);
|
|
24
|
+
const changed = [...names].filter((name) => left.get(name) !== right.get(name)).sort();
|
|
25
|
+
return { ok: changed.length === 0, changed };
|
|
26
|
+
}
|
package/dist/src/cache.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
export declare const CACHE_FORMAT_VERSION = 2;
|
|
2
|
+
export declare const GENERATOR_REVISION = 3;
|
|
3
|
+
export declare const CACHE_MANIFEST_FILE = "cache-manifest.json";
|
|
4
|
+
export type CacheManifest = {
|
|
5
|
+
cacheFormat: typeof CACHE_FORMAT_VERSION;
|
|
6
|
+
generatorRevision: typeof GENERATOR_REVISION;
|
|
7
|
+
configHash: string;
|
|
8
|
+
};
|
|
1
9
|
export type CacheColumn = {
|
|
2
10
|
name: string;
|
|
3
11
|
typeOid: number;
|
|
@@ -19,6 +27,7 @@ export type CacheEntry = {
|
|
|
19
27
|
reason: string;
|
|
20
28
|
};
|
|
21
29
|
};
|
|
30
|
+
export declare function portableCacheOid(oid: number): number;
|
|
22
31
|
export declare function fingerprint(query: string): string;
|
|
23
32
|
export declare function effectiveNullable(c: CacheColumn): boolean;
|
|
24
33
|
export declare class Cache {
|
|
@@ -28,6 +37,10 @@ export declare class Cache {
|
|
|
28
37
|
has(fp: string): boolean;
|
|
29
38
|
read(fp: string): CacheEntry | null;
|
|
30
39
|
write(fp: string, entry: CacheEntry): void;
|
|
40
|
+
replaceAll(entries: Iterable<{
|
|
41
|
+
fp: string;
|
|
42
|
+
entry: CacheEntry;
|
|
43
|
+
}>, prune?: boolean): string[];
|
|
31
44
|
list(): {
|
|
32
45
|
fp: string;
|
|
33
46
|
entry: CacheEntry;
|
|
@@ -35,3 +48,7 @@ export declare class Cache {
|
|
|
35
48
|
remove(fp: string): void;
|
|
36
49
|
prune(keep: Iterable<string>): string[];
|
|
37
50
|
}
|
|
51
|
+
export declare function cacheManifestPath(cacheDir: string): string;
|
|
52
|
+
export declare function writeCacheManifest(cacheDir: string, configHash: string): void;
|
|
53
|
+
export declare function readCacheManifest(cacheDir: string): CacheManifest | null;
|
|
54
|
+
export declare function assertCacheManifest(cacheDir: string, configHash: string): CacheManifest;
|