@prisma-next/sql-contract-psl 0.5.0-dev.52 → 0.5.0-dev.54

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.
@@ -35,11 +35,13 @@ import {
35
35
  type ForeignKeyNode,
36
36
  type IndexNode,
37
37
  type ModelNode,
38
+ type PrimaryKeyNode,
38
39
  type UniqueConstraintNode,
39
40
  } from '@prisma-next/sql-contract-ts/contract-builder';
40
41
  import { ifDefined } from '@prisma-next/utils/defined';
41
42
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
42
43
  import {
44
+ findDuplicateFieldName,
43
45
  getAttribute,
44
46
  getPositionalArgument,
45
47
  mapFieldNamesToColumns,
@@ -464,18 +466,24 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
464
466
  scalarTypeDescriptors: input.scalarTypeDescriptors,
465
467
  });
466
468
 
467
- const primaryKeyFields = resolvedFields.filter((field) => field.isId);
468
- const primaryKeyColumns = primaryKeyFields.map((field) => field.columnName);
469
- const primaryKeyName = primaryKeyFields.length === 1 ? primaryKeyFields[0]?.idName : undefined;
470
- const isVariantModel = model.attributes.some((attr) => attr.name === 'base');
471
- if (primaryKeyColumns.length === 0 && !isVariantModel) {
469
+ const inlineIdFields = resolvedFields.filter((field) => field.isId);
470
+ if (inlineIdFields.length > 1) {
472
471
  diagnostics.push({
473
- code: 'PSL_MISSING_PRIMARY_KEY',
474
- message: `Model "${model.name}" must declare at least one @id field for SQL provider`,
472
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
473
+ message: `Model "${model.name}" cannot declare inline @id on multiple fields; use model-level @@id([...]) for composite identity`,
475
474
  sourceId,
476
475
  span: model.span,
477
476
  });
478
477
  }
478
+ const singleInlineIdField = inlineIdFields.length === 1 ? inlineIdFields[0] : undefined;
479
+ let primaryKey: PrimaryKeyNode | undefined = singleInlineIdField
480
+ ? {
481
+ columns: [singleInlineIdField.columnName],
482
+ ...ifDefined('name', singleInlineIdField.idName),
483
+ }
484
+ : undefined;
485
+ const hasInlinePrimaryKey = primaryKey !== undefined;
486
+ let blockPrimaryKeyDeclared = false;
479
487
 
480
488
  const resultBackrelationCandidates: ModelBackrelationCandidate[] = [];
481
489
  for (const field of model.fields) {
@@ -562,17 +570,107 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
562
570
  if (modelAttribute.name === 'discriminator' || modelAttribute.name === 'base') {
563
571
  continue;
564
572
  }
573
+ const attributeLabel = `Model "${model.name}" @@${modelAttribute.name}`;
574
+ if (modelAttribute.name === 'id') {
575
+ if (blockPrimaryKeyDeclared) {
576
+ diagnostics.push({
577
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
578
+ message: `Model "${model.name}" declares @@id more than once`,
579
+ sourceId,
580
+ span: modelAttribute.span,
581
+ });
582
+ continue;
583
+ }
584
+ if (hasInlinePrimaryKey) {
585
+ diagnostics.push({
586
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
587
+ message: `Model "${model.name}" cannot declare both field-level @id and model-level @@id`,
588
+ sourceId,
589
+ span: modelAttribute.span,
590
+ });
591
+ blockPrimaryKeyDeclared = true;
592
+ continue;
593
+ }
594
+ const fieldNames = parseAttributeFieldList({
595
+ attribute: modelAttribute,
596
+ sourceId,
597
+ diagnostics,
598
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
599
+ entityLabel: attributeLabel,
600
+ });
601
+ if (!fieldNames) {
602
+ continue;
603
+ }
604
+ const duplicateFieldName = findDuplicateFieldName(fieldNames);
605
+ if (duplicateFieldName !== undefined) {
606
+ diagnostics.push({
607
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
608
+ message: `${attributeLabel} list contains duplicate field "${duplicateFieldName}"`,
609
+ sourceId,
610
+ span: modelAttribute.span,
611
+ });
612
+ continue;
613
+ }
614
+ const nullableFieldName = fieldNames.find(
615
+ (name) => model.fields.find((f) => f.name === name)?.optional === true,
616
+ );
617
+ if (nullableFieldName !== undefined) {
618
+ diagnostics.push({
619
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
620
+ message: `${attributeLabel} cannot include optional field "${nullableFieldName}"; primary key columns must be NOT NULL`,
621
+ sourceId,
622
+ span: modelAttribute.span,
623
+ });
624
+ continue;
625
+ }
626
+ const columnNames = mapFieldNamesToColumns({
627
+ modelName: model.name,
628
+ fieldNames,
629
+ mapping,
630
+ sourceId,
631
+ diagnostics,
632
+ span: modelAttribute.span,
633
+ entityLabel: attributeLabel,
634
+ });
635
+ if (!columnNames) {
636
+ continue;
637
+ }
638
+ const constraintName = parseConstraintMapArgument({
639
+ attribute: modelAttribute,
640
+ sourceId,
641
+ diagnostics,
642
+ entityLabel: attributeLabel,
643
+ span: modelAttribute.span,
644
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
645
+ });
646
+ primaryKey = {
647
+ columns: columnNames,
648
+ ...ifDefined('name', constraintName),
649
+ };
650
+ blockPrimaryKeyDeclared = true;
651
+ continue;
652
+ }
565
653
  if (modelAttribute.name === 'unique' || modelAttribute.name === 'index') {
566
654
  const fieldNames = parseAttributeFieldList({
567
655
  attribute: modelAttribute,
568
656
  sourceId,
569
657
  diagnostics,
570
658
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
571
- messagePrefix: `Model "${model.name}" @@${modelAttribute.name}`,
659
+ entityLabel: attributeLabel,
572
660
  });
573
661
  if (!fieldNames) {
574
662
  continue;
575
663
  }
664
+ const duplicateFieldName = findDuplicateFieldName(fieldNames);
665
+ if (duplicateFieldName !== undefined) {
666
+ diagnostics.push({
667
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
668
+ message: `${attributeLabel} list contains duplicate field "${duplicateFieldName}"`,
669
+ sourceId,
670
+ span: modelAttribute.span,
671
+ });
672
+ continue;
673
+ }
576
674
  const columnNames = mapFieldNamesToColumns({
577
675
  modelName: model.name,
578
676
  fieldNames,
@@ -580,7 +678,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
580
678
  sourceId,
581
679
  diagnostics,
582
680
  span: modelAttribute.span,
583
- contextLabel: `Model "${model.name}" @@${modelAttribute.name}`,
681
+ entityLabel: attributeLabel,
584
682
  });
585
683
  if (!columnNames) {
586
684
  continue;
@@ -589,7 +687,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
589
687
  attribute: modelAttribute,
590
688
  sourceId,
591
689
  diagnostics,
592
- entityLabel: `Model "${model.name}" @@${modelAttribute.name}`,
690
+ entityLabel: attributeLabel,
593
691
  span: modelAttribute.span,
594
692
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
595
693
  });
@@ -687,7 +785,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
687
785
  sourceId,
688
786
  diagnostics,
689
787
  span: relationAttribute.relation.span,
690
- contextLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`,
788
+ entityLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`,
691
789
  });
692
790
  if (!localColumns) {
693
791
  continue;
@@ -699,7 +797,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
699
797
  sourceId,
700
798
  diagnostics,
701
799
  span: relationAttribute.relation.span,
702
- contextLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`,
800
+ entityLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`,
703
801
  });
704
802
  if (!referencedColumns) {
705
803
  continue;
@@ -773,14 +871,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
773
871
  ...ifDefined('default', resolvedField.defaultValue),
774
872
  ...ifDefined('executionDefaults', resolvedField.executionDefaults),
775
873
  })),
776
- ...(primaryKeyColumns.length > 0
777
- ? {
778
- id: {
779
- columns: primaryKeyColumns,
780
- ...ifDefined('name', primaryKeyName),
781
- },
782
- }
783
- : {}),
874
+ ...ifDefined('id', primaryKey),
784
875
  ...(uniqueConstraints.length > 0 ? { uniques: uniqueConstraints } : {}),
785
876
  ...(indexNodes.length > 0 ? { indexes: indexNodes } : {}),
786
877
  ...(foreignKeyNodes.length > 0 ? { foreignKeys: foreignKeyNodes } : {}),
@@ -251,13 +251,13 @@ export function parseAttributeFieldList(input: {
251
251
  readonly sourceId: string;
252
252
  readonly diagnostics: ContractSourceDiagnostic[];
253
253
  readonly code: string;
254
- readonly messagePrefix: string;
254
+ readonly entityLabel: string;
255
255
  }): readonly string[] | undefined {
256
256
  const raw = getNamedArgument(input.attribute, 'fields') ?? getPositionalArgument(input.attribute);
257
257
  if (!raw) {
258
258
  input.diagnostics.push({
259
259
  code: input.code,
260
- message: `${input.messagePrefix} requires fields list argument`,
260
+ message: `${input.entityLabel} requires fields list argument`,
261
261
  sourceId: input.sourceId,
262
262
  span: input.attribute.span,
263
263
  });
@@ -267,7 +267,7 @@ export function parseAttributeFieldList(input: {
267
267
  if (!fields || fields.length === 0) {
268
268
  input.diagnostics.push({
269
269
  code: input.code,
270
- message: `${input.messagePrefix} requires bracketed field list argument`,
270
+ message: `${input.entityLabel} requires bracketed field list argument`,
271
271
  sourceId: input.sourceId,
272
272
  span: input.attribute.span,
273
273
  });
@@ -276,6 +276,15 @@ export function parseAttributeFieldList(input: {
276
276
  return fields;
277
277
  }
278
278
 
279
+ export function findDuplicateFieldName(fieldNames: readonly string[]): string | undefined {
280
+ const seen = new Set<string>();
281
+ for (const name of fieldNames) {
282
+ if (seen.has(name)) return name;
283
+ seen.add(name);
284
+ }
285
+ return undefined;
286
+ }
287
+
279
288
  export function mapFieldNamesToColumns(input: {
280
289
  readonly modelName: string;
281
290
  readonly fieldNames: readonly string[];
@@ -283,7 +292,7 @@ export function mapFieldNamesToColumns(input: {
283
292
  readonly sourceId: string;
284
293
  readonly diagnostics: ContractSourceDiagnostic[];
285
294
  readonly span: PslSpan;
286
- readonly contextLabel: string;
295
+ readonly entityLabel: string;
287
296
  }): readonly string[] | undefined {
288
297
  const columns: string[] = [];
289
298
  for (const fieldName of input.fieldNames) {
@@ -291,7 +300,7 @@ export function mapFieldNamesToColumns(input: {
291
300
  if (!columnName) {
292
301
  input.diagnostics.push({
293
302
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
294
- message: `${input.contextLabel} references unknown field "${input.modelName}.${fieldName}"`,
303
+ message: `${input.entityLabel} references unknown field "${input.modelName}.${fieldName}"`,
295
304
  sourceId: input.sourceId,
296
305
  span: input.span,
297
306
  });
@@ -366,6 +366,16 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
366
366
  sourceId,
367
367
  diagnostics,
368
368
  });
369
+ let isIdField = Boolean(idAttribute);
370
+ if (idAttribute && field.optional) {
371
+ diagnostics.push({
372
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
373
+ message: `Field "${model.name}.${field.name}" @id cannot be optional; primary key columns must be NOT NULL`,
374
+ sourceId,
375
+ span: idAttribute.span,
376
+ });
377
+ isIdField = false;
378
+ }
369
379
 
370
380
  // Field presets contribute their own default / executionDefaults / id /
371
381
  // unique. They take precedence over attribute-derived contributions for
@@ -394,7 +404,7 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
394
404
  descriptor,
395
405
  ...ifDefined('defaultValue', fieldDefaultValue),
396
406
  ...ifDefined('executionDefaults', fieldExecutionDefaults),
397
- isId: Boolean(idAttribute) || Boolean(presetContributions?.id),
407
+ isId: isIdField || Boolean(presetContributions?.id),
398
408
  isUnique: Boolean(uniqueAttribute) || Boolean(presetContributions?.unique),
399
409
  ...ifDefined('idName', idName),
400
410
  ...ifDefined('uniqueName', uniqueName),