@happyvertical/smrt-core 0.43.10 → 0.44.1
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/AGENTS.md +8 -8
- package/agents/generators.md +104 -0
- package/agents/revision-guard.md +66 -0
- package/agents/schema-paths.md +99 -0
- package/dist/cascade.d.ts +5 -9
- package/dist/cascade.d.ts.map +1 -1
- package/dist/cascade.js +65 -30
- package/dist/cascade.js.map +1 -1
- package/dist/embedded-write-queue.d.ts +8 -0
- package/dist/embedded-write-queue.d.ts.map +1 -1
- package/dist/embedded-write-queue.js +12 -2
- package/dist/embedded-write-queue.js.map +1 -1
- package/dist/generators/custom-action.d.ts.map +1 -1
- package/dist/generators/custom-action.js +1 -1
- package/dist/generators/custom-action.js.map +1 -1
- package/dist/generators/index.d.ts +1 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +2 -1
- package/dist/generators/preflight-route.d.ts +151 -0
- package/dist/generators/preflight-route.d.ts.map +1 -0
- package/dist/generators/preflight-route.js +194 -0
- package/dist/generators/preflight-route.js.map +1 -0
- package/dist/generators/rest.d.ts +12 -0
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +12 -15
- package/dist/generators/rest.js.map +1 -1
- package/dist/generators.js +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/knowledge.d.ts +17 -1
- package/dist/knowledge.d.ts.map +1 -1
- package/dist/knowledge.js +33 -3
- package/dist/knowledge.js.map +1 -1
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +6 -2
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest.json +8 -2
- package/dist/migrations/differ.d.ts +49 -0
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +220 -4
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/orchestrate.d.ts +23 -0
- package/dist/migrations/orchestrate.d.ts.map +1 -1
- package/dist/migrations/orchestrate.js +12 -3
- package/dist/migrations/orchestrate.js.map +1 -1
- package/dist/object.d.ts +31 -1
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +57 -9
- package/dist/object.js.map +1 -1
- package/dist/registry/types.d.ts +8 -3
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/revision-guard.d.ts +84 -0
- package/dist/revision-guard.d.ts.map +1 -0
- package/dist/revision-guard.js +120 -0
- package/dist/revision-guard.js.map +1 -0
- package/dist/schema/foreign-key-ddl.d.ts +7 -0
- package/dist/schema/foreign-key-ddl.d.ts.map +1 -1
- package/dist/schema/foreign-key-ddl.js +7 -1
- package/dist/schema/foreign-key-ddl.js.map +1 -1
- package/dist/schema/schema-manager.d.ts +15 -0
- package/dist/schema/schema-manager.d.ts.map +1 -1
- package/dist/schema/schema-manager.js +52 -9
- package/dist/schema/schema-manager.js.map +1 -1
- package/dist/schema/types.d.ts +10 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/uuid-convergence.d.ts +142 -0
- package/dist/schema/uuid-convergence.d.ts.map +1 -0
- package/dist/schema/uuid-convergence.js +0 -0
- package/dist/schema/uuid-convergence.js.map +1 -0
- package/dist/smrt-knowledge.json +18 -9
- package/dist/utils/scanner-module.d.ts +44 -0
- package/dist/utils/scanner-module.d.ts.map +1 -1
- package/dist/vite-plugin/index.d.ts +14 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +54 -2
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin.js +3 -2
- package/package.json +4 -4
package/dist/registry/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DomainKnowledgeConfig } from '@happyvertical/smrt-types';
|
|
1
|
+
import { CapabilityEffect, DomainKnowledgeConfig } from '@happyvertical/smrt-types';
|
|
2
2
|
import { SmrtCollection } from '../collection';
|
|
3
3
|
import { CollectionCacheConfig } from '../collection-cache';
|
|
4
4
|
import { FieldOptions } from '../decorators/index.js';
|
|
@@ -11,8 +11,13 @@ import { DeclaredIndexDefinition, SchemaDefinition } from '../schema/types.js';
|
|
|
11
11
|
*/
|
|
12
12
|
export type SmrtObjectConstructor = new (...args: any[]) => SmrtObject;
|
|
13
13
|
export type ApiHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Browser/agent-visible effect classification for a generated API action.
|
|
16
|
+
* Aliases the shared `smrt-types` contract (#2587) — see
|
|
17
|
+
* `CapabilityEffect` there for the fail-closed defaulting rule this
|
|
18
|
+
* classification is bound by.
|
|
19
|
+
*/
|
|
20
|
+
export type ToolEffect = CapabilityEffect;
|
|
16
21
|
export interface ApiSerializerReference {
|
|
17
22
|
/**
|
|
18
23
|
* Module specifier to import the serializer from.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/registry/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/registry/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revision compare-and-swap predicate construction (#2620).
|
|
3
|
+
*
|
|
4
|
+
* `SmrtObject.save()` and {@link SmrtObject.claimRevision} guard every write to
|
|
5
|
+
* a persisted row with a predicate on the revision the caller loaded. On
|
|
6
|
+
* PostgreSQL that predicate cannot be a plain equality against
|
|
7
|
+
* `Date.prototype.toISOString()`, because the JavaScript `Date` that carries the
|
|
8
|
+
* revision is two lossy conversions away from the stored value:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Precision.** `updated_at` is a microsecond column — `timestamptz(6)` on
|
|
11
|
+
* schemas this version materializes, `timestamp(6)` on older ones — so any
|
|
12
|
+
* row last written by raw SQL (`updated_at = CURRENT_TIMESTAMP` / `now()`)
|
|
13
|
+
* stores microseconds, for example `2026-09-02 08:11:28.939980`. A `Date`
|
|
14
|
+
* holds milliseconds, so the exact-equality predicate matched no row and
|
|
15
|
+
* every later `save()` on that row raised `RUNTIME_REVISION_CONFLICT`
|
|
16
|
+
* forever — a permanent failure, not a lost race.
|
|
17
|
+
* 2. **Process timezone.** Schemas created before the `TIMESTAMPTZ` mapping
|
|
18
|
+
* still hold `updated_at` as `timestamp WITHOUT time zone`, and `pg`
|
|
19
|
+
* hydrates that type by reading the stored wall clock in the *process* zone.
|
|
20
|
+
* On a non-UTC host the resulting `Date` is offset from the instant the
|
|
21
|
+
* writer meant, so `toISOString()` produced a wall clock the row never held
|
|
22
|
+
* and every guarded save conflicted. The same columns are also written under
|
|
23
|
+
* three different conventions — `pg` serializes a `Date` in the process
|
|
24
|
+
* zone, `claimRevision()` writes a UTC ISO string, and raw
|
|
25
|
+
* `CURRENT_TIMESTAMP` writes in the *server* zone — so no single rendering
|
|
26
|
+
* can match every row.
|
|
27
|
+
*
|
|
28
|
+
* Both are fixed here without asking callers to compensate:
|
|
29
|
+
*
|
|
30
|
+
* - the column is truncated to milliseconds in SQL, the finest precision a
|
|
31
|
+
* `Date` can represent, so a microsecond tail can no longer hide the row; and
|
|
32
|
+
* - the revision is offered in **both** wall-clock renderings — the process-zone
|
|
33
|
+
* one (the inverse of `pg`'s local hydration of `timestamp`) and the UTC one
|
|
34
|
+
* (what `timestamptz` hydration and SMRT's own ISO writes produce). Each is
|
|
35
|
+
* tagged `+00`, which a `timestamptz` comparison honours and a `timestamp`
|
|
36
|
+
* comparison discards, so the predicate never depends on the *session*
|
|
37
|
+
* TimeZone either. Accepting either candidate keeps the guard correct
|
|
38
|
+
* whatever convention the column and driver use, so a future UTC-hydration
|
|
39
|
+
* fix in `@happyvertical/sql` cannot silently break it. That driver-layer
|
|
40
|
+
* half is tracked as happyvertical/sdk#1223; this guard deliberately does not
|
|
41
|
+
* wait for it.
|
|
42
|
+
*
|
|
43
|
+
* Lost-race semantics are preserved. A concurrent writer advances `updated_at`
|
|
44
|
+
* to roughly "now" (see `nextRevisionTimestamp`), which would have to land on
|
|
45
|
+
* the loaded revision — or, on a non-UTC process only, on exactly that revision
|
|
46
|
+
* shifted by the process's whole-hour-scale UTC offset — to the millisecond
|
|
47
|
+
* before it could slip past. Any ordinary concurrent write differs by at least
|
|
48
|
+
* one millisecond and still conflicts. On a UTC process the two renderings
|
|
49
|
+
* coincide and the predicate is single-valued, so it is strictly no weaker than
|
|
50
|
+
* the exact equality it replaces. Making it single-valued on a non-UTC process
|
|
51
|
+
* too — by resolving the column's actual type, or by deleting the process-zone
|
|
52
|
+
* rendering once happyvertical/sdk#1223 hydrates `timestamp` as UTC — is
|
|
53
|
+
* tracked as #2623.
|
|
54
|
+
*
|
|
55
|
+
* The predicate is PostgreSQL-only. Embedded engines take the compare/upsert
|
|
56
|
+
* fallback in `usesEmbeddedRevisionFallback`, and remote LibSQL stores ISO text
|
|
57
|
+
* whose exact equality already round-trips losslessly.
|
|
58
|
+
*/
|
|
59
|
+
/** The SQL expression the PostgreSQL revision predicate compares against. */
|
|
60
|
+
export declare const POSTGRES_REVISION_GUARD_EXPRESSION = "date_trunc('milliseconds', updated_at)";
|
|
61
|
+
/**
|
|
62
|
+
* Render a revision as every `timestamp without time zone` wall clock it could
|
|
63
|
+
* legitimately correspond to, at millisecond precision.
|
|
64
|
+
*
|
|
65
|
+
* The process-zone rendering comes first because it is the inverse of `pg`'s
|
|
66
|
+
* current hydration; the UTC rendering is what SMRT's own writes persist. On a
|
|
67
|
+
* UTC process the two coincide and a single candidate is returned.
|
|
68
|
+
*
|
|
69
|
+
* @param revision - Revision loaded from the row, or supplied by the caller as
|
|
70
|
+
* `save({ expectedUpdatedAt })` / `claimRevision()`. Strings are parsed with
|
|
71
|
+
* `Date` semantics, so an ISO instant and a bare SQL wall clock both work.
|
|
72
|
+
* @returns One or two `YYYY-MM-DD HH:MM:SS.mmm+00` strings
|
|
73
|
+
* @throws {RangeError} If `revision` does not parse to a valid date
|
|
74
|
+
*/
|
|
75
|
+
export declare function postgresRevisionCandidates(revision: Date | string): string[];
|
|
76
|
+
/**
|
|
77
|
+
* Build the PostgreSQL revision condition for a generic `db.update()` WHERE
|
|
78
|
+
* clause.
|
|
79
|
+
*
|
|
80
|
+
* @param revision - The revision the writer loaded
|
|
81
|
+
* @returns A single-entry condition object to spread into the update predicate
|
|
82
|
+
*/
|
|
83
|
+
export declare function postgresRevisionCondition(revision: Date | string): Record<string, string[]>;
|
|
84
|
+
//# sourceMappingURL=revision-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revision-guard.d.ts","sourceRoot":"","sources":["../src/revision-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AAIH,6EAA6E;AAC7E,eAAO,MAAM,kCAAkC,2CACL,CAAC;AAgB3C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,EAAE,CA0B5E;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,IAAI,GAAG,MAAM,GACtB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAK1B"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { raw } from "@happyvertical/sql";
|
|
2
|
+
//#region src/revision-guard.ts
|
|
3
|
+
/**
|
|
4
|
+
* Revision compare-and-swap predicate construction (#2620).
|
|
5
|
+
*
|
|
6
|
+
* `SmrtObject.save()` and {@link SmrtObject.claimRevision} guard every write to
|
|
7
|
+
* a persisted row with a predicate on the revision the caller loaded. On
|
|
8
|
+
* PostgreSQL that predicate cannot be a plain equality against
|
|
9
|
+
* `Date.prototype.toISOString()`, because the JavaScript `Date` that carries the
|
|
10
|
+
* revision is two lossy conversions away from the stored value:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Precision.** `updated_at` is a microsecond column — `timestamptz(6)` on
|
|
13
|
+
* schemas this version materializes, `timestamp(6)` on older ones — so any
|
|
14
|
+
* row last written by raw SQL (`updated_at = CURRENT_TIMESTAMP` / `now()`)
|
|
15
|
+
* stores microseconds, for example `2026-09-02 08:11:28.939980`. A `Date`
|
|
16
|
+
* holds milliseconds, so the exact-equality predicate matched no row and
|
|
17
|
+
* every later `save()` on that row raised `RUNTIME_REVISION_CONFLICT`
|
|
18
|
+
* forever — a permanent failure, not a lost race.
|
|
19
|
+
* 2. **Process timezone.** Schemas created before the `TIMESTAMPTZ` mapping
|
|
20
|
+
* still hold `updated_at` as `timestamp WITHOUT time zone`, and `pg`
|
|
21
|
+
* hydrates that type by reading the stored wall clock in the *process* zone.
|
|
22
|
+
* On a non-UTC host the resulting `Date` is offset from the instant the
|
|
23
|
+
* writer meant, so `toISOString()` produced a wall clock the row never held
|
|
24
|
+
* and every guarded save conflicted. The same columns are also written under
|
|
25
|
+
* three different conventions — `pg` serializes a `Date` in the process
|
|
26
|
+
* zone, `claimRevision()` writes a UTC ISO string, and raw
|
|
27
|
+
* `CURRENT_TIMESTAMP` writes in the *server* zone — so no single rendering
|
|
28
|
+
* can match every row.
|
|
29
|
+
*
|
|
30
|
+
* Both are fixed here without asking callers to compensate:
|
|
31
|
+
*
|
|
32
|
+
* - the column is truncated to milliseconds in SQL, the finest precision a
|
|
33
|
+
* `Date` can represent, so a microsecond tail can no longer hide the row; and
|
|
34
|
+
* - the revision is offered in **both** wall-clock renderings — the process-zone
|
|
35
|
+
* one (the inverse of `pg`'s local hydration of `timestamp`) and the UTC one
|
|
36
|
+
* (what `timestamptz` hydration and SMRT's own ISO writes produce). Each is
|
|
37
|
+
* tagged `+00`, which a `timestamptz` comparison honours and a `timestamp`
|
|
38
|
+
* comparison discards, so the predicate never depends on the *session*
|
|
39
|
+
* TimeZone either. Accepting either candidate keeps the guard correct
|
|
40
|
+
* whatever convention the column and driver use, so a future UTC-hydration
|
|
41
|
+
* fix in `@happyvertical/sql` cannot silently break it. That driver-layer
|
|
42
|
+
* half is tracked as happyvertical/sdk#1223; this guard deliberately does not
|
|
43
|
+
* wait for it.
|
|
44
|
+
*
|
|
45
|
+
* Lost-race semantics are preserved. A concurrent writer advances `updated_at`
|
|
46
|
+
* to roughly "now" (see `nextRevisionTimestamp`), which would have to land on
|
|
47
|
+
* the loaded revision — or, on a non-UTC process only, on exactly that revision
|
|
48
|
+
* shifted by the process's whole-hour-scale UTC offset — to the millisecond
|
|
49
|
+
* before it could slip past. Any ordinary concurrent write differs by at least
|
|
50
|
+
* one millisecond and still conflicts. On a UTC process the two renderings
|
|
51
|
+
* coincide and the predicate is single-valued, so it is strictly no weaker than
|
|
52
|
+
* the exact equality it replaces. Making it single-valued on a non-UTC process
|
|
53
|
+
* too — by resolving the column's actual type, or by deleting the process-zone
|
|
54
|
+
* rendering once happyvertical/sdk#1223 hydrates `timestamp` as UTC — is
|
|
55
|
+
* tracked as #2623.
|
|
56
|
+
*
|
|
57
|
+
* The predicate is PostgreSQL-only. Embedded engines take the compare/upsert
|
|
58
|
+
* fallback in `usesEmbeddedRevisionFallback`, and remote LibSQL stores ISO text
|
|
59
|
+
* whose exact equality already round-trips losslessly.
|
|
60
|
+
*/
|
|
61
|
+
/** The SQL expression the PostgreSQL revision predicate compares against. */
|
|
62
|
+
var POSTGRES_REVISION_GUARD_EXPRESSION = "date_trunc('milliseconds', updated_at)";
|
|
63
|
+
function pad(value, width = 2) {
|
|
64
|
+
return String(value).padStart(width, "0");
|
|
65
|
+
}
|
|
66
|
+
function formatWallClock(parts) {
|
|
67
|
+
const [year, month, day, hours, minutes, seconds, milliseconds] = parts;
|
|
68
|
+
return `${pad(year, 4)}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}:${pad(seconds)}.${pad(milliseconds, 3)}+00`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Render a revision as every `timestamp without time zone` wall clock it could
|
|
72
|
+
* legitimately correspond to, at millisecond precision.
|
|
73
|
+
*
|
|
74
|
+
* The process-zone rendering comes first because it is the inverse of `pg`'s
|
|
75
|
+
* current hydration; the UTC rendering is what SMRT's own writes persist. On a
|
|
76
|
+
* UTC process the two coincide and a single candidate is returned.
|
|
77
|
+
*
|
|
78
|
+
* @param revision - Revision loaded from the row, or supplied by the caller as
|
|
79
|
+
* `save({ expectedUpdatedAt })` / `claimRevision()`. Strings are parsed with
|
|
80
|
+
* `Date` semantics, so an ISO instant and a bare SQL wall clock both work.
|
|
81
|
+
* @returns One or two `YYYY-MM-DD HH:MM:SS.mmm+00` strings
|
|
82
|
+
* @throws {RangeError} If `revision` does not parse to a valid date
|
|
83
|
+
*/
|
|
84
|
+
function postgresRevisionCandidates(revision) {
|
|
85
|
+
const date = revision instanceof Date ? revision : new Date(revision);
|
|
86
|
+
if (Number.isNaN(date.getTime())) throw new RangeError(`Revision guard requires a valid timestamp, received: ${String(revision)}`);
|
|
87
|
+
const local = formatWallClock([
|
|
88
|
+
date.getFullYear(),
|
|
89
|
+
date.getMonth() + 1,
|
|
90
|
+
date.getDate(),
|
|
91
|
+
date.getHours(),
|
|
92
|
+
date.getMinutes(),
|
|
93
|
+
date.getSeconds(),
|
|
94
|
+
date.getMilliseconds()
|
|
95
|
+
]);
|
|
96
|
+
const utc = formatWallClock([
|
|
97
|
+
date.getUTCFullYear(),
|
|
98
|
+
date.getUTCMonth() + 1,
|
|
99
|
+
date.getUTCDate(),
|
|
100
|
+
date.getUTCHours(),
|
|
101
|
+
date.getUTCMinutes(),
|
|
102
|
+
date.getUTCSeconds(),
|
|
103
|
+
date.getUTCMilliseconds()
|
|
104
|
+
]);
|
|
105
|
+
return local === utc ? [local] : [local, utc];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Build the PostgreSQL revision condition for a generic `db.update()` WHERE
|
|
109
|
+
* clause.
|
|
110
|
+
*
|
|
111
|
+
* @param revision - The revision the writer loaded
|
|
112
|
+
* @returns A single-entry condition object to spread into the update predicate
|
|
113
|
+
*/
|
|
114
|
+
function postgresRevisionCondition(revision) {
|
|
115
|
+
return { [raw(`${POSTGRES_REVISION_GUARD_EXPRESSION} in`)]: postgresRevisionCandidates(revision) };
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
export { POSTGRES_REVISION_GUARD_EXPRESSION, postgresRevisionCandidates, postgresRevisionCondition };
|
|
119
|
+
|
|
120
|
+
//# sourceMappingURL=revision-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revision-guard.js","names":[],"sources":["../src/revision-guard.ts"],"sourcesContent":["/**\n * Revision compare-and-swap predicate construction (#2620).\n *\n * `SmrtObject.save()` and {@link SmrtObject.claimRevision} guard every write to\n * a persisted row with a predicate on the revision the caller loaded. On\n * PostgreSQL that predicate cannot be a plain equality against\n * `Date.prototype.toISOString()`, because the JavaScript `Date` that carries the\n * revision is two lossy conversions away from the stored value:\n *\n * 1. **Precision.** `updated_at` is a microsecond column — `timestamptz(6)` on\n * schemas this version materializes, `timestamp(6)` on older ones — so any\n * row last written by raw SQL (`updated_at = CURRENT_TIMESTAMP` / `now()`)\n * stores microseconds, for example `2026-09-02 08:11:28.939980`. A `Date`\n * holds milliseconds, so the exact-equality predicate matched no row and\n * every later `save()` on that row raised `RUNTIME_REVISION_CONFLICT`\n * forever — a permanent failure, not a lost race.\n * 2. **Process timezone.** Schemas created before the `TIMESTAMPTZ` mapping\n * still hold `updated_at` as `timestamp WITHOUT time zone`, and `pg`\n * hydrates that type by reading the stored wall clock in the *process* zone.\n * On a non-UTC host the resulting `Date` is offset from the instant the\n * writer meant, so `toISOString()` produced a wall clock the row never held\n * and every guarded save conflicted. The same columns are also written under\n * three different conventions — `pg` serializes a `Date` in the process\n * zone, `claimRevision()` writes a UTC ISO string, and raw\n * `CURRENT_TIMESTAMP` writes in the *server* zone — so no single rendering\n * can match every row.\n *\n * Both are fixed here without asking callers to compensate:\n *\n * - the column is truncated to milliseconds in SQL, the finest precision a\n * `Date` can represent, so a microsecond tail can no longer hide the row; and\n * - the revision is offered in **both** wall-clock renderings — the process-zone\n * one (the inverse of `pg`'s local hydration of `timestamp`) and the UTC one\n * (what `timestamptz` hydration and SMRT's own ISO writes produce). Each is\n * tagged `+00`, which a `timestamptz` comparison honours and a `timestamp`\n * comparison discards, so the predicate never depends on the *session*\n * TimeZone either. Accepting either candidate keeps the guard correct\n * whatever convention the column and driver use, so a future UTC-hydration\n * fix in `@happyvertical/sql` cannot silently break it. That driver-layer\n * half is tracked as happyvertical/sdk#1223; this guard deliberately does not\n * wait for it.\n *\n * Lost-race semantics are preserved. A concurrent writer advances `updated_at`\n * to roughly \"now\" (see `nextRevisionTimestamp`), which would have to land on\n * the loaded revision — or, on a non-UTC process only, on exactly that revision\n * shifted by the process's whole-hour-scale UTC offset — to the millisecond\n * before it could slip past. Any ordinary concurrent write differs by at least\n * one millisecond and still conflicts. On a UTC process the two renderings\n * coincide and the predicate is single-valued, so it is strictly no weaker than\n * the exact equality it replaces. Making it single-valued on a non-UTC process\n * too — by resolving the column's actual type, or by deleting the process-zone\n * rendering once happyvertical/sdk#1223 hydrates `timestamp` as UTC — is\n * tracked as #2623.\n *\n * The predicate is PostgreSQL-only. Embedded engines take the compare/upsert\n * fallback in `usesEmbeddedRevisionFallback`, and remote LibSQL stores ISO text\n * whose exact equality already round-trips losslessly.\n */\n\nimport { raw } from '@happyvertical/sql';\n\n/** The SQL expression the PostgreSQL revision predicate compares against. */\nexport const POSTGRES_REVISION_GUARD_EXPRESSION =\n \"date_trunc('milliseconds', updated_at)\";\n\nfunction pad(value: number, width = 2): string {\n return String(value).padStart(width, '0');\n}\n\nfunction formatWallClock(\n parts: [number, number, number, number, number, number, number],\n): string {\n const [year, month, day, hours, minutes, seconds, milliseconds] = parts;\n return (\n `${pad(year, 4)}-${pad(month)}-${pad(day)} ` +\n `${pad(hours)}:${pad(minutes)}:${pad(seconds)}.${pad(milliseconds, 3)}+00`\n );\n}\n\n/**\n * Render a revision as every `timestamp without time zone` wall clock it could\n * legitimately correspond to, at millisecond precision.\n *\n * The process-zone rendering comes first because it is the inverse of `pg`'s\n * current hydration; the UTC rendering is what SMRT's own writes persist. On a\n * UTC process the two coincide and a single candidate is returned.\n *\n * @param revision - Revision loaded from the row, or supplied by the caller as\n * `save({ expectedUpdatedAt })` / `claimRevision()`. Strings are parsed with\n * `Date` semantics, so an ISO instant and a bare SQL wall clock both work.\n * @returns One or two `YYYY-MM-DD HH:MM:SS.mmm+00` strings\n * @throws {RangeError} If `revision` does not parse to a valid date\n */\nexport function postgresRevisionCandidates(revision: Date | string): string[] {\n const date = revision instanceof Date ? revision : new Date(revision);\n if (Number.isNaN(date.getTime())) {\n throw new RangeError(\n `Revision guard requires a valid timestamp, received: ${String(revision)}`,\n );\n }\n const local = formatWallClock([\n date.getFullYear(),\n date.getMonth() + 1,\n date.getDate(),\n date.getHours(),\n date.getMinutes(),\n date.getSeconds(),\n date.getMilliseconds(),\n ]);\n const utc = formatWallClock([\n date.getUTCFullYear(),\n date.getUTCMonth() + 1,\n date.getUTCDate(),\n date.getUTCHours(),\n date.getUTCMinutes(),\n date.getUTCSeconds(),\n date.getUTCMilliseconds(),\n ]);\n return local === utc ? [local] : [local, utc];\n}\n\n/**\n * Build the PostgreSQL revision condition for a generic `db.update()` WHERE\n * clause.\n *\n * @param revision - The revision the writer loaded\n * @returns A single-entry condition object to spread into the update predicate\n */\nexport function postgresRevisionCondition(\n revision: Date | string,\n): Record<string, string[]> {\n return {\n [raw(`${POSTGRES_REVISION_GUARD_EXPRESSION} in`)]:\n postgresRevisionCandidates(revision),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8DA,IAAa,qCACX;AAEF,SAAS,IAAI,OAAe,QAAQ,GAAW;CAC7C,OAAO,OAAO,KAAK,CAAC,CAAC,SAAS,OAAO,GAAG;AAC1C;AAEA,SAAS,gBACP,OACQ;CACR,MAAM,CAAC,MAAM,OAAO,KAAK,OAAO,SAAS,SAAS,gBAAgB;CAClE,OACE,GAAG,IAAI,MAAM,CAAC,EAAE,GAAG,IAAI,KAAK,EAAE,GAAG,IAAI,GAAG,EAAE,GACvC,IAAI,KAAK,EAAE,GAAG,IAAI,OAAO,EAAE,GAAG,IAAI,OAAO,EAAE,GAAG,IAAI,cAAc,CAAC,EAAE;AAE1E;;;;;;;;;;;;;;;AAgBA,SAAgB,2BAA2B,UAAmC;CAC5E,MAAM,OAAO,oBAAoB,OAAO,WAAW,IAAI,KAAK,QAAQ;CACpE,IAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,GAC7B,MAAM,IAAI,WACR,wDAAwD,OAAO,QAAQ,GACzE;CAEF,MAAM,QAAQ,gBAAgB;EAC5B,KAAK,YAAY;EACjB,KAAK,SAAS,IAAI;EAClB,KAAK,QAAQ;EACb,KAAK,SAAS;EACd,KAAK,WAAW;EAChB,KAAK,WAAW;EAChB,KAAK,gBAAgB;CACvB,CAAC;CACD,MAAM,MAAM,gBAAgB;EAC1B,KAAK,eAAe;EACpB,KAAK,YAAY,IAAI;EACrB,KAAK,WAAW;EAChB,KAAK,YAAY;EACjB,KAAK,cAAc;EACnB,KAAK,cAAc;EACnB,KAAK,mBAAmB;CAC1B,CAAC;CACD,OAAO,UAAU,MAAM,CAAC,KAAK,IAAI,CAAC,OAAO,GAAG;AAC9C;;;;;;;;AASA,SAAgB,0BACd,UAC0B;CAC1B,OAAO,GACJ,IAAI,GAAG,mCAAmC,IAAI,IAC7C,2BAA2B,QAAQ,EACvC;AACF"}
|
|
@@ -21,6 +21,13 @@ export declare function renderForeignKeyOrphanRepair(tableName: string, foreignK
|
|
|
21
21
|
uuidCastSide?: ForeignKeyUuidCastSide;
|
|
22
22
|
nullable?: boolean;
|
|
23
23
|
}): string;
|
|
24
|
+
/**
|
|
25
|
+
* Canonical UUID text shape used by every PostgreSQL guard that has to decide
|
|
26
|
+
* whether a `text` column can be reinterpreted as `uuid`. Exported so the
|
|
27
|
+
* orphan probe, the FK provisioning guard, and the uuid convergence planner
|
|
28
|
+
* all test the same shape (#2608).
|
|
29
|
+
*/
|
|
30
|
+
export declare const CANONICAL_UUID_PATTERN = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$";
|
|
24
31
|
export declare function schemaForeignKeys(schema: Pick<SchemaDefinition, 'columns'> & Partial<Pick<SchemaDefinition, 'foreignKeys'>>): ForeignKeyDefinition[];
|
|
25
32
|
/** Stable identity for one physical relationship, independent of allocation. */
|
|
26
33
|
export declare function foreignKeyRelationshipKey(foreignKey: Pick<ForeignKeyDefinition, 'column' | 'referencesColumn' | 'referencesTable'>): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"foreign-key-ddl.d.ts","sourceRoot":"","sources":["../../src/schema/foreign-key-ddl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAIrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEzE,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1E,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,oBAAoB,GAC/B,MAAM,CAIR;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,oBAAoB,GAC/B,MAAM,CAmBR;AAED,4EAA4E;AAC5E,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GACrB,MAAM,CAER;AAED,yDAAyD;AACzD,wBAAgB,kCAAkC,CAChD,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GACrB,MAAM,CAER;AAED,mFAAmF;AACnF,wBAAgB,iCAAiC,CAC/C,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,GACd,MAAM,CAER;AAED,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,oBAAoB,EAChC,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,sBAAsB,CAAC;CAClC,GACL,MAAM,CAcR;AAED,wBAAgB,4BAA4B,CAC1C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,oBAAoB,EAChC,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACf,GACL,MAAM,CAuBR;
|
|
1
|
+
{"version":3,"file":"foreign-key-ddl.d.ts","sourceRoot":"","sources":["../../src/schema/foreign-key-ddl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAIrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEzE,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1E,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,oBAAoB,GAC/B,MAAM,CAIR;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,oBAAoB,GAC/B,MAAM,CAmBR;AAED,4EAA4E;AAC5E,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GACrB,MAAM,CAER;AAED,yDAAyD;AACzD,wBAAgB,kCAAkC,CAChD,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GACrB,MAAM,CAER;AAED,mFAAmF;AACnF,wBAAgB,iCAAiC,CAC/C,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,GACd,MAAM,CAER;AAED,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,oBAAoB,EAChC,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,sBAAsB,CAAC;CAClC,GACL,MAAM,CAcR;AAED,wBAAgB,4BAA4B,CAC1C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,oBAAoB,EAChC,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACf,GACL,MAAM,CAuBR;AAID;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,mEAC+B,CAAC;AAsCnE,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,IAAI,CAAC,gBAAgB,EAAE,SAAS,CAAC,GACvC,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,aAAa,CAAC,CAAC,GAC/C,oBAAoB,EAAE,CAmBxB;AAED,gFAAgF;AAChF,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,IAAI,CACd,oBAAoB,EACpB,QAAQ,GAAG,kBAAkB,GAAG,iBAAiB,CAClD,GACA,MAAM,CAMR;AAED,yEAAyE;AACzE,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,IAAI,CAAC,gBAAgB,EAAE,SAAS,CAAC,GACvC,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,aAAa,CAAC,CAAC,EAChD,MAAM,EAAE,cAAc,GACrB,oBAAoB,EAAE,CAuBxB;AAED,+EAA+E;AAC/E,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,IAAI,CAAC,gBAAgB,EAAE,SAAS,CAAC,GACvC,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,cAAc,GAAG,aAAa,CAAC,CAAC,EACjE,MAAM,EAAE,cAAc,GACrB,MAAM,EAAE,CA0BV"}
|
|
@@ -45,6 +45,12 @@ function renderForeignKeyOrphanRepair(tableName, foreignKey, options = {}) {
|
|
|
45
45
|
}
|
|
46
46
|
var FOREIGN_KEY_CHILD_ALIAS = "smrt_fk_child";
|
|
47
47
|
var FOREIGN_KEY_PARENT_ALIAS = "smrt_fk_parent";
|
|
48
|
+
/**
|
|
49
|
+
* Canonical UUID text shape used by every PostgreSQL guard that has to decide
|
|
50
|
+
* whether a `text` column can be reinterpreted as `uuid`. Exported so the
|
|
51
|
+
* orphan probe, the FK provisioning guard, and the uuid convergence planner
|
|
52
|
+
* all test the same shape (#2608).
|
|
53
|
+
*/
|
|
48
54
|
var CANONICAL_UUID_PATTERN = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$";
|
|
49
55
|
function foreignKeyOrphanParts(tableName, foreignKey, engine = "postgres", uuidComparison = false, uuidCastSide) {
|
|
50
56
|
const childAlias = quoteIdentifier(FOREIGN_KEY_CHILD_ALIAS);
|
|
@@ -110,6 +116,6 @@ function schemaDependenciesForEngine(schema, engine) {
|
|
|
110
116
|
return Array.from(/* @__PURE__ */ new Set([...(schema.dependencies ?? []).filter((dependency) => !disabledTargets.has(dependency) || enabledTargets.has(dependency)), ...enabledTargets]));
|
|
111
117
|
}
|
|
112
118
|
//#endregion
|
|
113
|
-
export { foreignKeyConstraintName, foreignKeyRelationshipKey, renderForeignKeyConstraint, renderForeignKeyConstraintComment, renderForeignKeyConstraintDrop, renderForeignKeyConstraintValidate, renderForeignKeyOrphanDetector, renderForeignKeyOrphanRepair, schemaDependenciesForEngine, schemaForeignKeys, schemaForeignKeysForEngine };
|
|
119
|
+
export { CANONICAL_UUID_PATTERN, foreignKeyConstraintName, foreignKeyRelationshipKey, renderForeignKeyConstraint, renderForeignKeyConstraintComment, renderForeignKeyConstraintDrop, renderForeignKeyConstraintValidate, renderForeignKeyOrphanDetector, renderForeignKeyOrphanRepair, schemaDependenciesForEngine, schemaForeignKeys, schemaForeignKeysForEngine };
|
|
114
120
|
|
|
115
121
|
//# sourceMappingURL=foreign-key-ddl.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"foreign-key-ddl.js","names":[],"sources":["../../src/schema/foreign-key-ddl.ts"],"sourcesContent":["import type { DatabaseEngine } from './ddl/types.js';\nimport { requireForeignKeyAction } from './foreign-key-policy.js';\nimport { shortenIdentifier } from './index-utils.js';\nimport { quoteIdentifier, quoteStringLiteral } from './sql-identifiers.js';\nimport type { ForeignKeyDefinition, SchemaDefinition } from './types.js';\n\nexport type ForeignKeyUuidCastSide = 'child' | 'parent' | 'both' | 'none';\n\nexport function foreignKeyConstraintName(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n): string {\n return shortenIdentifier(\n `${tableName}_${foreignKey.column}_${foreignKey.referencesTable}_${foreignKey.referencesColumn}_fkey`,\n );\n}\n\nexport function renderForeignKeyConstraint(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n): string {\n const action = (value: unknown, clause: 'DELETE' | 'UPDATE') => {\n if (value === undefined) return undefined;\n return requireForeignKeyAction(\n value,\n `${tableName}.${foreignKey.column} ON ${clause}`,\n );\n };\n const onDelete = action(foreignKey.onDelete, 'DELETE');\n const onUpdate = action(foreignKey.onUpdate, 'UPDATE');\n const name = foreignKeyConstraintName(tableName, foreignKey);\n const parts = [\n `CONSTRAINT ${quoteIdentifier(name)}`,\n `FOREIGN KEY (${quoteIdentifier(foreignKey.column)})`,\n `REFERENCES ${quoteIdentifier(foreignKey.referencesTable)} (${quoteIdentifier(foreignKey.referencesColumn)})`,\n ];\n if (onDelete) parts.push(`ON DELETE ${onDelete}`);\n if (onUpdate) parts.push(`ON UPDATE ${onUpdate}`);\n return parts.join(' ');\n}\n\n/** Render a PostgreSQL DROP CONSTRAINT statement for a catalog-owned FK. */\nexport function renderForeignKeyConstraintDrop(\n tableName: string,\n constraintName: string,\n): string {\n return `ALTER TABLE ${quoteIdentifier(tableName)} DROP CONSTRAINT ${quoteIdentifier(constraintName)};`;\n}\n\n/** Render a PostgreSQL VALIDATE CONSTRAINT statement. */\nexport function renderForeignKeyConstraintValidate(\n tableName: string,\n constraintName: string,\n): string {\n return `ALTER TABLE ${quoteIdentifier(tableName)} VALIDATE CONSTRAINT ${quoteIdentifier(constraintName)};`;\n}\n\n/** Render a PostgreSQL constraint comment used to identify framework ownership. */\nexport function renderForeignKeyConstraintComment(\n tableName: string,\n constraintName: string,\n comment: string,\n): string {\n return `COMMENT ON CONSTRAINT ${quoteIdentifier(constraintName)} ON ${quoteIdentifier(tableName)} IS ${quoteStringLiteral(comment)};`;\n}\n\nexport function renderForeignKeyOrphanDetector(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n options: {\n engine?: DatabaseEngine;\n limitOne?: boolean;\n uuidComparison?: boolean;\n uuidCastSide?: ForeignKeyUuidCastSide;\n } = {},\n): string {\n const parts = foreignKeyOrphanParts(\n tableName,\n foreignKey,\n options.engine,\n options.uuidComparison,\n options.uuidCastSide,\n );\n return (\n `SELECT ${parts.childColumn} AS orphan_key FROM ${parts.childTable} ` +\n `LEFT JOIN ${parts.parentTable} ON ${parts.joinPredicate} ` +\n `WHERE ${parts.childColumn} IS NOT NULL AND ${parts.parentColumn} IS NULL` +\n (options.limitOne ? ' LIMIT 1' : '')\n );\n}\n\nexport function renderForeignKeyOrphanRepair(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n options: {\n engine?: DatabaseEngine;\n uuidComparison?: boolean;\n uuidCastSide?: ForeignKeyUuidCastSide;\n nullable?: boolean;\n } = {},\n): string {\n const parts = foreignKeyOrphanParts(\n tableName,\n foreignKey,\n options.engine,\n options.uuidComparison,\n options.uuidCastSide,\n );\n const orphanPredicate =\n `${parts.childColumn} IS NOT NULL AND NOT EXISTS (` +\n `SELECT 1 FROM ${parts.parentTable} WHERE ${parts.joinPredicate})`;\n if (options.nullable === false) {\n return (\n `-- Manual repair required: ${quoteIdentifier(tableName)}.${quoteIdentifier(foreignKey.column)} is NOT NULL. ` +\n `Reassign each orphaned reference to a valid ${quoteIdentifier(foreignKey.referencesTable)}.${quoteIdentifier(foreignKey.referencesColumn)} value, ` +\n 'or deliberately delete rows only after preserving any required child data.'\n );\n }\n return (\n `UPDATE ${quoteIdentifier(tableName)} AS ${parts.childAlias} ` +\n `SET ${quoteIdentifier(foreignKey.column)} = NULL ` +\n `WHERE ${orphanPredicate}`\n );\n}\n\nconst FOREIGN_KEY_CHILD_ALIAS = 'smrt_fk_child';\nconst FOREIGN_KEY_PARENT_ALIAS = 'smrt_fk_parent';\nconst CANONICAL_UUID_PATTERN =\n '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$';\n\nfunction foreignKeyOrphanParts(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n engine: DatabaseEngine = 'postgres',\n uuidComparison = false,\n uuidCastSide?: ForeignKeyUuidCastSide,\n) {\n const childAlias = quoteIdentifier(FOREIGN_KEY_CHILD_ALIAS);\n const parentAlias = quoteIdentifier(FOREIGN_KEY_PARENT_ALIAS);\n const childColumn = `${childAlias}.${quoteIdentifier(foreignKey.column)}`;\n const parentColumn = `${parentAlias}.${quoteIdentifier(foreignKey.referencesColumn)}`;\n const guardedUuidValue = (column: string) =>\n `CASE WHEN ${column}::text ~* '${CANONICAL_UUID_PATTERN}' THEN ${column}::uuid ELSE NULL END`;\n const effectiveUuidCastSide =\n engine === 'postgres' && uuidComparison\n ? (uuidCastSide ?? 'child')\n : undefined;\n const childValue =\n effectiveUuidCastSide === 'child' || effectiveUuidCastSide === 'both'\n ? guardedUuidValue(childColumn)\n : childColumn;\n const parentValue =\n effectiveUuidCastSide === 'parent' || effectiveUuidCastSide === 'both'\n ? guardedUuidValue(parentColumn)\n : parentColumn;\n\n return {\n childAlias,\n childColumn,\n childTable: `${quoteIdentifier(tableName)} AS ${childAlias}`,\n joinPredicate: `${parentValue} = ${childValue}`,\n parentColumn,\n parentTable: `${quoteIdentifier(foreignKey.referencesTable)} AS ${parentAlias}`,\n };\n}\n\nexport function schemaForeignKeys(\n schema: Pick<SchemaDefinition, 'columns'> &\n Partial<Pick<SchemaDefinition, 'foreignKeys'>>,\n): ForeignKeyDefinition[] {\n if (schema.foreignKeys && schema.foreignKeys.length > 0) {\n return schema.foreignKeys;\n }\n return Object.entries(schema.columns).flatMap(([column, definition]) => {\n if (!definition.foreignKey) return [];\n return [\n {\n column,\n referencesTable: definition.foreignKey.table,\n referencesColumn: definition.foreignKey.column,\n onDelete: definition.foreignKey.onDelete,\n onUpdate: definition.foreignKey.onUpdate,\n ...(definition.foreignKey.engines\n ? { engines: definition.foreignKey.engines }\n : {}),\n },\n ];\n });\n}\n\n/** Stable identity for one physical relationship, independent of allocation. */\nexport function foreignKeyRelationshipKey(\n foreignKey: Pick<\n ForeignKeyDefinition,\n 'column' | 'referencesColumn' | 'referencesTable'\n >,\n): string {\n return JSON.stringify([\n foreignKey.column,\n foreignKey.referencesTable,\n foreignKey.referencesColumn,\n ]);\n}\n\n/** Return only physical constraints explicitly enabled for an engine. */\nexport function schemaForeignKeysForEngine(\n schema: Pick<SchemaDefinition, 'columns'> &\n Partial<Pick<SchemaDefinition, 'foreignKeys'>>,\n engine: DatabaseEngine,\n): ForeignKeyDefinition[] {\n return schemaForeignKeys(schema).filter((foreignKey) => {\n if (foreignKey.engines === undefined) return true;\n\n const validEngines = new Set<DatabaseEngine>([\n 'postgres',\n 'sqlite',\n 'duckdb',\n 'json',\n ]);\n if (\n foreignKey.engines.length === 0 ||\n foreignKey.engines.some(\n (declaredEngine) => !validEngines.has(declaredEngine),\n )\n ) {\n throw new Error(\n `[DDL:${engine}] Foreign key ${foreignKey.column} declares an invalid physical constraint engine allowlist. Use one or more of: postgres, sqlite, duckdb, json.`,\n );\n }\n\n return foreignKey.engines.includes(engine);\n });\n}\n\n/** Return declared schema dependencies after excluding engine-disabled FKs. */\nexport function schemaDependenciesForEngine(\n schema: Pick<SchemaDefinition, 'columns'> &\n Partial<Pick<SchemaDefinition, 'dependencies' | 'foreignKeys'>>,\n engine: DatabaseEngine,\n): string[] {\n const foreignKeys = schemaForeignKeys(schema);\n const enabledForeignKeys = schemaForeignKeysForEngine(schema, engine);\n const enabledKeys = new Set(\n enabledForeignKeys.map(foreignKeyRelationshipKey),\n );\n const enabledTargets = new Set(\n enabledForeignKeys.map((foreignKey) => foreignKey.referencesTable),\n );\n const disabledTargets = new Set(\n foreignKeys\n .filter(\n (foreignKey) => !enabledKeys.has(foreignKeyRelationshipKey(foreignKey)),\n )\n .map((foreignKey) => foreignKey.referencesTable),\n );\n\n return Array.from(\n new Set([\n ...(schema.dependencies ?? []).filter(\n (dependency) =>\n !disabledTargets.has(dependency) || enabledTargets.has(dependency),\n ),\n ...enabledTargets,\n ]),\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"foreign-key-ddl.js","names":[],"sources":["../../src/schema/foreign-key-ddl.ts"],"sourcesContent":["import type { DatabaseEngine } from './ddl/types.js';\nimport { requireForeignKeyAction } from './foreign-key-policy.js';\nimport { shortenIdentifier } from './index-utils.js';\nimport { quoteIdentifier, quoteStringLiteral } from './sql-identifiers.js';\nimport type { ForeignKeyDefinition, SchemaDefinition } from './types.js';\n\nexport type ForeignKeyUuidCastSide = 'child' | 'parent' | 'both' | 'none';\n\nexport function foreignKeyConstraintName(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n): string {\n return shortenIdentifier(\n `${tableName}_${foreignKey.column}_${foreignKey.referencesTable}_${foreignKey.referencesColumn}_fkey`,\n );\n}\n\nexport function renderForeignKeyConstraint(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n): string {\n const action = (value: unknown, clause: 'DELETE' | 'UPDATE') => {\n if (value === undefined) return undefined;\n return requireForeignKeyAction(\n value,\n `${tableName}.${foreignKey.column} ON ${clause}`,\n );\n };\n const onDelete = action(foreignKey.onDelete, 'DELETE');\n const onUpdate = action(foreignKey.onUpdate, 'UPDATE');\n const name = foreignKeyConstraintName(tableName, foreignKey);\n const parts = [\n `CONSTRAINT ${quoteIdentifier(name)}`,\n `FOREIGN KEY (${quoteIdentifier(foreignKey.column)})`,\n `REFERENCES ${quoteIdentifier(foreignKey.referencesTable)} (${quoteIdentifier(foreignKey.referencesColumn)})`,\n ];\n if (onDelete) parts.push(`ON DELETE ${onDelete}`);\n if (onUpdate) parts.push(`ON UPDATE ${onUpdate}`);\n return parts.join(' ');\n}\n\n/** Render a PostgreSQL DROP CONSTRAINT statement for a catalog-owned FK. */\nexport function renderForeignKeyConstraintDrop(\n tableName: string,\n constraintName: string,\n): string {\n return `ALTER TABLE ${quoteIdentifier(tableName)} DROP CONSTRAINT ${quoteIdentifier(constraintName)};`;\n}\n\n/** Render a PostgreSQL VALIDATE CONSTRAINT statement. */\nexport function renderForeignKeyConstraintValidate(\n tableName: string,\n constraintName: string,\n): string {\n return `ALTER TABLE ${quoteIdentifier(tableName)} VALIDATE CONSTRAINT ${quoteIdentifier(constraintName)};`;\n}\n\n/** Render a PostgreSQL constraint comment used to identify framework ownership. */\nexport function renderForeignKeyConstraintComment(\n tableName: string,\n constraintName: string,\n comment: string,\n): string {\n return `COMMENT ON CONSTRAINT ${quoteIdentifier(constraintName)} ON ${quoteIdentifier(tableName)} IS ${quoteStringLiteral(comment)};`;\n}\n\nexport function renderForeignKeyOrphanDetector(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n options: {\n engine?: DatabaseEngine;\n limitOne?: boolean;\n uuidComparison?: boolean;\n uuidCastSide?: ForeignKeyUuidCastSide;\n } = {},\n): string {\n const parts = foreignKeyOrphanParts(\n tableName,\n foreignKey,\n options.engine,\n options.uuidComparison,\n options.uuidCastSide,\n );\n return (\n `SELECT ${parts.childColumn} AS orphan_key FROM ${parts.childTable} ` +\n `LEFT JOIN ${parts.parentTable} ON ${parts.joinPredicate} ` +\n `WHERE ${parts.childColumn} IS NOT NULL AND ${parts.parentColumn} IS NULL` +\n (options.limitOne ? ' LIMIT 1' : '')\n );\n}\n\nexport function renderForeignKeyOrphanRepair(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n options: {\n engine?: DatabaseEngine;\n uuidComparison?: boolean;\n uuidCastSide?: ForeignKeyUuidCastSide;\n nullable?: boolean;\n } = {},\n): string {\n const parts = foreignKeyOrphanParts(\n tableName,\n foreignKey,\n options.engine,\n options.uuidComparison,\n options.uuidCastSide,\n );\n const orphanPredicate =\n `${parts.childColumn} IS NOT NULL AND NOT EXISTS (` +\n `SELECT 1 FROM ${parts.parentTable} WHERE ${parts.joinPredicate})`;\n if (options.nullable === false) {\n return (\n `-- Manual repair required: ${quoteIdentifier(tableName)}.${quoteIdentifier(foreignKey.column)} is NOT NULL. ` +\n `Reassign each orphaned reference to a valid ${quoteIdentifier(foreignKey.referencesTable)}.${quoteIdentifier(foreignKey.referencesColumn)} value, ` +\n 'or deliberately delete rows only after preserving any required child data.'\n );\n }\n return (\n `UPDATE ${quoteIdentifier(tableName)} AS ${parts.childAlias} ` +\n `SET ${quoteIdentifier(foreignKey.column)} = NULL ` +\n `WHERE ${orphanPredicate}`\n );\n}\n\nconst FOREIGN_KEY_CHILD_ALIAS = 'smrt_fk_child';\nconst FOREIGN_KEY_PARENT_ALIAS = 'smrt_fk_parent';\n/**\n * Canonical UUID text shape used by every PostgreSQL guard that has to decide\n * whether a `text` column can be reinterpreted as `uuid`. Exported so the\n * orphan probe, the FK provisioning guard, and the uuid convergence planner\n * all test the same shape (#2608).\n */\nexport const CANONICAL_UUID_PATTERN =\n '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$';\n\nfunction foreignKeyOrphanParts(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n engine: DatabaseEngine = 'postgres',\n uuidComparison = false,\n uuidCastSide?: ForeignKeyUuidCastSide,\n) {\n const childAlias = quoteIdentifier(FOREIGN_KEY_CHILD_ALIAS);\n const parentAlias = quoteIdentifier(FOREIGN_KEY_PARENT_ALIAS);\n const childColumn = `${childAlias}.${quoteIdentifier(foreignKey.column)}`;\n const parentColumn = `${parentAlias}.${quoteIdentifier(foreignKey.referencesColumn)}`;\n const guardedUuidValue = (column: string) =>\n `CASE WHEN ${column}::text ~* '${CANONICAL_UUID_PATTERN}' THEN ${column}::uuid ELSE NULL END`;\n const effectiveUuidCastSide =\n engine === 'postgres' && uuidComparison\n ? (uuidCastSide ?? 'child')\n : undefined;\n const childValue =\n effectiveUuidCastSide === 'child' || effectiveUuidCastSide === 'both'\n ? guardedUuidValue(childColumn)\n : childColumn;\n const parentValue =\n effectiveUuidCastSide === 'parent' || effectiveUuidCastSide === 'both'\n ? guardedUuidValue(parentColumn)\n : parentColumn;\n\n return {\n childAlias,\n childColumn,\n childTable: `${quoteIdentifier(tableName)} AS ${childAlias}`,\n joinPredicate: `${parentValue} = ${childValue}`,\n parentColumn,\n parentTable: `${quoteIdentifier(foreignKey.referencesTable)} AS ${parentAlias}`,\n };\n}\n\nexport function schemaForeignKeys(\n schema: Pick<SchemaDefinition, 'columns'> &\n Partial<Pick<SchemaDefinition, 'foreignKeys'>>,\n): ForeignKeyDefinition[] {\n if (schema.foreignKeys && schema.foreignKeys.length > 0) {\n return schema.foreignKeys;\n }\n return Object.entries(schema.columns).flatMap(([column, definition]) => {\n if (!definition.foreignKey) return [];\n return [\n {\n column,\n referencesTable: definition.foreignKey.table,\n referencesColumn: definition.foreignKey.column,\n onDelete: definition.foreignKey.onDelete,\n onUpdate: definition.foreignKey.onUpdate,\n ...(definition.foreignKey.engines\n ? { engines: definition.foreignKey.engines }\n : {}),\n },\n ];\n });\n}\n\n/** Stable identity for one physical relationship, independent of allocation. */\nexport function foreignKeyRelationshipKey(\n foreignKey: Pick<\n ForeignKeyDefinition,\n 'column' | 'referencesColumn' | 'referencesTable'\n >,\n): string {\n return JSON.stringify([\n foreignKey.column,\n foreignKey.referencesTable,\n foreignKey.referencesColumn,\n ]);\n}\n\n/** Return only physical constraints explicitly enabled for an engine. */\nexport function schemaForeignKeysForEngine(\n schema: Pick<SchemaDefinition, 'columns'> &\n Partial<Pick<SchemaDefinition, 'foreignKeys'>>,\n engine: DatabaseEngine,\n): ForeignKeyDefinition[] {\n return schemaForeignKeys(schema).filter((foreignKey) => {\n if (foreignKey.engines === undefined) return true;\n\n const validEngines = new Set<DatabaseEngine>([\n 'postgres',\n 'sqlite',\n 'duckdb',\n 'json',\n ]);\n if (\n foreignKey.engines.length === 0 ||\n foreignKey.engines.some(\n (declaredEngine) => !validEngines.has(declaredEngine),\n )\n ) {\n throw new Error(\n `[DDL:${engine}] Foreign key ${foreignKey.column} declares an invalid physical constraint engine allowlist. Use one or more of: postgres, sqlite, duckdb, json.`,\n );\n }\n\n return foreignKey.engines.includes(engine);\n });\n}\n\n/** Return declared schema dependencies after excluding engine-disabled FKs. */\nexport function schemaDependenciesForEngine(\n schema: Pick<SchemaDefinition, 'columns'> &\n Partial<Pick<SchemaDefinition, 'dependencies' | 'foreignKeys'>>,\n engine: DatabaseEngine,\n): string[] {\n const foreignKeys = schemaForeignKeys(schema);\n const enabledForeignKeys = schemaForeignKeysForEngine(schema, engine);\n const enabledKeys = new Set(\n enabledForeignKeys.map(foreignKeyRelationshipKey),\n );\n const enabledTargets = new Set(\n enabledForeignKeys.map((foreignKey) => foreignKey.referencesTable),\n );\n const disabledTargets = new Set(\n foreignKeys\n .filter(\n (foreignKey) => !enabledKeys.has(foreignKeyRelationshipKey(foreignKey)),\n )\n .map((foreignKey) => foreignKey.referencesTable),\n );\n\n return Array.from(\n new Set([\n ...(schema.dependencies ?? []).filter(\n (dependency) =>\n !disabledTargets.has(dependency) || enabledTargets.has(dependency),\n ),\n ...enabledTargets,\n ]),\n );\n}\n"],"mappings":";;;;AAQA,SAAgB,yBACd,WACA,YACQ;CACR,OAAO,kBACL,GAAG,UAAU,GAAG,WAAW,OAAO,GAAG,WAAW,gBAAgB,GAAG,WAAW,iBAAiB,MACjG;AACF;AAEA,SAAgB,2BACd,WACA,YACQ;CACR,MAAM,UAAU,OAAgB,WAAgC;EAC9D,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;EAChC,OAAO,wBACL,OACA,GAAG,UAAU,GAAG,WAAW,OAAO,MAAM,QAC1C;CACF;CACA,MAAM,WAAW,OAAO,WAAW,UAAU,QAAQ;CACrD,MAAM,WAAW,OAAO,WAAW,UAAU,QAAQ;CAErD,MAAM,QAAQ;EACZ,cAAc,gBAFH,yBAAyB,WAAW,UAEjB,CAAI;EAClC,gBAAgB,gBAAgB,WAAW,MAAM,EAAE;EACnD,cAAc,gBAAgB,WAAW,eAAe,EAAE,IAAI,gBAAgB,WAAW,gBAAgB,EAAE;CAC7G;CACA,IAAI,UAAU,MAAM,KAAK,aAAa,UAAU;CAChD,IAAI,UAAU,MAAM,KAAK,aAAa,UAAU;CAChD,OAAO,MAAM,KAAK,GAAG;AACvB;;AAGA,SAAgB,+BACd,WACA,gBACQ;CACR,OAAO,eAAe,gBAAgB,SAAS,EAAE,mBAAmB,gBAAgB,cAAc,EAAE;AACtG;;AAGA,SAAgB,mCACd,WACA,gBACQ;CACR,OAAO,eAAe,gBAAgB,SAAS,EAAE,uBAAuB,gBAAgB,cAAc,EAAE;AAC1G;;AAGA,SAAgB,kCACd,WACA,gBACA,SACQ;CACR,OAAO,yBAAyB,gBAAgB,cAAc,EAAE,MAAM,gBAAgB,SAAS,EAAE,MAAM,mBAAmB,OAAO,EAAE;AACrI;AAEA,SAAgB,+BACd,WACA,YACA,UAKI,CAAC,GACG;CACR,MAAM,QAAQ,sBACZ,WACA,YACA,QAAQ,QACR,QAAQ,gBACR,QAAQ,YACV;CACA,OACE,UAAU,MAAM,YAAY,sBAAsB,MAAM,WAAW,aACtD,MAAM,YAAY,MAAM,MAAM,cAAc,SAChD,MAAM,YAAY,mBAAmB,MAAM,aAAa,aAChE,QAAQ,WAAW,aAAa;AAErC;AAEA,SAAgB,6BACd,WACA,YACA,UAKI,CAAC,GACG;CACR,MAAM,QAAQ,sBACZ,WACA,YACA,QAAQ,QACR,QAAQ,gBACR,QAAQ,YACV;CACA,MAAM,kBACJ,GAAG,MAAM,YAAY,6CACJ,MAAM,YAAY,SAAS,MAAM,cAAc;CAClE,IAAI,QAAQ,aAAa,OACvB,OACE,8BAA8B,gBAAgB,SAAS,EAAE,GAAG,gBAAgB,WAAW,MAAM,EAAE,4DAChD,gBAAgB,WAAW,eAAe,EAAE,GAAG,gBAAgB,WAAW,gBAAgB,EAAE;CAI/I,OACE,UAAU,gBAAgB,SAAS,EAAE,MAAM,MAAM,WAAW,OACrD,gBAAgB,WAAW,MAAM,EAAE,gBACjC;AAEb;AAEA,IAAM,0BAA0B;AAChC,IAAM,2BAA2B;;;;;;;AAOjC,IAAa,yBACX;AAEF,SAAS,sBACP,WACA,YACA,SAAyB,YACzB,iBAAiB,OACjB,cACA;CACA,MAAM,aAAa,gBAAgB,uBAAuB;CAC1D,MAAM,cAAc,gBAAgB,wBAAwB;CAC5D,MAAM,cAAc,GAAG,WAAW,GAAG,gBAAgB,WAAW,MAAM;CACtE,MAAM,eAAe,GAAG,YAAY,GAAG,gBAAgB,WAAW,gBAAgB;CAClF,MAAM,oBAAoB,WACxB,aAAa,OAAO,aAAa,uBAAuB,SAAS,OAAO;CAC1E,MAAM,wBACJ,WAAW,cAAc,iBACpB,gBAAgB,UACjB,KAAA;CACN,MAAM,aACJ,0BAA0B,WAAW,0BAA0B,SAC3D,iBAAiB,WAAW,IAC5B;CACN,MAAM,cACJ,0BAA0B,YAAY,0BAA0B,SAC5D,iBAAiB,YAAY,IAC7B;CAEN,OAAO;EACL;EACA;EACA,YAAY,GAAG,gBAAgB,SAAS,EAAE,MAAM;EAChD,eAAe,GAAG,YAAY,KAAK;EACnC;EACA,aAAa,GAAG,gBAAgB,WAAW,eAAe,EAAE,MAAM;CACpE;AACF;AAEA,SAAgB,kBACd,QAEwB;CACxB,IAAI,OAAO,eAAe,OAAO,YAAY,SAAS,GACpD,OAAO,OAAO;CAEhB,OAAO,OAAO,QAAQ,OAAO,OAAO,CAAC,CAAC,SAAS,CAAC,QAAQ,gBAAgB;EACtE,IAAI,CAAC,WAAW,YAAY,OAAO,CAAC;EACpC,OAAO,CACL;GACE;GACA,iBAAiB,WAAW,WAAW;GACvC,kBAAkB,WAAW,WAAW;GACxC,UAAU,WAAW,WAAW;GAChC,UAAU,WAAW,WAAW;GAChC,GAAI,WAAW,WAAW,UACtB,EAAE,SAAS,WAAW,WAAW,QAAQ,IACzC,CAAC;EACP,CACF;CACF,CAAC;AACH;;AAGA,SAAgB,0BACd,YAIQ;CACR,OAAO,KAAK,UAAU;EACpB,WAAW;EACX,WAAW;EACX,WAAW;CACb,CAAC;AACH;;AAGA,SAAgB,2BACd,QAEA,QACwB;CACxB,OAAO,kBAAkB,MAAM,CAAC,CAAC,QAAQ,eAAe;EACtD,IAAI,WAAW,YAAY,KAAA,GAAW,OAAO;EAE7C,MAAM,+BAAe,IAAI,IAAoB;GAC3C;GACA;GACA;GACA;EACF,CAAC;EACD,IACE,WAAW,QAAQ,WAAW,KAC9B,WAAW,QAAQ,MAChB,mBAAmB,CAAC,aAAa,IAAI,cAAc,CACtD,GAEA,MAAM,IAAI,MACR,QAAQ,OAAO,gBAAgB,WAAW,OAAO,+GACnD;EAGF,OAAO,WAAW,QAAQ,SAAS,MAAM;CAC3C,CAAC;AACH;;AAGA,SAAgB,4BACd,QAEA,QACU;CACV,MAAM,cAAc,kBAAkB,MAAM;CAC5C,MAAM,qBAAqB,2BAA2B,QAAQ,MAAM;CACpE,MAAM,cAAc,IAAI,IACtB,mBAAmB,IAAI,yBAAyB,CAClD;CACA,MAAM,iBAAiB,IAAI,IACzB,mBAAmB,KAAK,eAAe,WAAW,eAAe,CACnE;CACA,MAAM,kBAAkB,IAAI,IAC1B,YACG,QACE,eAAe,CAAC,YAAY,IAAI,0BAA0B,UAAU,CAAC,CACxE,CAAC,CACA,KAAK,eAAe,WAAW,eAAe,CACnD;CAEA,OAAO,MAAM,qBACX,IAAI,IAAI,CACN,IAAI,OAAO,gBAAgB,CAAC,EAAA,CAAG,QAC5B,eACC,CAAC,gBAAgB,IAAI,UAAU,KAAK,eAAe,IAAI,UAAU,CACrE,GACA,GAAG,cACL,CAAC,CACH;AACF"}
|
|
@@ -79,6 +79,21 @@ export declare class SchemaManager {
|
|
|
79
79
|
uuidComparison?: boolean;
|
|
80
80
|
uuidCastSide?: ForeignKeyUuidCastSide;
|
|
81
81
|
}): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Read the live physical types of one child/parent column pair.
|
|
84
|
+
*
|
|
85
|
+
* Returns `undefined` when either side cannot be introspected (adapter
|
|
86
|
+
* without `getTableSchema`, table not created yet) — callers then keep the
|
|
87
|
+
* pre-#2608 behaviour rather than guessing at a mismatch.
|
|
88
|
+
*/
|
|
89
|
+
private resolveLiveForeignKeyTypes;
|
|
90
|
+
/**
|
|
91
|
+
* Classify which side of an orphan probe needs a guarded uuid cast.
|
|
92
|
+
*
|
|
93
|
+
* Only reachable with matching live types while the #2608 guard rejects
|
|
94
|
+
* every mismatch, but the classification stays correct for callers that
|
|
95
|
+
* reuse the helper with an unguarded pair.
|
|
96
|
+
*/
|
|
82
97
|
private resolvePostgresForeignKeyProbeOptions;
|
|
83
98
|
private normalizeForeignKeyProbeType;
|
|
84
99
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-manager.d.ts","sourceRoot":"","sources":["../../src/schema/schema-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EACL,KAAK,cAAc,EAEnB,KAAK,iBAAiB,EAEvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,KAAK,sBAAsB,EAK5B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"schema-manager.d.ts","sourceRoot":"","sources":["../../src/schema/schema-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EACL,KAAK,cAAc,EAEnB,KAAK,iBAAiB,EAEvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,KAAK,sBAAsB,EAK5B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAqBzE;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,mDAAmD;IACnD,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,+DAA+D;IAC/D,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;GAKG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,OAAO,CAAuB;IAGtC,OAAO,CAAC,MAAM,CAAkC;gBAEpC,EAAE,EAAE,iBAAiB,EAAE,OAAO,GAAE,oBAAyB;IAkBrE;;;;;;;OAOG;IACG,WAAW,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmH1D;;;;;;;;;OASG;YACW,kBAAkB;IAwChC;;;;;;;;;;;;;OAaG;YACW,iBAAiB;IAqE/B,OAAO,CAAC,eAAe;IAIvB;;;;OAIG;IACG,YAAY,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAqB9D;;;;;;OAMG;IACG,wBAAwB,CAC5B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,oBAAoB,EAChC,OAAO,GAAE;QACP,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,cAAc,CAAC,EAAE,OAAO,CAAC;QACzB,YAAY,CAAC,EAAE,sBAAsB,CAAC;KAClC,GACL,OAAO,CAAC,IAAI,CAAC;IAuGhB;;;;;;OAMG;YACW,0BAA0B;IA4BxC;;;;;;OAMG;IACH,OAAO,CAAC,qCAAqC;IA0B7C,OAAO,CAAC,4BAA4B;IAOpC;;;;;OAKG;IACH,WAAW,CAAC,MAAM,EAAE,gBAAgB,GAAG,iBAAiB;IASxD;;OAEG;IACH,SAAS,IAAI,cAAc;CAG5B;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,iBAAiB,EACrB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,aAAa,CAEf"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { foreignKeyConstraintName, renderForeignKeyConstraint, renderForeignKeyOrphanDetector, renderForeignKeyOrphanRepair } from "./foreign-key-ddl.js";
|
|
2
2
|
import { detectEngine, getDDLStrategy } from "./ddl/index.js";
|
|
3
3
|
import { planForeignKeyCreation } from "./foreign-key-planner.js";
|
|
4
|
+
import { describeForeignKeyTypeConflict, renderUuidConvergenceRepairHint } from "./uuid-convergence.js";
|
|
4
5
|
import { createLogger } from "@happyvertical/logger";
|
|
5
6
|
//#region src/schema/schema-manager.ts
|
|
6
7
|
/**
|
|
@@ -199,7 +200,27 @@ var SchemaManager = class {
|
|
|
199
200
|
}
|
|
200
201
|
const existingRows = extractRows(existing);
|
|
201
202
|
if (existingRows[0]?.convalidated === true) return;
|
|
202
|
-
const
|
|
203
|
+
const liveTypes = await this.resolveLiveForeignKeyTypes(tableName, foreignKey);
|
|
204
|
+
if (liveTypes && liveTypes.childNormalized !== liveTypes.parentNormalized) {
|
|
205
|
+
const conflict = describeForeignKeyTypeConflict({
|
|
206
|
+
childTable: tableName,
|
|
207
|
+
childColumn: foreignKey.column,
|
|
208
|
+
childType: liveTypes.childType,
|
|
209
|
+
parentTable: foreignKey.referencesTable,
|
|
210
|
+
parentColumn: foreignKey.referencesColumn,
|
|
211
|
+
parentType: liveTypes.parentType
|
|
212
|
+
});
|
|
213
|
+
const textNodes = options.uuidComparison ? [liveTypes.childNormalized === "TEXT" ? {
|
|
214
|
+
table: tableName,
|
|
215
|
+
column: foreignKey.column
|
|
216
|
+
} : void 0, liveTypes.parentNormalized === "TEXT" ? {
|
|
217
|
+
table: foreignKey.referencesTable,
|
|
218
|
+
column: foreignKey.referencesColumn
|
|
219
|
+
} : void 0].filter((node) => node !== void 0) : [];
|
|
220
|
+
const repair = textNodes.length > 0 ? ` Suggested repair: ${renderUuidConvergenceRepairHint(textNodes).join("; ")}` : ` Align ${tableName}.${foreignKey.column} (${liveTypes.childType}) and ${foreignKey.referencesTable}.${foreignKey.referencesColumn} (${liveTypes.parentType}) on one physical type deliberately, then retry.`;
|
|
221
|
+
throw new Error(`[SchemaManager] Cannot add ${constraintName}: incompatible column types. ${conflict}. Converge the columns first, then retry.${repair}`);
|
|
222
|
+
}
|
|
223
|
+
const probeOptions = this.resolvePostgresForeignKeyProbeOptions(options, liveTypes);
|
|
203
224
|
const detector = renderForeignKeyOrphanDetector(tableName, foreignKey, {
|
|
204
225
|
engine: "postgres",
|
|
205
226
|
limitOne: true,
|
|
@@ -221,24 +242,46 @@ var SchemaManager = class {
|
|
|
221
242
|
if (existingRows.length === 0) await this.db.query(`ALTER TABLE ${this.quoteIdentifier(tableName)} ADD ${renderForeignKeyConstraint(tableName, foreignKey)} NOT VALID`);
|
|
222
243
|
await this.db.query(`ALTER TABLE ${this.quoteIdentifier(tableName)} VALIDATE CONSTRAINT ${this.quoteIdentifier(constraintName)}`);
|
|
223
244
|
}
|
|
224
|
-
|
|
225
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Read the live physical types of one child/parent column pair.
|
|
247
|
+
*
|
|
248
|
+
* Returns `undefined` when either side cannot be introspected (adapter
|
|
249
|
+
* without `getTableSchema`, table not created yet) — callers then keep the
|
|
250
|
+
* pre-#2608 behaviour rather than guessing at a mismatch.
|
|
251
|
+
*/
|
|
252
|
+
async resolveLiveForeignKeyTypes(tableName, foreignKey) {
|
|
226
253
|
const childSchema = await this.db.getTableSchema?.(tableName);
|
|
227
254
|
const parentSchema = foreignKey.referencesTable === tableName ? childSchema : await this.db.getTableSchema?.(foreignKey.referencesTable);
|
|
228
255
|
const childType = childSchema?.columns[foreignKey.column]?.type;
|
|
229
256
|
const parentType = parentSchema?.columns[foreignKey.referencesColumn]?.type;
|
|
230
|
-
if (!childType || !parentType) return
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
257
|
+
if (!childType || !parentType) return void 0;
|
|
258
|
+
return {
|
|
259
|
+
childType,
|
|
260
|
+
parentType,
|
|
261
|
+
childNormalized: this.normalizeForeignKeyProbeType(childType),
|
|
262
|
+
parentNormalized: this.normalizeForeignKeyProbeType(parentType)
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Classify which side of an orphan probe needs a guarded uuid cast.
|
|
267
|
+
*
|
|
268
|
+
* Only reachable with matching live types while the #2608 guard rejects
|
|
269
|
+
* every mismatch, but the classification stays correct for callers that
|
|
270
|
+
* reuse the helper with an unguarded pair.
|
|
271
|
+
*/
|
|
272
|
+
resolvePostgresForeignKeyProbeOptions(options, liveTypes) {
|
|
273
|
+
if (!options.uuidComparison || options.uuidCastSide) return options;
|
|
274
|
+
if (!liveTypes) return options;
|
|
275
|
+
const { childNormalized, parentNormalized } = liveTypes;
|
|
276
|
+
if (childNormalized === parentNormalized) return {
|
|
234
277
|
...options,
|
|
235
278
|
uuidCastSide: "none"
|
|
236
279
|
};
|
|
237
|
-
if (
|
|
280
|
+
if (childNormalized === "UUID") return {
|
|
238
281
|
...options,
|
|
239
282
|
uuidCastSide: "parent"
|
|
240
283
|
};
|
|
241
|
-
if (
|
|
284
|
+
if (parentNormalized === "UUID") return {
|
|
242
285
|
...options,
|
|
243
286
|
uuidCastSide: "child"
|
|
244
287
|
};
|