@vibeorm/schema 3.0.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.
@@ -51,6 +51,8 @@ export type DialectCapabilities = {
51
51
  readonly exclusionConstraints: boolean;
52
52
  readonly exclusionExtensions: readonly string[];
53
53
  readonly checkNormalization: "rollback" | "temporary" | "sourceText";
54
+ /** Typed JSON text, text length and fail-closed boolean expressions in CHECKs. */
55
+ readonly schemaCheckExpressions: boolean;
54
56
  /** Native database row policies; PGlite additionally needs a verified restricted role. */
55
57
  readonly nativeRowLevelSecurity: boolean;
56
58
  /** INSERT/UPDATE/DELETE ... RETURNING. False ⇒ insertId + re-select fallback. */
package/dist/index.d.ts CHANGED
@@ -8,9 +8,9 @@ export type { DatabaseModuleOptions, DatabaseModuleManifest, DatabaseModuleArtif
8
8
  export { prepareDatabaseModuleSchema } from "./module-schema.ts";
9
9
  export type { ExclusionIR, ExclusionSpec, ExclusionKey, ExclusionEndpoint } from "./exclusion.ts";
10
10
  export { DIALECTS, ENUM_STORAGES, enumStorageOf, fieldTypeIdentity, isEnumType, providerToDialect } from "./ir.ts";
11
- export type { CascadeAction, DatasourceIR, DefaultValueIR, Dialect, EnumIR, EnumStorage, ExtensionDeclIR, FieldIR, FieldType, IndexFieldIR, CheckIR, IndexIR, IndexKind, ModelIR, Provider, RelationIR, RelationType, ScalarType, SchemaIR, } from "./ir.ts";
12
- export { PREDICATE_COMPARISON_OPS, isPredicateFieldRef, predicateFieldNames, predicateKey } from "./predicate.ts";
13
- export type { PredicateComparisonOp, PredicateFieldRef, PredicateIR, PredicateOperand, PredicateValue } from "./predicate.ts";
11
+ export type { CascadeAction, DatasourceIR, DefaultValueIR, Dialect, EnumIR, EnumStorage, ExtensionDeclIR, FieldIR, FieldType, JsonSchemaRefIR, IndexFieldIR, CheckIR, IndexIR, IndexKind, ModelIR, Provider, RelationIR, RelationType, ScalarType, SchemaIR, } from "./ir.ts";
12
+ export { PREDICATE_COMPARISON_OPS, assertPredicateCapabilities, inspectPredicate, isPredicateFieldRef, predicateFieldNames, predicateKey } from "./predicate.ts";
13
+ export type { PredicateComparisonOp, PredicateExpression, PredicateFieldRef, PredicateIR, PredicateOperand, PredicateValue } from "./predicate.ts";
14
14
  export { assertMigrationObjects, isSqlObjectDefinition, isSqlObjectIdentity, sqlObjectKey, sqlObjectNamespace } from "./migration-objects.ts";
15
15
  export type { MigrationObjectIR, QualifiedSqlName, SqlObjectDefinition, SqlObjectDefinitionFormat, SqlObjectFacts, SqlObjectIdentity } from "./migration-objects.ts";
16
16
  export type { AdvisoryLockMode, DialectCapabilities, FullTextEngine, RowLockStrength, RowLockWaitPolicy, TimeOfDayType, } from "./capabilities.ts";
@@ -35,4 +35,5 @@ export { RLS_CONTEXT_REASONS, RlsContextError } from "./errors.ts";
35
35
  export type { RlsContextReason } from "./errors.ts";
36
36
  export { isImplicitManyToManyRelation, validateRlsContextIR } from "./validate.ts";
37
37
  export { scalarListDefault, serializeJsonDefault } from "./default-values.ts";
38
+ export { assertJsonSchemaRef, hasJsonSchemaAnnotation, jsonSchemaRefIssue, jsonTypeAnnotationIssue, jsonTypeAnnotationRef, normalizeJsonSchemaRef } from "./json-schema-ref.ts";
38
39
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -260,6 +260,7 @@ var POSTGRES_CAPABILITIES = Object.freeze({
260
260
  exclusionConstraints: true,
261
261
  exclusionExtensions: Object.freeze(["btree_gist"]),
262
262
  checkNormalization: "rollback",
263
+ schemaCheckExpressions: true,
263
264
  nativeRowLevelSecurity: true,
264
265
  returning: true,
265
266
  scalarArrays: true,
@@ -286,6 +287,7 @@ var SQLITE_CAPABILITIES = Object.freeze({
286
287
  exclusionConstraints: false,
287
288
  exclusionExtensions: Object.freeze([]),
288
289
  checkNormalization: "sourceText",
290
+ schemaCheckExpressions: false,
289
291
  nativeRowLevelSecurity: false,
290
292
  returning: true,
291
293
  scalarArrays: false,
@@ -312,6 +314,7 @@ var MYSQL_CAPABILITIES = Object.freeze({
312
314
  exclusionConstraints: false,
313
315
  exclusionExtensions: Object.freeze([]),
314
316
  checkNormalization: "temporary",
317
+ schemaCheckExpressions: false,
315
318
  nativeRowLevelSecurity: false,
316
319
  returning: false,
317
320
  scalarArrays: false,
@@ -610,36 +613,121 @@ function isPredicateFieldRef(operand) {
610
613
  return operand.kind === "field";
611
614
  }
612
615
  function predicateFieldNames(params) {
613
- 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
+ };
614
644
  const walk = (node) => {
615
645
  switch (node.kind) {
616
646
  case "compare":
617
- seen.add(node.left.name);
618
- if (isPredicateFieldRef(node.right))
619
- 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);
620
650
  return;
621
651
  case "inList":
622
652
  case "isNull":
623
- seen.add(node.field.name);
653
+ expression(node.field);
624
654
  return;
625
655
  case "and":
626
656
  case "or":
627
657
  for (const operand of node.operands)
628
658
  walk(operand);
629
659
  return;
660
+ case "isTrue":
661
+ checkOnly = true;
662
+ walk(node.operand);
663
+ return;
630
664
  case "not":
631
665
  walk(node.operand);
632
666
  return;
633
667
  }
634
668
  };
635
669
  walk(params.predicate);
636
- 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" });
637
686
  }
638
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
+ };
639
727
  const encode = (node) => {
640
728
  switch (node.kind) {
641
729
  case "compare":
642
- 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)})`;
643
731
  case "inList":
644
732
  return `in${node.negate === true ? "!" : ""}(${node.field.name},[${node.values.map((entry) => `${typeof entry.value}:${String(entry.value)}`).join("|")}])`;
645
733
  case "isNull":
@@ -649,6 +737,8 @@ function predicateKey(params) {
649
737
  return `${node.kind}(${node.operands.map(encode).join(",")})`;
650
738
  case "not":
651
739
  return `not(${encode(node.operand)})`;
740
+ case "isTrue":
741
+ return `isTrue(${encode(node.operand)})`;
652
742
  }
653
743
  };
654
744
  return encode(params.predicate);
@@ -803,6 +893,7 @@ var RESERVED_CLIENT_NAMES = [
803
893
  "$disconnect",
804
894
  "$transaction",
805
895
  "$querySql",
896
+ "$querySqlResult",
806
897
  "$executeSql",
807
898
  "$queryRaw",
808
899
  "$queryRawUnsafe",
@@ -1097,6 +1188,80 @@ function scalarListDefault(params) {
1097
1188
  });
1098
1189
  }
1099
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
+
1100
1265
  // packages/schema/src/m2m.ts
1101
1266
  function implicitJoinTable(params) {
1102
1267
  const sorted = [params.modelA, params.modelB].sort();
@@ -1122,6 +1287,8 @@ var RESERVED_ENUM_NAMES = [
1122
1287
  "SqlColumn",
1123
1288
  "SqlFragment",
1124
1289
  "ResultDecoder",
1290
+ "ResultReadDecoder",
1291
+ "SqlQueryResult",
1125
1292
  "ExtendedDateTime",
1126
1293
  "enumValidators",
1127
1294
  "enumValidator",
@@ -1178,6 +1345,9 @@ var RESERVED_ENUM_NAMES = [
1178
1345
  "InputJsonValue",
1179
1346
  "InputJsonArray",
1180
1347
  "InputJsonObject",
1348
+ "BoundJsonFilter",
1349
+ "BoundJsonFilterCommon",
1350
+ "BoundJsonNullableFilter",
1181
1351
  "VibeTelemetryOptions",
1182
1352
  "VibeTelemetryExporter",
1183
1353
  "VibeTelemetryOperationEvent",
@@ -1874,6 +2044,23 @@ function validateModel(params) {
1874
2044
  });
1875
2045
  }
1876
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 });
1877
2064
  const typeIdentity = field.nativeTypeIdentity;
1878
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"))) {
1879
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 });
@@ -2117,6 +2304,8 @@ function validateModel(params) {
2117
2304
  model,
2118
2305
  fieldNames,
2119
2306
  site: `CHECK "${check.name}"`,
2307
+ usage: "check",
2308
+ dialect: providerToDialect({ provider: params.schema.datasource.provider }),
2120
2309
  issues
2121
2310
  });
2122
2311
  }
@@ -2138,13 +2327,55 @@ function validateModel(params) {
2138
2327
  model,
2139
2328
  fieldNames,
2140
2329
  site: `partial index on [${index.fields.map((entry) => entry.name).join(", ")}]`,
2330
+ usage: "indexPredicate",
2331
+ dialect: providerToDialect({ provider: params.schema.datasource.provider }),
2141
2332
  issues
2142
2333
  });
2143
2334
  }
2144
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
+ }
2145
2341
  }
2146
2342
  function validatePredicateFields(params) {
2147
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);
2148
2379
  for (const name of predicateFieldNames({ predicate })) {
2149
2380
  if (!fieldNames.has(name)) {
2150
2381
  issues.push({
@@ -2155,7 +2386,10 @@ function validatePredicateFields(params) {
2155
2386
  });
2156
2387
  continue;
2157
2388
  }
2158
- 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) {
2159
2393
  issues.push({
2160
2394
  rule: "predicate-list-field",
2161
2395
  message: `${site} on "${model.name}" compares scalar list "${name}" \u2014 use a raw expression for array operators`,
@@ -2182,6 +2416,12 @@ function assertValidSchemaIR(params) {
2182
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 } });
2183
2417
  }
2184
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" });
2185
2425
  if (model.exclusions !== undefined && !Array.isArray(model.exclusions))
2186
2426
  throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Exclusions must be an array", meta: { rule: "exclusion-definition" } });
2187
2427
  if ((model.exclusions?.length ?? 0) === 0)
@@ -2195,6 +2435,8 @@ function assertValidSchemaIR(params) {
2195
2435
  throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Invalid exclusion declaration", meta: { rule: "exclusion-definition" } });
2196
2436
  const source = exclusion.source;
2197
2437
  if (source !== undefined) {
2438
+ if (source.where !== undefined)
2439
+ assertPredicateCapabilities({ predicate: source.where, dialect, usage: "exclusionPredicate" });
2198
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 })];
2199
2441
  if (references.some((name) => !model.fields.some((field) => field.name === name && !field.isList)))
2200
2442
  throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Exclusion references an unknown or list field", meta: { rule: "exclusion-field", model: model.name } });
@@ -2456,7 +2698,9 @@ export {
2456
2698
  VibeError,
2457
2699
  assertIdentifierFits,
2458
2700
  assertIndexOperatorClasses,
2701
+ assertJsonSchemaRef,
2459
2702
  assertMigrationObjects,
2703
+ assertPredicateCapabilities,
2460
2704
  assertSqlNamespace,
2461
2705
  assertValidSchemaIR,
2462
2706
  buildComputedManifest,
@@ -2475,9 +2719,11 @@ export {
2475
2719
  extensionOptionsHash,
2476
2720
  fieldTypeIdentity,
2477
2721
  fitDefaultName,
2722
+ hasJsonSchemaAnnotation,
2478
2723
  identifierLength,
2479
2724
  implicitJoinTable,
2480
2725
  implicitJoinTableObjectNames,
2726
+ inspectPredicate,
2481
2727
  internalError,
2482
2728
  invalidStoredData,
2483
2729
  isEnumType,
@@ -2487,7 +2733,11 @@ export {
2487
2733
  isReservedDelegateName,
2488
2734
  isSqlObjectDefinition,
2489
2735
  isSqlObjectIdentity,
2736
+ jsonSchemaRefIssue,
2737
+ jsonTypeAnnotationIssue,
2738
+ jsonTypeAnnotationRef,
2490
2739
  mergeExtensionDeclarations,
2740
+ normalizeJsonSchemaRef,
2491
2741
  postgresGeneratedObjectName,
2492
2742
  predicateFieldNames,
2493
2743
  predicateKey,
@@ -2506,4 +2756,4 @@ export {
2506
2756
  validateSchemaIR
2507
2757
  };
2508
2758
 
2509
- //# debugId=99BBBD6C3E58D9DD64756E2164756E21
2759
+ //# debugId=6D288D6F1DD93E5864756E2164756E21