@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 CHANGED
@@ -54,6 +54,8 @@ This package defines the core types for the SQL Schema IR, a target-agnostic rep
54
54
 
55
55
  3. **Shared Plane**: This package is in the **shared plane**, meaning it can be safely imported by both migration-plane (verification, migration planning) and runtime-plane code.
56
56
 
57
+ 4. **Name-identified indexes**: `SqlIndexIR` is identified by its full physical name (its diff-tree `id` is the name, so same-column-tuple siblings and expression indexes are representable). Managed indexes carry a `prefix` plus a content-hash wire name; the shared naming helpers (`formatWireName`, `parseWireName`, `normalizeSqlBody`, `computeIndexContentHash`) live in `@prisma-next/sql-schema-ir/naming`.
58
+
57
59
  ## Usage
58
60
 
59
61
  ### Basic Usage
@@ -1,5 +1,87 @@
1
- //#region src/exports/naming.d.ts
1
+ //#region src/naming.d.ts
2
2
  declare function defaultIndexName(tableName: string, columns: readonly string[]): string;
3
+ interface WireName {
4
+ /** The user-supplied part before the `_<8hex>` suffix. */
5
+ readonly prefix: string;
6
+ /** The 8-lowercase-hex content-hash suffix. */
7
+ readonly hash: string;
8
+ }
9
+ /**
10
+ * Assembles a wire name from its user-supplied prefix and its 8-hex
11
+ * content-hash suffix. This module owns the `<prefix>_<hash>` format on both
12
+ * sides — construction here and parsing in {@link parseWireName} — so the two
13
+ * never drift.
14
+ */
15
+ declare function formatWireName(prefix: string, hash: string): string;
16
+ /**
17
+ * Splits a wire name (`<prefix>_<8hex>`) into its prefix and content-hash
18
+ * suffix. Returns `undefined` when the name does not follow the wire-name
19
+ * shape (e.g. an object created outside the toolchain) — callers treat such
20
+ * names as all-prefix. Consumed by introspection (prefix extraction) and by
21
+ * rename pairing (same hash, different prefix).
22
+ */
23
+ declare function parseWireName(name: string): WireName | undefined;
24
+ /**
25
+ * Stabilizes an authored SQL body (index expression, partial-index predicate,
26
+ * RLS policy predicate) for hashing: trim, and collapse runs of internal
27
+ * whitespace to a single space.
28
+ *
29
+ * This is deliberately minimal. The content hash is the equivalence relation
30
+ * for a wire-named object, and the wire name (prefix + hash) is the only
31
+ * thing ever compared — the hash is never recomputed from an introspected
32
+ * body, so there is no need to match the database's reprinted form. Minimal
33
+ * normalization also protects the no-collision property: aggressive rewriting
34
+ * (lowercasing, paren-stripping, cast-alias folding) risks collapsing two
35
+ * distinct bodies onto one hash.
36
+ *
37
+ * The normalizer is a stability commitment: any change re-suffixes all wire names.
38
+ */
39
+ declare function normalizeSqlBody(sql: string): string;
40
+ interface IndexContentHashParts {
41
+ readonly expression?: string;
42
+ readonly where?: string;
43
+ readonly columns?: readonly string[];
44
+ readonly unique: boolean;
45
+ readonly type?: string;
46
+ readonly options?: Record<string, unknown>;
47
+ }
48
+ /**
49
+ * Returns the first 8 lowercase hex characters of the SHA-256 digest over the
50
+ * canonical content tuple for an index:
51
+ *
52
+ * [normalizeSqlBody(expression), normalizeSqlBody(where), columns, unique, type, sortedOptions]
53
+ *
54
+ * Columns hash in authored order — column order is semantic in an index.
55
+ * Option values are `String()`-coerced (matching the loose option equality
56
+ * used for diffing) so a hash computed from typed contract values agrees with
57
+ * one recomputed from introspected reloptions strings. The prefix, schema,
58
+ * and table are excluded (they are orthogonal to index equivalence).
59
+ *
60
+ * The tuple order and encoding are a stability commitment with the same
61
+ * status as the RLS tuple: any change re-suffixes every wire name.
62
+ */
63
+ /**
64
+ * Canonicalizes one index option VALUE to the `on`/`off` boolean spelling:
65
+ * JS booleans and the common catalog spellings (`pg_class.reloptions`
66
+ * stores whatever spelling the DDL used, so a live index may carry
67
+ * `'true'`/`'false'` or `'on'`/`'off'`) all map to one form; everything
68
+ * else via `String()` (fully specified for numbers, so no platform
69
+ * variance). Shared by the wire-name hash tuple, the node's option
70
+ * equality, and the DDL renderer, so an authored `{ fastupdate: true }`
71
+ * agrees with a live index created under any boolean spelling.
72
+ */
73
+ declare function normalizeIndexOptionValue(value: unknown): string;
74
+ declare function computeIndexContentHash(parts: IndexContentHashParts): string;
75
+ /**
76
+ * Postgres identifiers cap at 63 characters and the wire name appends a
77
+ * 9-character `_<8hex>` suffix, so an authored prefix is bounded at 54.
78
+ */
79
+ declare const WIRE_NAME_PREFIX_MAX_LENGTH = 54;
80
+ /**
81
+ * Rejects a wire-name prefix over {@link WIRE_NAME_PREFIX_MAX_LENGTH}.
82
+ * `subject` opens the error message (e.g. `defineContract: policy prefix`).
83
+ */
84
+ declare function assertWireNamePrefixLength(prefix: string, subject: string): void;
3
85
  //#endregion
4
- export { defaultIndexName };
86
+ export { type IndexContentHashParts, WIRE_NAME_PREFIX_MAX_LENGTH, type WireName, assertWireNamePrefixLength, computeIndexContentHash, defaultIndexName, formatWireName, normalizeIndexOptionValue, normalizeSqlBody, parseWireName };
5
87
  //# sourceMappingURL=naming.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"naming.d.mts","names":[],"sources":["../../src/exports/naming.ts"],"mappings":";iBAAgB,iBAAiB,mBAAmB"}
1
+ {"version":3,"file":"naming.d.mts","names":[],"sources":["../../src/naming.ts"],"mappings":";iBAGgB,iBAAiB,mBAAmB;UAInC;;WAEN;;WAEA;;;;;;;;iBAWK,eAAe,gBAAgB;;;;;;;;iBAW/B,cAAc,eAAe;;;;;;;;;;;;;;;;iBAuB7B,iBAAiB;UAIhB;WACN;WACA;WACA;WACA;WACA;WACA,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA4BL,0BAA0B;iBAM1B,wBAAwB,OAAO;;;;;cAoBlC;;;;;iBAMG,2BAA2B,gBAAgB"}
@@ -1,8 +1,2 @@
1
- //#region src/exports/naming.ts
2
- function defaultIndexName(tableName, columns) {
3
- return `${tableName}_${columns.join("_")}_idx`;
4
- }
5
- //#endregion
6
- export { defaultIndexName };
7
-
8
- //# sourceMappingURL=naming.mjs.map
1
+ import { a as formatWireName, c as parseWireName, i as defaultIndexName, n as assertWireNamePrefixLength, o as normalizeIndexOptionValue, r as computeIndexContentHash, s as normalizeSqlBody, t as WIRE_NAME_PREFIX_MAX_LENGTH } from "../naming-BD7Y-Fq1.mjs";
2
+ export { WIRE_NAME_PREFIX_MAX_LENGTH, assertWireNamePrefixLength, computeIndexContentHash, defaultIndexName, formatWireName, normalizeIndexOptionValue, normalizeSqlBody, parseWireName };
@@ -1,2 +1,2 @@
1
- import { C as relationalNodeGranularity, D as assertNode, E as SqlSchemaIRNode, O as defineNonEnumerable, S as relationalNodeEntityKind, T as PrimaryKeyInput, _ as SqlColumnDefaultIR, a as SqlTableIR, b as SqlCheckConstraintIRInput, c as SqlUniqueIRInput, d as SqlForeignKeyIR, f as SqlForeignKeyIRInput, g as SqlColumnIRInput, h as SqlColumnIR, i as SqlSchemaIRInput, l as SqlIndexIR, m as SqlAnnotations, n as SqlTypeMetadataRegistry, o as SqlTableIRInput, p as SqlReferentialAction, r as SqlSchemaIR, s as SqlUniqueIR, t as SqlTypeMetadata, u as SqlIndexIRInput, v as SqlColumnDefaultIRInput, w as PrimaryKey, x as RelationalSchemaNodeKind, y as SqlCheckConstraintIR } from "../types-Bnh1XTEx.mjs";
1
+ import { C as relationalNodeGranularity, D as assertNode, E as SqlSchemaIRNode, O as defineNonEnumerable, S as relationalNodeEntityKind, T as PrimaryKeyInput, _ as SqlColumnDefaultIR, a as SqlTableIR, b as SqlCheckConstraintIRInput, c as SqlUniqueIRInput, d as SqlForeignKeyIR, f as SqlForeignKeyIRInput, g as SqlColumnIRInput, h as SqlColumnIR, i as SqlSchemaIRInput, l as SqlIndexIR, m as SqlAnnotations, n as SqlTypeMetadataRegistry, o as SqlTableIRInput, p as SqlReferentialAction, r as SqlSchemaIR, s as SqlUniqueIR, t as SqlTypeMetadata, u as SqlIndexIRInput, v as SqlColumnDefaultIRInput, w as PrimaryKey, x as RelationalSchemaNodeKind, y as SqlCheckConstraintIR } from "../types-DzLk5eOU.mjs";
2
2
  export { PrimaryKey, type PrimaryKeyInput, RelationalSchemaNodeKind, type SqlAnnotations, SqlCheckConstraintIR, type SqlCheckConstraintIRInput, SqlColumnDefaultIR, type SqlColumnDefaultIRInput, SqlColumnIR, type SqlColumnIRInput, SqlForeignKeyIR, type SqlForeignKeyIRInput, SqlIndexIR, type SqlIndexIRInput, type SqlReferentialAction, SqlSchemaIR, type SqlSchemaIRInput, SqlSchemaIRNode, SqlTableIR, type SqlTableIRInput, type SqlTypeMetadata, type SqlTypeMetadataRegistry, SqlUniqueIR, type SqlUniqueIRInput, assertNode, defineNonEnumerable, relationalNodeEntityKind, relationalNodeGranularity };
@@ -1,2 +1,2 @@
1
- import { a as SqlForeignKeyIR, c as SqlCheckConstraintIR, d as assertNode, f as defineNonEnumerable, h as relationalNodeGranularity, i as SqlIndexIR, l as PrimaryKey, m as relationalNodeEntityKind, n as SqlTableIR, o as SqlColumnIR, p as RelationalSchemaNodeKind, r as SqlUniqueIR, s as SqlColumnDefaultIR, t as SqlSchemaIR, u as SqlSchemaIRNode } from "../types-pxgoVAJq.mjs";
1
+ import { a as SqlForeignKeyIR, c as SqlCheckConstraintIR, d as assertNode, f as defineNonEnumerable, h as relationalNodeGranularity, i as SqlIndexIR, l as PrimaryKey, m as relationalNodeEntityKind, n as SqlTableIR, o as SqlColumnIR, p as RelationalSchemaNodeKind, r as SqlUniqueIR, s as SqlColumnDefaultIR, t as SqlSchemaIR, u as SqlSchemaIRNode } from "../types-Btj35AIt.mjs";
2
2
  export { PrimaryKey, RelationalSchemaNodeKind, SqlCheckConstraintIR, SqlColumnDefaultIR, SqlColumnIR, SqlForeignKeyIR, SqlIndexIR, SqlSchemaIR, SqlSchemaIRNode, SqlTableIR, SqlUniqueIR, assertNode, defineNonEnumerable, relationalNodeEntityKind, relationalNodeGranularity };
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { C as relationalNodeGranularity, D as assertNode, E as SqlSchemaIRNode, O as defineNonEnumerable, S as relationalNodeEntityKind, T as PrimaryKeyInput, _ as SqlColumnDefaultIR, a as SqlTableIR, b as SqlCheckConstraintIRInput, c as SqlUniqueIRInput, d as SqlForeignKeyIR, f as SqlForeignKeyIRInput, g as SqlColumnIRInput, h as SqlColumnIR, i as SqlSchemaIRInput, l as SqlIndexIR, m as SqlAnnotations, n as SqlTypeMetadataRegistry, o as SqlTableIRInput, p as SqlReferentialAction, r as SqlSchemaIR, s as SqlUniqueIR, t as SqlTypeMetadata, u as SqlIndexIRInput, v as SqlColumnDefaultIRInput, w as PrimaryKey, x as RelationalSchemaNodeKind, y as SqlCheckConstraintIR } from "./types-Bnh1XTEx.mjs";
1
+ import { C as relationalNodeGranularity, D as assertNode, E as SqlSchemaIRNode, O as defineNonEnumerable, S as relationalNodeEntityKind, T as PrimaryKeyInput, _ as SqlColumnDefaultIR, a as SqlTableIR, b as SqlCheckConstraintIRInput, c as SqlUniqueIRInput, d as SqlForeignKeyIR, f as SqlForeignKeyIRInput, g as SqlColumnIRInput, h as SqlColumnIR, i as SqlSchemaIRInput, l as SqlIndexIR, m as SqlAnnotations, n as SqlTypeMetadataRegistry, o as SqlTableIRInput, p as SqlReferentialAction, r as SqlSchemaIR, s as SqlUniqueIR, t as SqlTypeMetadata, u as SqlIndexIRInput, v as SqlColumnDefaultIRInput, w as PrimaryKey, x as RelationalSchemaNodeKind, y as SqlCheckConstraintIR } from "./types-DzLk5eOU.mjs";
2
2
  export { PrimaryKey, type PrimaryKeyInput, RelationalSchemaNodeKind, type SqlAnnotations, SqlCheckConstraintIR, type SqlCheckConstraintIRInput, SqlColumnDefaultIR, type SqlColumnDefaultIRInput, SqlColumnIR, type SqlColumnIRInput, SqlForeignKeyIR, type SqlForeignKeyIRInput, SqlIndexIR, type SqlIndexIRInput, type SqlReferentialAction, SqlSchemaIR, type SqlSchemaIRInput, SqlSchemaIRNode, SqlTableIR, type SqlTableIRInput, type SqlTypeMetadata, type SqlTypeMetadataRegistry, SqlUniqueIR, type SqlUniqueIRInput, assertNode, defineNonEnumerable, relationalNodeEntityKind, relationalNodeGranularity };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as SqlForeignKeyIR, c as SqlCheckConstraintIR, d as assertNode, f as defineNonEnumerable, h as relationalNodeGranularity, i as SqlIndexIR, l as PrimaryKey, m as relationalNodeEntityKind, n as SqlTableIR, o as SqlColumnIR, p as RelationalSchemaNodeKind, r as SqlUniqueIR, s as SqlColumnDefaultIR, t as SqlSchemaIR, u as SqlSchemaIRNode } from "./types-pxgoVAJq.mjs";
1
+ import { a as SqlForeignKeyIR, c as SqlCheckConstraintIR, d as assertNode, f as defineNonEnumerable, h as relationalNodeGranularity, i as SqlIndexIR, l as PrimaryKey, m as relationalNodeEntityKind, n as SqlTableIR, o as SqlColumnIR, p as RelationalSchemaNodeKind, r as SqlUniqueIR, s as SqlColumnDefaultIR, t as SqlSchemaIR, u as SqlSchemaIRNode } from "./types-Btj35AIt.mjs";
2
2
  export { PrimaryKey, RelationalSchemaNodeKind, SqlCheckConstraintIR, SqlColumnDefaultIR, SqlColumnIR, SqlForeignKeyIR, SqlIndexIR, SqlSchemaIR, SqlSchemaIRNode, SqlTableIR, SqlUniqueIR, assertNode, defineNonEnumerable, relationalNodeEntityKind, relationalNodeGranularity };
@@ -0,0 +1,112 @@
1
+ import { createHash } from "node:crypto";
2
+ import { structuredError } from "@prisma-next/utils/structured-error";
3
+ //#region src/naming.ts
4
+ function defaultIndexName(tableName, columns) {
5
+ return `${tableName}_${columns.join("_")}_idx`;
6
+ }
7
+ const WIRE_NAME_PATTERN = /^(.+)_([0-9a-f]{8})$/;
8
+ /**
9
+ * Assembles a wire name from its user-supplied prefix and its 8-hex
10
+ * content-hash suffix. This module owns the `<prefix>_<hash>` format on both
11
+ * sides — construction here and parsing in {@link parseWireName} — so the two
12
+ * never drift.
13
+ */
14
+ function formatWireName(prefix, hash) {
15
+ return `${prefix}_${hash}`;
16
+ }
17
+ /**
18
+ * Splits a wire name (`<prefix>_<8hex>`) into its prefix and content-hash
19
+ * suffix. Returns `undefined` when the name does not follow the wire-name
20
+ * shape (e.g. an object created outside the toolchain) — callers treat such
21
+ * names as all-prefix. Consumed by introspection (prefix extraction) and by
22
+ * rename pairing (same hash, different prefix).
23
+ */
24
+ function parseWireName(name) {
25
+ const match = WIRE_NAME_PATTERN.exec(name);
26
+ const prefix = match?.[1];
27
+ const hash = match?.[2];
28
+ if (prefix === void 0 || hash === void 0) return void 0;
29
+ return {
30
+ prefix,
31
+ hash
32
+ };
33
+ }
34
+ /**
35
+ * Stabilizes an authored SQL body (index expression, partial-index predicate,
36
+ * RLS policy predicate) for hashing: trim, and collapse runs of internal
37
+ * whitespace to a single space.
38
+ *
39
+ * This is deliberately minimal. The content hash is the equivalence relation
40
+ * for a wire-named object, and the wire name (prefix + hash) is the only
41
+ * thing ever compared — the hash is never recomputed from an introspected
42
+ * body, so there is no need to match the database's reprinted form. Minimal
43
+ * normalization also protects the no-collision property: aggressive rewriting
44
+ * (lowercasing, paren-stripping, cast-alias folding) risks collapsing two
45
+ * distinct bodies onto one hash.
46
+ *
47
+ * The normalizer is a stability commitment: any change re-suffixes all wire names.
48
+ */
49
+ function normalizeSqlBody(sql) {
50
+ return sql.replace(/\s+/g, " ").trim();
51
+ }
52
+ /**
53
+ * Returns the first 8 lowercase hex characters of the SHA-256 digest over the
54
+ * canonical content tuple for an index:
55
+ *
56
+ * [normalizeSqlBody(expression), normalizeSqlBody(where), columns, unique, type, sortedOptions]
57
+ *
58
+ * Columns hash in authored order — column order is semantic in an index.
59
+ * Option values are `String()`-coerced (matching the loose option equality
60
+ * used for diffing) so a hash computed from typed contract values agrees with
61
+ * one recomputed from introspected reloptions strings. The prefix, schema,
62
+ * and table are excluded (they are orthogonal to index equivalence).
63
+ *
64
+ * The tuple order and encoding are a stability commitment with the same
65
+ * status as the RLS tuple: any change re-suffixes every wire name.
66
+ */
67
+ /**
68
+ * Canonicalizes one index option VALUE to the `on`/`off` boolean spelling:
69
+ * JS booleans and the common catalog spellings (`pg_class.reloptions`
70
+ * stores whatever spelling the DDL used, so a live index may carry
71
+ * `'true'`/`'false'` or `'on'`/`'off'`) all map to one form; everything
72
+ * else via `String()` (fully specified for numbers, so no platform
73
+ * variance). Shared by the wire-name hash tuple, the node's option
74
+ * equality, and the DDL renderer, so an authored `{ fastupdate: true }`
75
+ * agrees with a live index created under any boolean spelling.
76
+ */
77
+ function normalizeIndexOptionValue(value) {
78
+ if (value === true || value === "true" || value === "on") return "on";
79
+ if (value === false || value === "false" || value === "off") return "off";
80
+ return String(value);
81
+ }
82
+ function computeIndexContentHash(parts) {
83
+ const sortedOptions = Object.entries(parts.options ?? {}).map(([key, value]) => [key, normalizeIndexOptionValue(value)]).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
84
+ const tuple = JSON.stringify([
85
+ normalizeSqlBody(parts.expression ?? ""),
86
+ normalizeSqlBody(parts.where ?? ""),
87
+ parts.columns ?? [],
88
+ parts.unique,
89
+ parts.type ?? "",
90
+ sortedOptions
91
+ ]);
92
+ return createHash("sha256").update(tuple).digest("hex").slice(0, 8);
93
+ }
94
+ /**
95
+ * Postgres identifiers cap at 63 characters and the wire name appends a
96
+ * 9-character `_<8hex>` suffix, so an authored prefix is bounded at 54.
97
+ */
98
+ const WIRE_NAME_PREFIX_MAX_LENGTH = 54;
99
+ /**
100
+ * Rejects a wire-name prefix over {@link WIRE_NAME_PREFIX_MAX_LENGTH}.
101
+ * `subject` opens the error message (e.g. `defineContract: policy prefix`).
102
+ */
103
+ function assertWireNamePrefixLength(prefix, subject) {
104
+ if (prefix.length > 54) throw structuredError("CONTRACT.WIRE_NAME_PREFIX_TOO_LONG", `${subject} "${prefix}" exceeds the 54-character maximum (Postgres identifiers cap at 63 characters and the wire name appends a 9-character hash suffix).`, { meta: {
105
+ prefix,
106
+ maxLength: 54
107
+ } });
108
+ }
109
+ //#endregion
110
+ export { formatWireName as a, parseWireName as c, defaultIndexName as i, assertWireNamePrefixLength as n, normalizeIndexOptionValue as o, computeIndexContentHash as r, normalizeSqlBody as s, WIRE_NAME_PREFIX_MAX_LENGTH as t };
111
+
112
+ //# sourceMappingURL=naming-BD7Y-Fq1.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"naming-BD7Y-Fq1.mjs","names":[],"sources":["../src/naming.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { structuredError } from '@prisma-next/utils/structured-error';\n\nexport function defaultIndexName(tableName: string, columns: readonly string[]): string {\n return `${tableName}_${columns.join('_')}_idx`;\n}\n\nexport interface WireName {\n /** The user-supplied part before the `_<8hex>` suffix. */\n readonly prefix: string;\n /** The 8-lowercase-hex content-hash suffix. */\n readonly hash: string;\n}\n\nconst WIRE_NAME_PATTERN = /^(.+)_([0-9a-f]{8})$/;\n\n/**\n * Assembles a wire name from its user-supplied prefix and its 8-hex\n * content-hash suffix. This module owns the `<prefix>_<hash>` format on both\n * sides — construction here and parsing in {@link parseWireName} — so the two\n * never drift.\n */\nexport function formatWireName(prefix: string, hash: string): string {\n return `${prefix}_${hash}`;\n}\n\n/**\n * Splits a wire name (`<prefix>_<8hex>`) into its prefix and content-hash\n * suffix. Returns `undefined` when the name does not follow the wire-name\n * shape (e.g. an object created outside the toolchain) — callers treat such\n * names as all-prefix. Consumed by introspection (prefix extraction) and by\n * rename pairing (same hash, different prefix).\n */\nexport function parseWireName(name: string): WireName | undefined {\n const match = WIRE_NAME_PATTERN.exec(name);\n const prefix = match?.[1];\n const hash = match?.[2];\n if (prefix === undefined || hash === undefined) return undefined;\n return { prefix, hash };\n}\n\n/**\n * Stabilizes an authored SQL body (index expression, partial-index predicate,\n * RLS policy predicate) for hashing: trim, and collapse runs of internal\n * whitespace to a single space.\n *\n * This is deliberately minimal. The content hash is the equivalence relation\n * for a wire-named object, and the wire name (prefix + hash) is the only\n * thing ever compared — the hash is never recomputed from an introspected\n * body, so there is no need to match the database's reprinted form. Minimal\n * normalization also protects the no-collision property: aggressive rewriting\n * (lowercasing, paren-stripping, cast-alias folding) risks collapsing two\n * distinct bodies onto one hash.\n *\n * The normalizer is a stability commitment: any change re-suffixes all wire names.\n */\nexport function normalizeSqlBody(sql: string): string {\n return sql.replace(/\\s+/g, ' ').trim();\n}\n\nexport interface IndexContentHashParts {\n readonly expression?: string;\n readonly where?: string;\n readonly columns?: readonly string[];\n readonly unique: boolean;\n readonly type?: string;\n readonly options?: Record<string, unknown>;\n}\n\n/**\n * Returns the first 8 lowercase hex characters of the SHA-256 digest over the\n * canonical content tuple for an index:\n *\n * [normalizeSqlBody(expression), normalizeSqlBody(where), columns, unique, type, sortedOptions]\n *\n * Columns hash in authored order — column order is semantic in an index.\n * Option values are `String()`-coerced (matching the loose option equality\n * used for diffing) so a hash computed from typed contract values agrees with\n * one recomputed from introspected reloptions strings. The prefix, schema,\n * and table are excluded (they are orthogonal to index equivalence).\n *\n * The tuple order and encoding are a stability commitment with the same\n * status as the RLS tuple: any change re-suffixes every wire name.\n */\n/**\n * Canonicalizes one index option VALUE to the `on`/`off` boolean spelling:\n * JS booleans and the common catalog spellings (`pg_class.reloptions`\n * stores whatever spelling the DDL used, so a live index may carry\n * `'true'`/`'false'` or `'on'`/`'off'`) all map to one form; everything\n * else via `String()` (fully specified for numbers, so no platform\n * variance). Shared by the wire-name hash tuple, the node's option\n * equality, and the DDL renderer, so an authored `{ fastupdate: true }`\n * agrees with a live index created under any boolean spelling.\n */\nexport function normalizeIndexOptionValue(value: unknown): string {\n if (value === true || value === 'true' || value === 'on') return 'on';\n if (value === false || value === 'false' || value === 'off') return 'off';\n return String(value);\n}\n\nexport function computeIndexContentHash(parts: IndexContentHashParts): string {\n const sortedOptions = Object.entries(parts.options ?? {})\n .map(([key, value]): readonly [string, string] => [key, normalizeIndexOptionValue(value)])\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n\n const tuple = JSON.stringify([\n normalizeSqlBody(parts.expression ?? ''),\n normalizeSqlBody(parts.where ?? ''),\n parts.columns ?? [],\n parts.unique,\n parts.type ?? '',\n sortedOptions,\n ]);\n return createHash('sha256').update(tuple).digest('hex').slice(0, 8);\n}\n\n/**\n * Postgres identifiers cap at 63 characters and the wire name appends a\n * 9-character `_<8hex>` suffix, so an authored prefix is bounded at 54.\n */\nexport const WIRE_NAME_PREFIX_MAX_LENGTH = 54;\n\n/**\n * Rejects a wire-name prefix over {@link WIRE_NAME_PREFIX_MAX_LENGTH}.\n * `subject` opens the error message (e.g. `defineContract: policy prefix`).\n */\nexport function assertWireNamePrefixLength(prefix: string, subject: string): void {\n if (prefix.length > WIRE_NAME_PREFIX_MAX_LENGTH) {\n throw structuredError(\n 'CONTRACT.WIRE_NAME_PREFIX_TOO_LONG',\n `${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).`,\n { meta: { prefix, maxLength: WIRE_NAME_PREFIX_MAX_LENGTH } },\n );\n }\n}\n"],"mappings":";;;AAGA,SAAgB,iBAAiB,WAAmB,SAAoC;CACtF,OAAO,GAAG,UAAU,GAAG,QAAQ,KAAK,GAAG,EAAE;AAC3C;AASA,MAAM,oBAAoB;;;;;;;AAQ1B,SAAgB,eAAe,QAAgB,MAAsB;CACnE,OAAO,GAAG,OAAO,GAAG;AACtB;;;;;;;;AASA,SAAgB,cAAc,MAAoC;CAChE,MAAM,QAAQ,kBAAkB,KAAK,IAAI;CACzC,MAAM,SAAS,QAAQ;CACvB,MAAM,OAAO,QAAQ;CACrB,IAAI,WAAW,KAAA,KAAa,SAAS,KAAA,GAAW,OAAO,KAAA;CACvD,OAAO;EAAE;EAAQ;CAAK;AACxB;;;;;;;;;;;;;;;;AAiBA,SAAgB,iBAAiB,KAAqB;CACpD,OAAO,IAAI,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK;AACvC;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,0BAA0B,OAAwB;CAChE,IAAI,UAAU,QAAQ,UAAU,UAAU,UAAU,MAAM,OAAO;CACjE,IAAI,UAAU,SAAS,UAAU,WAAW,UAAU,OAAO,OAAO;CACpE,OAAO,OAAO,KAAK;AACrB;AAEA,SAAgB,wBAAwB,OAAsC;CAC5E,MAAM,gBAAgB,OAAO,QAAQ,MAAM,WAAW,CAAC,CAAC,CAAC,CACtD,KAAK,CAAC,KAAK,WAAsC,CAAC,KAAK,0BAA0B,KAAK,CAAC,CAAC,CAAC,CACzF,MAAM,CAAC,IAAI,CAAC,OAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CAAE;CAElD,MAAM,QAAQ,KAAK,UAAU;EAC3B,iBAAiB,MAAM,cAAc,EAAE;EACvC,iBAAiB,MAAM,SAAS,EAAE;EAClC,MAAM,WAAW,CAAC;EAClB,MAAM;EACN,MAAM,QAAQ;EACd;CACF,CAAC;CACD,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;AACpE;;;;;AAMA,MAAa,8BAA8B;;;;;AAM3C,SAAgB,2BAA2B,QAAgB,SAAuB;CAChF,IAAI,OAAO,SAAA,IACT,MAAM,gBACJ,sCACA,GAAG,QAAQ,IAAI,OAAO,sIACtB,EAAE,MAAM;EAAE;EAAQ,WAAA;CAAuC,EAAE,CAC7D;AAEJ"}
@@ -1,7 +1,10 @@
1
+ import { o as normalizeIndexOptionValue } from "./naming-BD7Y-Fq1.mjs";
1
2
  import { IRNodeBase, freezeNode } from "@prisma-next/framework-components/ir";
2
3
  import { blindCast } from "@prisma-next/utils/casts";
3
4
  import { canonicalStringify } from "@prisma-next/utils/canonical-stringify";
4
5
  import { ifDefined } from "@prisma-next/utils/defined";
6
+ import { isArrayEqual } from "@prisma-next/utils/array-equal";
7
+ import { InternalError } from "@prisma-next/utils/internal-error";
5
8
  //#region src/ir/schema-node-kinds.ts
6
9
  /**
7
10
  * The `nodeKind` discriminant for each relational schema-diff leaf node.
@@ -472,41 +475,49 @@ function referentialActionMatches(expected, actual) {
472
475
  * verifier needs to distinguish them when comparing to the Contract.
473
476
  *
474
477
  * Implements `DiffableNode` so an index is directly a table's diff-tree
475
- * child. Indexes are frequently unnamed, so `id` is derived from the column
476
- * tuplethe same tuple that makes two indexes the same index, so it
477
- * doubles as the pairing key. `isEqualTo` is symmetric structural equality
478
- * on the remaining attributes: `unique`, `type`, and `options`. A unique
479
- * index and a non-unique index on the same columns are different objects
480
- * and are not equal — there is no "stronger satisfies weaker".
478
+ * child. Indexes are name-identified: every index contract-derived or
479
+ * introspectedcarries its full physical name, and `id` is that name.
480
+ * Names are catalog-unique per schema, so two indexes legitimately sharing
481
+ * one column tuple (a unique index beside a redundant plain index) are two
482
+ * distinct siblings, and expression indexes need no column tuple at all.
481
483
  *
482
- * Deliberately column-tuple-only (not `unique`): a contract `@@index`
483
- * (non-unique) must still pair against a live unique index of the same
484
- * columns so the differ can report the mismatch as an incompatible index
485
- * change rather than a spurious missing+extra pair see
486
- * `planner.unique-index-structural.test.ts`. The corollary is that two
487
- * indexes legitimately coexisting on one table with the *same* column tuple
488
- * (e.g. a unique index and a redundant plain index Postgres has no problem
489
- * hosting side by side) collide on this id; the postgres control adapter's
490
- * introspection keeps only one such index per table+column-tuple rather
491
- * than handing the differ two same-tree siblings it cannot represent.
484
+ * `isEqualTo` is selected by the receiver (the differ always calls
485
+ * `expected.isEqualTo(actual)`) and delegates to {@link contentEquals}
486
+ * the single node-owned content relation: both modes compare `unique`
487
+ * strict, `type` and option values through the named normalization seams,
488
+ * and `columns` ordered-strict when both sides carry them; an exact-named
489
+ * receiver (`prefix === undefined`) additionally byte-compares
490
+ * `expression`/`where` (both sides are reprints in the supported flow
491
+ * normalizing would only mask real drift); a managed receiver never
492
+ * compares bodies (the wire-name hash already commits to them).
493
+ *
494
+ * `expression`, `where`, and `unique` are genuine SQL-family attributes —
495
+ * functional and partial indexes are standard SQL that any SQL target may
496
+ * introspect, so the family node must represent them; a target declining
497
+ * to author them is a capability decision, not target-specificity.
492
498
  */
493
499
  var SqlIndexIR = class SqlIndexIR extends SqlSchemaIRNode {
494
500
  nodeKind = RelationalSchemaNodeKind.index;
495
- columns;
501
+ name;
496
502
  unique;
497
503
  constructor(input) {
498
504
  super();
499
- this.columns = input.columns;
505
+ if (input.columns === void 0 === (input.expression === void 0)) throw new InternalError(`SqlIndexIR "${input.name}": exactly one of columns or expression must be set.`);
506
+ this.name = input.name;
500
507
  this.unique = input.unique;
501
- if (input.name !== void 0) this.name = input.name;
508
+ if (input.prefix !== void 0) this.prefix = input.prefix;
509
+ if (input.columns !== void 0) this.columns = input.columns;
510
+ if (input.expression !== void 0) this.expression = input.expression;
511
+ if (input.where !== void 0) this.where = input.where;
502
512
  if (input.type !== void 0) this.type = input.type;
503
513
  if (input.options !== void 0) this.options = input.options;
504
514
  if (input.annotations !== void 0) this.annotations = input.annotations;
505
515
  defineNonEnumerable(this, "dependsOn", input.dependsOn);
516
+ defineNonEnumerable(this, "partial", input.partial);
506
517
  freezeNode(this);
507
518
  }
508
519
  get id() {
509
- return `index:${this.columns.join(",")}`;
520
+ return `index:${this.name}`;
510
521
  }
511
522
  children() {
512
523
  return [];
@@ -515,32 +526,72 @@ var SqlIndexIR = class SqlIndexIR extends SqlSchemaIRNode {
515
526
  return node.nodeKind === RelationalSchemaNodeKind.index;
516
527
  }
517
528
  /**
518
- * Symmetric structural equality: two paired index nodes are equal iff their
519
- * `unique` flag, `type`, and (loosely-compared) `options` all match. There
520
- * is no satisfaction a unique index does not equal a non-unique index.
521
- * `options` compares loosely (introspection stringifies reloptions); `type`
522
- * compares strictly after the introspection-side btree→undefined
523
- * normalization done at construction.
529
+ * Mode-selected structural equality see the class doc. Delegates to the
530
+ * single node-owned relation: `columns` compare ordered-strict when both
531
+ * sides carry them; an exact receiver (`prefix === undefined`)
532
+ * byte-compares `expression ?? ''` and `where ?? ''`; a managed receiver
533
+ * never compares bodies (the wire-name hash already commits to them).
524
534
  */
525
535
  isEqualTo(other) {
526
536
  const node = blindCast(other);
527
537
  assertNode(node, "SqlIndexIR", SqlIndexIR.is);
528
- return this.unique === node.unique && this.type === node.type && indexOptionsLooselyEqual(this.options, node.options);
538
+ return this.contentEquals(node, {
539
+ columnPresence: "when-both-defined",
540
+ bodies: this.prefix !== void 0 ? "ignored" : "verbatim"
541
+ });
542
+ }
543
+ /**
544
+ * The single index content-equality relation — every comparer (the differ
545
+ * via {@link isEqualTo}, the planner's rename content-pairing) calls this
546
+ * with its mode-appropriate strictness rather than growing a parallel
547
+ * relation:
548
+ *
549
+ * - `columnPresence: 'when-both-defined'` (the differ's rule) compares
550
+ * the tuples ordered-strict only when both sides carry them — a paired
551
+ * node's identity already agreed, so a column node meeting an
552
+ * expression node skips the tuple.
553
+ * - `columnPresence: 'matching'` (the rename-pairing rule) additionally
554
+ * requires presence to agree: a column index never pairs an expression
555
+ * index.
556
+ * - `bodies: 'verbatim'` byte-compares `expression ?? ''` / `where ?? ''`
557
+ * (absent ≡ empty, no normalization — both sides are reprints in the
558
+ * supported flow); `bodies: 'ignored'` skips them (managed identity —
559
+ * the wire-name hash commits to the content).
560
+ *
561
+ * `unique` compares strictly; `type` and option VALUES compare through
562
+ * the named normalization seams below.
563
+ */
564
+ contentEquals(other, strictness) {
565
+ const columnsEqual = strictness.columnPresence === "matching" ? this.columns === void 0 === (other.columns === void 0) && (this.columns === void 0 || isArrayEqual(this.columns, other.columns ?? [])) : this.columns === void 0 || other.columns === void 0 || isArrayEqual(this.columns, other.columns);
566
+ if (!(this.unique === other.unique && normalizeIndexType(this.type) === normalizeIndexType(other.type) && indexOptionsEqual(this.options, other.options) && columnsEqual)) return false;
567
+ if (strictness.bodies === "ignored") return true;
568
+ return (this.expression ?? "") === (other.expression ?? "") && (this.where ?? "") === (other.where ?? "");
529
569
  }
530
570
  };
531
571
  /**
532
- * Option-bag equality ported from the relational walk: same key set, values
533
- * compared via `String()` coercion Postgres introspection returns
534
- * reloptions values as raw strings (`'70'`, `'false'`) while contract option
535
- * leaves are typed (number, boolean, string).
572
+ * Comparison-side normalization seam: the default access method (`btree` in
573
+ * every supported SQL target) compares as absent, so an authored
574
+ * `type: "btree"` and a default-method introspected index (whose type the
575
+ * adapter or constructor normalized away) are equal. Applied by
576
+ * {@link SqlIndexIR.contentEquals} only — the wire-name hash keeps the
577
+ * authored spelling.
578
+ */
579
+ function normalizeIndexType(type) {
580
+ return type === "btree" ? void 0 : type;
581
+ }
582
+ /**
583
+ * Option-bag equality: same key set, values compared through
584
+ * {@link normalizeIndexOptionValue} — Postgres introspection returns
585
+ * reloptions values as catalog-reprint strings (`'70'`, `'on'`) while
586
+ * contract option leaves are typed (number, boolean, string).
536
587
  */
537
- function indexOptionsLooselyEqual(a, b) {
588
+ function indexOptionsEqual(a, b) {
538
589
  const aKeys = a ? Object.keys(a).sort() : [];
539
590
  const bKeys = b ? Object.keys(b).sort() : [];
540
591
  if (aKeys.length !== bKeys.length) return false;
541
592
  for (let i = 0; i < aKeys.length; i += 1) if (aKeys[i] !== bKeys[i]) return false;
542
593
  if (aKeys.length === 0) return true;
543
- for (const key of aKeys) if (String(a?.[key]) !== String(b?.[key])) return false;
594
+ for (const key of aKeys) if (normalizeIndexOptionValue(a?.[key]) !== normalizeIndexOptionValue(b?.[key])) return false;
544
595
  return true;
545
596
  }
546
597
  //#endregion
@@ -680,4 +731,4 @@ var SqlSchemaIR = class extends SqlSchemaIRNode {
680
731
  //#endregion
681
732
  export { SqlForeignKeyIR as a, SqlCheckConstraintIR as c, assertNode as d, defineNonEnumerable as f, relationalNodeGranularity as h, SqlIndexIR as i, PrimaryKey as l, relationalNodeEntityKind as m, SqlTableIR as n, SqlColumnIR as o, RelationalSchemaNodeKind as p, SqlUniqueIR as r, SqlColumnDefaultIR as s, SqlSchemaIR as t, SqlSchemaIRNode as u };
682
733
 
683
- //# sourceMappingURL=types-pxgoVAJq.mjs.map
734
+ //# sourceMappingURL=types-Btj35AIt.mjs.map