@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 +21 -0
- package/dist/capabilities.d.ts +12 -0
- package/dist/errors.d.ts +10 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +161 -37
- package/dist/index.js.map +7 -6
- package/dist/time-of-day.d.ts +19 -0
- package/package.json +4 -2
- package/dist/capabilities.d.ts.map +0 -1
- package/dist/computed.d.ts.map +0 -1
- package/dist/database-module.d.ts.map +0 -1
- package/dist/default-values.d.ts.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/exclusion.d.ts.map +0 -1
- package/dist/extension.d.ts.map +0 -1
- package/dist/index-operator-classes.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/ir.d.ts.map +0 -1
- package/dist/m2m.d.ts.map +0 -1
- package/dist/migration-objects.d.ts.map +0 -1
- package/dist/module-schema.d.ts.map +0 -1
- package/dist/namespaces.d.ts.map +0 -1
- package/dist/physical-names.d.ts.map +0 -1
- package/dist/predicate.d.ts.map +0 -1
- package/dist/prisma-literal.d.ts.map +0 -1
- package/dist/reserved.d.ts.map +0 -1
- package/dist/validate.d.ts.map +0 -1
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.
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -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 =
|
|
1515
|
-
const
|
|
1516
|
-
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
1627
|
-
const key =
|
|
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 =
|
|
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 =
|
|
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(
|
|
1876
|
-
const
|
|
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
|
-
|
|
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=
|
|
2509
|
+
//# debugId=99BBBD6C3E58D9DD64756E2164756E21
|