@onreza/sqlx-js 0.0.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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +496 -0
  3. package/ROADMAP.md +28 -0
  4. package/dist/bin/sqlx-js.d.ts +2 -0
  5. package/dist/bin/sqlx-js.js +180 -0
  6. package/dist/src/bun-runtime.d.ts +7 -0
  7. package/dist/src/bun-runtime.js +45 -0
  8. package/dist/src/bun.d.ts +22 -0
  9. package/dist/src/bun.js +9 -0
  10. package/dist/src/cache.d.ts +36 -0
  11. package/dist/src/cache.js +104 -0
  12. package/dist/src/codegen.d.ts +2 -0
  13. package/dist/src/codegen.js +74 -0
  14. package/dist/src/commands/migrate.d.ts +63 -0
  15. package/dist/src/commands/migrate.js +283 -0
  16. package/dist/src/commands/prepare.d.ts +23 -0
  17. package/dist/src/commands/prepare.js +314 -0
  18. package/dist/src/commands/schema.d.ts +14 -0
  19. package/dist/src/commands/schema.js +72 -0
  20. package/dist/src/commands/watch.d.ts +21 -0
  21. package/dist/src/commands/watch.js +94 -0
  22. package/dist/src/config.d.ts +6 -0
  23. package/dist/src/config.js +23 -0
  24. package/dist/src/index.d.ts +23 -0
  25. package/dist/src/index.js +10 -0
  26. package/dist/src/pg/analyze.d.ts +13 -0
  27. package/dist/src/pg/analyze.js +479 -0
  28. package/dist/src/pg/extensions.d.ts +2 -0
  29. package/dist/src/pg/extensions.js +15 -0
  30. package/dist/src/pg/narrow.d.ts +3 -0
  31. package/dist/src/pg/narrow.js +146 -0
  32. package/dist/src/pg/oids.d.ts +10 -0
  33. package/dist/src/pg/oids.js +137 -0
  34. package/dist/src/pg/param-map.d.ts +12 -0
  35. package/dist/src/pg/param-map.js +180 -0
  36. package/dist/src/pg/schema.d.ts +61 -0
  37. package/dist/src/pg/schema.js +225 -0
  38. package/dist/src/pg/wire.d.ts +134 -0
  39. package/dist/src/pg/wire.js +705 -0
  40. package/dist/src/postgres-runtime.d.ts +11 -0
  41. package/dist/src/postgres-runtime.js +150 -0
  42. package/dist/src/runtime.d.ts +69 -0
  43. package/dist/src/runtime.js +446 -0
  44. package/dist/src/scan/scanner.d.ts +12 -0
  45. package/dist/src/scan/scanner.js +283 -0
  46. package/dist/src/schema-snapshot.d.ts +104 -0
  47. package/dist/src/schema-snapshot.js +473 -0
  48. package/dist/src/typed.d.ts +25 -0
  49. package/dist/src/typed.js +1 -0
  50. package/package.json +78 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sqlx-js contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,496 @@
1
+ # sqlx-js
2
+
3
+ Compile-time-checked raw SQL for TypeScript + PostgreSQL. Inspired by Rust's [sqlx](https://github.com/launchbadge/sqlx).
4
+
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, mismatched parameter types, stale queries after a migration — all become compile errors.
6
+
7
+ The default runtime adapter uses [Postgres.js](https://github.com/porsager/postgres). A `Bun.SQL` adapter is still available as `@onreza/sqlx-js/bun` for compatibility, but it is not the recommended production adapter; Bun.SQL has had production-affecting connection-state regressions such as [oven-sh/bun#16691](https://github.com/oven-sh/bun/issues/16691), fixed by [oven-sh/bun#17272](https://github.com/oven-sh/bun/pull/17272). Validate the exact Bun version and workload before using that adapter in production.
8
+
9
+ ```ts
10
+ import { sql } from "@onreza/sqlx-js";
11
+
12
+ const rows = await sql(
13
+ `SELECT id, name, role FROM users WHERE id = $1`,
14
+ 1n,
15
+ );
16
+ // ^ bigint
17
+ //
18
+ // rows: { id: bigint; name: string; role: "admin" | "editor" | "viewer" }[]
19
+ ```
20
+
21
+ ## Features
22
+
23
+ - **Compile-time validation** against a live PostgreSQL via `Parse` + `Describe Statement` (no query execution).
24
+ - **Precise nullability inference** through `libpg-query`: `JOIN` direction (LEFT/RIGHT/FULL), inner `JOIN ... ON` predicates, DML `RETURNING`, `COALESCE`, `CASE`, `COUNT`, expression propagation. Parameters become `T | null` when wrapped in `COALESCE`/`NULLIF`/`IS [NOT] NULL`/`IS [NOT] DISTINCT FROM`, or when bound to a nullable column in `INSERT`/`UPDATE`.
25
+ - **WHERE narrowing**: `IS NOT NULL`, equality chains, `IN`, `LIKE`, `BETWEEN` make columns non-null. Tracks `AND`/`OR` semantics.
26
+ - **PostgreSQL enums** generated as TypeScript literal unions (read + write side).
27
+ - **Schema-aware `jsonb`** via a `SqlxJsJson` global namespace and a config-driven column → type mapping. Works for both result columns and `INSERT`/`UPDATE`/`WHERE` parameters.
28
+ - **Extension types out of the box**: `pgvector` (`vector`, `halfvec`, `sparsevec`), `hstore`, `citext`, `ltree`/`lquery`/`ltxtquery`. Add your own through `customTypes` config.
29
+ - **Domains** resolve to their base TypeScript type (`CREATE DOMAIN email AS text` → `string`), including domains over extension types or other domains.
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
+ - **External SQL files** via `sql.file("queries/foo.sql", ...)` — typed exactly like inline queries. Watch mode re-prepares on `.sql` edits too.
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.
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
+ - **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
+ - **Linear migrations** with hash tampering detection.
37
+ - **Runtime `migrate()`** with PostgreSQL advisory lock, safe for multi-replica startup.
38
+ - **Offline cache** committed to your repo. CI verifies via `prepare --check` without a database.
39
+ - **Schema snapshot + LLM manifest** via `schema dump` / `schema check`: tables, columns, constraints, indexes, types, and function/procedure metadata are introspected from PostgreSQL.
40
+ - **Shadow database validation** via `--shadow-url` / `SHADOW_DATABASE_URL`: apply migrations to a throwaway DB, then prepare or introspect against it.
41
+ - **Safe identifier quoting** via `sql.id(...)`, backed by the committed schema snapshot whitelist.
42
+ - **Tree-shakeable runtime adapters**: `@onreza/sqlx-js` imports Postgres.js; `@onreza/sqlx-js/bun` imports `Bun.SQL` only when explicitly used.
43
+ - **Watch mode**: ~15ms incremental re-prepare on file change.
44
+ - **Cache pruning** removes orphaned entries automatically (toggleable with `--no-prune`).
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ npm install @onreza/sqlx-js
50
+ # or
51
+ bun add @onreza/sqlx-js
52
+ ```
53
+
54
+ 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.
55
+
56
+ ## Setup
57
+
58
+ ### 1. Configure the database URL
59
+
60
+ ```bash
61
+ # .env
62
+ DATABASE_URL=postgres://user:password@localhost:5432/your_db
63
+ # Or with TLS against managed Postgres:
64
+ # DATABASE_URL=postgres://user:password@db.example.com:5432/your_db?sslmode=require
65
+ ```
66
+
67
+ Supported `sslmode` values: `disable`, `prefer` (default — try TLS, fall back to plaintext), `require` (TLS or fail), `verify-ca`, `verify-full`. `application_name` and `connect_timeout` are also honored when provided as URL parameters.
68
+
69
+ ### 2. Create a migration
70
+
71
+ ```bash
72
+ npx @onreza/sqlx-js migrate add init
73
+ ```
74
+
75
+ Edit the created file (`migrations/0001_init.up.sql`):
76
+
77
+ ```sql
78
+ CREATE TABLE users (
79
+ id BIGSERIAL PRIMARY KEY,
80
+ name TEXT NOT NULL,
81
+ email TEXT NOT NULL UNIQUE,
82
+ age INT,
83
+ bio TEXT
84
+ );
85
+ ```
86
+
87
+ Apply:
88
+
89
+ ```bash
90
+ npx @onreza/sqlx-js migrate run
91
+ ```
92
+
93
+ ### 3. Write your first query
94
+
95
+ ```ts
96
+ // app.ts
97
+ import { sql } from "@onreza/sqlx-js";
98
+
99
+ const users = await sql(
100
+ `SELECT id, name FROM users WHERE id = $1`,
101
+ 1n,
102
+ );
103
+ ```
104
+
105
+ ### 4. Prepare types
106
+
107
+ ```bash
108
+ npx @onreza/sqlx-js prepare
109
+ ```
110
+
111
+ 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.
112
+
113
+ ### 5. Dev loop with watch
114
+
115
+ ```bash
116
+ npx @onreza/sqlx-js prepare --watch
117
+ ```
118
+
119
+ Save a `.ts` file, types regenerate in milliseconds, your editor picks up changes.
120
+
121
+ ## API
122
+
123
+ ### `sql(query, ...params)`
124
+
125
+ The typed query function. The first argument must be a string literal that exists in `KnownQueries` (populated by `prepare`).
126
+
127
+ ```ts
128
+ const rows = await sql(`SELECT id FROM users WHERE name = $1`, "alice");
129
+ // ^ literal — checked at compile time
130
+ ```
131
+
132
+ Unknown queries, wrong parameter types, and dynamic strings are compile errors. For genuinely dynamic SQL, use `unsafe`.
133
+
134
+ ### `sql.file(path, ...params)`
135
+
136
+ Load SQL from an external file. The path is resolved against the source file at scan time (so `prepare` can read it), and against `process.cwd()` at runtime (so the running process can read it). Both must point at the same content.
137
+
138
+ ```ts
139
+ // queries/top_admins.sql
140
+ // SELECT id AS "id!", name AS "name!" FROM users WHERE role = $1 ORDER BY id LIMIT $2::int
141
+
142
+ import { sql } from "@onreza/sqlx-js";
143
+
144
+ const admins = await sql.file("queries/top_admins.sql", "admin", 5);
145
+ // ^ string ^ number
146
+ // admins: { id: bigint; name: string }[]
147
+ ```
148
+
149
+ File-backed queries are emitted into a separate `KnownFileQueries` interface; the path becomes the type key.
150
+
151
+ ### `sql.one(query, ...params)` and `sql.optional(query, ...params)`
152
+
153
+ Convenience wrappers for single-row queries. `one` throws if the row count is not exactly 1; `optional` returns `null` for 0 rows and throws on more than 1. They keep working under `noUncheckedIndexedAccess: true` without `rows[0]!` patterns.
154
+
155
+ ```ts
156
+ const user = await sql.one(`SELECT id, name FROM users WHERE id = $1`, 1n);
157
+ // user: { id: bigint; name: string }
158
+
159
+ const maybe = await sql.optional(`SELECT id FROM users WHERE email = $1`, "x@y");
160
+ // maybe: { id: bigint } | null
161
+ ```
162
+
163
+ 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(...)`.
164
+
165
+ ### Array parameters
166
+
167
+ 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`.
168
+
169
+ ```ts
170
+ await sql("SELECT $1::text[] AS tags", ["alpha", "beta,gamma", "with \"quote\""]);
171
+ // → $1 sent as {alpha,"beta,gamma","with \"quote\""}
172
+ ```
173
+
174
+ 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(...)`.
175
+
176
+ 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([])`.
177
+
178
+ ### Parameter nullability
179
+
180
+ `prepare` infers param types as `T | null` when:
181
+
182
+ - `$N` appears inside `COALESCE($N, …)`, `NULLIF($N, …)`, `IS [NOT] NULL`, or `IS [NOT] DISTINCT FROM` — these patterns are only meaningful when the parameter can be `null`.
183
+ - `$N` is positionally bound in `INSERT … VALUES (…, $N, …)` or `UPDATE … SET col = $N` and the target column is nullable.
184
+
185
+ `WHERE col = $N` stays non-null even if `col` is nullable: `col = NULL` is always false in SQL, so passing `null` from the caller would be a bug. Use `col IS NOT DISTINCT FROM $N` (or an `OR $N IS NULL` clause) when you want NULL semantics.
186
+
187
+ ### `sql.transaction(fn)`
188
+
189
+ Wrap a function body in a database transaction. The callback receives a scoped `tx` that has the same typed `()` and `.file()` surface, but routes through the transaction's dedicated connection. The scanner recognises the callback parameter name and validates inner queries against `KnownQueries`.
190
+
191
+ ```ts
192
+ import { sql } from "@onreza/sqlx-js";
193
+
194
+ const { userId, postId } = await sql.transaction(async (tx) => {
195
+ const u = await tx(
196
+ `INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id AS "id!"`,
197
+ "Alice", "alice@example.com",
198
+ );
199
+ const p = await tx(
200
+ `INSERT INTO posts (user_id, title) VALUES ($1, $2) RETURNING id AS "id!"`,
201
+ u[0].id, "Hello",
202
+ );
203
+ return { userId: u[0].id, postId: p[0].id };
204
+ });
205
+ ```
206
+
207
+ If the callback throws, the transaction is rolled back. The return value of the callback becomes the return value of `transaction`.
208
+
209
+ ### `unsafe(query, ...params)`
210
+
211
+ Same runtime as `sql` but without type-checking. For dynamic SQL where compile-time validation isn't possible.
212
+
213
+ ### `sql.id(...parts)` / `id(...parts)`
214
+
215
+ Quote a dynamic identifier only if it exists in the generated schema snapshot. This is for the narrow cases where a table, column, function, type, index, or constraint name must be chosen dynamically.
216
+
217
+ ```ts
218
+ import { unsafe, sql } from "@onreza/sqlx-js";
219
+
220
+ const orderBy = sql.id("users", "created_at");
221
+ await unsafe(`SELECT id, email FROM ${sql.id("users")} ORDER BY ${orderBy} DESC`);
222
+ ```
223
+
224
+ The default snapshot path is `.sqlx-js/schema/schema.json`. Override it at runtime with `SQLX_JS_SCHEMA_PATH`. Pass schema-qualified identifiers as separate segments: `sql.id("public", "users")`, not `sql.id("public.users")`.
225
+
226
+ ### `migrate(options)`
227
+
228
+ Apply pending migrations from application startup with a PostgreSQL advisory lock. Safe to call from multiple replicas.
229
+
230
+ ```ts
231
+ import { migrate } from "@onreza/sqlx-js";
232
+
233
+ await migrate({ dir: "./migrations" });
234
+ ```
235
+
236
+ Options:
237
+
238
+ ```ts
239
+ type MigrateOptions = {
240
+ dir?: string;
241
+ databaseUrl?: string;
242
+ log?: (msg: string) => void;
243
+ lockKey?: number | bigint; // overrides DEFAULT_MIGRATE_LOCK_KEY
244
+ lockTimeoutMs?: number; // pg_try_advisory_lock + polling; default: block
245
+ };
246
+ ```
247
+
248
+ When `lockTimeoutMs` is set, acquisition uses `pg_try_advisory_lock` in a polling loop and throws if not obtained within the timeout — useful for CI / multi-replica startup to avoid an indefinitely-blocked pod.
249
+
250
+ ### `getClient()` / `setClient()` / `close()`
251
+
252
+ Low-level access to the underlying Postgres.js client, in case you need to manage the connection directly.
253
+ Use `createClient(...)` when replacing the default client; it preserves the built-in `bigint` and PostgreSQL array parsers expected by generated types.
254
+
255
+ ```ts
256
+ import { createClient, setClient } from "@onreza/sqlx-js";
257
+
258
+ setClient(createClient(process.env.DATABASE_URL));
259
+ ```
260
+
261
+ For the compatibility adapter, import from `@onreza/sqlx-js/bun`:
262
+
263
+ ```ts
264
+ import { SQL } from "bun";
265
+ import { setClient } from "@onreza/sqlx-js/bun";
266
+
267
+ setClient(new SQL({ url: process.env.DATABASE_URL!, bigint: true }));
268
+ ```
269
+
270
+ ### `clearSqlFileCache()`
271
+
272
+ Drops the in-memory cache used by `sql.file(...)`. The cache invalidates automatically on file mtime change, so this is rarely needed manually.
273
+
274
+ ### Typed errors
275
+
276
+ ```ts
277
+ import { NoRowsError, TooManyRowsError, PgError } from "@onreza/sqlx-js";
278
+
279
+ try {
280
+ const u = await sql.one(`SELECT id FROM users WHERE id = $1`, 99);
281
+ } catch (e) {
282
+ if (e instanceof NoRowsError) return null;
283
+ if (e instanceof TooManyRowsError) console.error("ambiguous query, got", e.actual);
284
+ if (e instanceof PgError) console.error("pg code:", e.code, "position:", e.position);
285
+ throw e;
286
+ }
287
+ ```
288
+
289
+ `sql.one` throws `NoRowsError` on 0 rows and `TooManyRowsError` (with `.actual`) on >1. `PgError` exposes `.code`, `.position`, `.hint`, `.detail`, `.severity`.
290
+
291
+ ### Transactions with options
292
+
293
+ `sql.transaction(fn)` and `sql.transaction(opts, fn)`:
294
+
295
+ ```ts
296
+ await sql.transaction({ isolation: "serializable", readOnly: true }, async (tx) => {
297
+ return await tx(`SELECT id FROM accounts WHERE owner = $1`, ownerId);
298
+ });
299
+ ```
300
+
301
+ Options: `{ isolation?: "read uncommitted" | "read committed" | "repeatable read" | "serializable"; readOnly?: boolean; deferrable?: boolean }`. Applied via `SET TRANSACTION` immediately after `BEGIN`.
302
+
303
+ ### Namespace imports
304
+
305
+ In addition to `import { sql } from "@onreza/sqlx-js"`, the scanner recognises `import * as ns from "@onreza/sqlx-js"` and the same forms from `@onreza/sqlx-js/bun`. It validates `ns.sql(...)`, `ns.sql.one(...)`, `ns.sql.file(...)`, and `ns.sql.transaction(...)` exactly like the named-import form. Local re-declarations (`const sql = ...`, `const { sql } = ...`) correctly shadow the alias inside their scope.
306
+
307
+ ## CLI
308
+
309
+ ```
310
+ sqlx-js prepare [--check | --watch] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]
311
+ sqlx-js migrate run [--lock-timeout <ms>] | info | revert | add <name> [--migrations <dir>]
312
+ sqlx-js schema dump | check [--schema <path>] [--manifest <path>] [--no-manifest] [--shadow-url <url>]
313
+ sqlx-js --version | --help
314
+ ```
315
+
316
+ | Flag | Meaning |
317
+ |-----------------------|--------------------------------------------------------------------------------------|
318
+ | `--check` | Offline: verify cache matches sources, no database required. |
319
+ | `--watch` | Persistent connection, re-prepare on file change. |
320
+ | `--root <dir>` | Source/cache/migrations root (default: cwd). |
321
+ | `--dts <path>` | Declarations output (default: `<root>/sqlx-js-env.d.ts`). |
322
+ | `--no-prune` | Keep orphaned cache entries instead of removing them. |
323
+ | `--migrations <dir>` | Migrations directory (default: `<root>/migrations`). |
324
+ | `--lock-timeout <ms>` | Advisory-lock acquisition timeout for `migrate run` / `migrate revert`. |
325
+ | `--shadow-url <url>` | Apply migrations to this database, then prepare/introspect against it. |
326
+ | `--schema <path>` | Schema snapshot path (default: `<root>/.sqlx-js/schema/schema.json`). |
327
+ | `--manifest <path>` | LLM schema manifest path (default: `<root>/.sqlx-js/schema/schema.md`). |
328
+ | `--no-manifest` | Skip writing the LLM schema manifest during `schema dump`. |
329
+
330
+ All flags accept both `--flag value` and `--flag=value` forms.
331
+
332
+ `DATABASE_URL` must be set for any command that touches the database, unless `--shadow-url` or `SHADOW_DATABASE_URL` is provided for that command. Supported URL search params: `sslmode`, `application_name`, `connect_timeout`.
333
+
334
+ ### Schema snapshot and manifest
335
+
336
+ `schema dump` introspects PostgreSQL and writes two generated files:
337
+
338
+ - `.sqlx-js/schema/schema.json` — machine-readable contract for runtime identifier whitelisting and CI drift checks.
339
+ - `.sqlx-js/schema/schema.md` — compact LLM-facing manifest with tables, columns, constraints, indexes, types, and functions.
340
+
341
+ `schema check` re-introspects the database and fails if the committed snapshot is stale. With `--shadow-url`, both `prepare` and `schema dump/check` first apply pending migrations to the shadow database, then use that database as the source of truth. In watch mode, pending shadow migrations are checked before every re-prepare; when a migration is applied, the prepare session is reopened so schema metadata is not reused across DDL changes.
342
+
343
+ ### Error output
344
+
345
+ When `prepare` fails, every diagnostic points back to the originating call site:
346
+
347
+ ```
348
+ ✗ src/users.ts:42:13 — describe failed: relation "userss" does not exist (pos 15, code 42P01)
349
+ query: SELECT * FROM userss WHERE id = $1
350
+ ```
351
+
352
+ Phases reported separately: `describe failed`, `analyze failed`, `paramMap failed`. PostgreSQL `position`, `code`, and `hint` are surfaced when present.
353
+
354
+ ## Configuration
355
+
356
+ `sqlx-js.config.ts` at the project root is optional.
357
+
358
+ ```ts
359
+ import type { SqlxJsConfig } from "@onreza/sqlx-js";
360
+
361
+ const config: SqlxJsConfig = {
362
+ jsonbTypes: {
363
+ "users.settings": "SqlxJsJson.UserSettings",
364
+ "posts.meta": "SqlxJsJson.PostMeta",
365
+ "posts.attachments": "SqlxJsJson.Attachment",
366
+ },
367
+ };
368
+
369
+ export default config;
370
+ ```
371
+
372
+ Declare the referenced types anywhere in your project (`.d.ts` file is conventional):
373
+
374
+ ```ts
375
+ // json-types.d.ts
376
+ declare global {
377
+ namespace SqlxJsJson {
378
+ type UserSettings = {
379
+ theme: "light" | "dark";
380
+ lang: string;
381
+ notifications?: { email: boolean; push: boolean };
382
+ };
383
+ type PostMeta = { tags?: string[]; pinned?: boolean };
384
+ type Attachment = { url: string; kind: "image" | "video" | "file"; sizeBytes: number };
385
+ }
386
+ }
387
+ export {};
388
+ ```
389
+
390
+ After re-running `prepare`, every `jsonb` column or parameter declared in `jsonbTypes` is checked against the corresponding TypeScript type.
391
+
392
+ ### Extension types and `customTypes`
393
+
394
+ sqlx-js ships with a built-in registry that resolves popular PostgreSQL extension types automatically:
395
+
396
+ | `pg_type.typname` | TS type | Source extension |
397
+ |-------------------|------------------------------------|-------------------|
398
+ | `vector` | `number[]` | pgvector |
399
+ | `halfvec` | `number[]` | pgvector |
400
+ | `sparsevec` | `string` | pgvector |
401
+ | `hstore` | `Record<string, string \| null>` | hstore |
402
+ | `citext` | `string` | citext |
403
+ | `ltree` | `string` | ltree |
404
+ | `lquery` | `string` | ltree |
405
+ | `ltxtquery` | `string` | ltree |
406
+
407
+ Add or override mappings via `customTypes` in `sqlx-js.config.ts`. Keys are `pg_type.typname` values (the bare type name; namespacing isn't required):
408
+
409
+ ```ts
410
+ import type { SqlxJsConfig } from "@onreza/sqlx-js";
411
+
412
+ const config: SqlxJsConfig = {
413
+ customTypes: {
414
+ vector: "Float32Array", // override pgvector default
415
+ geometry: "GeoJSON.Geometry", // postgis (not built-in by design)
416
+ myapp_color: "`#${string}`", // your own CREATE TYPE base/domain
417
+ },
418
+ };
419
+ export default config;
420
+ ```
421
+
422
+ 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[])[]`.
423
+
424
+ Composite types (`CREATE TYPE foo AS (a int, b text)`) still resolve to `unknown`; see ROADMAP.
425
+
426
+ ## How nullability is inferred
427
+
428
+ A result column is non-null if **all** of the following hold:
429
+
430
+ 1. The source column has a `NOT NULL` constraint (looked up via `pg_attribute`).
431
+ 2. The source table isn't on the nullable side of an outer join.
432
+ 3. Any wrapping expression is null-preserving — `COALESCE` with a non-null fallback, `CASE` with `ELSE`, `COUNT(*)`, `length(non_null)`, etc.
433
+
434
+ A column that doesn't satisfy the above is `T | null`. You can override:
435
+
436
+ - `SELECT id AS "id!"` → force non-null.
437
+ - `SELECT id AS "id?"` → force nullable.
438
+ - `WHERE col IS NOT NULL` / `WHERE col = …` / `WHERE col IN (…)` → narrows `col` to non-null in the result.
439
+
440
+ The runtime strips the `!`/`?` suffix from column keys so the row shape stays clean: `{ id: bigint }`, not `{ "id!": bigint }`.
441
+
442
+ ## CI workflow
443
+
444
+ Commit the generated `sqlx-js-env.d.ts` and the `.sqlx-js/` cache directory to your repo. In CI:
445
+
446
+ ```yaml
447
+ - run: bun install
448
+ - run: sqlx-js prepare --check # fails if any query is missing from cache
449
+ - run: sqlx-js schema check # fails if the committed schema snapshot is stale
450
+ - run: tsc --noEmit # fails if types are stale
451
+ - run: bun test
452
+ - run: bun run build # emits publishable JS + declarations under dist/
453
+ ```
454
+
455
+ The `prepare --check` step runs without a database — your offline cache is the source of truth. `schema check` intentionally uses a live or shadow database because it verifies the committed schema contract against PostgreSQL.
456
+
457
+ ## Contributing
458
+
459
+ The project uses [conventional commits](https://www.conventionalcommits.org/), validated locally by `cocogitto` through `lefthook` hooks. Install both before contributing:
460
+
461
+ ```bash
462
+ bun install # installs lefthook + wires git hooks
463
+ cargo install cocogitto # or: brew install cocogitto
464
+ ```
465
+
466
+ 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.
467
+
468
+ ## Limitations
469
+
470
+ `sqlx-js` is a young library. Known gaps:
471
+
472
+ - PostgreSQL only (no MySQL or SQLite).
473
+ - `INSERT INTO t VALUES (...)` without an explicit column list isn't parameter-mapped.
474
+ - `SELECT *` falls back to conservative nullability.
475
+ - 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.
476
+ - Composite types resolve to `unknown`. Domains and array types of registered types resolve correctly.
477
+ - 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.
478
+ - 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.
479
+ - `parseDatabaseUrl` parses `sslmode`, `application_name`, and `connect_timeout` for the **internal** wire client (used by `migrate run`, `prepare`, and the runtime `migrate()` helper). The default runtime `sql()` path delegates connection handling to Postgres.js.
480
+ - The `@onreza/sqlx-js/bun` adapter delegates runtime queries to `Bun.SQL`; it is kept for compatibility and is not recommended as the production adapter without workload-specific validation.
481
+ - `connect_timeout` covers the TCP-connect phase only; TLS handshake and SCRAM authentication have no timeout.
482
+ - `sql.file(path)` path is matched literally between scan time and runtime — they must agree on the working directory. Document a convention for your team (e.g. always run from the repo root).
483
+
484
+ See [ROADMAP.md](./ROADMAP.md) for what's planned.
485
+
486
+ ## Upgrading
487
+
488
+ ### Cache schema change (pre-1.0)
489
+
490
+ 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.
491
+
492
+ 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`.
493
+
494
+ ## License
495
+
496
+ MIT.
package/ROADMAP.md ADDED
@@ -0,0 +1,28 @@
1
+ # Roadmap
2
+
3
+ Future work, ordered by ROI (0–10) — how much real-world pain each item closes.
4
+
5
+ Items already shipped live in the [README](./README.md) feature list; this file tracks what's still ahead.
6
+
7
+ | Feature | ROI | Notes |
8
+ |---------|-----|-------|
9
+ | Composite & domain types | 6 | Resolve PG `CREATE TYPE foo AS (...)` and `CREATE DOMAIN` via `pg_type` recursion. Domain → base type's TS (`email DOMAIN AS text` → `string`). Composite → struct literal type. Currently both fall through to `unknown`. |
10
+ | 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. |
11
+ | `INSERT INTO t VALUES (...)` without column list | 3 | Map params by `pg_attribute attnum` ordering. Rare in practice — most teams use explicit column lists. |
12
+ | 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. |
13
+ | 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
+ | 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. |
15
+ | 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. |
16
+ | SQLite backend | 4 | SQLite's column types are dynamic. Would require running `EXPLAIN` and a heuristic mapper, or schema-driven inference per-statement. |
17
+ | `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. |
18
+ | `NOT (col IS NULL)` narrowing | 2 | Symmetric inversion in WHERE walker. Niche pattern. |
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. |
20
+ | Migration `down` reversal dry-run | 3 | Apply `down`, diff schema, compare to pre-`up` snapshot. Useful for catching irreversible migrations. |
21
+ | Stored procedure / function typing | 3 | `CALL proc(...)` and `SELECT func(...)` with parameter and return-type binding from `pg_proc`. |
22
+ | Streaming / cursor / COPY typing | 3 | Surface Postgres.js cursor / COPY APIs with proper row types. |
23
+
24
+ ## Long-term
25
+
26
+ - Full LSP with schema-driven autocomplete.
27
+ - Hooks for ORM-like helpers that build on top of the typed `sql()` primitive (joins, paginated queries, etc.) without becoming an ORM.
28
+ - Optional binary protocol support in the underlying wire client for measurable perf gain on large result sets.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};