Skip to content

Commit c02144c

Browse files
refactor(sql-authoring): authoring index types carry the disjoint element union; planning IDs leave names and comments
IndexNode and IndexConstraint express columns/fields xor expression as an input-type union (no stored discriminant) and IndexNode's remaining fields become required-but-undefined; construction sites pick the arm from the data or defer the xor decision to lowerAuthoredIndex's runtime guard via blindCast. Warning docs and test names describe the exact-name body warning by behavior, and the index-drift suite names its cases by what they exercise. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
1 parent 9a2bbae commit c02144c

11 files changed

Lines changed: 112 additions & 58 deletions

File tree

packages/2-sql/1-core/contract/src/foreign-key-materialization.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ export function materializeForeignKeysAndIndexes(
9696
synthesizedIndexes.push(
9797
lowerAuthoredIndex(tableName, {
9898
columns: reference.source.columns,
99+
where: undefined,
100+
unique: undefined,
99101
map: undefined,
100102
name: undefined,
101103
type: undefined,

packages/2-sql/1-core/contract/src/index-naming.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ function formatExactNameBodyWarning(warning: ExactNameBodyWarning): string {
5353
}
5454

5555
/**
56-
* Flushes collected D9 warnings once per contract build: per-item warnings
56+
* Flushes collected exact-name-body warnings once per contract build: per-item warnings
5757
* (each naming its object) up to the threshold, one summary with the name
5858
* list above it — an adopted contract re-emit (which carries `map:` + body
5959
* for every adopted object once infer emits them) must not wall-of-text.

packages/2-sql/1-core/contract/test/index-naming.test.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,31 @@
11
import { computeIndexContentHash } from '@prisma-next/sql-schema-ir/naming';
22
import { describe, expect, it, vi } from 'vitest';
33
import {
4+
type AuthoredIndexInput,
45
type ExactNameBodyWarning,
56
flushExactNameBodyWarnings,
6-
lowerAuthoredIndex,
7+
lowerAuthoredIndex as lowerAuthoredIndexStrict,
78
} from '../src/index-naming';
89

10+
type LooseAuthoredIndexInput = {
11+
readonly columns?: readonly string[];
12+
readonly expression?: string;
13+
readonly where?: string;
14+
readonly unique?: boolean;
15+
readonly map?: string;
16+
readonly name?: string;
17+
readonly type?: string;
18+
readonly options?: Record<string, unknown>;
19+
};
20+
21+
function lowerAuthoredIndex(
22+
tableName: string,
23+
authored: LooseAuthoredIndexInput,
24+
warnings?: { push(warning: ExactNameBodyWarning): void },
25+
) {
26+
return lowerAuthoredIndexStrict(tableName, authored as AuthoredIndexInput, warnings);
27+
}
28+
929
function captureWarnings(run: () => void) {
1030
const emitWarning = vi.spyOn(process, 'emitWarning').mockImplementation(() => {});
1131
try {
@@ -177,7 +197,7 @@ describe('lowerAuthoredIndex — cross-field guards', () => {
177197
});
178198
});
179199

180-
describe('lowerAuthoredIndex — D9 exact-name body warning collection', () => {
200+
describe('lowerAuthoredIndex — exact-name body warning collection', () => {
181201
it('pushes into a provided collector instead of emitting', () => {
182202
const collected: ExactNameBodyWarning[] = [];
183203
const warnings = captureWarnings(() => {
@@ -231,7 +251,7 @@ describe('flushExactNameBodyWarnings — threshold batching', () => {
231251
});
232252
});
233253

234-
describe('lowerAuthoredIndex — D9 exact-name body warning', () => {
254+
describe('lowerAuthoredIndex — exact-name body warning', () => {
235255
const expectedMessage =
236256
'index "users_email_eq" uses map: with a SQL body. Drift detection compares the authored ' +
237257
"SQL text byte-for-byte against Postgres's reprinted form, which is only reliable when the " +
@@ -240,7 +260,7 @@ describe('lowerAuthoredIndex — D9 exact-name body warning', () => {
240260
'replace map: with name: (keeping the body text unchanged) and apply the resulting rename ' +
241261
'migration.';
242262

243-
it('fires for map + expression with the exact D9 wording and code', () => {
263+
it('fires for map + expression with the pinned wording and code', () => {
244264
const warnings = captureWarnings(() => {
245265
lowerAuthoredIndex('user', { expression: 'lower(email)', map: 'users_email_eq' });
246266
});

packages/2-sql/2-authoring/contract-psl/src/interpreter.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,16 +1000,21 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
10001000
}
10011001
columnNames = mapped;
10021002
}
1003-
indexNodes.push({
1004-
...ifDefined('columns', columnNames),
1005-
...ifDefined('expression', parsed.expression),
1006-
...ifDefined('where', parsed.where),
1007-
...ifDefined('unique', parsed.unique),
1008-
...ifDefined('name', parsed.name),
1009-
...ifDefined('map', parsed.map),
1010-
...ifDefined('type', parsed.type),
1011-
...ifDefined('options', parsed.options),
1012-
});
1003+
indexNodes.push(
1004+
// The interpreter's own diagnostics pre-empt the neither/both
1005+
// element cases; the cast defers final enforcement to
1006+
// lowerAuthoredIndex's runtime guard.
1007+
blindCast<IndexNode, 'columns-xor-expression enforced by lowerAuthoredIndex'>({
1008+
...ifDefined('columns', columnNames),
1009+
...ifDefined('expression', parsed.expression),
1010+
where: parsed.where,
1011+
unique: parsed.unique,
1012+
name: parsed.name,
1013+
map: parsed.map,
1014+
type: parsed.type,
1015+
options: parsed.options,
1016+
}),
1017+
);
10131018
continue;
10141019
}
10151020
const contributedModelAttribute = input.modelAttributesByName.get(modelAttribute.name);

packages/2-sql/2-authoring/contract-psl/test/interpreter.index-naming.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ describe('@@index matrix threading at PSL lowering', () => {
139139
});
140140
});
141141

142-
it('map with an expression lowers exact and draws the D9 warning', () => {
142+
it('map with an expression lowers exact and draws the exact-name body warning', () => {
143143
const emitWarning = vi.spyOn(process, 'emitWarning').mockImplementation(() => {});
144144
try {
145145
const result = interpretMatrix(`model User {

packages/2-sql/2-authoring/contract-ts/src/build-contract.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -685,8 +685,9 @@ export function buildSqlContractFromDefinition(
685685
);
686686

687687
const tablesByNamespace: Record<string, Record<string, StorageTableInput>> = {};
688-
// D9 warnings collect across the whole build and flush once (threshold-
689-
// batched) — an adopted contract carries map: + body on many objects.
688+
// Exact-name body warnings collect across the whole build and flush once
689+
// (threshold-batched) — an adopted contract carries map: + body on many
690+
// objects.
690691
const exactNameBodyWarnings: ExactNameBodyWarning[] = [];
691692
const modelNameToNamespaceId = new Map<string, string>();
692693
const executionDefaults: ExecutionMutationDefault[] = [];

packages/2-sql/2-authoring/contract-ts/src/contract-definition.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,21 +55,30 @@ export interface UniqueConstraintNode {
5555
readonly name?: string;
5656
}
5757

58-
export interface IndexNode {
59-
/** Column tuple. Exactly one of `columns` / `expression` is set. */
60-
readonly columns?: readonly string[];
61-
/** Opaque SQL: the entire element list of CREATE INDEX — never parsed. */
62-
readonly expression?: string;
58+
/** A definition-tree index's element structure — column tuple xor expression. */
59+
export type IndexNodeElements =
60+
| {
61+
/** Column tuple. */
62+
readonly columns: readonly string[];
63+
readonly expression?: never;
64+
}
65+
| {
66+
readonly columns?: never;
67+
/** Opaque SQL: the entire element list of CREATE INDEX — never parsed. */
68+
readonly expression: string;
69+
};
70+
71+
export type IndexNode = IndexNodeElements & {
6372
/** Opaque SQL: partial-index predicate (WHERE body, without the keyword). */
64-
readonly where?: string;
65-
readonly unique?: boolean;
73+
readonly where: string | undefined;
74+
readonly unique: boolean | undefined;
6675
/** Exact physical name (`map:`) — adopted verbatim, no wire hash. */
67-
readonly map?: string;
76+
readonly map: string | undefined;
6877
/** Managed wire-name prefix (`name:`) — lowers to `<name>_<8hex>`. */
69-
readonly name?: string;
70-
readonly type?: string;
71-
readonly options?: Record<string, unknown>;
72-
}
78+
readonly name: string | undefined;
79+
readonly type: string | undefined;
80+
readonly options: Record<string, unknown> | undefined;
81+
};
7382

7483
export interface ForeignKeyNode {
7584
readonly columns: readonly string[];

packages/2-sql/2-authoring/contract-ts/src/contract-dsl.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -830,15 +830,24 @@ export type UniqueConstraint<FieldNames extends readonly string[] = readonly str
830830
readonly name?: string;
831831
};
832832

833+
/** An authored index constraint's element structure — field tuple xor expression. */
834+
export type IndexConstraintElements<FieldNames extends readonly string[] = readonly string[]> =
835+
| {
836+
/** Field-name tuple. */
837+
readonly fields: FieldNames;
838+
readonly expression?: never;
839+
}
840+
| {
841+
readonly fields?: never;
842+
/** Opaque SQL: the entire CREATE INDEX element list — never parsed. */
843+
readonly expression: string;
844+
};
845+
833846
export type IndexConstraint<
834847
FieldNames extends readonly string[] = readonly string[],
835848
Name extends string | undefined = string | undefined,
836-
> = {
849+
> = IndexConstraintElements<FieldNames> & {
837850
readonly kind: 'index';
838-
/** Field-name tuple. Exactly one of `fields` / `expression` is set. */
839-
readonly fields?: FieldNames;
840-
/** Opaque SQL: the entire CREATE INDEX element list — never parsed. */
841-
readonly expression?: string;
842851
readonly where?: string;
843852
readonly unique?: boolean;
844853
readonly name?: Name;
@@ -1034,18 +1043,18 @@ function createConstraintsDsl<IndexTypes extends IndexTypeMap = Record<never, ne
10341043
typeof fieldsOrOptions === 'object' &&
10351044
'expression' in fieldsOrOptions;
10361045
const opts = isExpressionForm ? fieldsOrOptions : options;
1037-
return {
1038-
kind: 'index',
1039-
...(isExpressionForm
1040-
? { expression: fieldsOrOptions.expression }
1041-
: { fields: normalizeFieldRefInput(fieldsOrOptions) }),
1046+
const carried = {
1047+
kind: 'index' as const,
10421048
...(opts?.name !== undefined ? { name: opts.name } : {}),
10431049
...(opts?.map !== undefined ? { map: opts.map } : {}),
10441050
...(opts?.where !== undefined ? { where: opts.where } : {}),
10451051
...(opts?.unique !== undefined ? { unique: opts.unique } : {}),
10461052
...(opts?.type !== undefined ? { type: opts.type } : {}),
10471053
...(opts?.options !== undefined ? { options: opts.options as Record<string, unknown> } : {}),
10481054
};
1055+
return isExpressionForm
1056+
? { ...carried, expression: fieldsOrOptions.expression }
1057+
: { ...carried, fields: normalizeFieldRefInput(fieldsOrOptions) };
10491058
}
10501059

10511060
function foreignKey<

packages/2-sql/2-authoring/contract-ts/src/contract-lowering.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -810,18 +810,26 @@ function resolveModelNode(
810810
columns: mapFieldNamesToColumnNames(spec.modelName, unique.fields, spec.fieldToColumn),
811811
...(unique.name ? { name: unique.name } : {}),
812812
})) satisfies readonly UniqueConstraintNode[];
813-
const indexes = (spec.sqlSpec?.indexes ?? []).map((index) => ({
814-
...(index.fields !== undefined
815-
? { columns: mapFieldNamesToColumnNames(spec.modelName, index.fields, spec.fieldToColumn) }
816-
: {}),
817-
...ifDefined('expression', index.expression),
818-
...ifDefined('where', index.where),
819-
...ifDefined('unique', index.unique),
820-
...ifDefined('name', index.name),
821-
...ifDefined('map', index.map),
822-
...ifDefined('type', index.type),
823-
...ifDefined('options', index.options),
824-
})) satisfies readonly IndexNode[];
813+
const indexes = (spec.sqlSpec?.indexes ?? []).map((index): IndexNode => {
814+
const carried = {
815+
where: index.where,
816+
unique: index.unique,
817+
name: index.name,
818+
map: index.map,
819+
type: index.type,
820+
options: index.options,
821+
};
822+
return index.expression !== undefined
823+
? { ...carried, expression: index.expression }
824+
: {
825+
...carried,
826+
columns: mapFieldNamesToColumnNames(
827+
spec.modelName,
828+
index.fields ?? [],
829+
spec.fieldToColumn,
830+
),
831+
};
832+
});
825833
const foreignKeys = resolveForeignKeyNodes(spec, allSpecs, extensions);
826834
const relations = Object.entries(spec.relations).map(([relationName, relationBuilder]) =>
827835
resolveRelationNode(relationName, relationBuilder.build(), spec, allSpecs, extensions),

packages/2-sql/2-authoring/contract-ts/test/contract-builder.index-naming.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ describe('constraints.index — full matrix', () => {
256256
]);
257257
});
258258

259-
it('map with an expression lowers exact and draws the D9 warning', () => {
259+
it('map with an expression lowers exact and draws the exact-name body warning', () => {
260260
const emitWarning = vi.spyOn(process, 'emitWarning').mockImplementation(() => {});
261261
try {
262262
const contract = defineTestContract({

0 commit comments

Comments
 (0)