@vibeorm/dsl 2.0.0 → 2.1.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
@@ -224,21 +224,224 @@ function createEnum(config) {
224
224
  }
225
225
 
226
226
  // packages/dsl/src/generator/prisma.ts
227
- import { VibeError as VibeError3, encodePrismaString, internalError } from "@vibeorm/schema";
227
+ import { VibeError as VibeError5, encodePrismaString, internalError, providerToDialect as providerToDialect2 } from "@vibeorm/schema";
228
+ import { getSqlDialect as getSqlDialect2 } from "@vibeorm/sql";
229
+
230
+ // packages/dsl/src/predicate.ts
231
+ import { VibeError } from "@vibeorm/schema";
232
+ import { compileExclusion, renderPredicate } from "@vibeorm/sql";
233
+ function isFieldExpr(operand) {
234
+ return typeof operand === "object" && operand !== null && "kind" in operand && operand.kind === "field";
235
+ }
236
+ function toRef(params) {
237
+ return { kind: "field", name: params.expr.name };
238
+ }
239
+ function toValue(params) {
240
+ const { input, at } = params;
241
+ if (typeof input === "bigint")
242
+ return { kind: "value", value: input.toString() };
243
+ if (typeof input === "string" || typeof input === "number" || typeof input === "boolean") {
244
+ return { kind: "value", value: input };
245
+ }
246
+ throw new VibeError({
247
+ code: "VIBE_SCHEMA_INVALID",
248
+ message: `${at}: ${input === null ? "null" : typeof input} is not a comparable literal \u2014 use isNull()/isNotNull() for null, and a string, number, bigint or boolean otherwise`,
249
+ meta: { rule: "predicate-literal-invalid", at }
250
+ });
251
+ }
252
+ function toOperand(params) {
253
+ const { operand, at } = params;
254
+ return isFieldExpr(operand) ? toRef({ expr: operand }) : toValue({ input: operand, at });
255
+ }
256
+ function requireFieldExpr(params) {
257
+ const { value, at } = params;
258
+ if (isFieldExpr(value))
259
+ return value;
260
+ throw new VibeError({
261
+ code: "VIBE_SCHEMA_INVALID",
262
+ message: `${at}: the left side of a comparison must be c.field("\u2026"), not a bare value`,
263
+ meta: { rule: "predicate-operand-invalid", at }
264
+ });
265
+ }
266
+ function requireOperands(params) {
267
+ const { operands, at } = params;
268
+ if (operands.length === 0) {
269
+ throw new VibeError({
270
+ code: "VIBE_SCHEMA_INVALID",
271
+ message: `${at}: an empty boolean group has no honest SQL spelling \u2014 pass at least one operand`,
272
+ meta: { rule: "predicate-empty", at }
273
+ });
274
+ }
275
+ return operands;
276
+ }
277
+ function createPredicateBuilder() {
278
+ const compare = (op) => (left, right) => ({
279
+ kind: "compare",
280
+ op,
281
+ left: toRef({ expr: requireFieldExpr({ value: left, at: `c.${op}()` }) }),
282
+ right: toOperand({ operand: right, at: `c.${op}()` })
283
+ });
284
+ const membership = (negate) => (field, values) => {
285
+ const at = negate ? "c.notInList()" : "c.inList()";
286
+ const ref = toRef({ expr: requireFieldExpr({ value: field, at }) });
287
+ if (values.length === 0) {
288
+ throw new VibeError({
289
+ code: "VIBE_SCHEMA_INVALID",
290
+ message: `${at}: an empty IN list is UNKNOWN on every row and is never what an author means`,
291
+ meta: { rule: "predicate-empty", at, field: ref.name }
292
+ });
293
+ }
294
+ return {
295
+ kind: "inList",
296
+ field: ref,
297
+ values: values.map((value) => toValue({ input: value, at })),
298
+ ...negate ? { negate: true } : {}
299
+ };
300
+ };
301
+ return {
302
+ field: (name) => ({ kind: "field", name, __kind: "String" }),
303
+ eq: compare("eq"),
304
+ ne: compare("ne"),
305
+ gt: compare("gt"),
306
+ gte: compare("gte"),
307
+ lt: compare("lt"),
308
+ lte: compare("lte"),
309
+ inList: membership(false),
310
+ notInList: membership(true),
311
+ isNull: (name) => ({ kind: "isNull", field: { kind: "field", name } }),
312
+ isNotNull: (name) => ({ kind: "isNull", field: { kind: "field", name }, negate: true }),
313
+ and: (...operands) => ({ kind: "and", operands: requireOperands({ operands, at: "c.and()" }) }),
314
+ or: (...operands) => ({ kind: "or", operands: requireOperands({ operands, at: "c.or()" }) }),
315
+ not: (operand) => ({ kind: "not", operand }),
316
+ named: (name, predicate) => {
317
+ if (name.trim() === "") {
318
+ throw new VibeError({
319
+ code: "VIBE_SCHEMA_INVALID",
320
+ message: "c.named(): a CHECK needs a non-empty name \u2014 the name is what keys the migration diff",
321
+ meta: { rule: "predicate-unnamed-check" }
322
+ });
323
+ }
324
+ return { __type: "check", name, predicate };
325
+ }
326
+ };
327
+ }
328
+ function evaluateChecks(params) {
329
+ const { checks, model } = params;
330
+ if (checks === undefined)
331
+ return [];
332
+ if (typeof checks !== "function") {
333
+ return checks.map((check) => ({ kind: "raw", name: check.name, expression: check.expression }));
334
+ }
335
+ const declared = checks(createPredicateBuilder());
336
+ return declared.map((entry, index) => {
337
+ if (entry === null || typeof entry !== "object" || entry.__type !== "check") {
338
+ throw new VibeError({
339
+ code: "VIBE_SCHEMA_INVALID",
340
+ message: `Model "${model}": checks[${index}] is not a named CHECK \u2014 wrap the expression in c.named("name", \u2026)`,
341
+ meta: { rule: "predicate-unnamed-check", model, index }
342
+ });
343
+ }
344
+ return { kind: "typed", name: entry.name, predicate: entry.predicate };
345
+ });
346
+ }
347
+ function compileExclusions(params) {
348
+ const specs = typeof params.exclusions === "function" ? params.exclusions(createPredicateBuilder()) : params.exclusions ?? [];
349
+ return specs.map((exclusion) => compileExclusion({ exclusion, model: params.model, dialect: params.dialect }));
350
+ }
351
+ function compileChecks(params) {
352
+ const { checks, model, dialect, enums } = params;
353
+ return evaluateChecks({ checks, model: model.name }).map((check) => check.kind === "raw" ? { name: check.name, expression: check.expression } : {
354
+ name: check.name,
355
+ expression: renderPredicate({
356
+ predicate: check.predicate,
357
+ model,
358
+ dialect,
359
+ enums,
360
+ usage: "check",
361
+ site: `CHECK "${check.name}"`
362
+ }),
363
+ predicate: check.predicate
364
+ });
365
+ }
366
+ function compileIndexPredicate(params) {
367
+ const { where, wherePredicate, columns, model, dialect, enums } = params;
368
+ if (where !== undefined && wherePredicate !== undefined) {
369
+ throw new VibeError({
370
+ code: "VIBE_SCHEMA_INVALID",
371
+ message: `Index on "${model.name}" [${columns.join(", ")}] carries both a raw where: string and a typed .where() \u2014 an index has ONE predicate`,
372
+ meta: { rule: "predicate-duplicate-where", model: model.name, columns: [...columns] }
373
+ });
374
+ }
375
+ if (where !== undefined)
376
+ return { where };
377
+ if (wherePredicate === undefined)
378
+ return;
379
+ return {
380
+ where: renderPredicate({
381
+ predicate: wherePredicate,
382
+ model,
383
+ dialect,
384
+ enums,
385
+ usage: "indexPredicate",
386
+ site: `partial index on [${columns.join(", ")}]`
387
+ }),
388
+ wherePredicate
389
+ };
390
+ }
228
391
 
229
392
  // packages/dsl/src/to-ir.ts
230
393
  import {
231
394
  EMPTY_EXTENSION_REGISTRY,
232
- VibeError as VibeError2,
395
+ VibeError as VibeError4,
233
396
  assertValidSchemaIR,
234
397
  exactIntegerLiteral,
398
+ providerToDialect,
235
399
  scalarListDefault,
236
400
  serializeJsonDefault,
237
401
  mergeExtensionDeclarations
238
402
  } from "@vibeorm/schema";
403
+ import { getSqlDialect } from "@vibeorm/sql";
404
+
405
+ // packages/dsl/src/field-type.ts
406
+ import { fieldTypeIdentity } from "@vibeorm/schema";
407
+ function columnFieldType(params) {
408
+ const { column } = params;
409
+ switch (column.__columnType) {
410
+ case "string":
411
+ return "String";
412
+ case "int":
413
+ return "Int";
414
+ case "bigint":
415
+ return "BigInt";
416
+ case "float":
417
+ return "Float";
418
+ case "decimal":
419
+ return "Decimal";
420
+ case "boolean":
421
+ return "Boolean";
422
+ case "datetime":
423
+ return "DateTime";
424
+ case "json":
425
+ return "Json";
426
+ case "bytes":
427
+ return "Bytes";
428
+ case "id":
429
+ return column.__config.type === "autoincrement" ? "Int" : "String";
430
+ case "enum":
431
+ return column.__enumName === undefined ? "String" : { enum: column.__enumName };
432
+ case "unsupported":
433
+ return "String";
434
+ }
435
+ }
436
+ function fieldTypeKey(params) {
437
+ return fieldTypeIdentity({ type: columnFieldType({ column: params.column }) });
438
+ }
239
439
 
240
440
  // packages/dsl/src/relation-plan.ts
241
- import { VibeError } from "@vibeorm/schema";
441
+ import { VibeError as VibeError3, predicateKey } from "@vibeorm/schema";
442
+
443
+ // packages/dsl/src/model.ts
444
+ import { VibeError as VibeError2 } from "@vibeorm/schema";
242
445
 
243
446
  // packages/dsl/src/naming.ts
244
447
  function capitalize(params) {
@@ -277,6 +480,24 @@ function isChildrenDef(value) {
277
480
  return typeof value === "object" && value !== null && "__type" in value && value.__type === "children";
278
481
  }
279
482
  function createIndexBuilder() {
483
+ const partial = (index) => {
484
+ const where = (predicate) => {
485
+ if (index.__where !== undefined) {
486
+ throw new VibeError2({
487
+ code: "VIBE_SCHEMA_INVALID",
488
+ message: `Index on [${index.__columns.join(", ")}] already carries a raw where: predicate \u2014 an index has ONE predicate, so use either the raw string or .where(), not both`,
489
+ meta: { rule: "predicate-duplicate-where", columns: [...index.__columns] }
490
+ });
491
+ }
492
+ return { ...index, __wherePredicate: predicate(createPredicateBuilder()) };
493
+ };
494
+ return Object.defineProperty({ ...index }, "where", {
495
+ value: where,
496
+ enumerable: false,
497
+ writable: false,
498
+ configurable: true
499
+ });
500
+ };
280
501
  const createIndex = (columns, type, where, map) => ({
281
502
  __type: "index",
282
503
  __columns: [...columns],
@@ -288,38 +509,38 @@ function createIndexBuilder() {
288
509
  on(...args) {
289
510
  const [first, second] = args;
290
511
  if (args.length === 2 && Array.isArray(first) && typeof second === "object" && !Array.isArray(second)) {
291
- return createIndex(first, second.type, second.where, second.map);
512
+ return partial(createIndex(first, second.type, second.where, second.map));
292
513
  }
293
- return createIndex(args);
514
+ return partial(createIndex(args));
294
515
  },
295
516
  index(...columns) {
296
- return createIndex(columns);
517
+ return partial(createIndex(columns));
297
518
  },
298
519
  unique(...args) {
299
520
  const [first, second] = args;
300
521
  if (args.length === 2 && Array.isArray(first) && typeof second === "object" && !Array.isArray(second)) {
301
- return {
522
+ return partial({
302
523
  __type: "unique",
303
524
  __columns: [...first],
304
525
  ...second.map === undefined ? {} : { __map: second.map },
305
526
  ...second.where === undefined ? {} : { __where: second.where },
306
527
  ...second.nullsNotDistinct === true ? { __nullsNotDistinct: true } : {},
307
528
  ...second.name === undefined ? {} : { __clientName: second.name }
308
- };
529
+ });
309
530
  }
310
- return { __type: "unique", __columns: [...args] };
531
+ return partial({ __type: "unique", __columns: [...args] });
311
532
  },
312
533
  gin(...columns) {
313
- return createIndex(columns, "Gin");
534
+ return partial(createIndex(columns, "Gin"));
314
535
  },
315
536
  gist(...columns) {
316
- return createIndex(columns, "Gist");
537
+ return partial(createIndex(columns, "Gist"));
317
538
  },
318
539
  hash(...columns) {
319
- return createIndex(columns, "Hash");
540
+ return partial(createIndex(columns, "Hash"));
320
541
  },
321
542
  brin(...columns) {
322
- return createIndex(columns, "Brin");
543
+ return partial(createIndex(columns, "Brin"));
323
544
  }
324
545
  };
325
546
  }
@@ -361,6 +582,7 @@ function create(config) {
361
582
  __children: children2,
362
583
  __map: config.map,
363
584
  __primaryKey: config.primaryKey,
585
+ __exclusions: config.exclusions,
364
586
  __checks: config.checks,
365
587
  __extensions: config.extensions,
366
588
  __policy: config.policy,
@@ -386,7 +608,7 @@ function detectOneToOnePairs(params) {
386
608
  for (const [modelName, modelRelations] of Object.entries(relations)) {
387
609
  for (const [relName, rel] of Object.entries(modelRelations)) {
388
610
  if (rel.__ownsForeignKey !== undefined && (typeof rel.__ownsForeignKey !== "boolean" || rel.__relationType !== "one" || rel.__ownsForeignKey && rel.__fromModel !== modelName)) {
389
- throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: `Invalid foreign-key ownership declaration on ${modelName}.${relName}` });
611
+ throw new VibeError3({ code: "VIBE_SCHEMA_INVALID", message: `Invalid foreign-key ownership declaration on ${modelName}.${relName}` });
390
612
  }
391
613
  if (rel.__relationType === "one") {
392
614
  allOnes.push({ modelName, relName, rel });
@@ -408,12 +630,13 @@ function detectOneToOnePairs(params) {
408
630
  const keyB = `${b.modelName}:${b.relName}`;
409
631
  if (paired.has(keyB))
410
632
  continue;
411
- const sameColumnPair = a.rel.__fromModel === b.rel.__toModel && a.rel.__fromColumn === b.rel.__toColumn && a.rel.__toModel === b.rel.__fromModel && a.rel.__toColumn === b.rel.__fromColumn;
633
+ const sameColumnPair = a.rel.__fromModel === b.rel.__toModel && sameColumnTuple({ left: a.rel.__fromColumns, right: b.rel.__toColumns }) && a.rel.__toModel === b.rel.__fromModel && sameColumnTuple({ left: a.rel.__toColumns, right: b.rel.__fromColumns });
412
634
  if (!sameColumnPair || (a.rel.__alias ?? "") !== (b.rel.__alias ?? ""))
413
635
  continue;
414
636
  const { fkOwner, backRelation } = resolveFkOwner({ a, b, models });
415
637
  const onDelete = fkOwner.rel.__onDelete ?? backRelation.rel.__onDelete;
416
638
  const onUpdate = fkOwner.rel.__onUpdate ?? backRelation.rel.__onUpdate;
639
+ const constraintName = fkOwner.rel.__constraintName ?? backRelation.rel.__constraintName;
417
640
  const fkKey = `${fkOwner.modelName}:${fkOwner.relName}`;
418
641
  const backKey = `${backRelation.modelName}:${backRelation.relName}`;
419
642
  mergeMap.set(fkKey, {
@@ -421,14 +644,16 @@ function detectOneToOnePairs(params) {
421
644
  pairedRelName: backRelation.relName,
422
645
  pairedModelName: backRelation.modelName,
423
646
  onDelete,
424
- onUpdate
647
+ onUpdate,
648
+ ...constraintName === undefined ? {} : { constraintName }
425
649
  });
426
650
  mergeMap.set(backKey, {
427
651
  role: "backRelation",
428
652
  pairedRelName: fkOwner.relName,
429
653
  pairedModelName: fkOwner.modelName,
430
654
  onDelete,
431
- onUpdate
655
+ onUpdate,
656
+ ...constraintName === undefined ? {} : { constraintName }
432
657
  });
433
658
  paired.add(fkKey);
434
659
  paired.add(backKey);
@@ -437,10 +662,14 @@ function detectOneToOnePairs(params) {
437
662
  }
438
663
  return mergeMap;
439
664
  }
665
+ function sameColumnTuple(params) {
666
+ const { left, right } = params;
667
+ return left.length === right.length && left.every((name, index) => name === right[index]);
668
+ }
440
669
  function resolveFkOwner(params) {
441
670
  const { a, b, models } = params;
442
671
  const ownershipError = () => {
443
- throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: `Ambiguous foreign-key ownership between ${a.modelName}.${a.relName} and ${b.modelName}.${b.relName}; set ownsForeignKey: true on the foreign-key side`, meta: { rule: "ambiguous-foreign-key-owner" } });
672
+ throw new VibeError3({ code: "VIBE_SCHEMA_INVALID", message: `Ambiguous foreign-key ownership between ${a.modelName}.${a.relName} and ${b.modelName}.${b.relName}; set ownsForeignKey: true on the foreign-key side`, meta: { rule: "ambiguous-foreign-key-owner" } });
444
673
  };
445
674
  const aOwns = a.rel.__ownsForeignKey;
446
675
  const bOwns = b.rel.__ownsForeignKey;
@@ -449,8 +678,8 @@ function resolveFkOwner(params) {
449
678
  return ownershipError();
450
679
  return aOwns === true || bOwns === false ? { fkOwner: a, backRelation: b } : { fkOwner: b, backRelation: a };
451
680
  }
452
- const aFromIsId = models.get(a.modelName)?.__columns.get(a.rel.__fromColumn)?.__columnType === "id" || models.get(a.modelName)?.__primaryKey?.includes(a.rel.__fromColumn) === true;
453
- const bFromIsId = models.get(b.modelName)?.__columns.get(b.rel.__fromColumn)?.__columnType === "id" || models.get(b.modelName)?.__primaryKey?.includes(b.rel.__fromColumn) === true;
681
+ const aFromIsId = fromIsPrimaryKey({ model: models.get(a.modelName), columns: a.rel.__fromColumns });
682
+ const bFromIsId = fromIsPrimaryKey({ model: models.get(b.modelName), columns: b.rel.__fromColumns });
454
683
  if (aFromIsId && !bFromIsId)
455
684
  return { fkOwner: b, backRelation: a };
456
685
  if (!aFromIsId && bFromIsId)
@@ -463,12 +692,25 @@ function resolveFkOwner(params) {
463
692
  if (bActions && !aActions) {
464
693
  return { fkOwner: b, backRelation: a };
465
694
  }
466
- const aLooksFk = looksLikeForeignKey({ column: a.rel.__fromColumn });
467
- const bLooksFk = looksLikeForeignKey({ column: b.rel.__fromColumn });
695
+ const aLooksFk = a.rel.__fromColumns.some((column) => looksLikeForeignKey({ column }));
696
+ const bLooksFk = b.rel.__fromColumns.some((column) => looksLikeForeignKey({ column }));
468
697
  if (aLooksFk === bLooksFk)
469
698
  return ownershipError();
470
699
  return aLooksFk ? { fkOwner: a, backRelation: b } : { fkOwner: b, backRelation: a };
471
700
  }
701
+ function fromIsPrimaryKey(params) {
702
+ const { model, columns } = params;
703
+ if (model === undefined)
704
+ return false;
705
+ if (columns.length > 1) {
706
+ const primaryKey = model.__primaryKey;
707
+ return primaryKey !== undefined && primaryKey.length === columns.length && primaryKey.every((name) => columns.includes(name));
708
+ }
709
+ const column = columns[0];
710
+ if (column === undefined)
711
+ return false;
712
+ return model.__columns.get(column)?.__columnType === "id" || model.__primaryKey?.includes(column) === true;
713
+ }
472
714
  function looksLikeForeignKey(params) {
473
715
  const { column } = params;
474
716
  return column.toLowerCase().includes("id") && column !== "id";
@@ -498,14 +740,16 @@ function planExplicitRelations(params) {
498
740
  planned.push({ ...base, kind: "backOne", isList: false, isOptional: rel.__optional });
499
741
  continue;
500
742
  }
501
- const fkColumn = model.__columns.get(rel.__fromColumn);
743
+ const anyMemberOptional = rel.__fromColumns.some((name2) => model.__columns.get(name2)?.__config.optional === true);
744
+ const constraintName = mergeInfo?.constraintName ?? rel.__constraintName;
502
745
  planned.push({
503
746
  ...base,
504
747
  kind: "fkOwner",
505
748
  isList: false,
506
- isOptional: fkColumn?.__config.optional === true || rel.__optional,
507
- fields: [rel.__fromColumn],
508
- references: [rel.__toColumn],
749
+ isOptional: anyMemberOptional || rel.__optional,
750
+ fields: [...rel.__fromColumns],
751
+ references: [...rel.__toColumns],
752
+ ...constraintName === undefined ? {} : { constraintName },
509
753
  onDelete: mergeInfo?.onDelete ?? rel.__onDelete,
510
754
  onUpdate: mergeInfo?.onUpdate ?? rel.__onUpdate
511
755
  });
@@ -602,7 +846,8 @@ function planIndexes(params) {
602
846
  }
603
847
  }
604
848
  for (const index of evaluateIndexes({ model })) {
605
- const key = index.__columns.join(",") + (index.__where === undefined ? "" : `|where:${index.__where}`);
849
+ const predicateSuffix = index.__where !== undefined ? `|where:${index.__where}` : index.__wherePredicate === undefined ? "" : `|where:${predicateKey({ predicate: index.__wherePredicate })}`;
850
+ const key = index.__columns.join(",") + predicateSuffix;
606
851
  const existing = byKey.get(key);
607
852
  const isUnique = index.__type === "unique";
608
853
  if (existing === undefined || isUnique) {
@@ -612,6 +857,7 @@ function planIndexes(params) {
612
857
  indexType: index.__indexType,
613
858
  ...index.__map === undefined ? {} : { map: index.__map },
614
859
  ...index.__where === undefined ? {} : { where: index.__where },
860
+ ...index.__wherePredicate === undefined ? {} : { wherePredicate: index.__wherePredicate },
615
861
  ...index.__nullsNotDistinct === true ? { nullsNotDistinct: true } : {},
616
862
  ...index.__clientName === undefined ? {} : { clientName: index.__clientName }
617
863
  });
@@ -623,6 +869,77 @@ function planIndexes(params) {
623
869
  }
624
870
  return Array.from(byKey.values());
625
871
  }
872
+ function referenceableKeys(params) {
873
+ const { model } = params;
874
+ const keys = [];
875
+ for (const [name, column] of model.__columns) {
876
+ if (column.__columnType === "id" || column.__config.unique === true)
877
+ keys.push([name]);
878
+ }
879
+ if (model.__primaryKey !== undefined && model.__primaryKey.length > 0)
880
+ keys.push([...model.__primaryKey]);
881
+ for (const index of evaluateIndexes({ model })) {
882
+ if (index.__type === "unique" && index.__where === undefined && index.__wherePredicate === undefined) {
883
+ keys.push([...index.__columns]);
884
+ }
885
+ }
886
+ return keys;
887
+ }
888
+ function assertForeignKeyIntegrity(params) {
889
+ const { relation, models } = params;
890
+ const fields = relation.fields ?? [];
891
+ const references = relation.references ?? [];
892
+ const at = `${relation.model}.${relation.name}`;
893
+ const refuse = (message, rule, meta = {}) => {
894
+ throw new VibeError3({
895
+ code: "VIBE_SCHEMA_INVALID",
896
+ message: `Relation ${at}: ${message}`,
897
+ meta: { rule, model: relation.model, relation: relation.name, ...meta }
898
+ });
899
+ };
900
+ if (fields.length !== references.length) {
901
+ refuse(`pairs ${fields.length} foreign-key column(s) with ${references.length} referenced column(s)`, "relation-tuple-arity", { fields, references });
902
+ }
903
+ if (new Set(fields).size !== fields.length) {
904
+ refuse(`repeats a foreign-key column (${fields.join(", ")})`, "relation-duplicate-column", { fields });
905
+ }
906
+ if (new Set(references).size !== references.length) {
907
+ refuse(`repeats a referenced column (${references.join(", ")})`, "relation-duplicate-column", { references });
908
+ }
909
+ const owner = models.get(relation.model);
910
+ const target = models.get(relation.targetModel);
911
+ if (owner === undefined || target === undefined)
912
+ return;
913
+ fields.forEach((name, index) => {
914
+ const referenced = references[index];
915
+ const column = owner.__columns.get(name);
916
+ const refColumn = target.__columns.get(referenced);
917
+ if (column === undefined) {
918
+ refuse(`uses foreign-key column "${name}", which ${relation.model} does not declare`, "unknown-relation-field", { field: name });
919
+ return;
920
+ }
921
+ if (refColumn === undefined) {
922
+ refuse(`references "${relation.targetModel}.${referenced}", which does not exist`, "unknown-relation-reference", { reference: referenced });
923
+ return;
924
+ }
925
+ if (column.__config.array === true || refColumn.__config.array === true) {
926
+ refuse(`pairs a scalar list (${name} \u2194 ${referenced}) \u2014 a foreign key compares single values`, "relation-list-column", { field: name, reference: referenced });
927
+ }
928
+ if (column.__columnType === "json" || refColumn.__columnType === "json" || column.__columnType === "unsupported" || refColumn.__columnType === "unsupported") {
929
+ refuse(`pairs ${name} \u2194 ${referenced}, and Json/unsupported columns cannot be a foreign key`, "relation-unsupported-column", { field: name, reference: referenced });
930
+ }
931
+ const ownType = fieldTypeKey({ column });
932
+ const refType = fieldTypeKey({ column: refColumn });
933
+ if (ownType !== refType) {
934
+ refuse(`pairs ${relation.model}.${name} (${ownType}) with ${relation.targetModel}.${referenced} (${refType}) \u2014 both ends of a foreign key need the same scalar type`, "relation-type-mismatch", { field: name, reference: referenced, fieldType: ownType, referenceType: refType });
935
+ }
936
+ });
937
+ const keys = referenceableKeys({ model: target });
938
+ const matched = keys.some((key) => key.length === references.length && key.every((name) => references.includes(name)));
939
+ if (!matched) {
940
+ refuse(`references ${relation.targetModel}.(${references.join(", ")}), which is not a unique key there \u2014 declare those columns as the primary key, a unique column, or a total @@unique index`, "relation-reference-not-unique", { references, candidates: keys.map((key) => [...key]) });
941
+ }
942
+ }
626
943
  function planSchema(params) {
627
944
  const { schema } = params;
628
945
  const mergeMap = detectOneToOnePairs({
@@ -645,7 +962,12 @@ function planSchema(params) {
645
962
  existing: explicit
646
963
  });
647
964
  const children2 = planChildRelations({ modelName, model, models: schema.__models });
648
- relations.set(modelName, [...explicit, ...reverses, ...children2]);
965
+ const planned = [...explicit, ...reverses, ...children2];
966
+ for (const relation of planned) {
967
+ if (relation.kind === "fkOwner")
968
+ assertForeignKeyIntegrity({ relation, models: schema.__models });
969
+ }
970
+ relations.set(modelName, planned);
649
971
  indexes.set(modelName, planIndexes({ model }));
650
972
  }
651
973
  return { relations, indexes };
@@ -671,35 +993,7 @@ function findCounterparts(params) {
671
993
  }
672
994
 
673
995
  // packages/dsl/src/to-ir.ts
674
- function fieldType(params) {
675
- const { column } = params;
676
- switch (column.__columnType) {
677
- case "string":
678
- return "String";
679
- case "int":
680
- return "Int";
681
- case "bigint":
682
- return "BigInt";
683
- case "float":
684
- return "Float";
685
- case "decimal":
686
- return "Decimal";
687
- case "boolean":
688
- return "Boolean";
689
- case "datetime":
690
- return "DateTime";
691
- case "json":
692
- return "Json";
693
- case "bytes":
694
- return "Bytes";
695
- case "id":
696
- return column.__config.type === "autoincrement" ? "Int" : "String";
697
- case "enum":
698
- return column.__enumName === undefined ? "String" : { enum: column.__enumName };
699
- case "unsupported":
700
- return "String";
701
- }
702
- }
996
+ var fieldType = columnFieldType;
703
997
  function idDefault(params) {
704
998
  switch (params.column.__config.type) {
705
999
  case "ulid":
@@ -720,16 +1014,16 @@ function fieldDefault(params) {
720
1014
  const { column } = params;
721
1015
  if (column.__columnType === "id") {
722
1016
  if (column.__config.default !== undefined || column.__config.dbgenerated !== undefined || column.__config.autoincrement !== undefined)
723
- throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "Use the id generator strategy instead of a conflicting id default" });
1017
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "Use the id generator strategy instead of a conflicting id default" });
724
1018
  return idDefault({ column });
725
1019
  }
726
1020
  const { dbgenerated, default: value, autoincrement } = column.__config;
727
1021
  if ([dbgenerated !== undefined, value !== undefined, autoincrement === true].filter(Boolean).length > 1) {
728
- throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: `Column "${column.__name ?? column.__columnType}" has conflicting defaults` });
1022
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: `Column "${column.__name ?? column.__columnType}" has conflicting defaults` });
729
1023
  }
730
1024
  if (autoincrement === true) {
731
1025
  if (column.__columnType !== "int" && column.__columnType !== "bigint" || column.__config.array === true || column.__config.optional === true) {
732
- throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "autoincrement requires a required scalar Int or BigInt column" });
1026
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "autoincrement requires a required scalar Int or BigInt column" });
733
1027
  }
734
1028
  return { kind: "autoincrement" };
735
1029
  }
@@ -739,7 +1033,7 @@ function fieldDefault(params) {
739
1033
  return;
740
1034
  if (column.__config.array === true) {
741
1035
  if (!Array.isArray(value))
742
- throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "A scalar-list default must be an array" });
1036
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "A scalar-list default must be an array" });
743
1037
  return { kind: "list", values: scalarListDefault({ type: fieldType({ column }), values: column.__columnType === "json" ? value.map((entry) => typeof entry === "string" ? entry : serializeJsonDefault({ value: entry })) : value }) };
744
1038
  }
745
1039
  if (column.__columnType === "json" && typeof value !== "string") {
@@ -751,14 +1045,14 @@ function fieldDefault(params) {
751
1045
  return { kind: "enumValue", value };
752
1046
  }
753
1047
  if (typeof value === "number" && (!Number.isFinite(value) || (column.__columnType === "int" || column.__columnType === "bigint") && !Number.isSafeInteger(value))) {
754
- throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "Numeric defaults must be finite; integer defaults must be exact" });
1048
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "Numeric defaults must be finite; integer defaults must be exact" });
755
1049
  }
756
1050
  if (typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
757
1051
  return { kind: "literal", value };
758
1052
  }
759
1053
  if (typeof value === "bigint")
760
1054
  return { kind: "literal", value: exactIntegerLiteral({ text: value.toString() }) };
761
- throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: `Unsupported default for ${column.__columnType}` });
1055
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: `Unsupported default for ${column.__columnType}` });
762
1056
  }
763
1057
  function nativeType(params) {
764
1058
  const { column } = params;
@@ -767,7 +1061,7 @@ function nativeType(params) {
767
1061
  if (maxLength !== undefined || text !== undefined) {
768
1062
  const invalid = column.__enumStorage !== "string" || maxLength !== undefined && (typeof maxLength !== "number" || !Number.isSafeInteger(maxLength) || maxLength <= 0) || text !== undefined && typeof text !== "boolean" || text === true && maxLength !== undefined;
769
1063
  if (invalid) {
770
- throw new VibeError2({
1064
+ throw new VibeError4({
771
1065
  code: "VIBE_SCHEMA_INVALID",
772
1066
  message: "Enum text/maxLength options require string storage and either a positive integer maxLength or text: true, not both",
773
1067
  meta: { issues: [{ rule: "enum-string-storage-options", enum: column.__enumName }] }
@@ -832,6 +1126,7 @@ function buildRelation(params) {
832
1126
  isList: relation.isList,
833
1127
  isOptional: relation.isOptional,
834
1128
  ...relation.relationName === undefined ? {} : { relationName: relation.relationName },
1129
+ ...relation.constraintName === undefined ? {} : { constraintName: relation.constraintName },
835
1130
  ...relation.fields === undefined ? {} : { fields: [...relation.fields] },
836
1131
  ...relation.references === undefined ? {} : { references: [...relation.references] },
837
1132
  ...relation.onDelete === undefined ? {} : { onDelete: relation.onDelete },
@@ -847,19 +1142,28 @@ var INDEX_KINDS = {
847
1142
  Brin: "brin"
848
1143
  };
849
1144
  function buildIndex(params) {
850
- const { index } = params;
1145
+ const { index, model, dialect, enums } = params;
851
1146
  const kind = index.isUnique ? "unique" : index.indexType === undefined ? "btree" : INDEX_KINDS[index.indexType];
1147
+ const predicate = compileIndexPredicate({
1148
+ where: index.where,
1149
+ wherePredicate: index.wherePredicate,
1150
+ columns: index.columns,
1151
+ model,
1152
+ dialect,
1153
+ enums
1154
+ });
852
1155
  return {
853
1156
  kind,
854
1157
  fields: index.columns.map((name) => ({ name })),
855
1158
  ...index.map === undefined ? {} : { name: index.map },
856
- ...index.where === undefined ? {} : { where: index.where },
1159
+ ...predicate === undefined ? {} : { where: predicate.where },
1160
+ ...predicate?.wherePredicate === undefined ? {} : { wherePredicate: predicate.wherePredicate },
857
1161
  ...index.nullsNotDistinct === true ? { nullsNotDistinct: true } : {},
858
1162
  ...index.clientName === undefined ? {} : { clientName: index.clientName }
859
1163
  };
860
1164
  }
861
1165
  function buildModel(params) {
862
- const { model, plan, extensions, declarations, issues } = params;
1166
+ const { model, plan, extensions, declarations, issues, dialect, enums } = params;
863
1167
  const primaryKey = model.__primaryKey !== undefined && model.__primaryKey.length > 0 ? { fields: [...model.__primaryKey] } : undefined;
864
1168
  const policy = model.__policy === undefined ? undefined : { field: model.__policy.field, context: model.__policy.context ?? model.__policy.field };
865
1169
  const fields = [];
@@ -935,13 +1239,17 @@ function buildModel(params) {
935
1239
  declaration: parsed.declaration
936
1240
  });
937
1241
  }
1242
+ const predicateModel = { name: model.__name, fields };
1243
+ const exclusions = compileExclusions({ exclusions: model.__exclusions, model: predicateModel, dialect });
1244
+ const checks = compileChecks({ checks: model.__checks, model: predicateModel, dialect, enums });
938
1245
  return {
939
1246
  name: model.__name,
940
1247
  ...model.__map === undefined ? {} : { dbName: model.__map },
941
1248
  fields,
942
1249
  relations: (plan.relations.get(model.__name) ?? []).map((relation) => buildRelation({ relation, plan })),
943
- indexes: (plan.indexes.get(model.__name) ?? []).map((index) => buildIndex({ index })),
944
- ...model.__checks === undefined || model.__checks.length === 0 ? {} : { checks: model.__checks.map((check) => ({ name: check.name, expression: check.expression })) },
1250
+ indexes: (plan.indexes.get(model.__name) ?? []).map((index) => buildIndex({ index, model: predicateModel, dialect, enums })),
1251
+ ...exclusions.length === 0 ? {} : { exclusions },
1252
+ ...checks.length === 0 ? {} : { checks },
945
1253
  ...policy === undefined ? {} : { policy },
946
1254
  ...primaryKey === undefined ? {} : { primaryKey },
947
1255
  isView: false,
@@ -972,8 +1280,14 @@ function toIR(params) {
972
1280
  const plan = planSchema({ schema });
973
1281
  const declarations = new ExtensionDeclarationCollector;
974
1282
  const issues = [];
1283
+ const enums = Array.from(schema.__enums, ([name, values]) => ({
1284
+ name,
1285
+ values: [...values],
1286
+ ...schema.__enumStorage.get(name) === "string" ? { storage: "string" } : {}
1287
+ }));
1288
+ const dialect = getSqlDialect({ dialect: providerToDialect({ provider }) });
975
1289
  const models = Array.from(schema.__models.values(), (model) => {
976
- const built = buildModel({ model, plan, extensions, declarations, issues });
1290
+ const built = buildModel({ model, plan, extensions, declarations, issues, dialect, enums });
977
1291
  const rls = schema.__rls?.policies[model.__name];
978
1292
  return rls === undefined ? built : { ...built, rls };
979
1293
  });
@@ -984,13 +1298,8 @@ function toIR(params) {
984
1298
  }
985
1299
  }
986
1300
  }
987
- const enums = Array.from(schema.__enums, ([name, values]) => ({
988
- name,
989
- values: [...values],
990
- ...schema.__enumStorage.get(name) === "string" ? { storage: "string" } : {}
991
- }));
992
1301
  if (issues.length > 0) {
993
- throw new VibeError2({
1302
+ throw new VibeError4({
994
1303
  code: "VIBE_SCHEMA_INVALID",
995
1304
  message: `Schema validation failed with ${issues.length} issue(s): ${issues.map((issue) => issue.message).join("; ")}`,
996
1305
  meta: { issues }
@@ -1008,6 +1317,12 @@ function toIR(params) {
1008
1317
  }
1009
1318
 
1010
1319
  // packages/dsl/src/generator/prisma.ts
1320
+ function predicateModel(params) {
1321
+ return {
1322
+ name: params.model.__name,
1323
+ fields: Array.from(params.model.__columns, ([name, column]) => buildField({ name, column }))
1324
+ };
1325
+ }
1011
1326
  function prismaType(params) {
1012
1327
  const { column } = params;
1013
1328
  const list = column.__config.array === true ? "[]" : "";
@@ -1148,12 +1463,23 @@ function formatRelation(params) {
1148
1463
  args.push(`onDelete: ${relation.onDelete}`);
1149
1464
  if (relation.onUpdate !== undefined)
1150
1465
  args.push(`onUpdate: ${relation.onUpdate}`);
1466
+ if (relation.constraintName !== undefined) {
1467
+ args.push(`map: ${encodePrismaString({ value: relation.constraintName })}`);
1468
+ }
1151
1469
  return ` ${name} ${targetModel}${optional} @relation(${args.join(", ")})`;
1152
1470
  }
1153
1471
  function formatIndex(params) {
1154
1472
  const { index } = params;
1155
1473
  const columns = index.columns.join(", ");
1156
- const whereArg = index.where === undefined ? "" : `, where: ${encodePrismaString({ value: index.where })}`;
1474
+ const predicate = compileIndexPredicate({
1475
+ where: index.where,
1476
+ wherePredicate: index.wherePredicate,
1477
+ columns: index.columns,
1478
+ model: predicateModel({ model: params.model }),
1479
+ dialect: params.dialect,
1480
+ enums: params.enums
1481
+ });
1482
+ const whereArg = predicate === undefined ? "" : `, where: ${encodePrismaString({ value: predicate.where })}`;
1157
1483
  const mapArg = index.map === undefined ? "" : `, map: ${encodePrismaString({ value: index.map })}`;
1158
1484
  const nndArg = index.nullsNotDistinct === true ? ", nullsNotDistinct: true" : "";
1159
1485
  const nameArg = index.clientName === undefined ? "" : `, name: ${encodePrismaString({ value: index.clientName })}`;
@@ -1192,9 +1518,14 @@ function formatModel(params) {
1192
1518
  lines.push(" //> Indexes");
1193
1519
  lines.push("");
1194
1520
  for (const index of plan.indexes.get(model.__name) ?? []) {
1195
- lines.push(formatIndex({ index }));
1196
- }
1197
- for (const check of model.__checks ?? []) {
1521
+ lines.push(formatIndex({ index, model: params.model, dialect: params.dialect, enums: params.enums }));
1522
+ }
1523
+ for (const check of compileChecks({
1524
+ checks: model.__checks,
1525
+ model: predicateModel({ model: params.model }),
1526
+ dialect: params.dialect,
1527
+ enums: params.enums
1528
+ })) {
1198
1529
  lines.push(` @@check(${encodePrismaString({ value: check.expression })}, name: ${encodePrismaString({ value: check.name })})`);
1199
1530
  }
1200
1531
  for (const use of model.__extensions ?? []) {
@@ -1226,17 +1557,22 @@ function formatEnum(params) {
1226
1557
  return [`enum ${name} {`, ...values.map((value) => ` ${value}`), "}"].join(`
1227
1558
  `);
1228
1559
  }
1229
- function generatePrisma(schema, _config = {}) {
1560
+ function generatePrisma(schema, config = {}) {
1561
+ for (const model of schema.__models.values()) {
1562
+ const exclusions = compileExclusions({ exclusions: model.__exclusions, model: { name: model.__name, fields: Array.from(model.__columns, ([name, column]) => buildField({ name, column })) }, dialect: getSqlDialect2({ dialect: providerToDialect2({ provider: config.provider ?? "postgresql" }) }) });
1563
+ if (exclusions.length > 0)
1564
+ throw new VibeError5({ code: "VIBE_CONFIG", message: "Prisma output cannot preserve exclusion constraints; keep the DSL source", meta: { feature: "exclusionConstraints", target: "prisma" } });
1565
+ }
1230
1566
  const stringEnums = [...schema.__enumStorage].filter(([, storage]) => storage === "string").map(([name]) => name);
1231
1567
  if (stringEnums.length > 0) {
1232
- throw new VibeError3({
1568
+ throw new VibeError5({
1233
1569
  code: "VIBE_CONFIG",
1234
1570
  message: `Cannot print to Prisma: enum(s) ${stringEnums.join(", ")} use storage: "string", which .prisma cannot express losslessly. Keep the DSL as the source or switch the enum to database storage.`,
1235
1571
  meta: { enums: stringEnums, target: "prisma" }
1236
1572
  });
1237
1573
  }
1238
1574
  if (schema.__rls !== undefined) {
1239
- throw new VibeError3({
1575
+ throw new VibeError5({
1240
1576
  code: "VIBE_CONFIG",
1241
1577
  message: "Cannot print to Prisma: this schema uses native RLS, which .prisma cannot express losslessly. Keep the DSL as the source.",
1242
1578
  meta: { feature: "nativeRowLevelSecurity", target: "prisma" }
@@ -1244,6 +1580,8 @@ function generatePrisma(schema, _config = {}) {
1244
1580
  }
1245
1581
  const plan = planSchema({ schema });
1246
1582
  const sections = [];
1583
+ const dialect = getSqlDialect2({ dialect: providerToDialect2({ provider: config.provider ?? "postgresql" }) });
1584
+ const enums = Array.from(schema.__enums, ([name, values]) => ({ name, values: [...values] }));
1247
1585
  if (schema.__enums.size > 0) {
1248
1586
  sections.push("// ============================================");
1249
1587
  sections.push("// Enums");
@@ -1259,7 +1597,7 @@ function generatePrisma(schema, _config = {}) {
1259
1597
  sections.push("// ============================================");
1260
1598
  sections.push("");
1261
1599
  for (const model of schema.__models.values()) {
1262
- sections.push(formatModel({ model, plan }));
1600
+ sections.push(formatModel({ model, plan, dialect, enums }));
1263
1601
  sections.push("");
1264
1602
  }
1265
1603
  return sections.join(`
@@ -1267,12 +1605,28 @@ function generatePrisma(schema, _config = {}) {
1267
1605
  }
1268
1606
 
1269
1607
  // packages/dsl/src/relations.ts
1608
+ import { VibeError as VibeError6 } from "@vibeorm/schema";
1609
+ var REF_KIND_BY_COLUMN_TYPE = {
1610
+ string: "String",
1611
+ int: "Int",
1612
+ bigint: "BigInt",
1613
+ float: "Float",
1614
+ decimal: "Decimal",
1615
+ boolean: "Boolean",
1616
+ datetime: "DateTime",
1617
+ json: "Json",
1618
+ bytes: "Bytes",
1619
+ id: "Id",
1620
+ enum: "Enum",
1621
+ unsupported: "Unsupported"
1622
+ };
1270
1623
  function createColumnRef(params) {
1271
- const { model, column } = params;
1624
+ const { model, column, kind } = params;
1272
1625
  return {
1273
1626
  __type: "columnRef",
1274
1627
  __model: model,
1275
1628
  __column: column,
1629
+ __kind: kind,
1276
1630
  through(junction) {
1277
1631
  return {
1278
1632
  __type: "throughRef",
@@ -1285,38 +1639,77 @@ function createColumnRef(params) {
1285
1639
  };
1286
1640
  }
1287
1641
  function extractRefInfo(params) {
1288
- const { ref } = params;
1289
- return { model: ref.__model, column: ref.__column };
1642
+ const { ref, side, target } = params;
1643
+ if (!Array.isArray(ref)) {
1644
+ const single = ref;
1645
+ return { model: single.__model, columns: [single.__column] };
1646
+ }
1647
+ const members = ref;
1648
+ if (members.length === 0) {
1649
+ throw new VibeError6({
1650
+ code: "VIBE_SCHEMA_INVALID",
1651
+ message: `Relation to ${target}: "${side}" is an empty tuple \u2014 name at least one column`,
1652
+ meta: { rule: "relation-empty-tuple", side, targetModel: target }
1653
+ });
1654
+ }
1655
+ const models = [...new Set(members.map((member) => member.__model))].sort();
1656
+ if (models.length > 1) {
1657
+ throw new VibeError6({
1658
+ code: "VIBE_SCHEMA_INVALID",
1659
+ message: `Relation to ${target}: "${side}" mixes models ${models.join(", ")} \u2014 every member of a composite key must sit on one model`,
1660
+ meta: { rule: "relation-mixed-models", side, targetModel: target, models }
1661
+ });
1662
+ }
1663
+ if (members.some((member) => member.__type !== "columnRef")) {
1664
+ throw new VibeError6({
1665
+ code: "VIBE_SCHEMA_INVALID",
1666
+ message: `Relation to ${target}: "${side}" routes through a junction inside a tuple \u2014 implicit many-to-many is single-key; model the join with an explicit junction model`,
1667
+ meta: { rule: "relation-through-in-tuple", side, targetModel: target }
1668
+ });
1669
+ }
1670
+ return { model: members[0].__model, columns: members.map((member) => member.__column) };
1290
1671
  }
1291
1672
  function createRelationFactory(params) {
1292
1673
  const { relationType, targetModelName } = params;
1293
1674
  return (config) => {
1294
- const from = extractRefInfo({ ref: config.from });
1295
- const to = extractRefInfo({ ref: config.to });
1675
+ const from = extractRefInfo({ ref: config.from, side: "from", target: targetModelName });
1676
+ const to = extractRefInfo({ ref: config.to, side: "to", target: targetModelName });
1677
+ if (from.columns.length !== to.columns.length) {
1678
+ throw new VibeError6({
1679
+ code: "VIBE_SCHEMA_INVALID",
1680
+ message: `Relation to ${targetModelName}: "from" names ${from.columns.length} column(s) but "to" names ${to.columns.length} \u2014 a composite key pairs member by member`,
1681
+ meta: { rule: "relation-tuple-arity", targetModel: targetModelName, from: from.columns, to: to.columns }
1682
+ });
1683
+ }
1296
1684
  let throughJunction;
1297
1685
  let throughFromCol;
1298
1686
  let throughToCol;
1299
- if (config.from.__type === "throughRef") {
1300
- throughJunction = config.from.__junction;
1301
- throughFromCol = config.from.__junctionColumn;
1687
+ const fromRef = Array.isArray(config.from) ? undefined : config.from;
1688
+ const toRef2 = Array.isArray(config.to) ? undefined : config.to;
1689
+ if (fromRef?.__type === "throughRef") {
1690
+ throughJunction = fromRef.__junction;
1691
+ throughFromCol = fromRef.__junctionColumn;
1302
1692
  }
1303
- if (config.to.__type === "throughRef") {
1304
- throughJunction = throughJunction ?? config.to.__junction;
1305
- throughToCol = config.to.__junctionColumn;
1693
+ if (toRef2?.__type === "throughRef") {
1694
+ throughJunction = throughJunction ?? toRef2.__junction;
1695
+ throughToCol = toRef2.__junctionColumn;
1306
1696
  }
1307
1697
  return {
1308
1698
  __type: "storedRelation",
1309
1699
  __relationType: relationType,
1310
1700
  __targetModel: targetModelName,
1311
1701
  __fromModel: from.model,
1312
- __fromColumn: from.column,
1702
+ __fromColumn: from.columns[0],
1703
+ __fromColumns: from.columns,
1313
1704
  __toModel: to.model,
1314
- __toColumn: to.column,
1705
+ __toColumn: to.columns[0],
1706
+ __toColumns: to.columns,
1315
1707
  __throughJunction: throughJunction,
1316
1708
  __throughFromCol: throughFromCol,
1317
1709
  __throughToCol: throughToCol,
1318
1710
  __optional: config.optional ?? relationType === "many",
1319
1711
  __alias: config.alias,
1712
+ ...config.constraintName === undefined ? {} : { __constraintName: config.constraintName },
1320
1713
  ..."ownsForeignKey" in config ? { __ownsForeignKey: config.ownsForeignKey } : {},
1321
1714
  __onDelete: config.onDelete,
1322
1715
  __onUpdate: config.onUpdate
@@ -1338,8 +1731,12 @@ function createRelationsBuilder(params) {
1338
1731
  if (model === undefined)
1339
1732
  continue;
1340
1733
  const columnRefs = {};
1341
- for (const columnName of model.__columns.keys()) {
1342
- columnRefs[columnName] = createColumnRef({ model: modelName, column: columnName });
1734
+ for (const [columnName, column] of model.__columns) {
1735
+ columnRefs[columnName] = createColumnRef({
1736
+ model: modelName,
1737
+ column: columnName,
1738
+ kind: REF_KIND_BY_COLUMN_TYPE[column.__columnType]
1739
+ });
1343
1740
  }
1344
1741
  builder[modelName] = columnRefs;
1345
1742
  }
@@ -1376,7 +1773,7 @@ function mergeRelations(...parts) {
1376
1773
  }
1377
1774
 
1378
1775
  // packages/dsl/src/schema.ts
1379
- import { VibeError as VibeError4, validateRlsContextIR } from "@vibeorm/schema";
1776
+ import { VibeError as VibeError7, validateRlsContextIR } from "@vibeorm/schema";
1380
1777
  function sameValues(left, right) {
1381
1778
  return left.length === right.length && left.every((value, index) => value === right[index]);
1382
1779
  }
@@ -1395,7 +1792,7 @@ function registerEnum(params) {
1395
1792
  ...valuesDiffer ? [`values [${existing.values.join(", ")}] vs [${entry.values.join(", ")}]`] : [],
1396
1793
  ...storageDiffers ? [`storage "${existing.storage}" vs "${entry.storage}"`] : []
1397
1794
  ].join("; ");
1398
- throw new VibeError4({
1795
+ throw new VibeError7({
1399
1796
  code: "VIBE_SCHEMA_INVALID",
1400
1797
  message: `Enum "${name}" is defined more than once with conflicting definitions (${origin}): ${detail} \u2014 share one enum object across modules, or give the definitions different names`,
1401
1798
  meta: {
@@ -1421,7 +1818,7 @@ function addModelTree(params) {
1421
1818
  const { model, models, origin } = params;
1422
1819
  const existing = models.get(model.__name);
1423
1820
  if (existing !== undefined && existing !== model) {
1424
- throw new VibeError4({
1821
+ throw new VibeError7({
1425
1822
  code: "VIBE_SCHEMA_INVALID",
1426
1823
  message: `Model "${model.__name}" is defined more than once with different definitions (${origin}) \u2014 ` + `share one model object across modules, or give the definitions different names`,
1427
1824
  meta: {
@@ -1499,7 +1896,7 @@ var RLS_CONTEXT_TYPES = {
1499
1896
  };
1500
1897
  function defineRls(params) {
1501
1898
  if (typeof params.context !== "object" || params.context === null || Array.isArray(params.context)) {
1502
- throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "RLS context must be a record of scalar declarations", meta: { issues: [{ rule: "rls-invalid-context" }] } });
1899
+ throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "RLS context must be a record of scalar declarations", meta: { issues: [{ rule: "rls-invalid-context" }] } });
1503
1900
  }
1504
1901
  const entries = [];
1505
1902
  const refs = [];
@@ -1507,7 +1904,7 @@ function defineRls(params) {
1507
1904
  const descriptor = Object.getOwnPropertyDescriptor(params.context, key);
1508
1905
  const column = descriptor?.value;
1509
1906
  if (typeof key !== "string" || descriptor?.enumerable !== true || typeof column !== "object" || column === null || !("__columnType" in column) || !("__config" in column) || typeof column.__config !== "object" || column.__config === null || typeof column.__columnType !== "string" || !Object.hasOwn(RLS_CONTEXT_TYPES, column.__columnType) || "array" in column.__config && column.__config.array === true) {
1510
- throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "RLS context slots must be named scalar column declarations", meta: { issues: [{ rule: "rls-invalid-context" }] } });
1907
+ throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "RLS context slots must be named scalar column declarations", meta: { issues: [{ rule: "rls-invalid-context" }] } });
1511
1908
  }
1512
1909
  entries.push([key, { type: RLS_CONTEXT_TYPES[column.__columnType], nullable: "optional" in column.__config && column.__config.optional === true }]);
1513
1910
  refs.push([key, Object.freeze({ context: key })]);
@@ -1515,22 +1912,22 @@ function defineRls(params) {
1515
1912
  const context = Object.freeze(Object.fromEntries(entries));
1516
1913
  const issues = validateRlsContextIR({ context });
1517
1914
  if (issues.length > 0) {
1518
- throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "Invalid RLS context declaration", meta: { issues } });
1915
+ throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "Invalid RLS context declaration", meta: { issues } });
1519
1916
  }
1520
1917
  const policies = params.policies({ context: Object.freeze(Object.fromEntries(refs)) });
1521
1918
  if (typeof policies !== "object" || policies === null || Array.isArray(policies)) {
1522
- throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "RLS policies must be a record of model classifications", meta: { issues: [{ rule: "rls-invalid-rule" }] } });
1919
+ throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "RLS policies must be a record of model classifications", meta: { issues: [{ rule: "rls-invalid-rule" }] } });
1523
1920
  }
1524
1921
  const named = [];
1525
1922
  const classified = new Set;
1526
1923
  for (const key of Reflect.ownKeys(policies)) {
1527
1924
  const descriptor = Object.getOwnPropertyDescriptor(policies, key);
1528
1925
  if (typeof key !== "string" || descriptor?.enumerable !== true || !("value" in descriptor)) {
1529
- throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "RLS policies must be named data properties", meta: { issues: [{ rule: "rls-invalid-rule" }] } });
1926
+ throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "RLS policies must be named data properties", meta: { issues: [{ rule: "rls-invalid-rule" }] } });
1530
1927
  }
1531
1928
  const name = params.models[key]?.__name ?? key;
1532
1929
  if (classified.has(name)) {
1533
- throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: `RLS classifies model "${name}" more than once`, meta: { issues: [{ rule: "rls-duplicate-classification", model: name }] } });
1930
+ throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: `RLS classifies model "${name}" more than once`, meta: { issues: [{ rule: "rls-duplicate-classification", model: name }] } });
1534
1931
  }
1535
1932
  classified.add(name);
1536
1933
  named.push([name, descriptor.value]);
@@ -1704,18 +2101,30 @@ function validateSchema(schema2) {
1704
2101
  continue;
1705
2102
  }
1706
2103
  if (relation.__relationType === "one" && relation.__fromModel === modelName) {
1707
- if (!model.__columns.has(relation.__fromColumn)) {
1708
- errors.push({
1709
- type: "error",
1710
- message: `Relation "${relationName}" references FK column "${relation.__fromColumn}" which doesn't exist on model "${modelName}"`,
1711
- model: modelName,
1712
- relation: relationName
1713
- });
2104
+ for (const column of relation.__fromColumns) {
2105
+ if (!model.__columns.has(column)) {
2106
+ errors.push({
2107
+ type: "error",
2108
+ message: `Relation "${relationName}" references FK column "${column}" which doesn't exist on model "${modelName}"`,
2109
+ model: modelName,
2110
+ relation: relationName
2111
+ });
2112
+ }
2113
+ }
2114
+ for (const column of relation.__toColumns) {
2115
+ if (!targetModel.__columns.has(column)) {
2116
+ errors.push({
2117
+ type: "error",
2118
+ message: `Relation "${relationName}" references column "${column}" which doesn't exist on target model "${relation.__targetModel}"`,
2119
+ model: modelName,
2120
+ relation: relationName
2121
+ });
2122
+ }
1714
2123
  }
1715
- if (!targetModel.__columns.has(relation.__toColumn)) {
2124
+ if (relation.__fromColumns.length !== relation.__toColumns.length) {
1716
2125
  errors.push({
1717
2126
  type: "error",
1718
- message: `Relation "${relationName}" references column "${relation.__toColumn}" which doesn't exist on target model "${relation.__targetModel}"`,
2127
+ message: `Relation "${relationName}" pairs ${relation.__fromColumns.length} FK column(s) with ${relation.__toColumns.length} referenced column(s)`,
1719
2128
  model: modelName,
1720
2129
  relation: relationName
1721
2130
  });
@@ -1802,16 +2211,20 @@ export {
1802
2211
  generatePrisma,
1803
2212
  fields,
1804
2213
  evaluateIndexes,
2214
+ evaluateChecks,
1805
2215
  dsl,
1806
2216
  domain,
1807
2217
  detectOneToOnePairs,
1808
2218
  defineRls,
1809
2219
  defineRelationsPart,
1810
2220
  defineRelations,
2221
+ createPredicateBuilder,
1811
2222
  createIndexBuilder,
1812
2223
  createEnum,
1813
2224
  createChildrenBuilder,
1814
2225
  create,
2226
+ compileIndexPredicate,
2227
+ compileChecks,
1815
2228
  cloneColumnDef,
1816
2229
  children,
1817
2230
  baseWithWorkspace,
@@ -1825,4 +2238,4 @@ export {
1825
2238
  ChainableColumn
1826
2239
  };
1827
2240
 
1828
- //# debugId=88D4E8D0B695042E64756E2164756E21
2241
+ //# debugId=13292A89548F16A064756E2164756E21