@vibeorm/schema 2.5.0 → 3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VibeORM contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -34,6 +34,11 @@ export type RowLockStrength = "update" | "noKeyUpdate" | "share" | "keyShare";
34
34
  export type RowLockWaitPolicy = "wait" | "nowait" | "skipLocked";
35
35
  /** Advisory locks are exclusive (one holder) or shared (many readers, no writer). */
36
36
  export type AdvisoryLockMode = "exclusive" | "shared";
37
+ /**
38
+ * Time-of-day column types a `DateTime` field can declare: `@db.Time(n)`
39
+ * (`"time"`) and `@db.Timetz(n)` (`"timetz"`, a time with a UTC offset).
40
+ */
41
+ export type TimeOfDayType = "time" | "timetz";
37
42
  export type DialectCapabilities = {
38
43
  /** Fixed PostgreSQL model/module placement and namespace ownership. */
39
44
  readonly fixedNamespaces: boolean;
@@ -91,6 +96,13 @@ export type DialectCapabilities = {
91
96
  * survives a rollback — so it does not satisfy this capability.
92
97
  */
93
98
  readonly advisoryLocks: boolean;
99
+ /**
100
+ * Time-of-day column types (`DateTime @db.Time` / `@db.Timetz`) this dialect
101
+ * can hold. A field declaring any other is refused when the schema is
102
+ * validated. Values follow one contract everywhere: the UTC time of day of
103
+ * the `Date` is written, and a read returns that time at `1970-01-01` UTC.
104
+ */
105
+ readonly timeOfDayTypes: readonly TimeOfDayType[];
94
106
  };
95
107
  export declare const POSTGRES_CAPABILITIES: DialectCapabilities;
96
108
  export declare const SQLITE_CAPABILITIES: DialectCapabilities;
package/dist/errors.d.ts CHANGED
@@ -60,6 +60,16 @@ export declare const VIBE_ERROR_CODES: readonly [
60
60
  "VIBE_PERMISSION_DENIED",
61
61
  /** Driver/connection failure not mapped to a more specific code. */
62
62
  "VIBE_ADAPTER",
63
+ /**
64
+ * The adapter was disconnected: `disconnect()` ends an adapter for good, so
65
+ * work it receives after that call refuses instead of opening a new pool or
66
+ * connection. `meta` is fixed — `{ driver, state }`, where `driver` names the
67
+ * adapter (`"pg"`, `"bun"`, `"mysql"`, `"pglite"`, `"sqlite"`,
68
+ * `"better-sqlite3"`) and `state` is `"closing"` (admitted work is still
69
+ * draining) or `"closed"`; never an address. Create a new adapter to connect
70
+ * again.
71
+ */
72
+ "VIBE_ADAPTER_CLOSED",
63
73
  /** Invariant broken inside VibeORM itself — always a bug, please report. */
64
74
  "VIBE_INTERNAL"];
65
75
  export type VibeErrorCode = (typeof VIBE_ERROR_CODES)[number];
package/dist/index.d.ts CHANGED
@@ -13,7 +13,8 @@ export { PREDICATE_COMPARISON_OPS, isPredicateFieldRef, predicateFieldNames, pre
13
13
  export type { PredicateComparisonOp, 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
- export type { AdvisoryLockMode, DialectCapabilities, FullTextEngine, RowLockStrength, RowLockWaitPolicy, } from "./capabilities.ts";
16
+ export type { AdvisoryLockMode, DialectCapabilities, FullTextEngine, RowLockStrength, RowLockWaitPolicy, TimeOfDayType, } from "./capabilities.ts";
17
+ export { timeOfDayType } from "./time-of-day.ts";
17
18
  export { READ_ONLY_REFUSAL_REASONS, ReadOnlyClientError, SCOPED_REFUSAL_REASONS, ScopedQueryError, VIBE_ERROR_CODES, VibeError, internalError, invalidStoredData, unsupportedCapability, } from "./errors.ts";
18
19
  export type { ReadOnlyRefusalReason, ScopedRefusalReason, VibeErrorCode } from "./errors.ts";
19
20
  export { decodePrismaString, encodePrismaString, exactDecimalLiteral, exactIntegerLiteral } from "./prisma-literal.ts";
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
 
@@ -275,7 +276,8 @@ var POSTGRES_CAPABILITIES = Object.freeze({
275
276
  fullTextEngines: Object.freeze(["bm25", "tsvector"]),
276
277
  rowLockStrengths: Object.freeze(["update", "noKeyUpdate", "share", "keyShare"]),
277
278
  rowLockWaitPolicies: Object.freeze(["wait", "nowait", "skipLocked"]),
278
- advisoryLocks: true
279
+ advisoryLocks: true,
280
+ timeOfDayTypes: Object.freeze(["time", "timetz"])
279
281
  });
280
282
  var SQLITE_CAPABILITIES = Object.freeze({
281
283
  fixedNamespaces: false,
@@ -300,7 +302,8 @@ var SQLITE_CAPABILITIES = Object.freeze({
300
302
  fullTextEngines: Object.freeze(["fts5"]),
301
303
  rowLockStrengths: Object.freeze([]),
302
304
  rowLockWaitPolicies: Object.freeze([]),
303
- advisoryLocks: false
305
+ advisoryLocks: false,
306
+ timeOfDayTypes: Object.freeze([])
304
307
  });
305
308
  var MYSQL_CAPABILITIES = Object.freeze({
306
309
  fixedNamespaces: false,
@@ -325,7 +328,8 @@ var MYSQL_CAPABILITIES = Object.freeze({
325
328
  fullTextEngines: Object.freeze(["fulltext"]),
326
329
  rowLockStrengths: Object.freeze(["update", "share"]),
327
330
  rowLockWaitPolicies: Object.freeze(["wait", "nowait", "skipLocked"]),
328
- advisoryLocks: false
331
+ advisoryLocks: false,
332
+ timeOfDayTypes: Object.freeze(["time"])
329
333
  });
330
334
  var DIALECT_CAPABILITIES = Object.freeze({
331
335
  postgres: POSTGRES_CAPABILITIES,
@@ -649,6 +653,20 @@ function predicateKey(params) {
649
653
  };
650
654
  return encode(params.predicate);
651
655
  }
656
+ // packages/schema/src/time-of-day.ts
657
+ var TIME_OF_DAY_NATIVE_TYPE = /^time(tz)?(?:\(\d+\))?$|^time(?:\(\d+\))?with(out)?timezone$/;
658
+ function timeOfDayType(params) {
659
+ if (params.nativeType === undefined)
660
+ return;
661
+ const match = TIME_OF_DAY_NATIVE_TYPE.exec(params.nativeType.toLowerCase().replace(/\s+/g, ""));
662
+ if (match === null)
663
+ return;
664
+ if (match[1] !== undefined)
665
+ return "timetz";
666
+ if (match[0].endsWith("withtimezone") && match[2] === undefined)
667
+ return "timetz";
668
+ return "time";
669
+ }
652
670
  // packages/schema/src/prisma-literal.ts
653
671
  var STRING_ESCAPES = {
654
672
  '"': '"',
@@ -1113,6 +1131,11 @@ var RESERVED_ENUM_NAMES = [
1113
1131
  "UncertainKeys",
1114
1132
  "HasSelectedField",
1115
1133
  "Provided",
1134
+ "SelectionMember",
1135
+ "SelectionValue",
1136
+ "BagKeys",
1137
+ "IncludedRelation",
1138
+ "IncludedCount",
1116
1139
  "ViewDelegateMap",
1117
1140
  "extensionManifest",
1118
1141
  "extensionMounts",
@@ -1226,6 +1249,18 @@ var GENERATED_TYPE_NAMES = {
1226
1249
  "SumAggregateInput",
1227
1250
  "MinAggregateInput",
1228
1251
  "MaxAggregateInput",
1252
+ "CountAggregateHavingInput",
1253
+ "AvgAggregateHavingInput",
1254
+ "SumAggregateHavingInput",
1255
+ "MinAggregateHavingInput",
1256
+ "MaxAggregateHavingInput",
1257
+ "CountAggregateOrderByInput",
1258
+ "AvgAggregateOrderByInput",
1259
+ "SumAggregateOrderByInput",
1260
+ "MinAggregateOrderByInput",
1261
+ "MaxAggregateOrderByInput",
1262
+ "HavingInput",
1263
+ "GroupByOrderByInput",
1229
1264
  "FindManyArgs",
1230
1265
  "FindPageArgs",
1231
1266
  "FindFirstArgs",
@@ -1254,6 +1289,7 @@ var GENERATED_TYPE_NAMES = {
1254
1289
  "AggregateSelection",
1255
1290
  "CountOutput",
1256
1291
  "RelationPayload",
1292
+ "ResolvedSelection",
1257
1293
  "IncludePayload",
1258
1294
  "GetPayload",
1259
1295
  "AggregateResult",
@@ -1395,6 +1431,57 @@ function validateGeneratedNames(params) {
1395
1431
  register({ name: `${declared.name}${suffix}`, owner: `enum "${declared.name}"` });
1396
1432
  }
1397
1433
  }
1434
+ function foreignKeyKeys(params) {
1435
+ const distinct = new Set;
1436
+ const repeated = [];
1437
+ for (const fields of params.keys) {
1438
+ if (new Set(fields).size === fields.length)
1439
+ distinct.add(JSON.stringify([...fields].sort()));
1440
+ else
1441
+ repeated.push(fields);
1442
+ }
1443
+ return { distinct, repeated };
1444
+ }
1445
+ function matchesForeignKey(params) {
1446
+ if (params.keys.distinct.has(JSON.stringify([...params.references].sort())))
1447
+ return true;
1448
+ return params.keys.repeated.some((fields) => fields.length === params.references.length && fields.every((name) => params.references.includes(name)));
1449
+ }
1450
+ function referencesPartialOnly(params) {
1451
+ const { target, references, schema, lookups } = params;
1452
+ let partial = lookups.partialKeys.get(target);
1453
+ if (partial === undefined) {
1454
+ partial = foreignKeyKeys({ keys: target.indexes.filter((index) => index.kind === "unique" && (index.where !== undefined || index.wherePredicate !== undefined)).map((index) => index.fields.map((field) => field.name)) });
1455
+ lookups.partialKeys.set(target, partial);
1456
+ }
1457
+ if (partial.distinct.size === 0 && partial.repeated.length === 0)
1458
+ return false;
1459
+ if (!matchesForeignKey({ keys: partial, references }))
1460
+ return false;
1461
+ let total = lookups.totalKeys.get(target);
1462
+ if (total === undefined) {
1463
+ total = foreignKeyKeys({ keys: [
1464
+ ...target.fields.filter((field) => field.isId || field.isUnique).map((field) => [field.name]),
1465
+ ...target.primaryKey === undefined ? [] : [target.primaryKey.fields],
1466
+ ...target.indexes.filter((index) => index.kind === "unique" && index.where === undefined && index.wherePredicate === undefined).map((index) => index.fields.map((field) => field.name))
1467
+ ] });
1468
+ lookups.totalKeys.set(target, total);
1469
+ }
1470
+ if (matchesForeignKey({ keys: total, references }))
1471
+ return false;
1472
+ if (lookups.independentTables === undefined) {
1473
+ const tables = new Set;
1474
+ for (const object of schema.migrationObjects ?? []) {
1475
+ const identity = object.identity;
1476
+ if (identity.kind === "index" || identity.kind === "constraint" && (identity.constraintType === "unique" || identity.constraintType === "primaryKey")) {
1477
+ tables.add(JSON.stringify([identity.table.namespace, identity.table.name]));
1478
+ }
1479
+ }
1480
+ lookups.independentTables = tables;
1481
+ }
1482
+ const placement = qualifiedSqlName({ name: target.dbName ?? target.name, namespace: target.namespace });
1483
+ return !lookups.independentTables.has(JSON.stringify([placement.namespace, placement.name]));
1484
+ }
1398
1485
  function validateSchemaIR(params) {
1399
1486
  const { schema } = params;
1400
1487
  const issues = [];
@@ -1465,8 +1552,18 @@ function validateSchemaIR(params) {
1465
1552
  });
1466
1553
  }
1467
1554
  }
1555
+ const fieldsByModel = new Map;
1556
+ for (const model of schema.models) {
1557
+ const fields = new Map;
1558
+ for (const field of model.fields)
1559
+ if (!fields.has(field.name))
1560
+ fields.set(field.name, field);
1561
+ fieldsByModel.set(model, fields);
1562
+ }
1563
+ const lookups = { fieldsByModel, partialKeys: new Map, totalKeys: new Map };
1564
+ const postgres = providerToDialect({ provider: schema.datasource.provider }) === "postgres";
1468
1565
  for (const model of schema.models) {
1469
- validateModel({ model, modelByName, enumByName, issues });
1566
+ validateModel({ model, modelByName, enumByName, issues, schema, lookups, postgres });
1470
1567
  }
1471
1568
  for (const model of schema.models) {
1472
1569
  for (const relation of model.relations) {
@@ -1511,19 +1608,11 @@ function validateSchemaIR(params) {
1511
1608
  }
1512
1609
  }
1513
1610
  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;
1611
+ const fieldNames = fieldsByModel.get(model);
1612
+ const seenSelectorNames = new Map;
1613
+ const addressable = (names) => names.every((name) => fieldNames.get(name)?.isUnsupported !== true);
1525
1614
  const checkSelectorName = (params) => {
1526
- const { clientName, fieldCount, where, isUnique, site } = params;
1615
+ const { clientName, fields, where, isUnique, site } = params;
1527
1616
  if (!isUnique) {
1528
1617
  issues.push({
1529
1618
  rule: "client-name-non-unique",
@@ -1540,7 +1629,7 @@ function validateSchemaIR(params) {
1540
1629
  });
1541
1630
  return;
1542
1631
  }
1543
- if (fieldCount < 2) {
1632
+ if (fields.length < 2) {
1544
1633
  issues.push({
1545
1634
  rule: "client-name-single-field",
1546
1635
  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 +1637,9 @@ function validateSchemaIR(params) {
1548
1637
  });
1549
1638
  return;
1550
1639
  }
1551
- if (fieldNames.has(clientName) || defaultKeys.has(clientName) || seenSelectorNames.has(clientName)) {
1640
+ const tuple = JSON.stringify(fields);
1641
+ const previous = seenSelectorNames.get(clientName);
1642
+ if (fieldNames.has(clientName) || previous !== undefined && previous !== tuple) {
1552
1643
  issues.push({
1553
1644
  rule: "client-name-collision",
1554
1645
  message: `name: ("${clientName}") on "${model.name}" collides with a field or another unique selector \u2014 WhereUniqueInput keys must be unambiguous`,
@@ -1556,22 +1647,22 @@ function validateSchemaIR(params) {
1556
1647
  });
1557
1648
  return;
1558
1649
  }
1559
- seenSelectorNames.add(clientName);
1650
+ seenSelectorNames.set(clientName, tuple);
1560
1651
  };
1561
- if (model.primaryKey !== undefined && model.primaryKey.clientName !== undefined) {
1652
+ if (model.primaryKey !== undefined && (model.primaryKey.clientName !== undefined || model.primaryKey.fields.length > 1 && addressable(model.primaryKey.fields))) {
1562
1653
  checkSelectorName({
1563
- clientName: model.primaryKey.clientName,
1564
- fieldCount: model.primaryKey.fields.length,
1654
+ clientName: model.primaryKey.clientName ?? model.primaryKey.fields.join("_"),
1655
+ fields: model.primaryKey.fields,
1565
1656
  isUnique: true,
1566
1657
  site: "@@id"
1567
1658
  });
1568
1659
  }
1569
1660
  for (const index of model.indexes) {
1570
- if (index.clientName === undefined)
1661
+ if (index.clientName === undefined && (index.kind !== "unique" || index.where !== undefined || index.fields.length < 2 || !addressable(index.fields.map((field) => field.name))))
1571
1662
  continue;
1572
1663
  checkSelectorName({
1573
- clientName: index.clientName,
1574
- fieldCount: index.fields.length,
1664
+ clientName: index.clientName ?? index.fields.map((field) => field.name).join("_"),
1665
+ fields: index.fields.map((field) => field.name),
1575
1666
  ...index.where === undefined ? {} : { where: index.where },
1576
1667
  isUnique: index.kind === "unique",
1577
1668
  site: "@@unique"
@@ -1579,13 +1670,13 @@ function validateSchemaIR(params) {
1579
1670
  }
1580
1671
  }
1581
1672
  validateIdentifierLengths({ schema, issues });
1582
- validatePhysicalNameUniqueness({ schema, issues });
1673
+ validatePhysicalNameUniqueness({ schema, issues, modelByName, fieldsByModel });
1583
1674
  validateGeneratedNames({ schema, issues });
1584
1675
  validateNativeRls({ schema, issues });
1585
1676
  return issues;
1586
1677
  }
1587
1678
  function validatePhysicalNameUniqueness(params) {
1588
- const { schema, issues } = params;
1679
+ const { schema, issues, modelByName, fieldsByModel } = params;
1589
1680
  const dialect = providerToDialect({ provider: schema.datasource.provider });
1590
1681
  const owners = new Map;
1591
1682
  const claim = (entry) => {
@@ -1615,7 +1706,8 @@ function validatePhysicalNameUniqueness(params) {
1615
1706
  }
1616
1707
  if (model.isView)
1617
1708
  continue;
1618
- const columnOf = (name) => model.fields.find((candidate) => candidate.name === name)?.dbName ?? name;
1709
+ const fields = fieldsByModel.get(model);
1710
+ const columnOf = (name) => fields.get(name)?.dbName ?? name;
1619
1711
  const seenIndexKeys = new Set;
1620
1712
  const declarations = [
1621
1713
  ...model.indexes.map((index) => ({ index, site: index.kind === "unique" ? "@@unique" : "@@index" })),
@@ -1623,8 +1715,8 @@ function validatePhysicalNameUniqueness(params) {
1623
1715
  ];
1624
1716
  for (const { index, site } of declarations) {
1625
1717
  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() ?? ""}`;
1718
+ const columns = index.fields.map((entry) => [columnOf(entry.name), entry.sort === "desc", entry.operatorClass === undefined ? null : [entry.operatorClass.namespace, entry.operatorClass.name]]);
1719
+ const key = JSON.stringify([unique, index.nullsNotDistinct === true, unique ? "btree" : index.kind, columns, index.where?.trim() ?? ""]);
1628
1720
  if (seenIndexKeys.has(key))
1629
1721
  continue;
1630
1722
  seenIndexKeys.add(key);
@@ -1642,7 +1734,7 @@ function validatePhysicalNameUniqueness(params) {
1642
1734
  for (const relation of model.relations) {
1643
1735
  if (relation.fields === undefined || relation.fields.length === 0)
1644
1736
  continue;
1645
- const target = schema.models.find((candidate) => candidate.name === relation.targetModel);
1737
+ const target = modelByName.get(relation.targetModel);
1646
1738
  if (target !== undefined && target.isView)
1647
1739
  continue;
1648
1740
  const name = relation.constraintName ?? defaultForeignKeyName({ table, columns: relation.fields.map((entry) => columnOf(entry)), dialect });
@@ -1671,7 +1763,7 @@ function validatePhysicalNameUniqueness(params) {
1671
1763
  continue;
1672
1764
  if (!isImplicitManyToManyRelation({ model, relation, models: schema.models }))
1673
1765
  continue;
1674
- const target = schema.models.find((candidate) => candidate.name === relation.targetModel);
1766
+ const target = modelByName.get(relation.targetModel);
1675
1767
  const physicalNamespace = model.namespace ?? "public";
1676
1768
  if ((target?.namespace ?? "public") !== physicalNamespace) {
1677
1769
  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 +1862,7 @@ function validateIdentifierLengths(params) {
1770
1862
  }
1771
1863
  }
1772
1864
  function validateModel(params) {
1773
- const { model, modelByName, enumByName, issues } = params;
1865
+ const { model, modelByName, enumByName, issues, lookups, postgres } = params;
1774
1866
  const memberNames = new Set;
1775
1867
  for (const field of model.fields) {
1776
1868
  if (memberNames.has(field.name)) {
@@ -1872,8 +1964,23 @@ function validateModel(params) {
1872
1964
  }
1873
1965
  memberNames.add(relation.name);
1874
1966
  }
1875
- const fieldNames = new Set(model.fields.map((f) => f.name));
1876
- const hasIdField = model.fields.some((f) => f.isId);
1967
+ const fieldNames = new Set(lookups.fieldsByModel.get(model).keys());
1968
+ const idFields = model.fields.filter((f) => f.isId);
1969
+ const hasIdField = idFields.length > 0;
1970
+ if (idFields.length > 1) {
1971
+ issues.push({
1972
+ rule: "multiple-field-ids",
1973
+ message: `Model "${model.name}" has multiple @id fields (${idFields.map((field) => field.name).join(", ")}); declare one compound primary key instead`,
1974
+ model: model.name
1975
+ });
1976
+ }
1977
+ if (idFields.length === 1 && model.primaryKey !== undefined && (model.primaryKey.fields.length !== 1 || model.primaryKey.fields[0] !== idFields[0]?.name)) {
1978
+ issues.push({
1979
+ rule: "conflicting-primary-key",
1980
+ 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`,
1981
+ model: model.name
1982
+ });
1983
+ }
1877
1984
  if (!model.isView && !hasIdField && model.primaryKey === undefined) {
1878
1985
  issues.push({
1879
1986
  rule: "missing-id",
@@ -1932,8 +2039,14 @@ function validateModel(params) {
1932
2039
  });
1933
2040
  }
1934
2041
  }
2042
+ if (postgres && new Set(relation.references).size !== relation.references.length) {
2043
+ issues.push({ rule: "relation-duplicate-column", message: `Relation "${model.name}.${relation.name}" repeats a referenced column`, model: model.name, field: relation.name });
2044
+ }
1935
2045
  if (target !== undefined) {
1936
- const targetFieldNames = new Set(target.fields.map((f) => f.name));
2046
+ if (postgres && referencesPartialOnly({ target, references: relation.references, schema: params.schema, lookups })) {
2047
+ 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 });
2048
+ }
2049
+ const targetFieldNames = lookups.fieldsByModel.get(target);
1937
2050
  for (const refField of relation.references) {
1938
2051
  if (!targetFieldNames.has(refField)) {
1939
2052
  issues.push({
@@ -2101,6 +2214,16 @@ function assertValidSchemaIR(params) {
2101
2214
  if (dialect === "mysql" && counters.length > 1)
2102
2215
  throw unsupportedCapability({ dialect, feature: "multiple auto-increment columns" });
2103
2216
  for (const field of model.fields) {
2217
+ const timeOfDay = field.isUnsupported === true || field.type !== "DateTime" ? undefined : timeOfDayType({ nativeType: field.nativeType });
2218
+ if (timeOfDay !== undefined && !DIALECT_CAPABILITIES[dialect].timeOfDayTypes.includes(timeOfDay)) {
2219
+ 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";
2220
+ const error = unsupportedCapability({ dialect, feature: "timeOfDayTypes", hint: `${model.name}.${field.name} declares @db.${field.nativeType ?? ""} (${timeOfDay}) \u2014 ${instead}` });
2221
+ throw new VibeError({ code: error.code, message: error.message, meta: { ...error.meta, model: model.name, field: field.name, nativeType: field.nativeType } });
2222
+ }
2223
+ if (timeOfDay !== undefined && dialect === "mysql" && field.default?.kind === "now") {
2224
+ 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)` });
2225
+ throw new VibeError({ code: error.code, message: error.message, meta: { ...error.meta, model: model.name, field: field.name, nativeType: field.nativeType } });
2226
+ }
2104
2227
  if (field.default?.kind === "list") {
2105
2228
  if (dialect !== "postgres")
2106
2229
  throw unsupportedCapability({ dialect, feature: "scalar-list defaults" });
@@ -2376,10 +2499,11 @@ export {
2376
2499
  sqlNamespaceIssue,
2377
2500
  sqlObjectKey,
2378
2501
  sqlObjectNamespace,
2502
+ timeOfDayType,
2379
2503
  unsupportedCapability,
2380
2504
  validateComputedConfig,
2381
2505
  validateRlsContextIR,
2382
2506
  validateSchemaIR
2383
2507
  };
2384
2508
 
2385
- //# debugId=20984641ACA6D36E64756E2164756E21
2509
+ //# debugId=99BBBD6C3E58D9DD64756E2164756E21