@onreza/sqlx-js 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,7 @@ import { introspectFunctions } from "../pg/functions.js";
14
14
  import { buildParamMap } from "../pg/param-map.js";
15
15
  import { mergeExtensionTypes } from "../pg/extensions.js";
16
16
  import { compareArtifacts } from "../artifacts.js";
17
+ import { containsUnknownType } from "../type-inspection.js";
17
18
  const JSON_OIDS = new Set([114, 3802]);
18
19
  const JSON_ARRAY_OIDS = new Set([199, 3807]);
19
20
  const JSON_INPUT_VALUE = 'import("@onreza/sqlx-js").JsonInputValue';
@@ -148,6 +149,14 @@ function isAliasOrExpression(f, schema) {
148
149
  const real = schema.columnNameByAttno(f.tableOid, f.columnAttr);
149
150
  return real !== undefined && real !== f.name;
150
151
  }
152
+ function duplicateOutputColumns(fields) {
153
+ const counts = new Map();
154
+ for (const field of fields) {
155
+ const name = parseColumnOverride(field.name).name;
156
+ counts.set(name, (counts.get(name) ?? 0) + 1);
157
+ }
158
+ return [...counts].filter(([, count]) => count > 1).map(([name]) => name).sort();
159
+ }
151
160
  export class PrepareFatalError extends Error {
152
161
  phase;
153
162
  file;
@@ -187,6 +196,32 @@ function siteUsage(sites) {
187
196
  ...(filePaths.length > 0 ? { filePaths } : {}),
188
197
  };
189
198
  }
199
+ function inferenceIssues(entry) {
200
+ const issues = [];
201
+ if (entry.degraded)
202
+ issues.push(`nullability inference degraded: ${entry.degraded.reason}`);
203
+ entry.paramTsTypes.forEach((type, index) => {
204
+ if (containsUnknownType(type))
205
+ issues.push(`parameter $${index + 1} resolved to ${type}`);
206
+ });
207
+ for (const column of entry.columns) {
208
+ if (containsUnknownType(column.tsType)) {
209
+ issues.push(`result column ${JSON.stringify(column.name)} resolved to ${column.tsType}`);
210
+ }
211
+ }
212
+ return issues;
213
+ }
214
+ function inferenceDiagnostics(entry, site, strict) {
215
+ return inferenceIssues(entry).map((message) => ({
216
+ severity: strict ? "error" : "warning",
217
+ phase: "inference",
218
+ message,
219
+ file: site.file,
220
+ line: site.line,
221
+ column: site.column,
222
+ query: entry.query,
223
+ }));
224
+ }
190
225
  export async function openSession(opts) {
191
226
  let userCfg;
192
227
  try {
@@ -295,6 +330,23 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
295
330
  const site = ss[0];
296
331
  const outcome = describeResults.get(fp);
297
332
  if (outcome.ok) {
333
+ const duplicates = duplicateOutputColumns(outcome.fields);
334
+ if (duplicates.length > 0) {
335
+ failures++;
336
+ const message = `duplicate output column name(s): ${duplicates.join(", ")}. Alias each result column to a unique name`;
337
+ diagnostics.push({
338
+ severity: "error",
339
+ phase: "result-shape",
340
+ message,
341
+ file: site.file,
342
+ line: site.line,
343
+ column: site.column,
344
+ query,
345
+ });
346
+ err(` ✗ ${formatSite(site)} — ${message}`);
347
+ err(` query: ${snippet(query)}`);
348
+ continue;
349
+ }
298
350
  raw.push({ fp, query, sites: ss, paramOids: outcome.paramOids, fields: outcome.fields });
299
351
  continue;
300
352
  }
@@ -470,6 +522,18 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
470
522
  hasResultSet: r.fields.length > 0,
471
523
  ...(analysis.degraded ? { degraded: analysis.degraded } : {}),
472
524
  };
525
+ const entryDiagnostics = inferenceDiagnostics(entry, r.sites[0], opts.strictInference === true);
526
+ diagnostics.push(...entryDiagnostics);
527
+ if (entryDiagnostics.length > 0) {
528
+ for (const diagnostic of entryDiagnostics) {
529
+ const label = diagnostic.severity === "error" ? "inference failed" : "inference warning";
530
+ err(` ${label}: ${formatSite(r.sites[0])} — ${diagnostic.message}`);
531
+ }
532
+ if (opts.strictInference) {
533
+ failures++;
534
+ continue;
535
+ }
536
+ }
473
537
  entries.push(entry);
474
538
  generated.push({ fp: r.fp, entry });
475
539
  const nn = entry.columns.filter((c) => !effectiveNullable(c)).length;
@@ -588,14 +652,46 @@ export async function runPrepare(opts) {
588
652
  process.exitCode = 1;
589
653
  return;
590
654
  }
591
- let entries;
655
+ const entries = [];
656
+ let inferenceFailures = 0;
592
657
  let functions;
593
658
  try {
594
- entries = [...unique.values()].map((u) => {
659
+ for (const u of unique.values()) {
595
660
  const entry = cache.read(u.fp);
596
- return entry ? { ...entry, ...siteUsage(u.sites) } : null;
597
- }).filter((e) => e !== null);
661
+ if (!entry)
662
+ continue;
663
+ const current = { ...entry, ...siteUsage(u.sites) };
664
+ const entryDiagnostics = inferenceDiagnostics(current, u.sites[0], opts.strictInference === true);
665
+ diagnostics.push(...entryDiagnostics);
666
+ if (opts.strictInference && entryDiagnostics.length > 0) {
667
+ inferenceFailures++;
668
+ continue;
669
+ }
670
+ entries.push(current);
671
+ }
598
672
  functions = readFunctionCache(opts.cacheDir);
673
+ if (inferenceFailures > 0) {
674
+ if (opts.json) {
675
+ console.log(JSON.stringify({
676
+ formatVersion: 1,
677
+ ok: false,
678
+ mode: "check",
679
+ sites: sites.length,
680
+ entries: entries.length,
681
+ failures: inferenceFailures,
682
+ pruned: 0,
683
+ functions: functions.length,
684
+ diagnostics,
685
+ }, null, 2));
686
+ }
687
+ else {
688
+ for (const diagnostic of diagnostics) {
689
+ console.error(`inference failed: ${diagnostic.file}:${diagnostic.line}:${diagnostic.column} — ${diagnostic.message}`);
690
+ }
691
+ }
692
+ process.exitCode = 1;
693
+ return;
694
+ }
599
695
  emitDts(opts.dtsPath, entries, functions);
600
696
  }
601
697
  catch (error) {
@@ -611,10 +707,13 @@ export async function runPrepare(opts) {
611
707
  failures: 0,
612
708
  pruned: 0,
613
709
  functions: functions.length,
614
- diagnostics: [],
710
+ diagnostics,
615
711
  }, null, 2));
616
712
  }
617
713
  else {
714
+ for (const diagnostic of diagnostics) {
715
+ console.error(`inference warning: ${diagnostic.file}:${diagnostic.line}:${diagnostic.column} — ${diagnostic.message}`);
716
+ }
618
717
  console.log(`ok — ${entries.length} unique queries, ${functions.length} function(s), types regenerated`);
619
718
  }
620
719
  return;
@@ -1,6 +1,6 @@
1
1
  import { introspectDatabase, readSchemaSnapshot, schemaSnapshotEqual, schemaSnapshotExists, writeSchemaManifest, writeSchemaSnapshot } from "../schema-snapshot.js";
2
2
  import { PgClient, parseDatabaseUrl } from "../pg/wire.js";
3
- import { acquireMigrateLock, applyPending, DEFAULT_MIGRATE_LOCK_KEY, releaseMigrateLock } from "./migrate.js";
3
+ import { acquireMigrateLock, applyPending, DEFAULT_MIGRATE_LOCK_KEY, releaseMigrateLock } from "../migration-core.js";
4
4
  export async function applyShadowMigrations(databaseUrl, migrationsDir, log = console.log) {
5
5
  const client = new PgClient(parseDatabaseUrl(databaseUrl));
6
6
  await client.connect();
@@ -1,6 +1,7 @@
1
1
  export type ScanConfig = {
2
2
  include?: string[];
3
3
  exclude?: string[];
4
+ modules?: string[];
4
5
  };
5
6
  export type SqlxJsConfig = {
6
7
  jsonbTypes?: Record<string, string>;
@@ -52,6 +52,12 @@ function validateStringArray(value, name, path) {
52
52
  throw new Error(`sqlx-js: ${path} ${name} must be an array of strings`);
53
53
  }
54
54
  }
55
+ function validateModuleArray(value, path) {
56
+ validateStringArray(value, "scan.modules", path);
57
+ if (value.length === 0 || value.some((item) => item.trim() === "")) {
58
+ throw new Error(`sqlx-js: ${path} scan.modules must contain at least one non-empty module name`);
59
+ }
60
+ }
55
61
  function validateConfig(value, path) {
56
62
  if (!value || typeof value !== "object" || Array.isArray(value)) {
57
63
  throw new Error(`sqlx-js: ${path} must default-export a config object`);
@@ -70,6 +76,8 @@ function validateConfig(value, path) {
70
76
  validateStringArray(scan.include, "scan.include", path);
71
77
  if (scan.exclude !== undefined)
72
78
  validateStringArray(scan.exclude, "scan.exclude", path);
79
+ if (scan.modules !== undefined)
80
+ validateModuleArray(scan.modules, path);
73
81
  }
74
82
  if (config.schema !== undefined) {
75
83
  if (!config.schema || typeof config.schema !== "object" || Array.isArray(config.schema)) {
@@ -0,0 +1,157 @@
1
+ import { PgClient } from "./pg/wire.js";
2
+ export type MigrationFile = {
3
+ version: number;
4
+ name: string;
5
+ upPath: string;
6
+ downPath: string | null;
7
+ upSql: string;
8
+ upHash: string;
9
+ squash: SquashMetadata | null;
10
+ };
11
+ export type SquashReplacement = {
12
+ version: number;
13
+ name: string;
14
+ upHash: string;
15
+ };
16
+ export type SquashMetadata = {
17
+ format: 1;
18
+ replaces: SquashReplacement[];
19
+ };
20
+ export type MigrationStore = {
21
+ table: string;
22
+ };
23
+ export declare const SQUASH_PREFIX = "-- sqlx-js-squash:";
24
+ export declare const DEFAULT_MIGRATE_LOCK_KEY = 18750938867203960n;
25
+ export declare function readMigrations(dir: string): MigrationFile[];
26
+ export declare function parseSquashMetadata(sql: string): SquashMetadata | null;
27
+ export declare function ensureTable(client: PgClient): Promise<MigrationStore>;
28
+ export declare function listApplied(client: PgClient, store?: MigrationStore): Promise<Map<number, {
29
+ name: string;
30
+ hash: string;
31
+ }>>;
32
+ export type ApplyOutcome = {
33
+ kind: "applied";
34
+ version: number;
35
+ name: string;
36
+ } | {
37
+ kind: "adopted";
38
+ version: number;
39
+ name: string;
40
+ replaced: number;
41
+ } | {
42
+ kind: "tampered";
43
+ version: number;
44
+ name: string;
45
+ applied: string;
46
+ current: string;
47
+ } | {
48
+ kind: "failed";
49
+ version: number;
50
+ name: string;
51
+ error: string;
52
+ };
53
+ export type PlanOutcome = {
54
+ kind: "pending";
55
+ version: number;
56
+ name: string;
57
+ } | {
58
+ kind: "adoptable";
59
+ version: number;
60
+ name: string;
61
+ replaced: number;
62
+ } | {
63
+ kind: "tampered";
64
+ version: number;
65
+ name: string;
66
+ applied: string;
67
+ current: string;
68
+ } | {
69
+ kind: "failed";
70
+ version: number;
71
+ name: string;
72
+ error: string;
73
+ };
74
+ export type MigrationPlanItem = {
75
+ kind: "apply";
76
+ version: number;
77
+ name: string;
78
+ } | {
79
+ kind: "adopt";
80
+ version: number;
81
+ name: string;
82
+ replaced: number;
83
+ };
84
+ export type MigrationPlanDiagnostic = Extract<PlanOutcome, {
85
+ kind: "tampered" | "failed";
86
+ }>;
87
+ export type MigrationPlanSnapshot = {
88
+ ok: boolean;
89
+ pending: number;
90
+ adoptable: number;
91
+ tampered: number;
92
+ failed: number;
93
+ steps: MigrationPlanItem[];
94
+ diagnostics: MigrationPlanDiagnostic[];
95
+ };
96
+ export type MigrationInfoStatus = "applied" | "pending" | "adoptable" | "superseded" | "tampered" | "failed";
97
+ export type MigrationInfoItem = {
98
+ version: number;
99
+ name: string;
100
+ status: MigrationInfoStatus;
101
+ detail?: string;
102
+ };
103
+ export type MigrationInfoSnapshot = {
104
+ historyTable: string | null;
105
+ summary: Record<MigrationInfoStatus, number>;
106
+ items: MigrationInfoItem[];
107
+ };
108
+ type InternalMigrationPlanItem = {
109
+ kind: "apply";
110
+ migration: MigrationFile;
111
+ } | {
112
+ kind: "adopt";
113
+ migration: MigrationFile;
114
+ replaced: number;
115
+ };
116
+ export type MigrationValidationOutcome = {
117
+ kind: "tampered";
118
+ version: number;
119
+ name: string;
120
+ applied: string;
121
+ current: string;
122
+ } | {
123
+ kind: "failed";
124
+ version: number;
125
+ name: string;
126
+ error: string;
127
+ };
128
+ export declare function effectiveSquashReplacements(all: MigrationFile[]): SquashReplacement[];
129
+ export declare function buildMigrationPlan(all: MigrationFile[], applied: Map<number, {
130
+ name: string;
131
+ hash: string;
132
+ }>, onEvent?: (event: MigrationValidationOutcome) => void): {
133
+ kind: "ok";
134
+ steps: InternalMigrationPlanItem[];
135
+ } | {
136
+ kind: "failed";
137
+ } | {
138
+ kind: "tampered";
139
+ };
140
+ export declare function resetMigrationSession(client: PgClient): Promise<void>;
141
+ export declare function planPending(client: PgClient, migrationsDir: string, onEvent?: (event: PlanOutcome) => void): Promise<{
142
+ pending: number;
143
+ adoptable: number;
144
+ tampered: number;
145
+ failed: number;
146
+ steps: MigrationPlanItem[];
147
+ }>;
148
+ export declare function inspectMigrationPlan(client: PgClient, migrationsDir: string): Promise<MigrationPlanSnapshot>;
149
+ export declare function inspectMigrations(client: PgClient, migrationsDir: string): Promise<MigrationInfoSnapshot>;
150
+ export declare function applyPending(client: PgClient, migrationsDir: string, onEvent?: (event: ApplyOutcome) => void): Promise<{
151
+ applied: number;
152
+ tampered: number;
153
+ failed: number;
154
+ }>;
155
+ export declare function acquireMigrateLock(client: PgClient, lockKey?: number | bigint, timeoutMs?: number): Promise<void>;
156
+ export declare function releaseMigrateLock(client: PgClient, lockKey?: number | bigint): Promise<void>;
157
+ export {};