@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
@@ -0,0 +1,283 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { PgClient, parseDatabaseUrl, decodeText } from "../pg/wire.js";
5
+ export const DEFAULT_MIGRATE_LOCK_KEY = 18750938867203960n;
6
+ const FILE_RE = /^(\d+)_(.+)\.up\.sql$/;
7
+ const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
8
+ function readMigrations(dir) {
9
+ if (!existsSync(dir))
10
+ return [];
11
+ const out = [];
12
+ for (const f of readdirSync(dir).sort()) {
13
+ const m = FILE_RE.exec(f);
14
+ if (!m)
15
+ continue;
16
+ const version = parseInt(m[1], 10);
17
+ const name = m[2];
18
+ if (!SAFE_NAME_RE.test(name)) {
19
+ throw new Error(`sqlx-js.migrate: unsafe migration filename ${f} — name must match /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/`);
20
+ }
21
+ const upPath = join(dir, f);
22
+ const downPath = join(dir, `${m[1]}_${name}.down.sql`);
23
+ const upSql = readFileSync(upPath, "utf8");
24
+ out.push({
25
+ version,
26
+ name,
27
+ upPath,
28
+ downPath: existsSync(downPath) ? downPath : null,
29
+ upSql,
30
+ upHash: createHash("sha256").update(upSql).digest("hex"),
31
+ });
32
+ }
33
+ return out;
34
+ }
35
+ export async function ensureTable(c) {
36
+ await c.simpleQuery(`
37
+ CREATE TABLE IF NOT EXISTS _sqlx_js_migrations (
38
+ version BIGINT PRIMARY KEY,
39
+ name TEXT NOT NULL,
40
+ up_hash TEXT NOT NULL,
41
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
42
+ )
43
+ `);
44
+ }
45
+ export async function listApplied(c) {
46
+ const r = await c.simpleQuery("SELECT version, name, up_hash FROM _sqlx_js_migrations ORDER BY version");
47
+ const out = new Map();
48
+ for (const row of r.rows) {
49
+ out.set(Number(decodeText(row[0])), { name: decodeText(row[1]), hash: decodeText(row[2]) });
50
+ }
51
+ return out;
52
+ }
53
+ export async function applyPending(c, migrationsDir, onEvent) {
54
+ await ensureTable(c);
55
+ const applied = await listApplied(c);
56
+ const all = readMigrations(migrationsDir);
57
+ const counts = { applied: 0, tampered: 0, failed: 0 };
58
+ for (const m of all) {
59
+ const a = applied.get(m.version);
60
+ if (a) {
61
+ if (a.hash !== m.upHash) {
62
+ counts.tampered++;
63
+ onEvent?.({ kind: "tampered", version: m.version, name: m.name, applied: a.hash, current: m.upHash });
64
+ return counts;
65
+ }
66
+ continue;
67
+ }
68
+ await c.simpleQuery("BEGIN");
69
+ try {
70
+ await c.simpleQuery(m.upSql);
71
+ await c.execParamsText("INSERT INTO _sqlx_js_migrations (version, name, up_hash) VALUES ($1, $2, $3)", [String(m.version), m.name, m.upHash]);
72
+ await c.simpleQuery("COMMIT");
73
+ counts.applied++;
74
+ onEvent?.({ kind: "applied", version: m.version, name: m.name });
75
+ }
76
+ catch (err) {
77
+ let rollbackErr;
78
+ try {
79
+ await c.simpleQuery("ROLLBACK");
80
+ }
81
+ catch (rb) {
82
+ rollbackErr = rb.message;
83
+ }
84
+ counts.failed++;
85
+ const message = rollbackErr
86
+ ? `${err.message} (rollback also failed: ${rollbackErr})`
87
+ : err.message;
88
+ onEvent?.({ kind: "failed", version: m.version, name: m.name, error: message });
89
+ return counts;
90
+ }
91
+ }
92
+ return counts;
93
+ }
94
+ function lockKeyToString(lockKey) {
95
+ if (typeof lockKey === "bigint")
96
+ return lockKey.toString();
97
+ if (!Number.isSafeInteger(lockKey)) {
98
+ throw new Error(`sqlx-js.migrate: lockKey must be a safe integer or bigint, got ${lockKey}`);
99
+ }
100
+ return BigInt(lockKey).toString();
101
+ }
102
+ export async function acquireMigrateLock(c, lockKey = DEFAULT_MIGRATE_LOCK_KEY, timeoutMs) {
103
+ if (timeoutMs !== undefined && !Number.isFinite(timeoutMs)) {
104
+ throw new Error(`sqlx-js.migrate: lockTimeoutMs must be a finite number, got ${timeoutMs}`);
105
+ }
106
+ const key = lockKeyToString(lockKey);
107
+ if (timeoutMs === undefined || timeoutMs <= 0) {
108
+ await c.simpleQuery(`SELECT pg_advisory_lock(${key})`);
109
+ return;
110
+ }
111
+ const start = Date.now();
112
+ let delay = 50;
113
+ while (true) {
114
+ const r = await c.simpleQuery(`SELECT pg_try_advisory_lock(${key})`);
115
+ const got = decodeText(r.rows[0]?.[0] ?? null) === "t";
116
+ if (got)
117
+ return;
118
+ const elapsed = Date.now() - start;
119
+ if (elapsed >= timeoutMs) {
120
+ throw new Error(`sqlx-js.migrate: failed to acquire advisory lock ${key} within ${timeoutMs}ms`);
121
+ }
122
+ const remaining = timeoutMs - elapsed;
123
+ await new Promise((resolve) => setTimeout(resolve, Math.min(delay, remaining)));
124
+ delay = Math.min(delay * 2, 2000);
125
+ }
126
+ }
127
+ export async function releaseMigrateLock(c, lockKey = DEFAULT_MIGRATE_LOCK_KEY) {
128
+ const key = lockKeyToString(lockKey);
129
+ await c.simpleQuery(`SELECT pg_advisory_unlock(${key})`);
130
+ }
131
+ export async function migrateRun(opts) {
132
+ const cfg = parseDatabaseUrl(opts.databaseUrl);
133
+ const c = new PgClient(cfg);
134
+ await c.connect();
135
+ let exitCode = 0;
136
+ let locked = false;
137
+ const lockKey = opts.lockKey ?? DEFAULT_MIGRATE_LOCK_KEY;
138
+ try {
139
+ await acquireMigrateLock(c, lockKey, opts.lockTimeoutMs);
140
+ locked = true;
141
+ await applyPending(c, opts.migrationsDir, (e) => {
142
+ if (e.kind === "applied")
143
+ console.log(`applying ${e.version}_${e.name}…\n ✓ applied`);
144
+ else if (e.kind === "tampered") {
145
+ console.error(`migration ${e.version}_${e.name} was tampered with (hash mismatch)`);
146
+ console.error(` applied: ${e.applied.slice(0, 16)}…`);
147
+ console.error(` current: ${e.current.slice(0, 16)}…`);
148
+ exitCode = 1;
149
+ }
150
+ else {
151
+ console.error(`applying ${e.version}_${e.name}…\n ✗ ${e.error}`);
152
+ exitCode = 1;
153
+ }
154
+ });
155
+ }
156
+ finally {
157
+ if (locked) {
158
+ try {
159
+ await releaseMigrateLock(c, lockKey);
160
+ }
161
+ catch (e) {
162
+ console.warn(`sqlx-js.migrate: failed to release advisory lock: ${e.message}`);
163
+ }
164
+ }
165
+ await c.end();
166
+ }
167
+ if (exitCode !== 0)
168
+ process.exit(exitCode);
169
+ }
170
+ export async function migrateInfo(opts) {
171
+ const cfg = parseDatabaseUrl(opts.databaseUrl);
172
+ const c = new PgClient(cfg);
173
+ await c.connect();
174
+ await ensureTable(c);
175
+ const applied = await listApplied(c);
176
+ const all = readMigrations(opts.migrationsDir);
177
+ console.log(`migrations in ${opts.migrationsDir}:`);
178
+ for (const m of all) {
179
+ const a = applied.get(m.version);
180
+ const status = !a ? "pending" : a.hash === m.upHash ? "applied" : "applied (tampered!)";
181
+ console.log(` ${String(m.version).padStart(4, "0")}_${m.name}: ${status}`);
182
+ }
183
+ await c.end();
184
+ }
185
+ export async function revertLast(c, migrationsDir) {
186
+ await ensureTable(c);
187
+ const applied = await listApplied(c);
188
+ const all = readMigrations(migrationsDir);
189
+ let last = null;
190
+ for (let i = all.length - 1; i >= 0; i--) {
191
+ if (applied.has(all[i].version)) {
192
+ last = all[i];
193
+ break;
194
+ }
195
+ }
196
+ if (!last)
197
+ return { kind: "noop" };
198
+ if (!last.downPath)
199
+ return { kind: "no-down", version: last.version, name: last.name };
200
+ const downSql = readFileSync(last.downPath, "utf8");
201
+ await c.simpleQuery("BEGIN");
202
+ try {
203
+ await c.simpleQuery(downSql);
204
+ await c.execParamsText("DELETE FROM _sqlx_js_migrations WHERE version = $1", [String(last.version)]);
205
+ await c.simpleQuery("COMMIT");
206
+ return { kind: "reverted", version: last.version, name: last.name };
207
+ }
208
+ catch (err) {
209
+ let rollbackErr;
210
+ try {
211
+ await c.simpleQuery("ROLLBACK");
212
+ }
213
+ catch (rb) {
214
+ rollbackErr = rb.message;
215
+ }
216
+ const msg = rollbackErr
217
+ ? `${err.message} (rollback also failed: ${rollbackErr})`
218
+ : err.message;
219
+ return { kind: "failed", version: last.version, name: last.name, error: msg };
220
+ }
221
+ }
222
+ export async function migrateRevert(opts) {
223
+ const cfg = parseDatabaseUrl(opts.databaseUrl);
224
+ const c = new PgClient(cfg);
225
+ await c.connect();
226
+ const lockKey = opts.lockKey ?? DEFAULT_MIGRATE_LOCK_KEY;
227
+ let locked = false;
228
+ let exitCode = 0;
229
+ try {
230
+ await acquireMigrateLock(c, lockKey, opts.lockTimeoutMs);
231
+ locked = true;
232
+ const outcome = await revertLast(c, opts.migrationsDir);
233
+ if (outcome.kind === "noop") {
234
+ console.log("nothing to revert");
235
+ }
236
+ else if (outcome.kind === "no-down") {
237
+ console.error(`migration ${outcome.version}_${outcome.name} has no .down.sql`);
238
+ exitCode = 1;
239
+ }
240
+ else if (outcome.kind === "reverted") {
241
+ console.log(`reverting ${outcome.version}_${outcome.name}…`);
242
+ console.log(` ✓ reverted`);
243
+ }
244
+ else {
245
+ console.error(`reverting ${outcome.version}_${outcome.name}…`);
246
+ console.error(` ✗ ${outcome.error}`);
247
+ exitCode = 1;
248
+ }
249
+ }
250
+ finally {
251
+ if (locked) {
252
+ try {
253
+ await releaseMigrateLock(c, lockKey);
254
+ }
255
+ catch (e) {
256
+ console.warn(`sqlx-js.migrate: failed to release advisory lock: ${e.message}`);
257
+ }
258
+ }
259
+ await c.end();
260
+ }
261
+ if (exitCode !== 0)
262
+ process.exit(exitCode);
263
+ }
264
+ export function migrateAdd(opts) {
265
+ if (!existsSync(opts.migrationsDir))
266
+ mkdirSync(opts.migrationsDir, { recursive: true });
267
+ const existing = readMigrations(opts.migrationsDir);
268
+ const nextVersion = (existing[existing.length - 1]?.version ?? 0) + 1;
269
+ const safe = opts.name.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^[^a-zA-Z0-9]+/, "");
270
+ if (!safe)
271
+ throw new Error(`sqlx-js.migrate: invalid migration name "${opts.name}"`);
272
+ const padded = String(nextVersion).padStart(4, "0");
273
+ const upFname = `${padded}_${safe}.up.sql`;
274
+ const downFname = `${padded}_${safe}.down.sql`;
275
+ const upFull = join(opts.migrationsDir, upFname);
276
+ const downFull = join(opts.migrationsDir, downFname);
277
+ writeFileSync(upFull, `-- ${opts.name}\n-- write up DDL/DML here\n`);
278
+ if (!existsSync(downFull)) {
279
+ writeFileSync(downFull, `-- revert ${opts.name}\n-- write down DDL/DML here\n`);
280
+ }
281
+ console.log(`created ${upFull}`);
282
+ console.log(`created ${downFull}`);
283
+ }
@@ -0,0 +1,23 @@
1
+ import { PgClient } from "../pg/wire.js";
2
+ import { SchemaCache } from "../pg/schema.js";
3
+ import { type SqlxJsConfig } from "../config.js";
4
+ export type PrepareOptions = {
5
+ root: string;
6
+ databaseUrl: string;
7
+ cacheDir: string;
8
+ dtsPath: string;
9
+ check: boolean;
10
+ prune?: boolean;
11
+ };
12
+ export type PrepareSession = {
13
+ client: PgClient;
14
+ schema: SchemaCache;
15
+ userCfg: SqlxJsConfig;
16
+ };
17
+ export declare function openSession(opts: PrepareOptions): Promise<PrepareSession>;
18
+ export declare function prepareOnce(opts: PrepareOptions, session: PrepareSession, log?: (msg: string) => void, err?: (msg: string) => void): Promise<{
19
+ entries: number;
20
+ failures: number;
21
+ pruned: number;
22
+ }>;
23
+ export declare function runPrepare(opts: PrepareOptions): Promise<void>;
@@ -0,0 +1,314 @@
1
+ import { PgClient, parseDatabaseUrl, PgError } from "../pg/wire.js";
2
+ import { SchemaCache } from "../pg/schema.js";
3
+ import { analyzeQuery } from "../pg/analyze.js";
4
+ import { isBuiltinOid, oidToTs } from "../pg/oids.js";
5
+ import { scanProject } from "../scan/scanner.js";
6
+ import { Cache, fingerprint, effectiveNullable } from "../cache.js";
7
+ import { emitDts } from "../codegen.js";
8
+ import { loadConfig, lookupJsonbType } from "../config.js";
9
+ import { buildParamMap } from "../pg/param-map.js";
10
+ import { mergeExtensionTypes } from "../pg/extensions.js";
11
+ const JSON_OIDS = new Set([114, 3802]);
12
+ const JSON_ARRAY_OIDS = new Set([199, 3807]);
13
+ function enumUnion(values) {
14
+ if (values.length === 0)
15
+ return "never";
16
+ return values.map((v) => JSON.stringify(v)).join(" | ");
17
+ }
18
+ function resolveTs(oid, customLookup) {
19
+ const c = customLookup(oid);
20
+ if (c) {
21
+ if (c.kind === "enum")
22
+ return enumUnion(c.values);
23
+ if (c.kind === "enumArray")
24
+ return `(${enumUnion(c.element.values)})[]`;
25
+ if (c.kind === "scalar")
26
+ return c.tsType;
27
+ if (c.kind === "scalarArray")
28
+ return `(${c.element.tsType})[]`;
29
+ }
30
+ return oidToTs(oid).ts;
31
+ }
32
+ function resolveColumnTs(f, schema, cfg) {
33
+ if (f.tableOid !== 0 && f.columnAttr !== 0) {
34
+ const tbl = schema.tableNameByOid(f.tableOid);
35
+ const colName = schema.columnNameByAttno(f.tableOid, f.columnAttr);
36
+ if (tbl && colName) {
37
+ if (JSON_OIDS.has(f.typeOid)) {
38
+ const decl = lookupJsonbType(cfg, tbl.schema, tbl.name, colName);
39
+ if (decl)
40
+ return decl;
41
+ }
42
+ if (JSON_ARRAY_OIDS.has(f.typeOid)) {
43
+ const decl = lookupJsonbType(cfg, tbl.schema, tbl.name, colName);
44
+ if (decl)
45
+ return `(${decl})[]`;
46
+ }
47
+ }
48
+ }
49
+ return resolveTs(f.typeOid, (oid) => schema.customType(oid));
50
+ }
51
+ function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
52
+ if (JSON_OIDS.has(paramOid) || JSON_ARRAY_OIDS.has(paramOid)) {
53
+ const target = paramMap.get(paramIndex);
54
+ if (target) {
55
+ const decl = lookupJsonbType(cfg, target.schema ?? "public", target.table, target.column);
56
+ if (decl)
57
+ return JSON_ARRAY_OIDS.has(paramOid) ? `(${decl})[]` : decl;
58
+ }
59
+ }
60
+ return resolveTs(paramOid, (oid) => schema.customType(oid));
61
+ }
62
+ function resolveParamNullable(paramIndex, pm, schema) {
63
+ if (pm.forceNullable.has(paramIndex))
64
+ return true;
65
+ if (pm.dmlBound.has(paramIndex)) {
66
+ const t = pm.targets.get(paramIndex);
67
+ if (!t)
68
+ return false;
69
+ const oid = schema.resolveTable(t.schema, t.table);
70
+ if (oid === undefined)
71
+ return false;
72
+ const col = schema.columnsOf(oid)?.get(t.column);
73
+ if (!col)
74
+ return false;
75
+ return !col.notNull;
76
+ }
77
+ return false;
78
+ }
79
+ const ALIAS_OVERRIDE = /^(.+?)([!?])$/;
80
+ function parseColumnOverride(name) {
81
+ const m = ALIAS_OVERRIDE.exec(name);
82
+ if (!m)
83
+ return { name };
84
+ return { name: m[1], override: m[2] === "!" ? "non-null" : "nullable" };
85
+ }
86
+ function isAliasOrExpression(f, schema) {
87
+ if (f.tableOid === 0 || f.columnAttr === 0)
88
+ return true;
89
+ const real = schema.columnNameByAttno(f.tableOid, f.columnAttr);
90
+ return real !== undefined && real !== f.name;
91
+ }
92
+ function formatSite(s) {
93
+ return `${s.file}:${s.line}:${s.column}`;
94
+ }
95
+ function snippet(query, max = 80) {
96
+ const oneLine = query.replace(/\s+/g, " ").trim();
97
+ return oneLine.length > max ? oneLine.slice(0, max) + "…" : oneLine;
98
+ }
99
+ export async function openSession(opts) {
100
+ const userCfg = await loadConfig(opts.root);
101
+ const cfg = parseDatabaseUrl(opts.databaseUrl);
102
+ const client = new PgClient(cfg);
103
+ await client.connect();
104
+ const schema = new SchemaCache(client);
105
+ schema.setTypeRegistry(mergeExtensionTypes(userCfg.customTypes));
106
+ return { client, schema, userCfg };
107
+ }
108
+ export async function prepareOnce(opts, session, log = console.log, err = console.error) {
109
+ const sites = scanProject(opts.root);
110
+ log(`scanned: found ${sites.length} sql() call site(s)`);
111
+ const cache = new Cache(opts.cacheDir);
112
+ const unique = new Map();
113
+ for (const s of sites) {
114
+ const fp = fingerprint(s.query);
115
+ const existing = unique.get(fp);
116
+ if (existing)
117
+ existing.sites.push(s);
118
+ else
119
+ unique.set(fp, { fp, query: s.query, sites: [s] });
120
+ }
121
+ const raw = [];
122
+ let failures = 0;
123
+ const { client, schema, userCfg } = session;
124
+ for (const { fp, query, sites: ss } of unique.values()) {
125
+ const site = ss[0];
126
+ try {
127
+ const d = await client.describe(query);
128
+ raw.push({ fp, query, sites: ss, paramOids: d.paramOids, fields: d.fields });
129
+ }
130
+ catch (e) {
131
+ failures++;
132
+ if (e instanceof PgError) {
133
+ const extras = [];
134
+ if (e.position)
135
+ extras.push(`pos ${e.position}`);
136
+ if (e.code)
137
+ extras.push(`code ${e.code}`);
138
+ const tail = extras.length > 0 ? ` (${extras.join(", ")})` : "";
139
+ err(` ✗ ${formatSite(site)} — describe failed: ${e.message}${tail}`);
140
+ if (e.hint)
141
+ err(` hint: ${e.hint}`);
142
+ err(` query: ${snippet(query)}`);
143
+ }
144
+ else {
145
+ err(` ✗ ${formatSite(site)} — describe failed: ${e.message}`);
146
+ err(` query: ${snippet(query)}`);
147
+ }
148
+ }
149
+ }
150
+ const allAttrRefs = [];
151
+ const allTableOids = [];
152
+ for (const r of raw) {
153
+ for (const f of r.fields) {
154
+ if (f.tableOid !== 0 && f.columnAttr !== 0) {
155
+ allAttrRefs.push({ tableOid: f.tableOid, attno: f.columnAttr });
156
+ allTableOids.push(f.tableOid);
157
+ }
158
+ }
159
+ }
160
+ await schema.loadAttributes(allAttrRefs);
161
+ await schema.loadTableNamesByOid(allTableOids);
162
+ const analyses = new Map();
163
+ const paramMaps = new Map();
164
+ const failedFps = new Set();
165
+ for (const r of raw) {
166
+ const site = r.sites[0];
167
+ try {
168
+ analyses.set(r.fp, await analyzeQuery(r.query, r.fields, schema));
169
+ }
170
+ catch (e) {
171
+ failures++;
172
+ failedFps.add(r.fp);
173
+ err(` ✗ ${formatSite(site)} — analyze failed: ${e.message}`);
174
+ err(` query: ${snippet(r.query)}`);
175
+ continue;
176
+ }
177
+ try {
178
+ paramMaps.set(r.fp, await buildParamMap(r.query));
179
+ }
180
+ catch (e) {
181
+ failures++;
182
+ failedFps.add(r.fp);
183
+ err(` ✗ ${formatSite(site)} — paramMap failed: ${e.message}`);
184
+ err(` query: ${snippet(r.query)}`);
185
+ }
186
+ }
187
+ const dmlTablesToLoad = new Set();
188
+ for (const pm of paramMaps.values()) {
189
+ for (const idx of pm.dmlBound) {
190
+ const t = pm.targets.get(idx);
191
+ if (t)
192
+ dmlTablesToLoad.add(`${t.schema ?? "public"}.${t.table}`);
193
+ }
194
+ }
195
+ if (dmlTablesToLoad.size > 0) {
196
+ const names = [...dmlTablesToLoad].map((k) => {
197
+ const [schema, name] = k.split(".");
198
+ return { schema, name: name };
199
+ });
200
+ await schema.loadTableNames(names);
201
+ const oids = [];
202
+ for (const n of names) {
203
+ const oid = schema.resolveTable(n.schema, n.name);
204
+ if (oid !== undefined)
205
+ oids.push(oid);
206
+ }
207
+ await schema.loadColumnsForTables(oids);
208
+ }
209
+ const unknownOids = new Set();
210
+ for (const r of raw) {
211
+ for (const o of r.paramOids)
212
+ if (!isBuiltinOid(o))
213
+ unknownOids.add(o);
214
+ for (const f of r.fields)
215
+ if (!isBuiltinOid(f.typeOid))
216
+ unknownOids.add(f.typeOid);
217
+ }
218
+ await schema.loadCustomTypes([...unknownOids]);
219
+ const entries = [];
220
+ for (const r of raw) {
221
+ if (failedFps.has(r.fp))
222
+ continue;
223
+ const analysis = analyses.get(r.fp);
224
+ const pm = paramMaps.get(r.fp) ?? {
225
+ targets: new Map(),
226
+ forceNullable: new Set(),
227
+ dmlBound: new Set(),
228
+ };
229
+ const fileSites = r.sites.filter((s) => s.kind === "file");
230
+ const hasInline = r.sites.some((s) => s.kind !== "file");
231
+ const filePaths = fileSites.length > 0
232
+ ? Array.from(new Set(fileSites.map((s) => s.sqlFilePath))).sort()
233
+ : undefined;
234
+ const entry = {
235
+ query: r.query,
236
+ paramOids: r.paramOids,
237
+ paramTsTypes: r.paramOids.map((o, idx) => resolveParamTs(idx + 1, o, pm.targets, schema, userCfg)),
238
+ paramNullable: r.paramOids.map((_o, idx) => resolveParamNullable(idx + 1, pm, schema)),
239
+ columns: r.fields.map((f, i) => {
240
+ const parsed = parseColumnOverride(f.name);
241
+ const treatAsOverride = parsed.override !== undefined && isAliasOrExpression(f, schema);
242
+ return {
243
+ name: parsed.name,
244
+ typeOid: f.typeOid,
245
+ tsType: resolveColumnTs(f, schema, userCfg),
246
+ nullable: analysis.perColumnNullable[i] ?? true,
247
+ ...(treatAsOverride ? { override: parsed.override } : {}),
248
+ };
249
+ }),
250
+ hasResultSet: r.fields.length > 0,
251
+ hasInline,
252
+ ...(filePaths ? { filePaths } : {}),
253
+ ...(analysis.degraded ? { degraded: analysis.degraded } : {}),
254
+ };
255
+ cache.write(r.fp, entry);
256
+ entries.push(entry);
257
+ const nn = entry.columns.filter((c) => !effectiveNullable(c)).length;
258
+ const tag = entry.degraded ? ` [degraded: ${entry.degraded.reason}]` : "";
259
+ log(` ✓ ${formatSite(r.sites[0])} → ${r.paramOids.length} param(s), ${r.fields.length} col(s) [${nn} non-null]${tag}`);
260
+ }
261
+ let pruned = 0;
262
+ if (opts.prune !== false) {
263
+ pruned = cache.prune(unique.keys()).length;
264
+ if (pruned > 0)
265
+ log(`pruned ${pruned} orphaned cache entry/entries`);
266
+ }
267
+ emitDts(opts.dtsPath, entries);
268
+ return { entries: entries.length, failures, pruned };
269
+ }
270
+ export async function runPrepare(opts) {
271
+ if (opts.check) {
272
+ const sites = scanProject(opts.root);
273
+ console.log(`scanned: found ${sites.length} sql() call site(s)`);
274
+ const cache = new Cache(opts.cacheDir);
275
+ const unique = new Map();
276
+ for (const s of sites) {
277
+ const fp = fingerprint(s.query);
278
+ const existing = unique.get(fp);
279
+ if (existing)
280
+ existing.sites.push(s);
281
+ else
282
+ unique.set(fp, { fp, query: s.query, sites: [s] });
283
+ }
284
+ let stale = 0;
285
+ for (const { fp, query, sites: ss } of unique.values()) {
286
+ if (!cache.has(fp)) {
287
+ stale++;
288
+ console.error(`stale: ${formatSite(ss[0])} — query not in cache`);
289
+ console.error(` query: ${snippet(query)}`);
290
+ }
291
+ }
292
+ if (stale > 0) {
293
+ console.error(`\nsqlx-js prepare --check: ${stale} stale/missing entries. Run \`sqlx-js prepare\` against a live DB.`);
294
+ process.exit(1);
295
+ }
296
+ const entries = [...unique.values()].map((u) => cache.read(u.fp)).filter(Boolean);
297
+ emitDts(opts.dtsPath, entries);
298
+ console.log(`ok — ${entries.length} unique queries, types regenerated`);
299
+ return;
300
+ }
301
+ const session = await openSession(opts);
302
+ try {
303
+ const r = await prepareOnce(opts, session);
304
+ if (r.failures > 0) {
305
+ console.error(`\n${r.failures} query/queries failed to prepare`);
306
+ await session.client.end();
307
+ process.exit(1);
308
+ }
309
+ console.log(`\nprepared ${r.entries} unique query/queries → ${opts.dtsPath}`);
310
+ }
311
+ finally {
312
+ await session.client.end();
313
+ }
314
+ }
@@ -0,0 +1,14 @@
1
+ export type SchemaCommandOptions = {
2
+ databaseUrl: string;
3
+ snapshotPath: string;
4
+ manifestPath: string;
5
+ writeManifest: boolean;
6
+ shadowUrl?: string;
7
+ migrationsDir: string;
8
+ };
9
+ export type ShadowMigrationResult = {
10
+ applied: number;
11
+ };
12
+ export declare function applyShadowMigrations(databaseUrl: string, migrationsDir: string, log?: (msg: string) => void): Promise<ShadowMigrationResult>;
13
+ export declare function runSchemaDump(opts: SchemaCommandOptions): Promise<void>;
14
+ export declare function runSchemaCheck(opts: SchemaCommandOptions): Promise<void>;