@prisma-next/family-sql 0.16.0-dev.3 → 0.16.0-dev.31

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.
Files changed (36) hide show
  1. package/README.md +1 -1
  2. package/dist/control.d.mts +1 -1
  3. package/dist/control.d.mts.map +1 -1
  4. package/dist/control.mjs +108 -33
  5. package/dist/control.mjs.map +1 -1
  6. package/dist/diff.d.mts +1 -1
  7. package/dist/errors-ZGKTAYsm.mjs +9 -0
  8. package/dist/errors-ZGKTAYsm.mjs.map +1 -0
  9. package/dist/ir.d.mts.map +1 -1
  10. package/dist/ir.mjs +1 -1
  11. package/dist/migration.d.mts +1 -1
  12. package/dist/psl-infer.d.mts +3 -5
  13. package/dist/psl-infer.d.mts.map +1 -1
  14. package/dist/psl-infer.mjs +11 -8
  15. package/dist/psl-infer.mjs.map +1 -1
  16. package/dist/{sql-contract-serializer-C75cfMSS.mjs → sql-contract-serializer-othnJUZ_.mjs} +7 -2
  17. package/dist/sql-contract-serializer-othnJUZ_.mjs.map +1 -0
  18. package/dist/{types-6JMCRorL.d.mts → types-DhAmOoxA.d.mts} +11 -4
  19. package/dist/{types-6JMCRorL.d.mts.map → types-DhAmOoxA.d.mts.map} +1 -1
  20. package/dist/{verify-C-G0obRm.mjs → verify-DuuJFiTm.mjs} +7 -2
  21. package/dist/verify-DuuJFiTm.mjs.map +1 -0
  22. package/dist/verify.d.mts.map +1 -1
  23. package/dist/verify.mjs +1 -1
  24. package/package.json +21 -21
  25. package/src/core/control-instance.ts +50 -24
  26. package/src/core/errors.ts +22 -0
  27. package/src/core/ir/sql-contract-serializer-base.ts +8 -1
  28. package/src/core/migrations/contract-to-schema-ir.ts +78 -17
  29. package/src/core/migrations/types.ts +10 -3
  30. package/src/core/psl-contract-infer/name-transforms.ts +0 -12
  31. package/src/core/psl-contract-infer/printer-config.ts +2 -3
  32. package/src/core/psl-contract-infer/relation-inference.ts +23 -1
  33. package/src/core/verify.ts +10 -1
  34. package/src/exports/psl-infer.ts +1 -2
  35. package/dist/sql-contract-serializer-C75cfMSS.mjs.map +0 -1
  36. package/dist/verify-C-G0obRm.mjs.map +0 -1
@@ -41,10 +41,11 @@ import type {
41
41
  LowererContext,
42
42
  SqlExecuteRequest,
43
43
  } from '@prisma-next/sql-relational-core/ast';
44
- import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
45
44
  import type { SqlSchemaIRNode, SqlTableIR } from '@prisma-next/sql-schema-ir/types';
46
45
  import { blindCast } from '@prisma-next/utils/casts';
47
46
  import { ifDefined } from '@prisma-next/utils/defined';
47
+ import { InternalError } from '@prisma-next/utils/internal-error';
48
+ import type { StructuredError } from '@prisma-next/utils/structured-error';
48
49
  import type { SqlControlAdapter } from './control-adapter';
49
50
  import type {
50
51
  SqlControlTargetDescriptor,
@@ -55,6 +56,7 @@ import {
55
56
  classifyDiffSubjectGranularity,
56
57
  verifySqlSchemaByDiff,
57
58
  } from './diff/schema-verify';
59
+ import { sqlFamilyError } from './errors';
58
60
  import { SqlContractSerializer } from './ir/sql-contract-serializer';
59
61
  import type { SqlSchemaDiffFn } from './migrations/schema-differ';
60
62
  import type {
@@ -64,6 +66,18 @@ import type {
64
66
  import { sqlOperationsToPreview } from './operation-preview';
65
67
  import { collectSupportedCodecTypeIds } from './verify';
66
68
 
69
+ function missingDescriptorOperationError(targetId: string, operation: string): StructuredError {
70
+ return sqlFamilyError(
71
+ 'CONTRACT.PACK_CONTRIBUTION_INVALID',
72
+ `SQL target "${targetId}" is missing the required ${operation} descriptor operation`,
73
+ {
74
+ why: `The target descriptor does not contribute the ${operation} operation the SQL family requires for this call.`,
75
+ fix: `Use a target package whose control descriptor implements ${operation}.`,
76
+ meta: { targetId, operation },
77
+ },
78
+ );
79
+ }
80
+
67
81
  function extractCodecTypeIdsFromContract(contract: unknown): readonly string[] {
68
82
  const typeIds = new Set<string>();
69
83
 
@@ -350,9 +364,9 @@ interface DescriptorWithStorageTypes {
350
364
  function buildSqlTypeMetadataRegistry(options: {
351
365
  readonly target: DescriptorWithStorageTypes;
352
366
  readonly adapter: DescriptorWithStorageTypes & { readonly targetId: string };
353
- readonly extensionPacks: readonly DescriptorWithStorageTypes[];
367
+ readonly extensions: readonly DescriptorWithStorageTypes[];
354
368
  }): SqlTypeMetadataRegistry {
355
- const { target, adapter, extensionPacks: extensions } = options;
369
+ const { target, adapter, extensions } = options;
356
370
  const registry = new Map<string, SqlTypeMetadata>();
357
371
  const targetId = adapter.targetId;
358
372
  const descriptors = [target, adapter, ...extensions];
@@ -384,7 +398,7 @@ interface CrossSpaceFkView {
384
398
  readonly id: string;
385
399
  readonly contractSpace?: {
386
400
  readonly contractJson?: {
387
- readonly extensionPacks?: Readonly<Record<string, unknown>>;
401
+ readonly extensions?: Readonly<Record<string, unknown>>;
388
402
  readonly storage?: {
389
403
  readonly namespaces?: Readonly<Record<string, unknown>>;
390
404
  };
@@ -402,7 +416,7 @@ function buildTransitiveDependsOnMap(
402
416
  ): Map<string, Set<string>> {
403
417
  const directDeps = new Map<string, readonly string[]>();
404
418
  for (const ext of extensions) {
405
- const packs = ext.contractSpace?.contractJson?.extensionPacks;
419
+ const packs = ext.contractSpace?.contractJson?.extensions;
406
420
  const deps = packs !== null && typeof packs === 'object' ? Object.keys(packs) : [];
407
421
  directDeps.set(ext.id, deps);
408
422
  }
@@ -476,8 +490,14 @@ export function assertNoCrossSpaceFkReverseReferences(
476
490
  // Check if targetSpaceId depends on ext.id (directly or transitively)
477
491
  const targetDeps = dependsOnMap.get(targetSpaceId);
478
492
  if (targetDeps?.has(ext.id)) {
479
- throw new Error(
493
+ throw sqlFamilyError(
494
+ 'CONTRACT.FOREIGN_KEY_INVALID',
480
495
  `Cross-space FK reverse-reference detected: extension "${ext.id}" has a cross-space FK targeting space "${targetSpaceId}", but "${targetSpaceId}" depends on "${ext.id}". Cross-space FKs must follow the dependency direction (a space can only reference spaces it depends on, not spaces that depend on it).`,
496
+ {
497
+ why: 'The foreign key points against the contract-space dependency direction.',
498
+ fix: 'Move the foreign key to the depending space, or restructure the extension dependencies so the referencing space depends on the referenced one.',
499
+ meta: { extensionId: ext.id, targetSpaceId },
500
+ },
481
501
  );
482
502
  }
483
503
  }
@@ -491,7 +511,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
491
511
  stack: ControlStack<'sql', TTargetId>,
492
512
  ): SqlFamilyInstance {
493
513
  if (!stack.adapter) {
494
- throw new Error('SQL family requires an adapter descriptor in ControlStack');
514
+ throw new InternalError('SQL family requires an adapter descriptor in ControlStack');
495
515
  }
496
516
 
497
517
  const target = stack.target as unknown as TargetDescriptor<'sql', TTargetId> &
@@ -499,7 +519,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
499
519
  const adapter = stack.adapter as unknown as SqlControlAdapterDescriptor<TTargetId> &
500
520
  DescriptorWithStorageTypes;
501
521
  const extensions =
502
- stack.extensionPacks as unknown as readonly (SqlControlExtensionDescriptor<TTargetId> &
522
+ stack.extensions as unknown as readonly (SqlControlExtensionDescriptor<TTargetId> &
503
523
  DescriptorWithStorageTypes)[];
504
524
 
505
525
  // Descriptor self-consistency check.
@@ -533,7 +553,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
533
553
  const typeMetadataRegistry = buildSqlTypeMetadataRegistry({
534
554
  target,
535
555
  adapter,
536
- extensionPacks: extensions,
556
+ extensions: extensions,
537
557
  });
538
558
 
539
559
  // Lazily construct the control adapter on first use, then memoize it.
@@ -759,14 +779,10 @@ export function createSqlFamilyInstance<TTargetId extends string>(
759
779
  }): VerifyDatabaseSchemaResult {
760
780
  const contract = deserializeWithTargetSerializer(options.contract) as Contract<SqlStorage>;
761
781
  if (!diffSchema) {
762
- throw new Error(
763
- `SQL target "${target.targetId}" is missing the required diffSchema descriptor operation`,
764
- );
782
+ throw missingDescriptorOperationError(target.targetId, 'diffSchema');
765
783
  }
766
784
  if (!targetGranularityOf) {
767
- throw new Error(
768
- `SQL target "${target.targetId}" is missing the required classifySubjectGranularity descriptor operation`,
769
- );
785
+ throw missingDescriptorOperationError(target.targetId, 'classifySubjectGranularity');
770
786
  }
771
787
  // THE VERDICT: the target's full-tree node diff, graded by the
772
788
  // family's post-diff filters (strict gating + control-policy
@@ -793,9 +809,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
793
809
  */
794
810
  classifySubjectGranularity(issue: SchemaDiffIssue): DiffSubjectGranularity | undefined {
795
811
  if (!targetGranularityOf) {
796
- throw new Error(
797
- `SQL target "${target.targetId}" is missing the required classifySubjectGranularity descriptor operation`,
798
- );
812
+ throw missingDescriptorOperationError(target.targetId, 'classifySubjectGranularity');
799
813
  }
800
814
  return classifyDiffSubjectGranularity(issue, targetGranularityOf);
801
815
  },
@@ -808,9 +822,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
808
822
  */
809
823
  classifyEntityKind(issue: SchemaDiffIssue): string | undefined {
810
824
  if (!targetEntityKindOf) {
811
- throw new Error(
812
- `SQL target "${target.targetId}" is missing the required classifyEntityKind descriptor operation`,
813
- );
825
+ throw missingDescriptorOperationError(target.targetId, 'classifyEntityKind');
814
826
  }
815
827
  return classifyDiffEntityKind(issue, targetEntityKindOf);
816
828
  },
@@ -873,7 +885,15 @@ export function createSqlFamilyInstance<TTargetId extends string>(
873
885
  },
874
886
  );
875
887
  if (!updated) {
876
- throw new Error('CAS conflict: marker was modified by another process during sign');
888
+ throw sqlFamilyError(
889
+ 'MIGRATION.MARKER_CAS_FAILURE',
890
+ 'CAS conflict: marker was modified by another process during sign',
891
+ {
892
+ why: 'Another process updated the contract marker between the read and the compare-and-swap write.',
893
+ fix: 'Re-run the sign command; if it keeps failing, make sure only one migration process runs at a time.',
894
+ meta: { space: APP_SPACE_ID },
895
+ },
896
+ );
877
897
  }
878
898
  markerUpdated = true;
879
899
  }
@@ -984,8 +1004,14 @@ export function createSqlFamilyInstance<TTargetId extends string>(
984
1004
 
985
1005
  inferPslContract(schemaIR: SqlSchemaIRNode): PslDocumentAst {
986
1006
  if (!targetInferPslContract) {
987
- throw new Error(
1007
+ throw sqlFamilyError(
1008
+ 'CONTRACT.INFER_UNSUPPORTED',
988
1009
  `Target "${target.targetId}" does not support contract infer (no inferPslContract on its descriptor).`,
1010
+ {
1011
+ why: 'The target descriptor does not provide the inferPslContract hook, so a PSL contract cannot be inferred from the database schema.',
1012
+ fix: 'Use a target package that supports contract infer, or author the contract instead of inferring it.',
1013
+ meta: { targetId: target.targetId },
1014
+ },
989
1015
  );
990
1016
  }
991
1017
  return targetInferPslContract(schemaIR, describedContracts);
@@ -1112,7 +1138,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
1112
1138
  }
1113
1139
 
1114
1140
  for (const index of table.indexes) {
1115
- const name = index.name ?? defaultIndexName(tableName, index.columns);
1141
+ const name = index.name;
1116
1142
  const label = index.unique ? `unique index ${name}` : `index ${name}`;
1117
1143
  children.push(
1118
1144
  new SchemaTreeNode({
@@ -0,0 +1,22 @@
1
+ import type { StructuredError, StructuredErrorOptions } from '@prisma-next/utils/structured-error';
2
+ import { structuredError } from '@prisma-next/utils/structured-error';
3
+
4
+ type SqlFamilyErrorCode =
5
+ | 'CONTRACT.ENUM_INVALID'
6
+ | 'CONTRACT.ENUM_UNKNOWN'
7
+ | 'CONTRACT.FOREIGN_KEY_INVALID'
8
+ | 'CONTRACT.INFER_UNSUPPORTED'
9
+ | 'CONTRACT.MARKER_ROW_CORRUPT'
10
+ | 'CONTRACT.NAMESPACE_UNKNOWN'
11
+ | 'CONTRACT.PACK_CONTRIBUTION_INVALID'
12
+ | 'CONTRACT.TABLE_AMBIGUOUS'
13
+ | 'CONTRACT.TYPE_UNKNOWN'
14
+ | 'MIGRATION.MARKER_CAS_FAILURE';
15
+
16
+ export function sqlFamilyError(
17
+ code: SqlFamilyErrorCode,
18
+ message: string,
19
+ options?: StructuredErrorOptions,
20
+ ): StructuredError {
21
+ return structuredError(code, message, options);
22
+ }
@@ -24,6 +24,7 @@ import { blindCast } from '@prisma-next/utils/casts';
24
24
  import { ifDefined } from '@prisma-next/utils/defined';
25
25
  import type { JsonObject, JsonValue } from '@prisma-next/utils/json';
26
26
  import { type Type, type } from 'arktype';
27
+ import { sqlFamilyError } from '../errors';
27
28
 
28
29
  const NamespaceRawSchema = type({
29
30
  id: 'string',
@@ -144,8 +145,14 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
144
145
  Object.entries(namespaces).map(([nsId, namespaceEntryRaw]) => {
145
146
  const namespaceHydrated = this.hydrateSqlNamespaceEntry(nsId, namespaceEntryRaw);
146
147
  if (!isMaterializedSqlNamespace(namespaceHydrated)) {
147
- throw new Error(
148
+ throw sqlFamilyError(
149
+ 'CONTRACT.PACK_CONTRIBUTION_INVALID',
148
150
  `Target serializer bug: hydrateSqlNamespaceEntry for namespace "${nsId}" returned a non-NamespaceBase value. Override hydrateSqlNamespaceEntry to produce a target namespace concretion.`,
151
+ {
152
+ why: 'The target contract serializer hydrated a namespace into a value that is not a materialized namespace concretion.',
153
+ fix: 'Fix the target package: its hydrateSqlNamespaceEntry override must return a target namespace concretion.',
154
+ meta: { namespaceId: nsId },
155
+ },
149
156
  );
150
157
  }
151
158
  return [nsId, namespaceHydrated];
@@ -28,6 +28,8 @@ import {
28
28
  } from '@prisma-next/sql-schema-ir/types';
29
29
  import { blindCast } from '@prisma-next/utils/casts';
30
30
  import { ifDefined } from '@prisma-next/utils/defined';
31
+ import { InternalError } from '@prisma-next/utils/internal-error';
32
+ import { sqlFamilyError } from '../errors';
31
33
 
32
34
  /**
33
35
  * Target-specific callback that expands a column's base `nativeType` and optional
@@ -187,8 +189,14 @@ function resolveColumnTypeMetadata(
187
189
  }
188
190
  const referenced = storageTypes[column.typeRef];
189
191
  if (!referenced) {
190
- throw new Error(
192
+ throw sqlFamilyError(
193
+ 'CONTRACT.TYPE_UNKNOWN',
191
194
  `Column references storage type "${column.typeRef}" but it is not defined in storage.types.`,
195
+ {
196
+ why: 'The column typeRef does not resolve to any entry in the contract storage.types map.',
197
+ fix: 'Regenerate the contract from its authoring source; do not hand-edit contract JSON.',
198
+ meta: { typeRef: column.typeRef },
199
+ },
192
200
  );
193
201
  }
194
202
  if (isStorageTypeInstance(referenced)) {
@@ -198,7 +206,7 @@ function resolveColumnTypeMetadata(
198
206
  typeParams: referenced.typeParams,
199
207
  };
200
208
  }
201
- throw new Error(
209
+ throw new InternalError(
202
210
  `Storage type "${column.typeRef}" has an unknown polymorphic kind; expected a codec-typed StorageTypeInstance.`,
203
211
  );
204
212
  }
@@ -224,14 +232,26 @@ export function resolveValueSetValues(
224
232
  ): readonly string[] {
225
233
  const ns = storage.namespaces[ref.namespaceId];
226
234
  if (!ns) {
227
- throw new Error(
235
+ throw sqlFamilyError(
236
+ 'CONTRACT.NAMESPACE_UNKNOWN',
228
237
  `resolveValueSetValues: namespace "${ref.namespaceId}" not found in storage (${contextLabel})`,
238
+ {
239
+ why: 'A value-set reference names a namespace the contract storage does not declare.',
240
+ fix: 'Regenerate the contract from its authoring source; do not hand-edit contract JSON.',
241
+ meta: { namespaceId: ref.namespaceId, context: contextLabel },
242
+ },
229
243
  );
230
244
  }
231
245
  const valueSet = ns.entries.valueSet?.[ref.entityName];
232
246
  if (!valueSet) {
233
- throw new Error(
247
+ throw sqlFamilyError(
248
+ 'CONTRACT.ENUM_UNKNOWN',
234
249
  `resolveValueSetValues: value-set "${ref.entityName}" not found in namespace "${ref.namespaceId}" (${contextLabel})`,
250
+ {
251
+ why: 'A value-set reference names an entry its namespace does not declare.',
252
+ fix: 'Regenerate the contract from its authoring source; do not hand-edit contract JSON.',
253
+ meta: { valueSet: ref.entityName, namespaceId: ref.namespaceId, context: contextLabel },
254
+ },
235
255
  );
236
256
  }
237
257
  // Only TEXT enums ship a CHECK-constraint round-trip in this slice. A
@@ -239,8 +259,14 @@ export function resolveValueSetValues(
239
259
  // is future work; fail loudly rather than emit a wrong numeric-as-text check.
240
260
  const values = valueSet.values;
241
261
  if (!allStrings(values)) {
242
- throw new Error(
262
+ throw sqlFamilyError(
263
+ 'CONTRACT.ENUM_INVALID',
243
264
  `resolveValueSetValues: value-set "${ref.entityName}" in namespace "${ref.namespaceId}" has a non-string value; numeric-enum CHECK constraints are not yet supported (${contextLabel})`,
265
+ {
266
+ why: 'Only string value-sets can be rendered as CHECK constraints; this value-set carries a non-string value.',
267
+ fix: 'Use string enum values, or drop the CHECK-constrained enum until numeric enums are supported.',
268
+ meta: { valueSet: ref.entityName, namespaceId: ref.namespaceId, context: contextLabel },
269
+ },
244
270
  );
245
271
  }
246
272
  return values;
@@ -276,17 +302,30 @@ function convertUnique(unique: UniqueConstraint, tableName: string): SqlUniqueIR
276
302
  };
277
303
  }
278
304
 
279
- function convertIndex(index: Index, tableName: string): SqlIndexIRInput {
280
- return {
281
- columns: index.columns,
282
- unique: false,
283
- ...ifDefined('name', index.name),
305
+ function convertIndex(
306
+ index: Index,
307
+ tableName: string,
308
+ tableColumns: readonly string[],
309
+ ): SqlIndexIRInput {
310
+ const base = {
311
+ name: index.name,
312
+ prefix: index.prefix,
313
+ where: index.where,
314
+ unique: index.unique,
315
+ partial: index.where !== undefined,
284
316
  // Carried so the derived index node compares type/options against the
285
317
  // introspected side (the legacy walk read them from the contract).
286
- ...ifDefined('type', index.type),
287
- ...ifDefined('options', index.options),
288
- dependsOn: flatColumnDependsOn(tableName, index.columns),
318
+ type: index.type,
319
+ options: index.options,
320
+ annotations: undefined,
321
+ // An expression index's chains cover every column of its table — the
322
+ // opaque expression is never parsed, so this deterministic
323
+ // over-approximation keeps drops ordered without a SQL parser.
324
+ dependsOn: flatColumnDependsOn(tableName, index.columns ?? tableColumns),
289
325
  };
326
+ return index.expression !== undefined
327
+ ? { ...base, expression: index.expression }
328
+ : { ...base, columns: index.columns ?? [] };
290
329
  }
291
330
 
292
331
  /**
@@ -399,7 +438,7 @@ function convertTable(
399
438
  // the object→own-column `dependsOn` edge like any other index.
400
439
  foreignKeys: table.foreignKeys.map((fk) => convertForeignKey(fk, storage)),
401
440
  uniques: table.uniques.map((u) => convertUnique(u, name)),
402
- indexes: table.indexes.map((i) => convertIndex(i, name)),
441
+ indexes: table.indexes.map((i) => convertIndex(i, name, Object.keys(table.columns))),
403
442
  ...ifDefined('checks', checks),
404
443
  });
405
444
  }
@@ -511,7 +550,15 @@ export function contractNamespaceToSchemaIR(
511
550
  options: ContractToSchemaIROptions,
512
551
  ): SqlSchemaIR {
513
552
  if (options.annotationNamespace.length === 0) {
514
- throw new Error('annotationNamespace must be a non-empty string');
553
+ throw sqlFamilyError(
554
+ 'CONTRACT.PACK_CONTRIBUTION_INVALID',
555
+ 'annotationNamespace must be a non-empty string',
556
+ {
557
+ why: 'The calling target pack passed an empty annotationNamespace to the contract-to-schema-IR projection.',
558
+ fix: 'Fix the target pack to pass its non-empty annotation namespace (e.g. "pg").',
559
+ meta: { option: 'annotationNamespace' },
560
+ },
561
+ );
515
562
  }
516
563
  const namespace = storage.namespaces[namespaceId];
517
564
  if (!namespace) {
@@ -539,7 +586,15 @@ export function contractToSchemaIR(
539
586
  options: ContractToSchemaIROptions,
540
587
  ): SqlSchemaIR {
541
588
  if (options.annotationNamespace.length === 0) {
542
- throw new Error('annotationNamespace must be a non-empty string');
589
+ throw sqlFamilyError(
590
+ 'CONTRACT.PACK_CONTRIBUTION_INVALID',
591
+ 'annotationNamespace must be a non-empty string',
592
+ {
593
+ why: 'The calling target pack passed an empty annotationNamespace to the contract-to-schema-IR projection.',
594
+ fix: 'Fix the target pack to pass its non-empty annotation namespace (e.g. "pg").',
595
+ meta: { option: 'annotationNamespace' },
596
+ },
597
+ );
543
598
  }
544
599
 
545
600
  if (!contract) {
@@ -554,8 +609,14 @@ export function contractToSchemaIR(
554
609
  StorageTable.assert(tableDefRaw, `namespaces.${ns.id}.entries.table.${tableName}`);
555
610
  const tableDef = tableDefRaw;
556
611
  if (tables[tableName] !== undefined) {
557
- throw new Error(
612
+ throw sqlFamilyError(
613
+ 'CONTRACT.TABLE_AMBIGUOUS',
558
614
  `contractToSchemaIR: duplicate SQL table name "${tableName}" across namespaces (ambiguous for flat SqlSchemaIR.tables).`,
615
+ {
616
+ why: 'Two namespaces declare a table with the same name, which is ambiguous for the flat schema-IR table map.',
617
+ fix: 'Rename one of the tables so every table name is unique across namespaces.',
618
+ meta: { table: tableName },
619
+ },
559
620
  );
560
621
  }
561
622
  tables[tableName] = convertTable(
@@ -334,9 +334,9 @@ export interface SqlMigrationPlannerPlanOptions {
334
334
  * or `null` for reconciliation flows that have no prior contract.
335
335
  *
336
336
  * Required at every call site so the structural fact "I have a prior
337
- * contract / I don't" is visible in the type. `migration plan` reads
338
- * the predecessor bundle's `end-contract.json` from disk and passes
339
- * the parsed value; `db update` / `db init` reconcile against the
337
+ * contract / I don't" is visible in the type. `migration plan` reads the
338
+ * predecessor's contract from the snapshot store by its storage hash and
339
+ * passes the parsed value; `db update` / `db init` reconcile against the
340
340
  * live schema and pass `null`. Strategies that
341
341
  * need from/to column-shape comparisons (unsafe type change, nullability
342
342
  * tightening) use this to decide whether to emit `dataTransform`
@@ -346,6 +346,13 @@ export interface SqlMigrationPlannerPlanOptions {
346
346
  * plan's `describe()` as `fromContract?.storage.storageHash ?? null`.
347
347
  */
348
348
  readonly fromContract: Contract<SqlStorage> | null;
349
+ /**
350
+ * POSIX-relative path from the migration package dir to
351
+ * `migrations/snapshots`, e.g. `'../../snapshots'`. Threaded straight
352
+ * into the produced plan's `renderTypeScript()` metadata so the
353
+ * scaffold's contract-snapshot imports point at the deduplicated store.
354
+ */
355
+ readonly snapshotsImportPath: string;
349
356
  /**
350
357
  * Active framework components participating in this composition.
351
358
  * Each component is target-bound so SQL targets can dispatch
@@ -169,15 +169,3 @@ export function deriveBackRelationFieldName(childModelName: string, isOneToOne:
169
169
  const base = childModelName.charAt(0).toLowerCase() + childModelName.slice(1);
170
170
  return isOneToOne ? base : pluralize(base);
171
171
  }
172
-
173
- export function toNamedTypeName(columnName: string): string {
174
- let name: string;
175
-
176
- if (hasSeparators(columnName)) {
177
- name = snakeToPascalCase(columnName);
178
- } else {
179
- name = columnName.charAt(0).toUpperCase() + columnName.slice(1);
180
- }
181
-
182
- return escapeIfNeeded(name);
183
- }
@@ -1,17 +1,16 @@
1
1
  import type { ColumnDefault } from '@prisma-next/contract/types';
2
2
  import type { DefaultMappingOptions } from './default-mapping';
3
3
 
4
- export type PslNativeTypeAttribute = {
4
+ export type PslTypeReference = {
5
5
  readonly name: string;
6
6
  readonly args?: readonly string[];
7
7
  };
8
8
 
9
9
  export type PslTypeResolution =
10
10
  | {
11
- readonly pslType: string;
11
+ readonly pslType: PslTypeReference;
12
12
  readonly nativeType: string;
13
13
  readonly typeParams?: Record<string, unknown>;
14
- readonly nativeTypeAttribute?: PslNativeTypeAttribute;
15
14
  }
16
15
  | {
17
16
  readonly unsupported: true;
@@ -123,6 +123,16 @@ function detectOneToOne(fk: SqlForeignKeyIR, table: SqlTableIR): boolean {
123
123
  }
124
124
  }
125
125
 
126
+ for (const index of table.indexes) {
127
+ if (!index.unique || index.partial || index.columns === undefined) {
128
+ continue;
129
+ }
130
+ const indexCols = [...index.columns].sort();
131
+ if (indexCols.length === fkCols.length && indexCols.every((c, i) => c === fkCols[i])) {
132
+ return true;
133
+ }
134
+ }
135
+
126
136
  return false;
127
137
  }
128
138
 
@@ -170,7 +180,19 @@ export function buildChildRelationField(
170
180
  const onDelete = fk.onDelete && fk.onDelete !== DEFAULT_ON_DELETE ? fk.onDelete : undefined;
171
181
  const onUpdate = fk.onUpdate && fk.onUpdate !== DEFAULT_ON_UPDATE ? fk.onUpdate : undefined;
172
182
  const index =
173
- hostTable && !isBackedByColumnKeys(fk.columns, backingIndexColumnKeys(hostTable))
183
+ hostTable &&
184
+ !isBackedByColumnKeys(
185
+ fk.columns,
186
+ // Only indexes the inferrer actually emits can back the FK in the
187
+ // emitted contract: partial indexes (and expression indexes, which
188
+ // carry no column tuple) are not emitted yet, so
189
+ // they must not suppress the explicit `index: false`.
190
+ backingIndexColumnKeys({
191
+ indexes: hostTable.indexes.filter((i) => i.where === undefined),
192
+ uniques: hostTable.uniques,
193
+ primaryKey: hostTable.primaryKey,
194
+ }),
195
+ )
174
196
  ? false
175
197
  : undefined;
176
198
 
@@ -1,5 +1,6 @@
1
1
  import type { ContractMarkerRecord } from '@prisma-next/contract/types';
2
2
  import { type } from 'arktype';
3
+ import { sqlFamilyError } from './errors';
3
4
 
4
5
  const MetaSchema = type({ '[string]': 'unknown' });
5
6
 
@@ -80,7 +81,15 @@ export function parseContractMarkerRow(row: unknown): ContractMarkerRecord {
80
81
  const result = ContractMarkerRowSchema(row);
81
82
  if (result instanceof type.errors) {
82
83
  const messages = result.map((p: { message: string }) => p.message).join('; ');
83
- throw new Error(`Invalid contract marker row: ${messages}`);
84
+ throw sqlFamilyError(
85
+ 'CONTRACT.MARKER_ROW_CORRUPT',
86
+ `Invalid contract marker row: ${messages}`,
87
+ {
88
+ why: 'The contract marker row read from the database does not match the expected marker shape.',
89
+ fix: 'Re-sign the database with `prisma-next db sign`, or repair the marker table.',
90
+ meta: { issues: messages },
91
+ },
92
+ );
84
93
  }
85
94
 
86
95
  const updatedAt = result.updated_at
@@ -22,13 +22,12 @@ export {
22
22
  toEnumName,
23
23
  toFieldName,
24
24
  toModelName,
25
- toNamedTypeName,
26
25
  } from '../core/psl-contract-infer/name-transforms';
27
26
  export type {
28
27
  EnumInfo,
29
- PslNativeTypeAttribute,
30
28
  PslPrinterOptions,
31
29
  PslTypeMap,
30
+ PslTypeReference,
32
31
  PslTypeResolution,
33
32
  RelationField,
34
33
  } from '../core/psl-contract-infer/printer-config';
@@ -1 +0,0 @@
1
- {"version":3,"file":"sql-contract-serializer-C75cfMSS.mjs","names":["isPlainRecord"],"sources":["../src/core/ir/sql-contract-serializer-base.ts","../src/core/ir/sql-contract-serializer.ts"],"sourcesContent":["import { ContractValidationError } from '@prisma-next/contract/contract-validation-error';\nimport { isPlainRecord } from '@prisma-next/contract/is-plain-record';\nimport type { Contract } from '@prisma-next/contract/types';\nimport type { ContractSerializer } from '@prisma-next/framework-components/control';\nimport {\n type AnyEntityKindDescriptor,\n hydrateNamespaceEntities,\n type Namespace,\n} from '@prisma-next/framework-components/ir';\nimport { sqlContractCanonicalizationHooks } from '@prisma-next/sql-contract/canonicalization-hooks';\nimport { composeSqlEntityKinds } from '@prisma-next/sql-contract/entity-kinds';\nimport {\n isMaterializedSqlNamespace,\n type SqlNamespaceInput,\n SqlStorage,\n type SqlStorageInput,\n type SqlStorageTypeEntry,\n} from '@prisma-next/sql-contract/types';\nimport {\n createSqlContractSchema,\n validateSqlContractFully,\n} from '@prisma-next/sql-contract/validators';\nimport { blindCast } from '@prisma-next/utils/casts';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport type { JsonObject, JsonValue } from '@prisma-next/utils/json';\nimport { type Type, type } from 'arktype';\n\nconst NamespaceRawSchema = type({\n id: 'string',\n 'kind?': 'string',\n entries: type({\n '+': 'ignore',\n }),\n});\n\nexport type SqlEntityHydrationFactory = (entry: unknown) => unknown;\n\n/**\n * SQL family `ContractSerializer` abstract base. Carries the SQL-shared\n * deserialization pipeline:\n *\n * 1. `parseSqlContractStructure` validates the on-disk JSON envelope\n * against the SQL contract arktype schema (`validateSqlContractFully`)\n * and returns the validated flat-data shape.\n * 2. `hydrateSqlStorage` walks the validated `storage` subtree and\n * constructs the family-shared SQL Contract IR class hierarchy\n * (`SqlStorage` -> `StorageTable` -> `StorageColumn` / `PrimaryKey`\n * / …). The rest of the contract envelope is JSON-clean primitive\n * data and passes through unchanged.\n * 3. `constructTargetContract` is the target-specific extension hook;\n * defaults to identity. Targets that need to attach target-only\n * fields (e.g. target-specific derived storage fields) override it.\n *\n * Default `serializeContract` is identity over the contract — concrete\n * SQL targets ship JSON-clean class instances, so the contract value\n * can be stringified directly. The non-enumerable family-level `kind`\n * discriminator on `SqlNode` instances stays out of the persisted\n * envelope automatically. Targets that need to canonicalize on the way\n * out (key ordering, dropping computed-only fields) override\n * `serializeContract` directly.\n */\nexport abstract class SqlContractSerializerBase<TContract extends Contract<SqlStorage>>\n implements ContractSerializer<TContract>\n{\n private readonly contractSchema: Type<unknown> | undefined;\n private readonly entryKinds: ReadonlyMap<string, AnyEntityKindDescriptor>;\n\n constructor(\n protected readonly entityHydrationRegistry: ReadonlyMap<\n string,\n SqlEntityHydrationFactory\n > = new Map(),\n packEntityKinds: readonly AnyEntityKindDescriptor[] = [],\n ) {\n this.entryKinds = composeSqlEntityKinds(packEntityKinds);\n this.contractSchema =\n packEntityKinds.length > 0 ? createSqlContractSchema(this.entryKinds) : undefined;\n }\n\n deserializeContract<T extends TContract = TContract>(json: unknown): T {\n const validated = this.parseSqlContractStructure(json);\n const hydrated = this.hydrateSqlStorage(validated);\n return this.constructTargetContract(hydrated) as T;\n }\n\n serializeContract(contract: TContract): JsonObject {\n return contract as unknown as JsonObject;\n }\n\n shouldPreserveEmpty = sqlContractCanonicalizationHooks.shouldPreserveEmpty;\n\n sortStorage = sqlContractCanonicalizationHooks.sortStorage;\n\n protected parseSqlContractStructure(json: unknown): Contract<SqlStorage> {\n return validateSqlContractFully<Contract<SqlStorage>>(\n json,\n this.contractSchema !== undefined ? { contractSchema: this.contractSchema } : undefined,\n );\n }\n\n protected hydrateSqlStorage(validated: Contract<SqlStorage>): Contract<SqlStorage> {\n const types = validated.storage.types;\n const hydratedTypes =\n types !== undefined\n ? Object.fromEntries(\n Object.entries(types).map(([name, entry]) => [\n name,\n this.hydrateStorageTypeEntry(entry),\n ]),\n )\n : undefined;\n\n const rawNamespaces = validated.storage.namespaces;\n if (rawNamespaces === undefined) {\n throw new ContractValidationError(\n 'Contract storage.namespaces is required after structural validation',\n 'structural',\n );\n }\n const hydratedNamespaces = this.hydrateSqlNamespaceMap(\n blindCast<\n Readonly<Record<string, Record<string, unknown>>>,\n 'parseSqlContractStructure validated raw JSON; namespace entries are plain objects, not SqlNamespace instances.'\n >(rawNamespaces),\n );\n\n return {\n ...validated,\n storage: new SqlStorage({\n storageHash: validated.storage.storageHash,\n ...ifDefined('types', hydratedTypes),\n namespaces: blindCast<\n SqlStorageInput['namespaces'],\n 'hydrateSqlNamespaceMap builds each namespace through the target serializer override, so every value is a SqlNamespace; the framework return type only promises the base Namespace.'\n >(hydratedNamespaces),\n }),\n };\n }\n\n protected hydrateSqlNamespaceMap(\n namespaces: Readonly<Record<string, Record<string, unknown>>>,\n ): Readonly<Record<string, Namespace>> {\n return Object.fromEntries(\n Object.entries(namespaces).map(([nsId, namespaceEntryRaw]) => {\n const namespaceHydrated = this.hydrateSqlNamespaceEntry(nsId, namespaceEntryRaw);\n if (!isMaterializedSqlNamespace(namespaceHydrated)) {\n throw new Error(\n `Target serializer bug: hydrateSqlNamespaceEntry for namespace \"${nsId}\" returned a non-NamespaceBase value. Override hydrateSqlNamespaceEntry to produce a target namespace concretion.`,\n );\n }\n return [nsId, namespaceHydrated];\n }),\n );\n }\n\n protected hydrateSqlNamespaceEntry(\n nsId: string,\n raw: Record<string, unknown>,\n ): Namespace | SqlNamespaceInput {\n const id = typeof raw['id'] === 'string' ? raw['id'] : nsId;\n const parsed = NamespaceRawSchema({ ...raw, id });\n if (parsed instanceof type.errors) {\n const messages = parsed.map((p: { message: string }) => p.message).join('; ');\n throw new ContractValidationError(`Namespace hydration failed: ${messages}`, 'structural');\n }\n const entriesRaw = parsed.entries;\n const rawEntriesMap = isPlainRecord(entriesRaw) ? entriesRaw : {};\n\n const entriesInput: Record<string, Readonly<Record<string, unknown>>> = {};\n for (const [key, innerMap] of Object.entries(rawEntriesMap)) {\n entriesInput[key] = isPlainRecord(innerMap) ? innerMap : Object.freeze({});\n }\n\n const entriesOutput = hydrateNamespaceEntities(entriesInput, this.entryKinds, 'fail', id);\n\n // Always ensure a 'table' key is present (may be empty).\n if (!Object.hasOwn(entriesOutput, 'table')) {\n entriesOutput['table'] = {};\n }\n\n return blindCast<\n SqlNamespaceInput,\n 'entriesOutput holds the hydrated SQL entity-kind maps (table always present); this wraps them as the SqlNamespaceInput the target createNamespace consumes.'\n >({\n id,\n entries: entriesOutput,\n });\n }\n\n protected hydrateStorageTypeEntry(entry: SqlStorageTypeEntry): SqlStorageTypeEntry {\n if (typeof entry !== 'object' || entry === null) {\n return entry;\n }\n const kind = (entry as { kind?: unknown }).kind;\n if (typeof kind !== 'string') {\n return entry;\n }\n const factory = this.entityHydrationRegistry.get(kind);\n if (factory === undefined) {\n return entry;\n }\n return blindCast<\n SqlStorageTypeEntry,\n 'entity registry factory returns SqlStorageTypeEntry for storage.types entries'\n >(factory(entry));\n }\n\n protected constructTargetContract(hydrated: Contract<SqlStorage>): TContract {\n return hydrated as TContract;\n }\n\n /**\n * Serializes a namespace's `entries` dict by walking every enumerable\n * kind — no kind is named here, mirroring the generic hydrate walk in\n * `hydrateSqlNamespaceEntry` above. `table` is the SQL family's one\n * universal base kind (every namespace carries it), so it is always\n * emitted, even when empty; every other kind — target- or\n * pack-contributed — is emitted only when it holds at least one entry.\n * A kind carried non-enumerable on `entries` is excluded here for free,\n * since `Object.entries` honors enumerability.\n */\n protected serializeNamespaceEntries(\n entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>,\n ): Record<string, Record<string, JsonObject>> {\n const out: Record<string, Record<string, JsonObject>> = {\n table: this.serializeEntries(entries['table'] ?? {}),\n };\n for (const [kind, record] of Object.entries(entries)) {\n if (kind === 'table' || record == null || Object.keys(record).length === 0) {\n continue;\n }\n out[kind] = this.serializeEntries(record);\n }\n return out;\n }\n\n private serializeEntries(entries: Readonly<Record<string, unknown>>): Record<string, JsonObject> {\n const out: Record<string, JsonObject> = {};\n for (const [name, entry] of Object.entries(entries)) {\n out[name] = this.serializeJsonObject(entry);\n }\n return out;\n }\n\n protected serializeJsonObject(value: unknown): JsonObject {\n return blindCast<\n JsonObject,\n 'serializeJsonValue round-trips an IR node through JSON, yielding a JsonObject'\n >(this.serializeJsonValue(value));\n }\n\n private serializeJsonValue(value: unknown): JsonValue {\n return blindCast<JsonValue, 'JSON.parse(JSON.stringify(x)) yields a JsonValue'>(\n JSON.parse(JSON.stringify(value)),\n );\n }\n}\n","import type { Contract } from '@prisma-next/contract/types';\nimport type { SqlStorage } from '@prisma-next/sql-contract/types';\nimport { SqlContractSerializerBase } from './sql-contract-serializer-base';\n\n/**\n * Default SQL family `ContractSerializer` concretion. Inherits the\n * full SQL-shared deserialization pipeline (structural validation +\n * IR-class hydration) without pack-registered `storage.types`\n * hydration factories — targets that emit polymorphic JSON outside the\n * codec-typed envelope wire a target-specific subclass with a populated\n * registry (see Postgres). Family-level call sites instantiate this\n * default directly when no target serializer is supplied.\n *\n * Because this serializer has no target concretion, deserialization of\n * contracts that include namespace entries from JSON will throw unless\n * the caller provides pre-hydrated `NamespaceBase` instances. Production\n * paths always supply a target-specific serializer.\n */\nexport class SqlContractSerializer extends SqlContractSerializerBase<Contract<SqlStorage>> {\n constructor() {\n super(new Map());\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"verify-C-G0obRm.mjs","names":[],"sources":["../src/core/verify.ts"],"sourcesContent":["import type { ContractMarkerRecord } from '@prisma-next/contract/types';\nimport { type } from 'arktype';\n\nconst MetaSchema = type({ '[string]': 'unknown' });\n\nfunction parseMeta(meta: unknown): Record<string, unknown> {\n if (meta === null || meta === undefined) {\n return {};\n }\n\n let parsed: unknown;\n if (typeof meta === 'string') {\n try {\n parsed = JSON.parse(meta);\n } catch {\n return {};\n }\n } else {\n parsed = meta;\n }\n\n const result = MetaSchema(parsed);\n if (result instanceof type.errors) {\n return {};\n }\n\n return result as Record<string, unknown>;\n}\n\n/**\n * SQLite stores `contract_json` as TEXT, so the wire shape is a JSON string;\n * Postgres uses `jsonb` and returns an already-parsed value. Normalize both\n * here so `ContractMarkerRecord.contractJson` is always the structured form.\n */\nfunction parseContractJson(value: unknown): unknown {\n if (value === null || value === undefined) return null;\n if (typeof value !== 'string') return value;\n try {\n return JSON.parse(value);\n } catch {\n return null;\n }\n}\n\n/**\n * Wire shape of a `prisma_contract.marker` row as it comes out of a SQL\n * driver. Snake-cased to match the on-disk column names. Shared by every\n * SQL target's `readMarker` so each runner doesn't redeclare it inline.\n */\nexport type ContractMarkerRow = {\n core_hash: string;\n profile_hash: string;\n contract_json: unknown | null;\n canonical_version: number | null;\n updated_at: Date | string;\n app_tag: string | null;\n meta: unknown | null;\n // SQLite stores arrays as JSON-TEXT, so this is `string` on the wire from\n // a SQLite driver and `string[]` from a Postgres driver. Targets normalize\n // before passing to `parseContractMarkerRow`.\n invariants: unknown;\n};\n\nconst ContractMarkerRowSchema = type({\n core_hash: 'string',\n profile_hash: 'string',\n 'contract_json?': 'unknown | null',\n 'canonical_version?': 'number | null',\n 'updated_at?': 'Date | string',\n 'app_tag?': 'string | null',\n 'meta?': 'unknown | null',\n invariants: type('string').array(),\n});\n\n/**\n * Parses a contract marker row from database query result.\n * This is SQL-specific parsing logic (handles SQL row structure with snake_case columns).\n */\nexport function parseContractMarkerRow(row: unknown): ContractMarkerRecord {\n const result = ContractMarkerRowSchema(row);\n if (result instanceof type.errors) {\n const messages = result.map((p: { message: string }) => p.message).join('; ');\n throw new Error(`Invalid contract marker row: ${messages}`);\n }\n\n const updatedAt = result.updated_at\n ? result.updated_at instanceof Date\n ? result.updated_at\n : new Date(result.updated_at)\n : new Date();\n\n return {\n storageHash: result.core_hash,\n profileHash: result.profile_hash,\n contractJson: parseContractJson(result.contract_json),\n canonicalVersion: result.canonical_version ?? null,\n updatedAt,\n appTag: result.app_tag ?? null,\n meta: parseMeta(result.meta),\n invariants: result.invariants,\n };\n}\n\n/**\n * Collects supported codec type IDs from adapter and extension manifests.\n * Returns a sorted, unique array of type IDs that are declared in the manifests.\n * This enables coverage checks by comparing contract column types against supported types.\n *\n * Note: This extracts type IDs from manifest type imports, not from runtime codec registries.\n * The manifests declare which codec types are available, but the actual type IDs\n * are defined in the codec-types TypeScript modules that are imported.\n *\n * For MVP, we return an empty array since extracting type IDs from TypeScript modules\n * would require runtime evaluation or static analysis. This can be enhanced later.\n */\nexport function collectSupportedCodecTypeIds(\n descriptors: ReadonlyArray<{ readonly id: string }>,\n): readonly string[] {\n // For MVP, return empty array\n // Future enhancement: Extract type IDs from codec-types modules via static analysis\n // or require manifests to explicitly list supported type IDs\n void descriptors;\n return [];\n}\n"],"mappings":";;AAGA,MAAM,aAAa,KAAK,EAAE,YAAY,UAAU,CAAC;AAEjD,SAAS,UAAU,MAAwC;CACzD,IAAI,SAAS,QAAQ,SAAS,KAAA,GAC5B,OAAO,CAAC;CAGV,IAAI;CACJ,IAAI,OAAO,SAAS,UAClB,IAAI;EACF,SAAS,KAAK,MAAM,IAAI;CAC1B,QAAQ;EACN,OAAO,CAAC;CACV;MAEA,SAAS;CAGX,MAAM,SAAS,WAAW,MAAM;CAChC,IAAI,kBAAkB,KAAK,QACzB,OAAO,CAAC;CAGV,OAAO;AACT;;;;;;AAOA,SAAS,kBAAkB,OAAyB;CAClD,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,OAAO;CAClD,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI;EACF,OAAO,KAAK,MAAM,KAAK;CACzB,QAAQ;EACN,OAAO;CACT;AACF;AAqBA,MAAM,0BAA0B,KAAK;CACnC,WAAW;CACX,cAAc;CACd,kBAAkB;CAClB,sBAAsB;CACtB,eAAe;CACf,YAAY;CACZ,SAAS;CACT,YAAY,KAAK,QAAQ,CAAC,CAAC,MAAM;AACnC,CAAC;;;;;AAMD,SAAgB,uBAAuB,KAAoC;CACzE,MAAM,SAAS,wBAAwB,GAAG;CAC1C,IAAI,kBAAkB,KAAK,QAAQ;EACjC,MAAM,WAAW,OAAO,KAAK,MAA2B,EAAE,OAAO,CAAC,CAAC,KAAK,IAAI;EAC5E,MAAM,IAAI,MAAM,gCAAgC,UAAU;CAC5D;CAEA,MAAM,YAAY,OAAO,aACrB,OAAO,sBAAsB,OAC3B,OAAO,aACP,IAAI,KAAK,OAAO,UAAU,oBAC5B,IAAI,KAAK;CAEb,OAAO;EACL,aAAa,OAAO;EACpB,aAAa,OAAO;EACpB,cAAc,kBAAkB,OAAO,aAAa;EACpD,kBAAkB,OAAO,qBAAqB;EAC9C;EACA,QAAQ,OAAO,WAAW;EAC1B,MAAM,UAAU,OAAO,IAAI;EAC3B,YAAY,OAAO;CACrB;AACF;;;;;;;;;;;;;AAcA,SAAgB,6BACd,aACmB;CAKnB,OAAO,CAAC;AACV"}