@prisma-next/sql-schema-ir 0.16.0-dev.3 → 0.16.0-dev.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/exports/naming.d.mts +84 -2
- package/dist/exports/naming.d.mts.map +1 -1
- package/dist/exports/naming.mjs +2 -8
- package/dist/exports/types.d.mts +1 -1
- package/dist/exports/types.mjs +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/naming-BD7Y-Fq1.mjs +112 -0
- package/dist/naming-BD7Y-Fq1.mjs.map +1 -0
- package/dist/{types-pxgoVAJq.mjs → types-Btj35AIt.mjs} +85 -34
- package/dist/{types-pxgoVAJq.mjs.map → types-Btj35AIt.mjs.map} +1 -1
- package/dist/{types-Bnh1XTEx.d.mts → types-DzLk5eOU.d.mts} +111 -33
- package/dist/{types-Bnh1XTEx.d.mts.map → types-DzLk5eOU.d.mts.map} +1 -1
- package/package.json +7 -7
- package/src/exports/naming.ts +12 -3
- package/src/ir/sql-index-ir.ts +172 -45
- package/src/naming.ts +135 -0
- package/src/types.ts +4 -1
- package/dist/exports/naming.mjs.map +0 -1
package/src/ir/sql-index-ir.ts
CHANGED
|
@@ -1,25 +1,77 @@
|
|
|
1
1
|
import type { DiffableNode, SchemaNodeRef } from '@prisma-next/framework-components/control';
|
|
2
2
|
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
3
|
+
import { isArrayEqual } from '@prisma-next/utils/array-equal';
|
|
3
4
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
5
|
+
import { InternalError } from '@prisma-next/utils/internal-error';
|
|
6
|
+
import { normalizeIndexOptionValue } from '../naming';
|
|
4
7
|
import { RelationalSchemaNodeKind } from './schema-node-kinds';
|
|
5
8
|
import type { SqlAnnotations } from './sql-column-ir';
|
|
6
9
|
import { assertNode, defineNonEnumerable, SqlSchemaIRNode } from './sql-schema-ir-node';
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
/**
|
|
12
|
+
* An index's element structure — exactly one of a column tuple or an opaque
|
|
13
|
+
* expression, unrepresentable-otherwise at the type level. No discriminant
|
|
14
|
+
* is stored (the node keeps flat readonly accessors); the constructor's xor
|
|
15
|
+
* throw stays as the backstop for introspection rows and JSON-derived
|
|
16
|
+
* inputs that bypass this union.
|
|
17
|
+
*/
|
|
18
|
+
export type SqlIndexElements =
|
|
19
|
+
| {
|
|
20
|
+
/** Column-tuple elements. */
|
|
21
|
+
readonly columns: readonly string[];
|
|
22
|
+
readonly expression?: never;
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
readonly columns?: never;
|
|
26
|
+
/**
|
|
27
|
+
* Opaque SQL: the entire element list between the parens of CREATE
|
|
28
|
+
* INDEX — one string, never parsed.
|
|
29
|
+
*/
|
|
30
|
+
readonly expression: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Every non-element field is a required key. Values that may legitimately
|
|
35
|
+
* be absent (an exact-named index's prefix, a default-method type) are
|
|
36
|
+
* typed `| undefined` instead of optional, so each construction site
|
|
37
|
+
* states the absence explicitly rather than omitting the key silently.
|
|
38
|
+
* Undefined values still produce an instance without the property.
|
|
39
|
+
*/
|
|
40
|
+
export type SqlIndexIRInput = SqlIndexElements & {
|
|
41
|
+
/** Full physical name — the node's identity. */
|
|
42
|
+
readonly name: string;
|
|
43
|
+
/**
|
|
44
|
+
* The managed-mode name prefix — its PRESENCE is the naming-mode
|
|
45
|
+
* discriminator (there is no stored enum). Present ⇔ managed: the
|
|
46
|
+
* toolchain owns the physical name and `name === formatWireName(prefix,
|
|
47
|
+
* <8hex content hash>)`. Absent ⇔ exact: `name` is an adopted verbatim
|
|
48
|
+
* physical name whose identity the author owns entirely.
|
|
49
|
+
*/
|
|
50
|
+
readonly prefix: string | undefined;
|
|
51
|
+
/** Opaque SQL: partial-index predicate (WHERE body, without the keyword). */
|
|
52
|
+
readonly where: string | undefined;
|
|
10
53
|
readonly unique: boolean;
|
|
11
|
-
readonly
|
|
12
|
-
readonly
|
|
13
|
-
readonly
|
|
14
|
-
readonly annotations?: SqlAnnotations;
|
|
54
|
+
readonly type: string | undefined;
|
|
55
|
+
readonly options: Record<string, unknown> | undefined;
|
|
56
|
+
readonly annotations: SqlAnnotations | undefined;
|
|
15
57
|
/**
|
|
16
58
|
* The index's own column nodes, as root-anchored chains. The derivation
|
|
17
59
|
* stamps them so an index is dropped before the columns it is built on
|
|
18
|
-
* (Postgres auto-drops the index when a covered column goes).
|
|
60
|
+
* (Postgres auto-drops the index when a covered column goes). An expression
|
|
61
|
+
* index stamps chains to every column of its table — a deterministic
|
|
62
|
+
* over-approximation, since the opaque expression is never parsed. Never
|
|
19
63
|
* compared by `isEqualTo`.
|
|
20
64
|
*/
|
|
21
|
-
readonly dependsOn
|
|
22
|
-
|
|
65
|
+
readonly dependsOn: readonly SchemaNodeRef[] | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Whether the index is partial (has a row predicate). Required: every
|
|
68
|
+
* producer must assert partiality explicitly, because a partial unique
|
|
69
|
+
* index does not guarantee at-most-one row per key and so cannot back a
|
|
70
|
+
* 1:1 relation — "unknown" must not silently default to "total". Never
|
|
71
|
+
* compared by `isEqualTo` and never serialized.
|
|
72
|
+
*/
|
|
73
|
+
readonly partial: boolean;
|
|
74
|
+
};
|
|
23
75
|
|
|
24
76
|
/**
|
|
25
77
|
* Schema IR node for a secondary index as observed by introspection.
|
|
@@ -29,50 +81,67 @@ export interface SqlIndexIRInput {
|
|
|
29
81
|
* verifier needs to distinguish them when comparing to the Contract.
|
|
30
82
|
*
|
|
31
83
|
* Implements `DiffableNode` so an index is directly a table's diff-tree
|
|
32
|
-
* child. Indexes are
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* and are not equal — there is no "stronger satisfies weaker".
|
|
84
|
+
* child. Indexes are name-identified: every index — contract-derived or
|
|
85
|
+
* introspected — carries its full physical name, and `id` is that name.
|
|
86
|
+
* Names are catalog-unique per schema, so two indexes legitimately sharing
|
|
87
|
+
* one column tuple (a unique index beside a redundant plain index) are two
|
|
88
|
+
* distinct siblings, and expression indexes need no column tuple at all.
|
|
38
89
|
*
|
|
39
|
-
*
|
|
40
|
-
* (
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* `
|
|
44
|
-
*
|
|
45
|
-
* (
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
90
|
+
* `isEqualTo` is selected by the receiver (the differ always calls
|
|
91
|
+
* `expected.isEqualTo(actual)`) and delegates to {@link contentEquals} —
|
|
92
|
+
* the single node-owned content relation: both modes compare `unique`
|
|
93
|
+
* strict, `type` and option values through the named normalization seams,
|
|
94
|
+
* and `columns` ordered-strict when both sides carry them; an exact-named
|
|
95
|
+
* receiver (`prefix === undefined`) additionally byte-compares
|
|
96
|
+
* `expression`/`where` (both sides are reprints in the supported flow —
|
|
97
|
+
* normalizing would only mask real drift); a managed receiver never
|
|
98
|
+
* compares bodies (the wire-name hash already commits to them).
|
|
99
|
+
*
|
|
100
|
+
* `expression`, `where`, and `unique` are genuine SQL-family attributes —
|
|
101
|
+
* functional and partial indexes are standard SQL that any SQL target may
|
|
102
|
+
* introspect, so the family node must represent them; a target declining
|
|
103
|
+
* to author them is a capability decision, not target-specificity.
|
|
49
104
|
*/
|
|
50
105
|
export class SqlIndexIR extends SqlSchemaIRNode implements DiffableNode {
|
|
51
106
|
override readonly nodeKind = RelationalSchemaNodeKind.index;
|
|
52
107
|
|
|
53
|
-
readonly
|
|
108
|
+
readonly name: string;
|
|
54
109
|
readonly unique: boolean;
|
|
55
|
-
declare readonly
|
|
110
|
+
declare readonly prefix?: string;
|
|
111
|
+
declare readonly columns?: readonly string[];
|
|
112
|
+
declare readonly expression?: string;
|
|
113
|
+
declare readonly where?: string;
|
|
56
114
|
declare readonly type?: string;
|
|
57
115
|
declare readonly options?: Record<string, unknown>;
|
|
58
116
|
declare readonly annotations?: SqlAnnotations;
|
|
59
117
|
/** See {@link SqlIndexIRInput.dependsOn}. Non-enumerable so it stays out of JSON and structural equality, matching `SqlColumnIR.codecRef`. */
|
|
60
118
|
declare readonly dependsOn?: readonly SchemaNodeRef[];
|
|
119
|
+
/** See {@link SqlIndexIRInput.partial}. Non-enumerable so it stays out of JSON and structural equality, matching `dependsOn`. */
|
|
120
|
+
declare readonly partial: boolean;
|
|
61
121
|
|
|
62
122
|
constructor(input: SqlIndexIRInput) {
|
|
63
123
|
super();
|
|
64
|
-
|
|
124
|
+
if ((input.columns === undefined) === (input.expression === undefined)) {
|
|
125
|
+
throw new InternalError(
|
|
126
|
+
`SqlIndexIR "${input.name}": exactly one of columns or expression must be set.`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
this.name = input.name;
|
|
65
130
|
this.unique = input.unique;
|
|
66
|
-
if (input.
|
|
131
|
+
if (input.prefix !== undefined) this.prefix = input.prefix;
|
|
132
|
+
if (input.columns !== undefined) this.columns = input.columns;
|
|
133
|
+
if (input.expression !== undefined) this.expression = input.expression;
|
|
134
|
+
if (input.where !== undefined) this.where = input.where;
|
|
67
135
|
if (input.type !== undefined) this.type = input.type;
|
|
68
136
|
if (input.options !== undefined) this.options = input.options;
|
|
69
137
|
if (input.annotations !== undefined) this.annotations = input.annotations;
|
|
70
138
|
defineNonEnumerable(this, 'dependsOn', input.dependsOn);
|
|
139
|
+
defineNonEnumerable(this, 'partial', input.partial);
|
|
71
140
|
freezeNode(this);
|
|
72
141
|
}
|
|
73
142
|
|
|
74
143
|
get id(): string {
|
|
75
|
-
return `index:${this.
|
|
144
|
+
return `index:${this.name}`;
|
|
76
145
|
}
|
|
77
146
|
|
|
78
147
|
children(): readonly DiffableNode[] {
|
|
@@ -84,12 +153,11 @@ export class SqlIndexIR extends SqlSchemaIRNode implements DiffableNode {
|
|
|
84
153
|
}
|
|
85
154
|
|
|
86
155
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* `
|
|
91
|
-
* compares
|
|
92
|
-
* normalization done at construction.
|
|
156
|
+
* Mode-selected structural equality — see the class doc. Delegates to the
|
|
157
|
+
* single node-owned relation: `columns` compare ordered-strict when both
|
|
158
|
+
* sides carry them; an exact receiver (`prefix === undefined`)
|
|
159
|
+
* byte-compares `expression ?? ''` and `where ?? ''`; a managed receiver
|
|
160
|
+
* never compares bodies (the wire-name hash already commits to them).
|
|
93
161
|
*/
|
|
94
162
|
isEqualTo(other: DiffableNode): boolean {
|
|
95
163
|
const node = blindCast<
|
|
@@ -97,21 +165,80 @@ export class SqlIndexIR extends SqlSchemaIRNode implements DiffableNode {
|
|
|
97
165
|
'every diff-tree node the differ pairs is a SqlSchemaIRNode'
|
|
98
166
|
>(other);
|
|
99
167
|
assertNode(node, 'SqlIndexIR', SqlIndexIR.is);
|
|
168
|
+
return this.contentEquals(node, {
|
|
169
|
+
columnPresence: 'when-both-defined',
|
|
170
|
+
bodies: this.prefix !== undefined ? 'ignored' : 'verbatim',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The single index content-equality relation — every comparer (the differ
|
|
176
|
+
* via {@link isEqualTo}, the planner's rename content-pairing) calls this
|
|
177
|
+
* with its mode-appropriate strictness rather than growing a parallel
|
|
178
|
+
* relation:
|
|
179
|
+
*
|
|
180
|
+
* - `columnPresence: 'when-both-defined'` (the differ's rule) compares
|
|
181
|
+
* the tuples ordered-strict only when both sides carry them — a paired
|
|
182
|
+
* node's identity already agreed, so a column node meeting an
|
|
183
|
+
* expression node skips the tuple.
|
|
184
|
+
* - `columnPresence: 'matching'` (the rename-pairing rule) additionally
|
|
185
|
+
* requires presence to agree: a column index never pairs an expression
|
|
186
|
+
* index.
|
|
187
|
+
* - `bodies: 'verbatim'` byte-compares `expression ?? ''` / `where ?? ''`
|
|
188
|
+
* (absent ≡ empty, no normalization — both sides are reprints in the
|
|
189
|
+
* supported flow); `bodies: 'ignored'` skips them (managed identity —
|
|
190
|
+
* the wire-name hash commits to the content).
|
|
191
|
+
*
|
|
192
|
+
* `unique` compares strictly; `type` and option VALUES compare through
|
|
193
|
+
* the named normalization seams below.
|
|
194
|
+
*/
|
|
195
|
+
contentEquals(
|
|
196
|
+
other: SqlIndexIR,
|
|
197
|
+
strictness: {
|
|
198
|
+
readonly columnPresence: 'when-both-defined' | 'matching';
|
|
199
|
+
readonly bodies: 'verbatim' | 'ignored';
|
|
200
|
+
},
|
|
201
|
+
): boolean {
|
|
202
|
+
const columnsEqual =
|
|
203
|
+
strictness.columnPresence === 'matching'
|
|
204
|
+
? (this.columns === undefined) === (other.columns === undefined) &&
|
|
205
|
+
(this.columns === undefined || isArrayEqual(this.columns, other.columns ?? []))
|
|
206
|
+
: this.columns === undefined ||
|
|
207
|
+
other.columns === undefined ||
|
|
208
|
+
isArrayEqual(this.columns, other.columns);
|
|
209
|
+
const structurallyEqual =
|
|
210
|
+
this.unique === other.unique &&
|
|
211
|
+
normalizeIndexType(this.type) === normalizeIndexType(other.type) &&
|
|
212
|
+
indexOptionsEqual(this.options, other.options) &&
|
|
213
|
+
columnsEqual;
|
|
214
|
+
if (!structurallyEqual) return false;
|
|
215
|
+
if (strictness.bodies === 'ignored') return true;
|
|
100
216
|
return (
|
|
101
|
-
this.
|
|
102
|
-
this.
|
|
103
|
-
indexOptionsLooselyEqual(this.options, node.options)
|
|
217
|
+
(this.expression ?? '') === (other.expression ?? '') &&
|
|
218
|
+
(this.where ?? '') === (other.where ?? '')
|
|
104
219
|
);
|
|
105
220
|
}
|
|
106
221
|
}
|
|
107
222
|
|
|
108
223
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
224
|
+
* Comparison-side normalization seam: the default access method (`btree` in
|
|
225
|
+
* every supported SQL target) compares as absent, so an authored
|
|
226
|
+
* `type: "btree"` and a default-method introspected index (whose type the
|
|
227
|
+
* adapter or constructor normalized away) are equal. Applied by
|
|
228
|
+
* {@link SqlIndexIR.contentEquals} only — the wire-name hash keeps the
|
|
229
|
+
* authored spelling.
|
|
230
|
+
*/
|
|
231
|
+
function normalizeIndexType(type: string | undefined): string | undefined {
|
|
232
|
+
return type === 'btree' ? undefined : type;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Option-bag equality: same key set, values compared through
|
|
237
|
+
* {@link normalizeIndexOptionValue} — Postgres introspection returns
|
|
238
|
+
* reloptions values as catalog-reprint strings (`'70'`, `'on'`) while
|
|
239
|
+
* contract option leaves are typed (number, boolean, string).
|
|
113
240
|
*/
|
|
114
|
-
function
|
|
241
|
+
function indexOptionsEqual(
|
|
115
242
|
a: Record<string, unknown> | undefined,
|
|
116
243
|
b: Record<string, unknown> | undefined,
|
|
117
244
|
): boolean {
|
|
@@ -123,7 +250,7 @@ function indexOptionsLooselyEqual(
|
|
|
123
250
|
}
|
|
124
251
|
if (aKeys.length === 0) return true;
|
|
125
252
|
for (const key of aKeys) {
|
|
126
|
-
if (
|
|
253
|
+
if (normalizeIndexOptionValue(a?.[key]) !== normalizeIndexOptionValue(b?.[key])) {
|
|
127
254
|
return false;
|
|
128
255
|
}
|
|
129
256
|
}
|
package/src/naming.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { structuredError } from '@prisma-next/utils/structured-error';
|
|
3
|
+
|
|
4
|
+
export function defaultIndexName(tableName: string, columns: readonly string[]): string {
|
|
5
|
+
return `${tableName}_${columns.join('_')}_idx`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface WireName {
|
|
9
|
+
/** The user-supplied part before the `_<8hex>` suffix. */
|
|
10
|
+
readonly prefix: string;
|
|
11
|
+
/** The 8-lowercase-hex content-hash suffix. */
|
|
12
|
+
readonly hash: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const WIRE_NAME_PATTERN = /^(.+)_([0-9a-f]{8})$/;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Assembles a wire name from its user-supplied prefix and its 8-hex
|
|
19
|
+
* content-hash suffix. This module owns the `<prefix>_<hash>` format on both
|
|
20
|
+
* sides — construction here and parsing in {@link parseWireName} — so the two
|
|
21
|
+
* never drift.
|
|
22
|
+
*/
|
|
23
|
+
export function formatWireName(prefix: string, hash: string): string {
|
|
24
|
+
return `${prefix}_${hash}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Splits a wire name (`<prefix>_<8hex>`) into its prefix and content-hash
|
|
29
|
+
* suffix. Returns `undefined` when the name does not follow the wire-name
|
|
30
|
+
* shape (e.g. an object created outside the toolchain) — callers treat such
|
|
31
|
+
* names as all-prefix. Consumed by introspection (prefix extraction) and by
|
|
32
|
+
* rename pairing (same hash, different prefix).
|
|
33
|
+
*/
|
|
34
|
+
export function parseWireName(name: string): WireName | undefined {
|
|
35
|
+
const match = WIRE_NAME_PATTERN.exec(name);
|
|
36
|
+
const prefix = match?.[1];
|
|
37
|
+
const hash = match?.[2];
|
|
38
|
+
if (prefix === undefined || hash === undefined) return undefined;
|
|
39
|
+
return { prefix, hash };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Stabilizes an authored SQL body (index expression, partial-index predicate,
|
|
44
|
+
* RLS policy predicate) for hashing: trim, and collapse runs of internal
|
|
45
|
+
* whitespace to a single space.
|
|
46
|
+
*
|
|
47
|
+
* This is deliberately minimal. The content hash is the equivalence relation
|
|
48
|
+
* for a wire-named object, and the wire name (prefix + hash) is the only
|
|
49
|
+
* thing ever compared — the hash is never recomputed from an introspected
|
|
50
|
+
* body, so there is no need to match the database's reprinted form. Minimal
|
|
51
|
+
* normalization also protects the no-collision property: aggressive rewriting
|
|
52
|
+
* (lowercasing, paren-stripping, cast-alias folding) risks collapsing two
|
|
53
|
+
* distinct bodies onto one hash.
|
|
54
|
+
*
|
|
55
|
+
* The normalizer is a stability commitment: any change re-suffixes all wire names.
|
|
56
|
+
*/
|
|
57
|
+
export function normalizeSqlBody(sql: string): string {
|
|
58
|
+
return sql.replace(/\s+/g, ' ').trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface IndexContentHashParts {
|
|
62
|
+
readonly expression?: string;
|
|
63
|
+
readonly where?: string;
|
|
64
|
+
readonly columns?: readonly string[];
|
|
65
|
+
readonly unique: boolean;
|
|
66
|
+
readonly type?: string;
|
|
67
|
+
readonly options?: Record<string, unknown>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Returns the first 8 lowercase hex characters of the SHA-256 digest over the
|
|
72
|
+
* canonical content tuple for an index:
|
|
73
|
+
*
|
|
74
|
+
* [normalizeSqlBody(expression), normalizeSqlBody(where), columns, unique, type, sortedOptions]
|
|
75
|
+
*
|
|
76
|
+
* Columns hash in authored order — column order is semantic in an index.
|
|
77
|
+
* Option values are `String()`-coerced (matching the loose option equality
|
|
78
|
+
* used for diffing) so a hash computed from typed contract values agrees with
|
|
79
|
+
* one recomputed from introspected reloptions strings. The prefix, schema,
|
|
80
|
+
* and table are excluded (they are orthogonal to index equivalence).
|
|
81
|
+
*
|
|
82
|
+
* The tuple order and encoding are a stability commitment with the same
|
|
83
|
+
* status as the RLS tuple: any change re-suffixes every wire name.
|
|
84
|
+
*/
|
|
85
|
+
/**
|
|
86
|
+
* Canonicalizes one index option VALUE to the `on`/`off` boolean spelling:
|
|
87
|
+
* JS booleans and the common catalog spellings (`pg_class.reloptions`
|
|
88
|
+
* stores whatever spelling the DDL used, so a live index may carry
|
|
89
|
+
* `'true'`/`'false'` or `'on'`/`'off'`) all map to one form; everything
|
|
90
|
+
* else via `String()` (fully specified for numbers, so no platform
|
|
91
|
+
* variance). Shared by the wire-name hash tuple, the node's option
|
|
92
|
+
* equality, and the DDL renderer, so an authored `{ fastupdate: true }`
|
|
93
|
+
* agrees with a live index created under any boolean spelling.
|
|
94
|
+
*/
|
|
95
|
+
export function normalizeIndexOptionValue(value: unknown): string {
|
|
96
|
+
if (value === true || value === 'true' || value === 'on') return 'on';
|
|
97
|
+
if (value === false || value === 'false' || value === 'off') return 'off';
|
|
98
|
+
return String(value);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function computeIndexContentHash(parts: IndexContentHashParts): string {
|
|
102
|
+
const sortedOptions = Object.entries(parts.options ?? {})
|
|
103
|
+
.map(([key, value]): readonly [string, string] => [key, normalizeIndexOptionValue(value)])
|
|
104
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
105
|
+
|
|
106
|
+
const tuple = JSON.stringify([
|
|
107
|
+
normalizeSqlBody(parts.expression ?? ''),
|
|
108
|
+
normalizeSqlBody(parts.where ?? ''),
|
|
109
|
+
parts.columns ?? [],
|
|
110
|
+
parts.unique,
|
|
111
|
+
parts.type ?? '',
|
|
112
|
+
sortedOptions,
|
|
113
|
+
]);
|
|
114
|
+
return createHash('sha256').update(tuple).digest('hex').slice(0, 8);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Postgres identifiers cap at 63 characters and the wire name appends a
|
|
119
|
+
* 9-character `_<8hex>` suffix, so an authored prefix is bounded at 54.
|
|
120
|
+
*/
|
|
121
|
+
export const WIRE_NAME_PREFIX_MAX_LENGTH = 54;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Rejects a wire-name prefix over {@link WIRE_NAME_PREFIX_MAX_LENGTH}.
|
|
125
|
+
* `subject` opens the error message (e.g. `defineContract: policy prefix`).
|
|
126
|
+
*/
|
|
127
|
+
export function assertWireNamePrefixLength(prefix: string, subject: string): void {
|
|
128
|
+
if (prefix.length > WIRE_NAME_PREFIX_MAX_LENGTH) {
|
|
129
|
+
throw structuredError(
|
|
130
|
+
'CONTRACT.WIRE_NAME_PREFIX_TOO_LONG',
|
|
131
|
+
`${subject} "${prefix}" exceeds the ${WIRE_NAME_PREFIX_MAX_LENGTH}-character maximum (Postgres identifiers cap at 63 characters and the wire name appends a 9-character hash suffix).`,
|
|
132
|
+
{ meta: { prefix, maxLength: WIRE_NAME_PREFIX_MAX_LENGTH } },
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -33,7 +33,10 @@ export {
|
|
|
33
33
|
type SqlForeignKeyIRInput,
|
|
34
34
|
type SqlReferentialAction,
|
|
35
35
|
} from './ir/sql-foreign-key-ir';
|
|
36
|
-
export {
|
|
36
|
+
export {
|
|
37
|
+
SqlIndexIR,
|
|
38
|
+
type SqlIndexIRInput,
|
|
39
|
+
} from './ir/sql-index-ir';
|
|
37
40
|
export { SqlSchemaIR, type SqlSchemaIRInput } from './ir/sql-schema-ir';
|
|
38
41
|
export { assertNode, defineNonEnumerable, SqlSchemaIRNode } from './ir/sql-schema-ir-node';
|
|
39
42
|
export { SqlTableIR, type SqlTableIRInput } from './ir/sql-table-ir';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"naming.mjs","names":[],"sources":["../../src/exports/naming.ts"],"sourcesContent":["export function defaultIndexName(tableName: string, columns: readonly string[]): string {\n return `${tableName}_${columns.join('_')}_idx`;\n}\n"],"mappings":";AAAA,SAAgB,iBAAiB,WAAmB,SAAoC;CACtF,OAAO,GAAG,UAAU,GAAG,QAAQ,KAAK,GAAG,EAAE;AAC3C"}
|