@vibeorm/schema 2.5.0 → 4.0.0

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/dist/index.js CHANGED
@@ -19,6 +19,7 @@ var VIBE_ERROR_CODES = [
19
19
  "VIBE_RLS_CONTEXT",
20
20
  "VIBE_PERMISSION_DENIED",
21
21
  "VIBE_ADAPTER",
22
+ "VIBE_ADAPTER_CLOSED",
22
23
  "VIBE_INTERNAL"
23
24
  ];
24
25
 
@@ -259,6 +260,7 @@ var POSTGRES_CAPABILITIES = Object.freeze({
259
260
  exclusionConstraints: true,
260
261
  exclusionExtensions: Object.freeze(["btree_gist"]),
261
262
  checkNormalization: "rollback",
263
+ schemaCheckExpressions: true,
262
264
  nativeRowLevelSecurity: true,
263
265
  returning: true,
264
266
  scalarArrays: true,
@@ -275,7 +277,8 @@ var POSTGRES_CAPABILITIES = Object.freeze({
275
277
  fullTextEngines: Object.freeze(["bm25", "tsvector"]),
276
278
  rowLockStrengths: Object.freeze(["update", "noKeyUpdate", "share", "keyShare"]),
277
279
  rowLockWaitPolicies: Object.freeze(["wait", "nowait", "skipLocked"]),
278
- advisoryLocks: true
280
+ advisoryLocks: true,
281
+ timeOfDayTypes: Object.freeze(["time", "timetz"])
279
282
  });
280
283
  var SQLITE_CAPABILITIES = Object.freeze({
281
284
  fixedNamespaces: false,
@@ -284,6 +287,7 @@ var SQLITE_CAPABILITIES = Object.freeze({
284
287
  exclusionConstraints: false,
285
288
  exclusionExtensions: Object.freeze([]),
286
289
  checkNormalization: "sourceText",
290
+ schemaCheckExpressions: false,
287
291
  nativeRowLevelSecurity: false,
288
292
  returning: true,
289
293
  scalarArrays: false,
@@ -300,7 +304,8 @@ var SQLITE_CAPABILITIES = Object.freeze({
300
304
  fullTextEngines: Object.freeze(["fts5"]),
301
305
  rowLockStrengths: Object.freeze([]),
302
306
  rowLockWaitPolicies: Object.freeze([]),
303
- advisoryLocks: false
307
+ advisoryLocks: false,
308
+ timeOfDayTypes: Object.freeze([])
304
309
  });
305
310
  var MYSQL_CAPABILITIES = Object.freeze({
306
311
  fixedNamespaces: false,
@@ -309,6 +314,7 @@ var MYSQL_CAPABILITIES = Object.freeze({
309
314
  exclusionConstraints: false,
310
315
  exclusionExtensions: Object.freeze([]),
311
316
  checkNormalization: "temporary",
317
+ schemaCheckExpressions: false,
312
318
  nativeRowLevelSecurity: false,
313
319
  returning: false,
314
320
  scalarArrays: false,
@@ -325,7 +331,8 @@ var MYSQL_CAPABILITIES = Object.freeze({
325
331
  fullTextEngines: Object.freeze(["fulltext"]),
326
332
  rowLockStrengths: Object.freeze(["update", "share"]),
327
333
  rowLockWaitPolicies: Object.freeze(["wait", "nowait", "skipLocked"]),
328
- advisoryLocks: false
334
+ advisoryLocks: false,
335
+ timeOfDayTypes: Object.freeze(["time"])
329
336
  });
330
337
  var DIALECT_CAPABILITIES = Object.freeze({
331
338
  postgres: POSTGRES_CAPABILITIES,
@@ -606,36 +613,121 @@ function isPredicateFieldRef(operand) {
606
613
  return operand.kind === "field";
607
614
  }
608
615
  function predicateFieldNames(params) {
609
- const seen = new Set;
616
+ return [...new Set(inspectPredicate(params).expressions.flatMap((expression) => expression.kind === "field" ? [expression.name] : []))];
617
+ }
618
+ function inspectPredicate(params) {
619
+ const expressions = [];
620
+ let checkOnly = false;
621
+ const expression = (node) => {
622
+ if (typeof node !== "object" || node === null)
623
+ throw invalidOperand();
624
+ switch (node.kind) {
625
+ case "field":
626
+ expressions.push(node);
627
+ return;
628
+ case "jsonText":
629
+ if (typeof node.field !== "object" || node.field === null || node.field.kind !== "field")
630
+ throw invalidOperand();
631
+ expressions.push(node);
632
+ checkOnly = true;
633
+ expression(node.field);
634
+ return;
635
+ case "length":
636
+ expressions.push(node);
637
+ checkOnly = true;
638
+ expression(node.expression);
639
+ return;
640
+ default:
641
+ throw invalidOperand();
642
+ }
643
+ };
610
644
  const walk = (node) => {
611
645
  switch (node.kind) {
612
646
  case "compare":
613
- seen.add(node.left.name);
614
- if (isPredicateFieldRef(node.right))
615
- seen.add(node.right.name);
647
+ expression(node.left);
648
+ if (typeof node.right !== "object" || node.right === null || node.right.kind !== "value")
649
+ expression(node.right);
616
650
  return;
617
651
  case "inList":
618
652
  case "isNull":
619
- seen.add(node.field.name);
653
+ expression(node.field);
620
654
  return;
621
655
  case "and":
622
656
  case "or":
623
657
  for (const operand of node.operands)
624
658
  walk(operand);
625
659
  return;
660
+ case "isTrue":
661
+ checkOnly = true;
662
+ walk(node.operand);
663
+ return;
626
664
  case "not":
627
665
  walk(node.operand);
628
666
  return;
629
667
  }
630
668
  };
631
669
  walk(params.predicate);
632
- return [...seen];
670
+ return { expressions, checkOnly };
671
+ }
672
+ function invalidOperand() {
673
+ return new VibeError({
674
+ code: "VIBE_SCHEMA_INVALID",
675
+ message: "A schema predicate operand must be a field, jsonText or length expression, or a literal on the right of a comparison",
676
+ meta: { rule: "predicate-operand-invalid" }
677
+ });
678
+ }
679
+ function assertPredicateCapabilities(params) {
680
+ if (!inspectPredicate(params).checkOnly)
681
+ return;
682
+ if (params.usage !== "check")
683
+ throw unsupportedCapability({ dialect: params.dialect, feature: "schema CHECK expressions outside CHECK constraints" });
684
+ if (!DIALECT_CAPABILITIES[params.dialect].schemaCheckExpressions)
685
+ throw unsupportedCapability({ dialect: params.dialect, feature: "schemaCheckExpressions" });
633
686
  }
634
687
  function predicateKey(params) {
688
+ if (inspectPredicate(params).checkOnly) {
689
+ const scalar = (node) => {
690
+ switch (node.kind) {
691
+ case "field":
692
+ return ["field", node.name];
693
+ case "jsonText":
694
+ return ["jsonText", node.field.name, node.path];
695
+ case "length":
696
+ return ["length", scalar(node.expression)];
697
+ }
698
+ };
699
+ const key = (node) => {
700
+ switch (node.kind) {
701
+ case "compare":
702
+ return [node.kind, node.op, scalar(node.left), node.right.kind === "value" ? ["value", node.right.value] : scalar(node.right)];
703
+ case "inList":
704
+ return [node.kind, node.field.name, node.negate === true, node.values.map((value) => value.value)];
705
+ case "isNull":
706
+ return [node.kind, node.field.name, node.negate === true];
707
+ case "and":
708
+ case "or":
709
+ return [node.kind, node.operands.map(key)];
710
+ case "not":
711
+ case "isTrue":
712
+ return [node.kind, key(node.operand)];
713
+ }
714
+ };
715
+ return `check:${JSON.stringify(key(params.predicate))}`;
716
+ }
717
+ const expression = (node) => {
718
+ switch (node.kind) {
719
+ case "field":
720
+ return `f:${node.name}`;
721
+ case "jsonText":
722
+ return `jsonText(${JSON.stringify([node.field.name, node.path])})`;
723
+ case "length":
724
+ return `length(${expression(node.expression)})`;
725
+ }
726
+ };
635
727
  const encode = (node) => {
636
728
  switch (node.kind) {
637
729
  case "compare":
638
- return `cmp(${node.op},${node.left.name},${isPredicateFieldRef(node.right) ? `f:${node.right.name}` : `v:${typeof node.right.value}:${String(node.right.value)}`})`;
730
+ return `cmp(${node.op},${node.left.kind === "field" ? node.left.name : expression(node.left)},${node.right.kind === "value" ? `v:${typeof node.right.value}:${String(node.right.value)}` : expression(node.right)})`;
639
731
  case "inList":
640
732
  return `in${node.negate === true ? "!" : ""}(${node.field.name},[${node.values.map((entry) => `${typeof entry.value}:${String(entry.value)}`).join("|")}])`;
641
733
  case "isNull":
@@ -645,10 +737,26 @@ function predicateKey(params) {
645
737
  return `${node.kind}(${node.operands.map(encode).join(",")})`;
646
738
  case "not":
647
739
  return `not(${encode(node.operand)})`;
740
+ case "isTrue":
741
+ return `isTrue(${encode(node.operand)})`;
648
742
  }
649
743
  };
650
744
  return encode(params.predicate);
651
745
  }
746
+ // packages/schema/src/time-of-day.ts
747
+ var TIME_OF_DAY_NATIVE_TYPE = /^time(tz)?(?:\(\d+\))?$|^time(?:\(\d+\))?with(out)?timezone$/;
748
+ function timeOfDayType(params) {
749
+ if (params.nativeType === undefined)
750
+ return;
751
+ const match = TIME_OF_DAY_NATIVE_TYPE.exec(params.nativeType.toLowerCase().replace(/\s+/g, ""));
752
+ if (match === null)
753
+ return;
754
+ if (match[1] !== undefined)
755
+ return "timetz";
756
+ if (match[0].endsWith("withtimezone") && match[2] === undefined)
757
+ return "timetz";
758
+ return "time";
759
+ }
652
760
  // packages/schema/src/prisma-literal.ts
653
761
  var STRING_ESCAPES = {
654
762
  '"': '"',
@@ -785,6 +893,7 @@ var RESERVED_CLIENT_NAMES = [
785
893
  "$disconnect",
786
894
  "$transaction",
787
895
  "$querySql",
896
+ "$querySqlResult",
788
897
  "$executeSql",
789
898
  "$queryRaw",
790
899
  "$queryRawUnsafe",
@@ -1079,6 +1188,80 @@ function scalarListDefault(params) {
1079
1188
  });
1080
1189
  }
1081
1190
 
1191
+ // packages/schema/src/json-schema-ref.ts
1192
+ var EXPORT_NAME = /^[$_\p{ID_Start}][$_\u200c\u200d\p{ID_Continue}]*$/u;
1193
+ var REFERENCE_ANNOTATION = /@(schema|type)\(\s*([$_\p{ID_Start}][$_\u200c\u200d\p{ID_Continue}]*)\s+from\s+("(?:[^"\\\r\n]|\\.)*")\s*\)/gu;
1194
+ function annotations(params) {
1195
+ const matches = [...params.documentation.matchAll(REFERENCE_ANNOTATION)].filter((match) => match[1] === params.kind);
1196
+ const remaining = params.documentation.replace(REFERENCE_ANNOTATION, "");
1197
+ const malformed = remaining.match(params.kind === "schema" ? /@schema(?=\s*\(|\s|$)/gu : /@type(?=\s*\(|\s|$)/gu) ?? [];
1198
+ return { matches, count: matches.length + malformed.length };
1199
+ }
1200
+ function hasJsonSchemaAnnotation(params) {
1201
+ return annotations({ documentation: params.documentation ?? "", kind: "schema" }).count > 0;
1202
+ }
1203
+ function jsonTypeAnnotationIssue(params) {
1204
+ const refs = annotations({ documentation: params.documentation ?? "", kind: "type" });
1205
+ if (refs.count === 0)
1206
+ return;
1207
+ if (refs.count > 1)
1208
+ return "A field may declare only one assertion-only JSON type";
1209
+ const match = refs.matches[0];
1210
+ if (match === undefined)
1211
+ return 'Expected @type(Name from "specifier")';
1212
+ return jsonSchemaRefIssue({ value: { name: match[2], from: decodePrismaString({ raw: match[3] }) } });
1213
+ }
1214
+ function jsonTypeAnnotationRef(params) {
1215
+ const issue = jsonTypeAnnotationIssue(params);
1216
+ if (issue !== undefined)
1217
+ throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: issue, meta: { issues: [{ rule: "invalid-json-type-ref", message: issue }] } });
1218
+ const match = annotations({ documentation: params.documentation ?? "", kind: "type" }).matches[0];
1219
+ return match === undefined ? undefined : assertJsonSchemaRef({ value: { name: match[2], from: decodePrismaString({ raw: match[3] }) } });
1220
+ }
1221
+ function jsonSchemaRefIssue(params) {
1222
+ const { value } = params;
1223
+ if (typeof value !== "object" || value === null || Array.isArray(value))
1224
+ return "JSON schema reference must be { name, from }; validators belong in the referenced export";
1225
+ const keys = Reflect.ownKeys(value);
1226
+ if (keys.length !== 2 || !keys.includes("name") || !keys.includes("from"))
1227
+ return "JSON schema reference accepts only name and from";
1228
+ const name = Object.getOwnPropertyDescriptor(value, "name");
1229
+ const from = Object.getOwnPropertyDescriptor(value, "from");
1230
+ if (name?.enumerable !== true || !("value" in name) || typeof name.value !== "string" || !EXPORT_NAME.test(name.value) || name.value === "default")
1231
+ return "JSON schema name must be a valid named export, not default";
1232
+ if (from?.enumerable !== true || !("value" in from) || typeof from.value !== "string" || from.value.trim().length === 0 || /[\u0000-\u001f\u007f]/u.test(from.value))
1233
+ return "JSON schema from must be a non-empty import specifier without control characters";
1234
+ return;
1235
+ }
1236
+ function assertJsonSchemaRef(params) {
1237
+ const issue = jsonSchemaRefIssue(params);
1238
+ if (issue !== undefined)
1239
+ throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: issue, meta: { issues: [{ rule: "invalid-json-schema-ref", message: issue }] } });
1240
+ const value = params.value;
1241
+ return Object.freeze({ name: value.name, from: value.from });
1242
+ }
1243
+ function normalizeJsonSchemaRef(params) {
1244
+ const { documentation, reference } = params;
1245
+ const jsonType = jsonTypeAnnotationRef({ documentation });
1246
+ const refs = annotations({ documentation: documentation ?? "", kind: "schema" });
1247
+ let jsonSchema = reference === undefined ? undefined : assertJsonSchemaRef({ value: reference });
1248
+ let remaining = documentation;
1249
+ if (refs.count > 1 || refs.count > 0 && jsonSchema !== undefined)
1250
+ throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "A field may declare only one JSON schema reference", meta: { issues: [{ rule: "duplicate-json-schema-ref" }] } });
1251
+ if (refs.count === 1) {
1252
+ const match = refs.matches[0];
1253
+ if (match === undefined)
1254
+ throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: 'Expected @schema(Name from "specifier")', meta: { issues: [{ rule: "invalid-json-schema-ref" }] } });
1255
+ jsonSchema = assertJsonSchemaRef({ value: { name: match[2], from: decodePrismaString({ raw: match[3] }) } });
1256
+ remaining = documentation?.replace(match[0], "").trim();
1257
+ if (remaining === "")
1258
+ remaining = undefined;
1259
+ }
1260
+ if (jsonSchema !== undefined && jsonType !== undefined)
1261
+ throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "JSON schema validation and assertion-only @type cannot be combined on one field", meta: { issues: [{ rule: "conflicting-json-schema-ref" }] } });
1262
+ return { ...remaining === undefined ? {} : { documentation: remaining }, ...jsonSchema === undefined ? {} : { jsonSchema } };
1263
+ }
1264
+
1082
1265
  // packages/schema/src/m2m.ts
1083
1266
  function implicitJoinTable(params) {
1084
1267
  const sorted = [params.modelA, params.modelB].sort();
@@ -1104,6 +1287,8 @@ var RESERVED_ENUM_NAMES = [
1104
1287
  "SqlColumn",
1105
1288
  "SqlFragment",
1106
1289
  "ResultDecoder",
1290
+ "ResultReadDecoder",
1291
+ "SqlQueryResult",
1107
1292
  "ExtendedDateTime",
1108
1293
  "enumValidators",
1109
1294
  "enumValidator",
@@ -1113,6 +1298,11 @@ var RESERVED_ENUM_NAMES = [
1113
1298
  "UncertainKeys",
1114
1299
  "HasSelectedField",
1115
1300
  "Provided",
1301
+ "SelectionMember",
1302
+ "SelectionValue",
1303
+ "BagKeys",
1304
+ "IncludedRelation",
1305
+ "IncludedCount",
1116
1306
  "ViewDelegateMap",
1117
1307
  "extensionManifest",
1118
1308
  "extensionMounts",
@@ -1155,6 +1345,9 @@ var RESERVED_ENUM_NAMES = [
1155
1345
  "InputJsonValue",
1156
1346
  "InputJsonArray",
1157
1347
  "InputJsonObject",
1348
+ "BoundJsonFilter",
1349
+ "BoundJsonFilterCommon",
1350
+ "BoundJsonNullableFilter",
1158
1351
  "VibeTelemetryOptions",
1159
1352
  "VibeTelemetryExporter",
1160
1353
  "VibeTelemetryOperationEvent",
@@ -1226,6 +1419,18 @@ var GENERATED_TYPE_NAMES = {
1226
1419
  "SumAggregateInput",
1227
1420
  "MinAggregateInput",
1228
1421
  "MaxAggregateInput",
1422
+ "CountAggregateHavingInput",
1423
+ "AvgAggregateHavingInput",
1424
+ "SumAggregateHavingInput",
1425
+ "MinAggregateHavingInput",
1426
+ "MaxAggregateHavingInput",
1427
+ "CountAggregateOrderByInput",
1428
+ "AvgAggregateOrderByInput",
1429
+ "SumAggregateOrderByInput",
1430
+ "MinAggregateOrderByInput",
1431
+ "MaxAggregateOrderByInput",
1432
+ "HavingInput",
1433
+ "GroupByOrderByInput",
1229
1434
  "FindManyArgs",
1230
1435
  "FindPageArgs",
1231
1436
  "FindFirstArgs",
@@ -1254,6 +1459,7 @@ var GENERATED_TYPE_NAMES = {
1254
1459
  "AggregateSelection",
1255
1460
  "CountOutput",
1256
1461
  "RelationPayload",
1462
+ "ResolvedSelection",
1257
1463
  "IncludePayload",
1258
1464
  "GetPayload",
1259
1465
  "AggregateResult",
@@ -1395,6 +1601,57 @@ function validateGeneratedNames(params) {
1395
1601
  register({ name: `${declared.name}${suffix}`, owner: `enum "${declared.name}"` });
1396
1602
  }
1397
1603
  }
1604
+ function foreignKeyKeys(params) {
1605
+ const distinct = new Set;
1606
+ const repeated = [];
1607
+ for (const fields of params.keys) {
1608
+ if (new Set(fields).size === fields.length)
1609
+ distinct.add(JSON.stringify([...fields].sort()));
1610
+ else
1611
+ repeated.push(fields);
1612
+ }
1613
+ return { distinct, repeated };
1614
+ }
1615
+ function matchesForeignKey(params) {
1616
+ if (params.keys.distinct.has(JSON.stringify([...params.references].sort())))
1617
+ return true;
1618
+ return params.keys.repeated.some((fields) => fields.length === params.references.length && fields.every((name) => params.references.includes(name)));
1619
+ }
1620
+ function referencesPartialOnly(params) {
1621
+ const { target, references, schema, lookups } = params;
1622
+ let partial = lookups.partialKeys.get(target);
1623
+ if (partial === undefined) {
1624
+ partial = foreignKeyKeys({ keys: target.indexes.filter((index) => index.kind === "unique" && (index.where !== undefined || index.wherePredicate !== undefined)).map((index) => index.fields.map((field) => field.name)) });
1625
+ lookups.partialKeys.set(target, partial);
1626
+ }
1627
+ if (partial.distinct.size === 0 && partial.repeated.length === 0)
1628
+ return false;
1629
+ if (!matchesForeignKey({ keys: partial, references }))
1630
+ return false;
1631
+ let total = lookups.totalKeys.get(target);
1632
+ if (total === undefined) {
1633
+ total = foreignKeyKeys({ keys: [
1634
+ ...target.fields.filter((field) => field.isId || field.isUnique).map((field) => [field.name]),
1635
+ ...target.primaryKey === undefined ? [] : [target.primaryKey.fields],
1636
+ ...target.indexes.filter((index) => index.kind === "unique" && index.where === undefined && index.wherePredicate === undefined).map((index) => index.fields.map((field) => field.name))
1637
+ ] });
1638
+ lookups.totalKeys.set(target, total);
1639
+ }
1640
+ if (matchesForeignKey({ keys: total, references }))
1641
+ return false;
1642
+ if (lookups.independentTables === undefined) {
1643
+ const tables = new Set;
1644
+ for (const object of schema.migrationObjects ?? []) {
1645
+ const identity = object.identity;
1646
+ if (identity.kind === "index" || identity.kind === "constraint" && (identity.constraintType === "unique" || identity.constraintType === "primaryKey")) {
1647
+ tables.add(JSON.stringify([identity.table.namespace, identity.table.name]));
1648
+ }
1649
+ }
1650
+ lookups.independentTables = tables;
1651
+ }
1652
+ const placement = qualifiedSqlName({ name: target.dbName ?? target.name, namespace: target.namespace });
1653
+ return !lookups.independentTables.has(JSON.stringify([placement.namespace, placement.name]));
1654
+ }
1398
1655
  function validateSchemaIR(params) {
1399
1656
  const { schema } = params;
1400
1657
  const issues = [];
@@ -1465,8 +1722,18 @@ function validateSchemaIR(params) {
1465
1722
  });
1466
1723
  }
1467
1724
  }
1725
+ const fieldsByModel = new Map;
1468
1726
  for (const model of schema.models) {
1469
- validateModel({ model, modelByName, enumByName, issues });
1727
+ const fields = new Map;
1728
+ for (const field of model.fields)
1729
+ if (!fields.has(field.name))
1730
+ fields.set(field.name, field);
1731
+ fieldsByModel.set(model, fields);
1732
+ }
1733
+ const lookups = { fieldsByModel, partialKeys: new Map, totalKeys: new Map };
1734
+ const postgres = providerToDialect({ provider: schema.datasource.provider }) === "postgres";
1735
+ for (const model of schema.models) {
1736
+ validateModel({ model, modelByName, enumByName, issues, schema, lookups, postgres });
1470
1737
  }
1471
1738
  for (const model of schema.models) {
1472
1739
  for (const relation of model.relations) {
@@ -1511,19 +1778,11 @@ function validateSchemaIR(params) {
1511
1778
  }
1512
1779
  }
1513
1780
  for (const model of schema.models) {
1514
- const fieldNames = new Set(model.fields.map((field) => field.name));
1515
- const defaultKeys = new Set;
1516
- if (model.primaryKey !== undefined && model.primaryKey.fields.length > 1 && model.primaryKey.clientName === undefined) {
1517
- defaultKeys.add(model.primaryKey.fields.join("_"));
1518
- }
1519
- for (const index of model.indexes) {
1520
- if (index.kind === "unique" && index.where === undefined && index.fields.length > 1 && index.clientName === undefined) {
1521
- defaultKeys.add(index.fields.map((field) => field.name).join("_"));
1522
- }
1523
- }
1524
- const seenSelectorNames = new Set;
1781
+ const fieldNames = fieldsByModel.get(model);
1782
+ const seenSelectorNames = new Map;
1783
+ const addressable = (names) => names.every((name) => fieldNames.get(name)?.isUnsupported !== true);
1525
1784
  const checkSelectorName = (params) => {
1526
- const { clientName, fieldCount, where, isUnique, site } = params;
1785
+ const { clientName, fields, where, isUnique, site } = params;
1527
1786
  if (!isUnique) {
1528
1787
  issues.push({
1529
1788
  rule: "client-name-non-unique",
@@ -1540,7 +1799,7 @@ function validateSchemaIR(params) {
1540
1799
  });
1541
1800
  return;
1542
1801
  }
1543
- if (fieldCount < 2) {
1802
+ if (fields.length < 2) {
1544
1803
  issues.push({
1545
1804
  rule: "client-name-single-field",
1546
1805
  message: `name: ("${clientName}") on a single-field ${site} of "${model.name}" \u2014 the field itself is the selector; name: applies to compound groups only`,
@@ -1548,7 +1807,9 @@ function validateSchemaIR(params) {
1548
1807
  });
1549
1808
  return;
1550
1809
  }
1551
- if (fieldNames.has(clientName) || defaultKeys.has(clientName) || seenSelectorNames.has(clientName)) {
1810
+ const tuple = JSON.stringify(fields);
1811
+ const previous = seenSelectorNames.get(clientName);
1812
+ if (fieldNames.has(clientName) || previous !== undefined && previous !== tuple) {
1552
1813
  issues.push({
1553
1814
  rule: "client-name-collision",
1554
1815
  message: `name: ("${clientName}") on "${model.name}" collides with a field or another unique selector \u2014 WhereUniqueInput keys must be unambiguous`,
@@ -1556,22 +1817,22 @@ function validateSchemaIR(params) {
1556
1817
  });
1557
1818
  return;
1558
1819
  }
1559
- seenSelectorNames.add(clientName);
1820
+ seenSelectorNames.set(clientName, tuple);
1560
1821
  };
1561
- if (model.primaryKey !== undefined && model.primaryKey.clientName !== undefined) {
1822
+ if (model.primaryKey !== undefined && (model.primaryKey.clientName !== undefined || model.primaryKey.fields.length > 1 && addressable(model.primaryKey.fields))) {
1562
1823
  checkSelectorName({
1563
- clientName: model.primaryKey.clientName,
1564
- fieldCount: model.primaryKey.fields.length,
1824
+ clientName: model.primaryKey.clientName ?? model.primaryKey.fields.join("_"),
1825
+ fields: model.primaryKey.fields,
1565
1826
  isUnique: true,
1566
1827
  site: "@@id"
1567
1828
  });
1568
1829
  }
1569
1830
  for (const index of model.indexes) {
1570
- if (index.clientName === undefined)
1831
+ if (index.clientName === undefined && (index.kind !== "unique" || index.where !== undefined || index.fields.length < 2 || !addressable(index.fields.map((field) => field.name))))
1571
1832
  continue;
1572
1833
  checkSelectorName({
1573
- clientName: index.clientName,
1574
- fieldCount: index.fields.length,
1834
+ clientName: index.clientName ?? index.fields.map((field) => field.name).join("_"),
1835
+ fields: index.fields.map((field) => field.name),
1575
1836
  ...index.where === undefined ? {} : { where: index.where },
1576
1837
  isUnique: index.kind === "unique",
1577
1838
  site: "@@unique"
@@ -1579,13 +1840,13 @@ function validateSchemaIR(params) {
1579
1840
  }
1580
1841
  }
1581
1842
  validateIdentifierLengths({ schema, issues });
1582
- validatePhysicalNameUniqueness({ schema, issues });
1843
+ validatePhysicalNameUniqueness({ schema, issues, modelByName, fieldsByModel });
1583
1844
  validateGeneratedNames({ schema, issues });
1584
1845
  validateNativeRls({ schema, issues });
1585
1846
  return issues;
1586
1847
  }
1587
1848
  function validatePhysicalNameUniqueness(params) {
1588
- const { schema, issues } = params;
1849
+ const { schema, issues, modelByName, fieldsByModel } = params;
1589
1850
  const dialect = providerToDialect({ provider: schema.datasource.provider });
1590
1851
  const owners = new Map;
1591
1852
  const claim = (entry) => {
@@ -1615,7 +1876,8 @@ function validatePhysicalNameUniqueness(params) {
1615
1876
  }
1616
1877
  if (model.isView)
1617
1878
  continue;
1618
- const columnOf = (name) => model.fields.find((candidate) => candidate.name === name)?.dbName ?? name;
1879
+ const fields = fieldsByModel.get(model);
1880
+ const columnOf = (name) => fields.get(name)?.dbName ?? name;
1619
1881
  const seenIndexKeys = new Set;
1620
1882
  const declarations = [
1621
1883
  ...model.indexes.map((index) => ({ index, site: index.kind === "unique" ? "@@unique" : "@@index" })),
@@ -1623,8 +1885,8 @@ function validatePhysicalNameUniqueness(params) {
1623
1885
  ];
1624
1886
  for (const { index, site } of declarations) {
1625
1887
  const unique = index.kind === "unique";
1626
- const columns = index.fields.map((entry) => `${columnOf(entry.name)}${entry.sort === "desc" ? " desc" : ""}`);
1627
- const key = `${unique ? "u" : "i"}${index.nullsNotDistinct === true ? "n" : ""}:${unique ? "btree" : index.kind}:${columns.join(",")}:${index.where?.trim() ?? ""}`;
1888
+ const columns = index.fields.map((entry) => [columnOf(entry.name), entry.sort === "desc", entry.operatorClass === undefined ? null : [entry.operatorClass.namespace, entry.operatorClass.name]]);
1889
+ const key = JSON.stringify([unique, index.nullsNotDistinct === true, unique ? "btree" : index.kind, columns, index.where?.trim() ?? ""]);
1628
1890
  if (seenIndexKeys.has(key))
1629
1891
  continue;
1630
1892
  seenIndexKeys.add(key);
@@ -1642,7 +1904,7 @@ function validatePhysicalNameUniqueness(params) {
1642
1904
  for (const relation of model.relations) {
1643
1905
  if (relation.fields === undefined || relation.fields.length === 0)
1644
1906
  continue;
1645
- const target = schema.models.find((candidate) => candidate.name === relation.targetModel);
1907
+ const target = modelByName.get(relation.targetModel);
1646
1908
  if (target !== undefined && target.isView)
1647
1909
  continue;
1648
1910
  const name = relation.constraintName ?? defaultForeignKeyName({ table, columns: relation.fields.map((entry) => columnOf(entry)), dialect });
@@ -1671,7 +1933,7 @@ function validatePhysicalNameUniqueness(params) {
1671
1933
  continue;
1672
1934
  if (!isImplicitManyToManyRelation({ model, relation, models: schema.models }))
1673
1935
  continue;
1674
- const target = schema.models.find((candidate) => candidate.name === relation.targetModel);
1936
+ const target = modelByName.get(relation.targetModel);
1675
1937
  const physicalNamespace = model.namespace ?? "public";
1676
1938
  if ((target?.namespace ?? "public") !== physicalNamespace) {
1677
1939
  issues.push({ rule: "implicit-join-namespace", message: "Implicit many-to-many endpoints must share one fixed namespace", model: model.name, field: relation.name });
@@ -1770,7 +2032,7 @@ function validateIdentifierLengths(params) {
1770
2032
  }
1771
2033
  }
1772
2034
  function validateModel(params) {
1773
- const { model, modelByName, enumByName, issues } = params;
2035
+ const { model, modelByName, enumByName, issues, lookups, postgres } = params;
1774
2036
  const memberNames = new Set;
1775
2037
  for (const field of model.fields) {
1776
2038
  if (memberNames.has(field.name)) {
@@ -1782,6 +2044,23 @@ function validateModel(params) {
1782
2044
  });
1783
2045
  }
1784
2046
  memberNames.add(field.name);
2047
+ const typedIssue = jsonTypeAnnotationIssue({ documentation: field.documentation });
2048
+ if (typedIssue !== undefined)
2049
+ issues.push({ rule: "invalid-json-type-ref", message: typedIssue, model: model.name, field: field.name });
2050
+ const hasType = typedIssue === undefined && jsonTypeAnnotationRef({ documentation: field.documentation }) !== undefined;
2051
+ if (hasType && field.type !== "Json")
2052
+ issues.push({ rule: "json-type-non-json", message: "JSON type assertions are only valid on Json fields", model: model.name, field: field.name });
2053
+ if (field.jsonSchema !== undefined) {
2054
+ const issue = jsonSchemaRefIssue({ value: field.jsonSchema });
2055
+ if (issue !== undefined)
2056
+ issues.push({ rule: "invalid-json-schema-ref", message: issue, model: model.name, field: field.name });
2057
+ if (field.type !== "Json" || field.isUnsupported === true)
2058
+ issues.push({ rule: "json-schema-non-json", message: "JSON schemas are only valid on queryable Json fields", model: model.name, field: field.name });
2059
+ if (hasType)
2060
+ issues.push({ rule: "conflicting-json-schema-ref", message: "A field cannot declare both @type and a JSON schema", model: model.name, field: field.name });
2061
+ }
2062
+ if (hasJsonSchemaAnnotation({ documentation: field.documentation }))
2063
+ issues.push({ rule: "unnormalized-json-schema-ref", message: "Normalize @schema documentation into FieldIR.jsonSchema before consuming SchemaIR", model: model.name, field: field.name });
1785
2064
  const typeIdentity = field.nativeTypeIdentity;
1786
2065
  if (typeIdentity !== undefined && (typeIdentity === null || typeof typeIdentity !== "object" || field.isUnsupported !== true || typeof typeIdentity.source !== "string" || typeIdentity.source !== field.nativeType || typeof typeIdentity.namespace !== "string" || typeIdentity.namespace.length === 0 || typeIdentity.namespace.includes("\x00"))) {
1787
2066
  issues.push({ rule: "invalid-native-type-identity", message: `PostgreSQL type identity on "${model.name}.${field.name}" must be bound to its unsupported declaration`, model: model.name, field: field.name });
@@ -1872,8 +2151,23 @@ function validateModel(params) {
1872
2151
  }
1873
2152
  memberNames.add(relation.name);
1874
2153
  }
1875
- const fieldNames = new Set(model.fields.map((f) => f.name));
1876
- const hasIdField = model.fields.some((f) => f.isId);
2154
+ const fieldNames = new Set(lookups.fieldsByModel.get(model).keys());
2155
+ const idFields = model.fields.filter((f) => f.isId);
2156
+ const hasIdField = idFields.length > 0;
2157
+ if (idFields.length > 1) {
2158
+ issues.push({
2159
+ rule: "multiple-field-ids",
2160
+ message: `Model "${model.name}" has multiple @id fields (${idFields.map((field) => field.name).join(", ")}); declare one compound primary key instead`,
2161
+ model: model.name
2162
+ });
2163
+ }
2164
+ if (idFields.length === 1 && model.primaryKey !== undefined && (model.primaryKey.fields.length !== 1 || model.primaryKey.fields[0] !== idFields[0]?.name)) {
2165
+ issues.push({
2166
+ rule: "conflicting-primary-key",
2167
+ message: `Model "${model.name}" declares @id on "${idFields[0]?.name}" and a different block primary key; both declarations must name the same single logical field`,
2168
+ model: model.name
2169
+ });
2170
+ }
1877
2171
  if (!model.isView && !hasIdField && model.primaryKey === undefined) {
1878
2172
  issues.push({
1879
2173
  rule: "missing-id",
@@ -1932,8 +2226,14 @@ function validateModel(params) {
1932
2226
  });
1933
2227
  }
1934
2228
  }
2229
+ if (postgres && new Set(relation.references).size !== relation.references.length) {
2230
+ issues.push({ rule: "relation-duplicate-column", message: `Relation "${model.name}.${relation.name}" repeats a referenced column`, model: model.name, field: relation.name });
2231
+ }
1935
2232
  if (target !== undefined) {
1936
- const targetFieldNames = new Set(target.fields.map((f) => f.name));
2233
+ if (postgres && referencesPartialOnly({ target, references: relation.references, schema: params.schema, lookups })) {
2234
+ issues.push({ rule: "relation-partial-only-target", message: `Relation "${model.name}.${relation.name}" references only a partial unique key`, model: model.name, field: relation.name });
2235
+ }
2236
+ const targetFieldNames = lookups.fieldsByModel.get(target);
1937
2237
  for (const refField of relation.references) {
1938
2238
  if (!targetFieldNames.has(refField)) {
1939
2239
  issues.push({
@@ -2004,6 +2304,8 @@ function validateModel(params) {
2004
2304
  model,
2005
2305
  fieldNames,
2006
2306
  site: `CHECK "${check.name}"`,
2307
+ usage: "check",
2308
+ dialect: providerToDialect({ provider: params.schema.datasource.provider }),
2007
2309
  issues
2008
2310
  });
2009
2311
  }
@@ -2025,13 +2327,55 @@ function validateModel(params) {
2025
2327
  model,
2026
2328
  fieldNames,
2027
2329
  site: `partial index on [${index.fields.map((entry) => entry.name).join(", ")}]`,
2330
+ usage: "indexPredicate",
2331
+ dialect: providerToDialect({ provider: params.schema.datasource.provider }),
2028
2332
  issues
2029
2333
  });
2030
2334
  }
2031
2335
  }
2336
+ for (const exclusion of model.exclusions ?? []) {
2337
+ const predicate = exclusion.source?.where;
2338
+ if (predicate !== undefined)
2339
+ validatePredicateFields({ predicate, model, fieldNames, site: `exclusion "${exclusion.name}"`, usage: "exclusionPredicate", dialect: providerToDialect({ provider: params.schema.datasource.provider }), issues });
2340
+ }
2032
2341
  }
2033
2342
  function validatePredicateFields(params) {
2034
2343
  const { predicate, model, fieldNames, site, issues } = params;
2344
+ const inspection = inspectPredicate({ predicate });
2345
+ if (inspection.checkOnly && (params.usage !== "check" || !DIALECT_CAPABILITIES[params.dialect].schemaCheckExpressions)) {
2346
+ issues.push({ rule: "predicate-check-capability", message: `${site}: computed schema predicates require a PostgreSQL CHECK`, model: model.name });
2347
+ }
2348
+ const expressionType = (expression) => {
2349
+ if (expression.kind === "field")
2350
+ return model.fields.find((field) => field.name === expression.name)?.type;
2351
+ if (expression.kind === "jsonText") {
2352
+ const field = model.fields.find((candidate) => candidate.name === expression.field.name);
2353
+ if (field !== undefined && (field.type !== "Json" || field.isUnsupported === true))
2354
+ issues.push({ rule: "predicate-type-mismatch", message: `${site}: jsonText requires a Json field`, model: model.name, field: field.name });
2355
+ if (!Array.isArray(expression.path) || Array.from(expression.path).some((segment) => typeof segment !== "string" || segment.includes("\x00")))
2356
+ issues.push({ rule: "predicate-json-path", message: `${site}: jsonText requires a string-only path without NUL bytes`, model: model.name });
2357
+ return "String";
2358
+ }
2359
+ if (expressionType(expression.expression) !== "String")
2360
+ issues.push({ rule: "predicate-type-mismatch", message: `${site}: length requires a text expression`, model: model.name });
2361
+ return "Int";
2362
+ };
2363
+ const validateComputedComparisons = (node) => {
2364
+ if (node.kind === "compare") {
2365
+ if (node.left.kind !== "field" || node.right.kind !== "value" && node.right.kind !== "field") {
2366
+ const left = expressionType(node.left);
2367
+ const right = node.right.kind === "value" ? undefined : expressionType(node.right);
2368
+ if (left !== undefined && right !== undefined && left !== right)
2369
+ issues.push({ rule: "predicate-type-mismatch", message: `${site}: computed comparison operands must have the same scalar type`, model: model.name });
2370
+ if (node.right.kind === "value" && left === "String" && typeof node.right.value !== "string")
2371
+ issues.push({ rule: "predicate-type-mismatch", message: `${site}: a text expression requires a string literal`, model: model.name });
2372
+ }
2373
+ } else if (node.kind === "and" || node.kind === "or")
2374
+ node.operands.forEach(validateComputedComparisons);
2375
+ else if (node.kind === "not" || node.kind === "isTrue")
2376
+ validateComputedComparisons(node.operand);
2377
+ };
2378
+ validateComputedComparisons(predicate);
2035
2379
  for (const name of predicateFieldNames({ predicate })) {
2036
2380
  if (!fieldNames.has(name)) {
2037
2381
  issues.push({
@@ -2042,7 +2386,10 @@ function validatePredicateFields(params) {
2042
2386
  });
2043
2387
  continue;
2044
2388
  }
2045
- if (model.fields.find((field) => field.name === name)?.isList === true) {
2389
+ const referenced = model.fields.find((field) => field.name === name);
2390
+ if (inspection.checkOnly && referenced?.isUnsupported === true)
2391
+ issues.push({ rule: "predicate-unsupported-type", message: `${site}: computed predicates cannot reference unsupported fields`, model: model.name, field: name });
2392
+ if (referenced?.isList === true) {
2046
2393
  issues.push({
2047
2394
  rule: "predicate-list-field",
2048
2395
  message: `${site} on "${model.name}" compares scalar list "${name}" \u2014 use a raw expression for array operators`,
@@ -2069,6 +2416,12 @@ function assertValidSchemaIR(params) {
2069
2416
  throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Enum reference namespace differs from its declared native type", meta: { rule: "enum-namespace", model: model.name, field: field.name } });
2070
2417
  }
2071
2418
  for (const model of params.schema.models) {
2419
+ for (const check of model.checks ?? [])
2420
+ if (check.predicate !== undefined)
2421
+ assertPredicateCapabilities({ predicate: check.predicate, dialect, usage: "check" });
2422
+ for (const index of model.indexes)
2423
+ if (index.wherePredicate !== undefined)
2424
+ assertPredicateCapabilities({ predicate: index.wherePredicate, dialect, usage: "indexPredicate" });
2072
2425
  if (model.exclusions !== undefined && !Array.isArray(model.exclusions))
2073
2426
  throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Exclusions must be an array", meta: { rule: "exclusion-definition" } });
2074
2427
  if ((model.exclusions?.length ?? 0) === 0)
@@ -2082,6 +2435,8 @@ function assertValidSchemaIR(params) {
2082
2435
  throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Invalid exclusion declaration", meta: { rule: "exclusion-definition" } });
2083
2436
  const source = exclusion.source;
2084
2437
  if (source !== undefined) {
2438
+ if (source.where !== undefined)
2439
+ assertPredicateCapabilities({ predicate: source.where, dialect, usage: "exclusionPredicate" });
2085
2440
  const references = [...source.keys.flatMap((key) => ("field" in key) ? [key.field] : key.fields), ...[source.range.lower, source.range.upper].flatMap((endpoint) => ("field" in endpoint) ? [endpoint.field] : []), ...source.where === undefined ? [] : predicateFieldNames({ predicate: source.where })];
2086
2441
  if (references.some((name) => !model.fields.some((field) => field.name === name && !field.isList)))
2087
2442
  throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Exclusion references an unknown or list field", meta: { rule: "exclusion-field", model: model.name } });
@@ -2101,6 +2456,16 @@ function assertValidSchemaIR(params) {
2101
2456
  if (dialect === "mysql" && counters.length > 1)
2102
2457
  throw unsupportedCapability({ dialect, feature: "multiple auto-increment columns" });
2103
2458
  for (const field of model.fields) {
2459
+ const timeOfDay = field.isUnsupported === true || field.type !== "DateTime" ? undefined : timeOfDayType({ nativeType: field.nativeType });
2460
+ if (timeOfDay !== undefined && !DIALECT_CAPABILITIES[dialect].timeOfDayTypes.includes(timeOfDay)) {
2461
+ const instead = dialect === "sqlite" ? "sqlite has no time-of-day column type; store a plain DateTime or a String" : "there is no time-with-offset type here; use @db.Time(n), which stores the UTC time of day";
2462
+ const error = unsupportedCapability({ dialect, feature: "timeOfDayTypes", hint: `${model.name}.${field.name} declares @db.${field.nativeType ?? ""} (${timeOfDay}) \u2014 ${instead}` });
2463
+ throw new VibeError({ code: error.code, message: error.message, meta: { ...error.meta, model: model.name, field: field.name, nativeType: field.nativeType } });
2464
+ }
2465
+ if (timeOfDay !== undefined && dialect === "mysql" && field.default?.kind === "now") {
2466
+ const error = unsupportedCapability({ dialect, feature: "@default(now()) on a TIME column", hint: `${model.name}.${field.name} \u2014 MySQL has no current-time default for TIME; drop the default and write the value (or use @updatedAt)` });
2467
+ throw new VibeError({ code: error.code, message: error.message, meta: { ...error.meta, model: model.name, field: field.name, nativeType: field.nativeType } });
2468
+ }
2104
2469
  if (field.default?.kind === "list") {
2105
2470
  if (dialect !== "postgres")
2106
2471
  throw unsupportedCapability({ dialect, feature: "scalar-list defaults" });
@@ -2333,7 +2698,9 @@ export {
2333
2698
  VibeError,
2334
2699
  assertIdentifierFits,
2335
2700
  assertIndexOperatorClasses,
2701
+ assertJsonSchemaRef,
2336
2702
  assertMigrationObjects,
2703
+ assertPredicateCapabilities,
2337
2704
  assertSqlNamespace,
2338
2705
  assertValidSchemaIR,
2339
2706
  buildComputedManifest,
@@ -2352,9 +2719,11 @@ export {
2352
2719
  extensionOptionsHash,
2353
2720
  fieldTypeIdentity,
2354
2721
  fitDefaultName,
2722
+ hasJsonSchemaAnnotation,
2355
2723
  identifierLength,
2356
2724
  implicitJoinTable,
2357
2725
  implicitJoinTableObjectNames,
2726
+ inspectPredicate,
2358
2727
  internalError,
2359
2728
  invalidStoredData,
2360
2729
  isEnumType,
@@ -2364,7 +2733,11 @@ export {
2364
2733
  isReservedDelegateName,
2365
2734
  isSqlObjectDefinition,
2366
2735
  isSqlObjectIdentity,
2736
+ jsonSchemaRefIssue,
2737
+ jsonTypeAnnotationIssue,
2738
+ jsonTypeAnnotationRef,
2367
2739
  mergeExtensionDeclarations,
2740
+ normalizeJsonSchemaRef,
2368
2741
  postgresGeneratedObjectName,
2369
2742
  predicateFieldNames,
2370
2743
  predicateKey,
@@ -2376,10 +2749,11 @@ export {
2376
2749
  sqlNamespaceIssue,
2377
2750
  sqlObjectKey,
2378
2751
  sqlObjectNamespace,
2752
+ timeOfDayType,
2379
2753
  unsupportedCapability,
2380
2754
  validateComputedConfig,
2381
2755
  validateRlsContextIR,
2382
2756
  validateSchemaIR
2383
2757
  };
2384
2758
 
2385
- //# debugId=20984641ACA6D36E64756E2164756E21
2759
+ //# debugId=6D288D6F1DD93E5864756E2164756E21