@vibeorm/schema 2.4.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 +18 -0
- package/dist/database-module.d.ts +2 -0
- package/dist/errors.d.ts +10 -0
- package/dist/index-operator-classes.d.ts +8 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +668 -304
- package/dist/index.js.map +14 -10
- package/dist/ir.d.ts +9 -1
- package/dist/migration-objects.d.ts +7 -0
- package/dist/module-schema.d.ts +8 -0
- package/dist/namespaces.d.ts +30 -0
- 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.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/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/dist/index.js
CHANGED
|
@@ -1,85 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// packages/schema/src/ir.ts
|
|
3
|
-
var DIALECTS = ["postgres", "sqlite", "mysql"];
|
|
4
|
-
function providerToDialect(params) {
|
|
5
|
-
const { provider } = params;
|
|
6
|
-
switch (provider) {
|
|
7
|
-
case "postgresql":
|
|
8
|
-
case "pglite":
|
|
9
|
-
return "postgres";
|
|
10
|
-
case "sqlite":
|
|
11
|
-
return "sqlite";
|
|
12
|
-
case "mysql":
|
|
13
|
-
return "mysql";
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
function isEnumType(fieldType) {
|
|
17
|
-
return typeof fieldType === "object" && "enum" in fieldType;
|
|
18
|
-
}
|
|
19
|
-
function fieldTypeIdentity(params) {
|
|
20
|
-
const { type } = params;
|
|
21
|
-
return isEnumType(type) ? `enum:${type.enum}` : type;
|
|
22
|
-
}
|
|
23
|
-
var ENUM_STORAGES = ["database", "string"];
|
|
24
|
-
function enumStorageOf(params) {
|
|
25
|
-
return params.enumDef.storage ?? "database";
|
|
26
|
-
}
|
|
27
|
-
// packages/schema/src/predicate.ts
|
|
28
|
-
var PREDICATE_COMPARISON_OPS = [
|
|
29
|
-
"eq",
|
|
30
|
-
"ne",
|
|
31
|
-
"gt",
|
|
32
|
-
"gte",
|
|
33
|
-
"lt",
|
|
34
|
-
"lte"
|
|
35
|
-
];
|
|
36
|
-
function isPredicateFieldRef(operand) {
|
|
37
|
-
return operand.kind === "field";
|
|
38
|
-
}
|
|
39
|
-
function predicateFieldNames(params) {
|
|
40
|
-
const seen = new Set;
|
|
41
|
-
const walk = (node) => {
|
|
42
|
-
switch (node.kind) {
|
|
43
|
-
case "compare":
|
|
44
|
-
seen.add(node.left.name);
|
|
45
|
-
if (isPredicateFieldRef(node.right))
|
|
46
|
-
seen.add(node.right.name);
|
|
47
|
-
return;
|
|
48
|
-
case "inList":
|
|
49
|
-
case "isNull":
|
|
50
|
-
seen.add(node.field.name);
|
|
51
|
-
return;
|
|
52
|
-
case "and":
|
|
53
|
-
case "or":
|
|
54
|
-
for (const operand of node.operands)
|
|
55
|
-
walk(operand);
|
|
56
|
-
return;
|
|
57
|
-
case "not":
|
|
58
|
-
walk(node.operand);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
walk(params.predicate);
|
|
63
|
-
return [...seen];
|
|
64
|
-
}
|
|
65
|
-
function predicateKey(params) {
|
|
66
|
-
const encode = (node) => {
|
|
67
|
-
switch (node.kind) {
|
|
68
|
-
case "compare":
|
|
69
|
-
return `cmp(${node.op},${node.left.name},${isPredicateFieldRef(node.right) ? `f:${node.right.name}` : `v:${typeof node.right.value}:${String(node.right.value)}`})`;
|
|
70
|
-
case "inList":
|
|
71
|
-
return `in${node.negate === true ? "!" : ""}(${node.field.name},[${node.values.map((entry) => `${typeof entry.value}:${String(entry.value)}`).join("|")}])`;
|
|
72
|
-
case "isNull":
|
|
73
|
-
return `null${node.negate === true ? "!" : ""}(${node.field.name})`;
|
|
74
|
-
case "and":
|
|
75
|
-
case "or":
|
|
76
|
-
return `${node.kind}(${node.operands.map(encode).join(",")})`;
|
|
77
|
-
case "not":
|
|
78
|
-
return `not(${encode(node.operand)})`;
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
return encode(params.predicate);
|
|
82
|
-
}
|
|
83
2
|
// packages/schema/src/errors.ts
|
|
84
3
|
var VIBE_ERROR_CODES = [
|
|
85
4
|
"VIBE_NOT_FOUND",
|
|
@@ -100,6 +19,7 @@ var VIBE_ERROR_CODES = [
|
|
|
100
19
|
"VIBE_RLS_CONTEXT",
|
|
101
20
|
"VIBE_PERMISSION_DENIED",
|
|
102
21
|
"VIBE_ADAPTER",
|
|
22
|
+
"VIBE_ADAPTER_CLOSED",
|
|
103
23
|
"VIBE_INTERNAL"
|
|
104
24
|
];
|
|
105
25
|
|
|
@@ -223,10 +143,206 @@ class ReadOnlyClientError extends VibeError {
|
|
|
223
143
|
}
|
|
224
144
|
}
|
|
225
145
|
|
|
146
|
+
// packages/schema/src/physical-names.ts
|
|
147
|
+
var IDENTIFIER_LIMITS = Object.freeze({
|
|
148
|
+
postgres: { max: 63, unit: "bytes" },
|
|
149
|
+
mysql: { max: 64, unit: "characters" },
|
|
150
|
+
sqlite: undefined
|
|
151
|
+
});
|
|
152
|
+
var ENCODER = new TextEncoder;
|
|
153
|
+
function identifierLength(params) {
|
|
154
|
+
const limit = IDENTIFIER_LIMITS[params.dialect];
|
|
155
|
+
return limit?.unit === "bytes" ? ENCODER.encode(params.name).length : [...params.name].length;
|
|
156
|
+
}
|
|
157
|
+
function cutIdentifier(params) {
|
|
158
|
+
let out = "";
|
|
159
|
+
let used = 0;
|
|
160
|
+
for (const char of params.name) {
|
|
161
|
+
const size = params.unit === "bytes" ? ENCODER.encode(char).length : 1;
|
|
162
|
+
if (used + size > params.length)
|
|
163
|
+
break;
|
|
164
|
+
out += char;
|
|
165
|
+
used += size;
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
function fitDefaultName(params) {
|
|
170
|
+
const limit = IDENTIFIER_LIMITS[params.dialect];
|
|
171
|
+
if (limit === undefined)
|
|
172
|
+
return `${params.prefix}${params.suffix}`;
|
|
173
|
+
const room = limit.max - identifierLength({ name: params.suffix, dialect: params.dialect });
|
|
174
|
+
if (identifierLength({ name: params.prefix, dialect: params.dialect }) <= room)
|
|
175
|
+
return `${params.prefix}${params.suffix}`;
|
|
176
|
+
return `${cutIdentifier({ name: params.prefix, length: room, unit: limit.unit })}${params.suffix}`;
|
|
177
|
+
}
|
|
178
|
+
function assertIdentifierFits(params) {
|
|
179
|
+
const limit = IDENTIFIER_LIMITS[params.dialect];
|
|
180
|
+
if (limit === undefined)
|
|
181
|
+
return;
|
|
182
|
+
const length = identifierLength({ name: params.name, dialect: params.dialect });
|
|
183
|
+
if (length <= limit.max)
|
|
184
|
+
return;
|
|
185
|
+
throw new VibeError({
|
|
186
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
187
|
+
message: `${params.what} "${params.name}" is ${length} ${limit.unit} \u2014 ${params.dialect} keeps at most ${limit.max}; the created object would never carry this name and migrations could not converge. ${params.remedy}`,
|
|
188
|
+
meta: { name: params.name, dialect: params.dialect, length, max: limit.max }
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function defaultIndexName(params) {
|
|
192
|
+
return fitDefaultName({
|
|
193
|
+
prefix: `${params.table}_${params.columns.join("_")}`,
|
|
194
|
+
suffix: params.unique ? "_key" : "_idx",
|
|
195
|
+
dialect: params.dialect
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function defaultForeignKeyName(params) {
|
|
199
|
+
return fitDefaultName({ prefix: `${params.table}_${params.columns.join("_")}`, suffix: "_fkey", dialect: params.dialect });
|
|
200
|
+
}
|
|
201
|
+
function defaultPrimaryKeyName(params) {
|
|
202
|
+
return fitDefaultName({ prefix: params.table, suffix: "_pkey", dialect: params.dialect });
|
|
203
|
+
}
|
|
204
|
+
function implicitJoinTableObjectNames(params) {
|
|
205
|
+
return {
|
|
206
|
+
uniqueIndex: `${params.table}_AB_unique`,
|
|
207
|
+
indexB: `${params.table}_B_index`,
|
|
208
|
+
foreignKeyA: `${params.table}_A_fkey`,
|
|
209
|
+
foreignKeyB: `${params.table}_B_fkey`
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function postgresGeneratedObjectName(params) {
|
|
213
|
+
const limit = IDENTIFIER_LIMITS.postgres;
|
|
214
|
+
const overhead = params.label.length + 2;
|
|
215
|
+
let chars1 = [...params.name1];
|
|
216
|
+
let chars2 = [...params.name2];
|
|
217
|
+
const bytes = (chars) => ENCODER.encode(chars.join("")).length;
|
|
218
|
+
while (bytes(chars1) + bytes(chars2) + overhead > limit.max) {
|
|
219
|
+
if (bytes(chars1) > bytes(chars2))
|
|
220
|
+
chars1 = chars1.slice(0, -1);
|
|
221
|
+
else
|
|
222
|
+
chars2 = chars2.slice(0, -1);
|
|
223
|
+
}
|
|
224
|
+
return `${chars1.join("")}_${chars2.join("")}_${params.label}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// packages/schema/src/namespaces.ts
|
|
228
|
+
var SQL_NAMESPACES = Object.freeze({
|
|
229
|
+
postgres: "public",
|
|
230
|
+
sqlite: "main",
|
|
231
|
+
private: Object.freeze(["_vibeorm_shadow", "_vibe_shadow"])
|
|
232
|
+
});
|
|
233
|
+
function sqlNamespaceIssue(params) {
|
|
234
|
+
const { namespace } = params;
|
|
235
|
+
if (typeof namespace !== "string" || namespace.trim().length === 0 || namespace.includes("\x00"))
|
|
236
|
+
return "Namespace must be a non-empty identifier without NUL bytes";
|
|
237
|
+
if (identifierLength({ name: namespace, dialect: "postgres" }) > IDENTIFIER_LIMITS.postgres.max)
|
|
238
|
+
return "PostgreSQL namespace exceeds its identifier byte limit";
|
|
239
|
+
if (namespace === "information_schema" || namespace.startsWith("pg_") || SQL_NAMESPACES.private.includes(namespace))
|
|
240
|
+
return "System and private VibeORM namespaces cannot be declared";
|
|
241
|
+
if (params.allowPublic === false && namespace === SQL_NAMESPACES.postgres)
|
|
242
|
+
return "A fixed package namespace must be non-public; omit namespace for a legacy public artifact";
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
function assertSqlNamespace(params) {
|
|
246
|
+
if (params.dialect !== "postgres")
|
|
247
|
+
throw unsupportedCapability({ dialect: params.dialect, feature: "fixedNamespaces" });
|
|
248
|
+
const issue = sqlNamespaceIssue(params);
|
|
249
|
+
if (issue !== undefined)
|
|
250
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: issue, meta: { rule: "namespace", namespace: params.namespace } });
|
|
251
|
+
}
|
|
252
|
+
function qualifiedSqlName(params) {
|
|
253
|
+
return { namespace: params.namespace ?? params.defaultNamespace ?? SQL_NAMESPACES.postgres, name: params.name };
|
|
254
|
+
}
|
|
255
|
+
// packages/schema/src/capabilities.ts
|
|
256
|
+
var POSTGRES_CAPABILITIES = Object.freeze({
|
|
257
|
+
fixedNamespaces: true,
|
|
258
|
+
externalObjects: true,
|
|
259
|
+
indexOperatorClasses: true,
|
|
260
|
+
exclusionConstraints: true,
|
|
261
|
+
exclusionExtensions: Object.freeze(["btree_gist"]),
|
|
262
|
+
checkNormalization: "rollback",
|
|
263
|
+
nativeRowLevelSecurity: true,
|
|
264
|
+
returning: true,
|
|
265
|
+
scalarArrays: true,
|
|
266
|
+
nativeEnums: true,
|
|
267
|
+
ilike: true,
|
|
268
|
+
lateralJoin: true,
|
|
269
|
+
distinctOn: true,
|
|
270
|
+
partialIndexes: true,
|
|
271
|
+
conflictTargetConstraint: true,
|
|
272
|
+
transactionalDdl: true,
|
|
273
|
+
createManyAndReturn: true,
|
|
274
|
+
jsonColumnType: true,
|
|
275
|
+
orderByNulls: true,
|
|
276
|
+
fullTextEngines: Object.freeze(["bm25", "tsvector"]),
|
|
277
|
+
rowLockStrengths: Object.freeze(["update", "noKeyUpdate", "share", "keyShare"]),
|
|
278
|
+
rowLockWaitPolicies: Object.freeze(["wait", "nowait", "skipLocked"]),
|
|
279
|
+
advisoryLocks: true,
|
|
280
|
+
timeOfDayTypes: Object.freeze(["time", "timetz"])
|
|
281
|
+
});
|
|
282
|
+
var SQLITE_CAPABILITIES = Object.freeze({
|
|
283
|
+
fixedNamespaces: false,
|
|
284
|
+
externalObjects: false,
|
|
285
|
+
indexOperatorClasses: false,
|
|
286
|
+
exclusionConstraints: false,
|
|
287
|
+
exclusionExtensions: Object.freeze([]),
|
|
288
|
+
checkNormalization: "sourceText",
|
|
289
|
+
nativeRowLevelSecurity: false,
|
|
290
|
+
returning: true,
|
|
291
|
+
scalarArrays: false,
|
|
292
|
+
nativeEnums: false,
|
|
293
|
+
ilike: false,
|
|
294
|
+
lateralJoin: false,
|
|
295
|
+
distinctOn: false,
|
|
296
|
+
partialIndexes: true,
|
|
297
|
+
conflictTargetConstraint: false,
|
|
298
|
+
transactionalDdl: true,
|
|
299
|
+
createManyAndReturn: true,
|
|
300
|
+
jsonColumnType: false,
|
|
301
|
+
orderByNulls: true,
|
|
302
|
+
fullTextEngines: Object.freeze(["fts5"]),
|
|
303
|
+
rowLockStrengths: Object.freeze([]),
|
|
304
|
+
rowLockWaitPolicies: Object.freeze([]),
|
|
305
|
+
advisoryLocks: false,
|
|
306
|
+
timeOfDayTypes: Object.freeze([])
|
|
307
|
+
});
|
|
308
|
+
var MYSQL_CAPABILITIES = Object.freeze({
|
|
309
|
+
fixedNamespaces: false,
|
|
310
|
+
externalObjects: false,
|
|
311
|
+
indexOperatorClasses: false,
|
|
312
|
+
exclusionConstraints: false,
|
|
313
|
+
exclusionExtensions: Object.freeze([]),
|
|
314
|
+
checkNormalization: "temporary",
|
|
315
|
+
nativeRowLevelSecurity: false,
|
|
316
|
+
returning: false,
|
|
317
|
+
scalarArrays: false,
|
|
318
|
+
nativeEnums: true,
|
|
319
|
+
ilike: false,
|
|
320
|
+
lateralJoin: false,
|
|
321
|
+
distinctOn: false,
|
|
322
|
+
partialIndexes: false,
|
|
323
|
+
conflictTargetConstraint: false,
|
|
324
|
+
transactionalDdl: false,
|
|
325
|
+
createManyAndReturn: false,
|
|
326
|
+
jsonColumnType: true,
|
|
327
|
+
orderByNulls: false,
|
|
328
|
+
fullTextEngines: Object.freeze(["fulltext"]),
|
|
329
|
+
rowLockStrengths: Object.freeze(["update", "share"]),
|
|
330
|
+
rowLockWaitPolicies: Object.freeze(["wait", "nowait", "skipLocked"]),
|
|
331
|
+
advisoryLocks: false,
|
|
332
|
+
timeOfDayTypes: Object.freeze(["time"])
|
|
333
|
+
});
|
|
334
|
+
var DIALECT_CAPABILITIES = Object.freeze({
|
|
335
|
+
postgres: POSTGRES_CAPABILITIES,
|
|
336
|
+
sqlite: SQLITE_CAPABILITIES,
|
|
337
|
+
mysql: MYSQL_CAPABILITIES
|
|
338
|
+
});
|
|
339
|
+
|
|
226
340
|
// packages/schema/src/migration-objects.ts
|
|
227
341
|
function sqlObjectKey(params) {
|
|
228
342
|
const object = params.identity;
|
|
229
343
|
switch (object.kind) {
|
|
344
|
+
case "namespace":
|
|
345
|
+
return JSON.stringify([object.kind, object.name]);
|
|
230
346
|
case "table":
|
|
231
347
|
case "view":
|
|
232
348
|
case "sequence":
|
|
@@ -246,6 +362,12 @@ function sqlObjectKey(params) {
|
|
|
246
362
|
return JSON.stringify([object.kind, object.table.namespace, object.table.name, object.column]);
|
|
247
363
|
}
|
|
248
364
|
}
|
|
365
|
+
function sqlObjectNamespace(params) {
|
|
366
|
+
const identity = params.identity;
|
|
367
|
+
if (identity.kind === "namespace")
|
|
368
|
+
return identity.name;
|
|
369
|
+
return "table" in identity ? identity.table.namespace : identity.name.namespace;
|
|
370
|
+
}
|
|
249
371
|
function record(value) {
|
|
250
372
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
251
373
|
}
|
|
@@ -256,6 +378,8 @@ function isSqlObjectIdentity(value) {
|
|
|
256
378
|
if (!record(value))
|
|
257
379
|
return false;
|
|
258
380
|
switch (value.kind) {
|
|
381
|
+
case "namespace":
|
|
382
|
+
return typeof value.name === "string" && value.name.length > 0;
|
|
259
383
|
case "table":
|
|
260
384
|
case "view":
|
|
261
385
|
case "sequence":
|
|
@@ -277,24 +401,271 @@ function isSqlObjectIdentity(value) {
|
|
|
277
401
|
return false;
|
|
278
402
|
}
|
|
279
403
|
}
|
|
280
|
-
function isSqlObjectDefinition(value) {
|
|
281
|
-
return record(value) && (value.format === "catalog-v1" || value.format === "catalog-v2") && isSqlObjectIdentity(value.identity) && record(value.facts) && Object.keys(value.facts).length > 0 && Object.values(value.facts).every((fact) => fact === null || typeof fact === "string" || typeof fact === "boolean" || typeof fact === "number" && Number.isFinite(fact) || Array.isArray(fact) && fact.every((entry) => typeof entry === "string")) && Array.isArray(value.dependencies) && value.dependencies.every(isSqlObjectIdentity);
|
|
404
|
+
function isSqlObjectDefinition(value) {
|
|
405
|
+
return record(value) && (value.format === "catalog-v1" || value.format === "catalog-v2") && isSqlObjectIdentity(value.identity) && record(value.facts) && Object.keys(value.facts).length > 0 && Object.values(value.facts).every((fact) => fact === null || typeof fact === "string" || typeof fact === "boolean" || typeof fact === "number" && Number.isFinite(fact) || Array.isArray(fact) && fact.every((entry) => typeof entry === "string")) && Array.isArray(value.dependencies) && value.dependencies.every(isSqlObjectIdentity);
|
|
406
|
+
}
|
|
407
|
+
function qualifiedNameIssue(params) {
|
|
408
|
+
if (!record(params.value))
|
|
409
|
+
return { path: params.path, expected: "qualified SQL name { namespace, name }" };
|
|
410
|
+
const member = typeof params.value.namespace !== "string" || params.value.namespace.length === 0 ? "namespace" : "name";
|
|
411
|
+
return { path: `${params.path}.${member}`, expected: "non-empty string" };
|
|
412
|
+
}
|
|
413
|
+
function identityIssue(params) {
|
|
414
|
+
const { value, path } = params;
|
|
415
|
+
if (!record(value))
|
|
416
|
+
return { path, expected: "SQL object identity" };
|
|
417
|
+
switch (value.kind) {
|
|
418
|
+
case "namespace":
|
|
419
|
+
return { path: `${path}.name`, expected: "non-empty namespace string" };
|
|
420
|
+
case "table":
|
|
421
|
+
case "view":
|
|
422
|
+
case "sequence":
|
|
423
|
+
case "postgresExtension":
|
|
424
|
+
case "enum":
|
|
425
|
+
return qualifiedNameIssue({ value: value.name, path: `${path}.name` });
|
|
426
|
+
case "function": {
|
|
427
|
+
if (!name(value.name))
|
|
428
|
+
return qualifiedNameIssue({ value: value.name, path: `${path}.name` });
|
|
429
|
+
if (!Array.isArray(value.argumentTypes))
|
|
430
|
+
return { path: `${path}.argumentTypes`, expected: "array of qualified SQL type names" };
|
|
431
|
+
const index = value.argumentTypes.findIndex((entry) => !name(entry));
|
|
432
|
+
return qualifiedNameIssue({ value: value.argumentTypes[index], path: `${path}.argumentTypes[${index}]` });
|
|
433
|
+
}
|
|
434
|
+
case "index":
|
|
435
|
+
case "trigger":
|
|
436
|
+
case "constraint":
|
|
437
|
+
case "column":
|
|
438
|
+
case "columnDefault":
|
|
439
|
+
case "generatedColumn": {
|
|
440
|
+
if (!name(value.table))
|
|
441
|
+
return qualifiedNameIssue({ value: value.table, path: `${path}.table` });
|
|
442
|
+
if (value.kind === "column" || value.kind === "columnDefault" || value.kind === "generatedColumn")
|
|
443
|
+
return { path: `${path}.column`, expected: "non-empty column name" };
|
|
444
|
+
if (typeof value.name !== "string" || value.name.length === 0)
|
|
445
|
+
return { path: `${path}.name`, expected: "non-empty object name" };
|
|
446
|
+
return { path: `${path}.constraintType`, expected: ["check", "foreignKey", "exclusion", "unique", "primaryKey"] };
|
|
447
|
+
}
|
|
448
|
+
default:
|
|
449
|
+
return { path: `${path}.kind`, expected: ["namespace", "table", "view", "sequence", "postgresExtension", "enum", "function", "index", "trigger", "constraint", "column", "columnDefault", "generatedColumn"] };
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function definitionIssue(params) {
|
|
453
|
+
const { value, path } = params;
|
|
454
|
+
if (!record(value))
|
|
455
|
+
return { path, expected: "independent SQL object definition" };
|
|
456
|
+
if (value.format !== "catalog-v1" && value.format !== "catalog-v2")
|
|
457
|
+
return { path: `${path}.format`, expected: ["catalog-v1", "catalog-v2"] };
|
|
458
|
+
if (!isSqlObjectIdentity(value.identity))
|
|
459
|
+
return identityIssue({ value: value.identity, path: `${path}.identity` });
|
|
460
|
+
if (!record(value.facts) || Object.keys(value.facts).length === 0)
|
|
461
|
+
return { path: `${path}.facts`, expected: "non-empty serializable catalog facts" };
|
|
462
|
+
for (const [key, fact] of Object.entries(value.facts)) {
|
|
463
|
+
if (!(fact === null || typeof fact === "string" || typeof fact === "boolean" || typeof fact === "number" && Number.isFinite(fact) || Array.isArray(fact) && fact.every((entry) => typeof entry === "string")))
|
|
464
|
+
return { path: `${path}.facts.${key}`, expected: "string, finite number, boolean, null or string array" };
|
|
465
|
+
}
|
|
466
|
+
if (!Array.isArray(value.dependencies))
|
|
467
|
+
return { path: `${path}.dependencies`, expected: "array of SQL object identities" };
|
|
468
|
+
const index = value.dependencies.findIndex((entry) => !isSqlObjectIdentity(entry));
|
|
469
|
+
return identityIssue({ value: value.dependencies[index], path: `${path}.dependencies[${index}]` });
|
|
470
|
+
}
|
|
471
|
+
function invalidDeclaration(params) {
|
|
472
|
+
const expected = typeof params.expected === "string" ? params.expected : params.expected.join(", ");
|
|
473
|
+
throw new VibeError({
|
|
474
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
475
|
+
message: `Invalid migration-only object declaration at ${params.path}; expected ${expected}`,
|
|
476
|
+
meta: { path: params.path, expected: params.expected, ...params.id === undefined ? {} : { id: params.id } }
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
function assertMigrationObjects(params) {
|
|
480
|
+
if (!Array.isArray(params.objects))
|
|
481
|
+
invalidDeclaration({ path: "migrationObjects", expected: "array" });
|
|
482
|
+
const ids = new Set;
|
|
483
|
+
const owners = new Set;
|
|
484
|
+
for (const [index, value] of params.objects.entries()) {
|
|
485
|
+
const object = value;
|
|
486
|
+
const path = `migrationObjects[${index}]`;
|
|
487
|
+
if (!record(object))
|
|
488
|
+
invalidDeclaration({ path, expected: "object" });
|
|
489
|
+
if (typeof object.id !== "string" || object.id.length === 0)
|
|
490
|
+
invalidDeclaration({ path: `${path}.id`, expected: "non-empty string" });
|
|
491
|
+
const id = object.id;
|
|
492
|
+
if (!["postgres", "sqlite", "mysql"].includes(String(object.dialect)))
|
|
493
|
+
invalidDeclaration({ path: `${path}.dialect`, expected: ["postgres", "sqlite", "mysql"], id });
|
|
494
|
+
const identity = object.identity;
|
|
495
|
+
if (!isSqlObjectIdentity(identity))
|
|
496
|
+
invalidDeclaration({ ...identityIssue({ value: identity, path: `${path}.identity` }), id });
|
|
497
|
+
for (const member of ["create", "drop"]) {
|
|
498
|
+
const statements = object[member];
|
|
499
|
+
if (!Array.isArray(statements))
|
|
500
|
+
invalidDeclaration({ path: `${path}.${member}`, expected: "array of SQL strings", id });
|
|
501
|
+
const invalid = statements.findIndex((entry) => typeof entry !== "string");
|
|
502
|
+
if (invalid !== -1)
|
|
503
|
+
invalidDeclaration({ path: `${path}.${member}[${invalid}]`, expected: "SQL string", id });
|
|
504
|
+
}
|
|
505
|
+
if (!Array.isArray(object.dependsOn))
|
|
506
|
+
invalidDeclaration({ path: `${path}.dependsOn`, expected: "array of SQL object identities", id });
|
|
507
|
+
for (const [dependencyIndex, dependency] of object.dependsOn.entries())
|
|
508
|
+
if (!isSqlObjectIdentity(dependency))
|
|
509
|
+
invalidDeclaration({ ...identityIssue({ value: dependency, path: `${path}.dependsOn[${dependencyIndex}]` }), id });
|
|
510
|
+
if (object.expected !== undefined) {
|
|
511
|
+
if (!isSqlObjectDefinition(object.expected))
|
|
512
|
+
invalidDeclaration({ ...definitionIssue({ value: object.expected, path: `${path}.expected` }), id });
|
|
513
|
+
if (sqlObjectKey({ identity: object.expected.identity }) !== sqlObjectKey({ identity }))
|
|
514
|
+
invalidDeclaration({ path: `${path}.expected.identity`, expected: "the declaration's SQL object identity", id });
|
|
515
|
+
}
|
|
516
|
+
const key = sqlObjectKey({ identity });
|
|
517
|
+
if (ids.has(id) || owners.has(key))
|
|
518
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Duplicate migration object identity or id", meta: { id, key, path: `${path}.${ids.has(id) ? "id" : "identity"}` } });
|
|
519
|
+
ids.add(id);
|
|
520
|
+
owners.add(key);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// packages/schema/src/index-operator-classes.ts
|
|
525
|
+
function assertIndexOperatorClasses(params) {
|
|
526
|
+
for (const model of params.schema.models)
|
|
527
|
+
for (const index of model.indexes)
|
|
528
|
+
for (const field of index.fields) {
|
|
529
|
+
if (field.operatorClass === undefined)
|
|
530
|
+
continue;
|
|
531
|
+
if (!DIALECT_CAPABILITIES[params.dialect].indexOperatorClasses)
|
|
532
|
+
throw unsupportedCapability({ dialect: params.dialect, feature: "indexOperatorClasses" });
|
|
533
|
+
if (index.kind === "unique" && params.physical !== true)
|
|
534
|
+
throw unsupportedCapability({ dialect: params.dialect, feature: "uniqueIndexOperatorClasses", hint: "custom unique operator classes can change client selector equality" });
|
|
535
|
+
if (!isSqlObjectIdentity({ kind: "table", name: field.operatorClass }))
|
|
536
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Index operator class must be a qualified SQL name { namespace, name }", meta: { rule: "index-operator-class-identity", model: model.name, field: field.name } });
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// packages/schema/src/ir.ts
|
|
540
|
+
var DIALECTS = ["postgres", "sqlite", "mysql"];
|
|
541
|
+
function providerToDialect(params) {
|
|
542
|
+
const { provider } = params;
|
|
543
|
+
switch (provider) {
|
|
544
|
+
case "postgresql":
|
|
545
|
+
case "pglite":
|
|
546
|
+
return "postgres";
|
|
547
|
+
case "sqlite":
|
|
548
|
+
return "sqlite";
|
|
549
|
+
case "mysql":
|
|
550
|
+
return "mysql";
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function isEnumType(fieldType) {
|
|
554
|
+
return typeof fieldType === "object" && "enum" in fieldType;
|
|
555
|
+
}
|
|
556
|
+
function fieldTypeIdentity(params) {
|
|
557
|
+
const { type } = params;
|
|
558
|
+
return isEnumType(type) ? type.namespace === undefined || type.namespace === "public" ? `enum:${type.enum}` : JSON.stringify(["enum", type.namespace, type.enum]) : type;
|
|
559
|
+
}
|
|
560
|
+
var ENUM_STORAGES = ["database", "string"];
|
|
561
|
+
function enumStorageOf(params) {
|
|
562
|
+
return params.enumDef.storage ?? "database";
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// packages/schema/src/module-schema.ts
|
|
566
|
+
function prepareDatabaseModuleSchema(params) {
|
|
567
|
+
const { schema, module } = params;
|
|
568
|
+
const namespace = module.namespace;
|
|
569
|
+
const dialect = providerToDialect({ provider: schema.datasource.provider });
|
|
570
|
+
if (namespace !== undefined)
|
|
571
|
+
assertSqlNamespace({ namespace, dialect, allowPublic: false });
|
|
572
|
+
const expected = namespace ?? SQL_NAMESPACES.postgres;
|
|
573
|
+
for (const entry of [...schema.models, ...schema.enums]) {
|
|
574
|
+
if (entry.namespace === undefined)
|
|
575
|
+
continue;
|
|
576
|
+
assertSqlNamespace({ namespace: entry.namespace, dialect });
|
|
577
|
+
if (qualifiedSqlName({ name: entry.dbName ?? entry.name, namespace: entry.namespace }).namespace !== expected)
|
|
578
|
+
throw new VibeError({ code: "VIBE_CONFIG", message: "Module schema placement differs from its immutable namespace; host configuration cannot remap it", meta: { reason: "module-placement-mismatch", moduleId: module.id, name: entry.name } });
|
|
579
|
+
}
|
|
580
|
+
for (const model of schema.models)
|
|
581
|
+
for (const field of model.fields) {
|
|
582
|
+
if (!isEnumType(field.type) || field.type.namespace === undefined)
|
|
583
|
+
continue;
|
|
584
|
+
assertSqlNamespace({ namespace: field.type.namespace, dialect });
|
|
585
|
+
if (field.type.namespace !== expected)
|
|
586
|
+
throw new VibeError({ code: "VIBE_CONFIG", message: "Module enum reference placement differs from its immutable namespace", meta: { reason: "module-placement-mismatch", moduleId: module.id, model: model.name, field: field.name } });
|
|
587
|
+
}
|
|
588
|
+
if (namespace === undefined)
|
|
589
|
+
return schema;
|
|
590
|
+
const nativeEnums = new Set(schema.enums.filter((entry) => enumStorageOf({ enumDef: entry }) === "database").map((entry) => entry.name));
|
|
591
|
+
return {
|
|
592
|
+
...schema,
|
|
593
|
+
models: schema.models.map((model) => ({ ...model, namespace, fields: model.fields.map((field) => {
|
|
594
|
+
const type = field.type;
|
|
595
|
+
return isEnumType(type) && nativeEnums.has(type.enum) ? { ...field, type: { ...type, namespace } } : field;
|
|
596
|
+
}) })),
|
|
597
|
+
enums: schema.enums.map((entry) => enumStorageOf({ enumDef: entry }) === "database" ? { ...entry, namespace } : entry)
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
// packages/schema/src/predicate.ts
|
|
601
|
+
var PREDICATE_COMPARISON_OPS = [
|
|
602
|
+
"eq",
|
|
603
|
+
"ne",
|
|
604
|
+
"gt",
|
|
605
|
+
"gte",
|
|
606
|
+
"lt",
|
|
607
|
+
"lte"
|
|
608
|
+
];
|
|
609
|
+
function isPredicateFieldRef(operand) {
|
|
610
|
+
return operand.kind === "field";
|
|
282
611
|
}
|
|
283
|
-
function
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
612
|
+
function predicateFieldNames(params) {
|
|
613
|
+
const seen = new Set;
|
|
614
|
+
const walk = (node) => {
|
|
615
|
+
switch (node.kind) {
|
|
616
|
+
case "compare":
|
|
617
|
+
seen.add(node.left.name);
|
|
618
|
+
if (isPredicateFieldRef(node.right))
|
|
619
|
+
seen.add(node.right.name);
|
|
620
|
+
return;
|
|
621
|
+
case "inList":
|
|
622
|
+
case "isNull":
|
|
623
|
+
seen.add(node.field.name);
|
|
624
|
+
return;
|
|
625
|
+
case "and":
|
|
626
|
+
case "or":
|
|
627
|
+
for (const operand of node.operands)
|
|
628
|
+
walk(operand);
|
|
629
|
+
return;
|
|
630
|
+
case "not":
|
|
631
|
+
walk(node.operand);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
walk(params.predicate);
|
|
636
|
+
return [...seen];
|
|
637
|
+
}
|
|
638
|
+
function predicateKey(params) {
|
|
639
|
+
const encode = (node) => {
|
|
640
|
+
switch (node.kind) {
|
|
641
|
+
case "compare":
|
|
642
|
+
return `cmp(${node.op},${node.left.name},${isPredicateFieldRef(node.right) ? `f:${node.right.name}` : `v:${typeof node.right.value}:${String(node.right.value)}`})`;
|
|
643
|
+
case "inList":
|
|
644
|
+
return `in${node.negate === true ? "!" : ""}(${node.field.name},[${node.values.map((entry) => `${typeof entry.value}:${String(entry.value)}`).join("|")}])`;
|
|
645
|
+
case "isNull":
|
|
646
|
+
return `null${node.negate === true ? "!" : ""}(${node.field.name})`;
|
|
647
|
+
case "and":
|
|
648
|
+
case "or":
|
|
649
|
+
return `${node.kind}(${node.operands.map(encode).join(",")})`;
|
|
650
|
+
case "not":
|
|
651
|
+
return `not(${encode(node.operand)})`;
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
return encode(params.predicate);
|
|
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";
|
|
298
669
|
}
|
|
299
670
|
// packages/schema/src/prisma-literal.ts
|
|
300
671
|
var STRING_ESCAPES = {
|
|
@@ -407,6 +778,7 @@ function exactDecimalLiteral(params) {
|
|
|
407
778
|
// packages/schema/src/reserved.ts
|
|
408
779
|
var RESERVED_DELEGATE_NAMES = [
|
|
409
780
|
"findMany",
|
|
781
|
+
"findPage",
|
|
410
782
|
"findFirst",
|
|
411
783
|
"findUnique",
|
|
412
784
|
"findFirstOrThrow",
|
|
@@ -740,160 +1112,6 @@ function implicitJoinTable(params) {
|
|
|
740
1112
|
};
|
|
741
1113
|
}
|
|
742
1114
|
|
|
743
|
-
// packages/schema/src/physical-names.ts
|
|
744
|
-
var IDENTIFIER_LIMITS = Object.freeze({
|
|
745
|
-
postgres: { max: 63, unit: "bytes" },
|
|
746
|
-
mysql: { max: 64, unit: "characters" },
|
|
747
|
-
sqlite: undefined
|
|
748
|
-
});
|
|
749
|
-
var ENCODER = new TextEncoder;
|
|
750
|
-
function identifierLength(params) {
|
|
751
|
-
const limit = IDENTIFIER_LIMITS[params.dialect];
|
|
752
|
-
return limit?.unit === "bytes" ? ENCODER.encode(params.name).length : [...params.name].length;
|
|
753
|
-
}
|
|
754
|
-
function cutIdentifier(params) {
|
|
755
|
-
let out = "";
|
|
756
|
-
let used = 0;
|
|
757
|
-
for (const char of params.name) {
|
|
758
|
-
const size = params.unit === "bytes" ? ENCODER.encode(char).length : 1;
|
|
759
|
-
if (used + size > params.length)
|
|
760
|
-
break;
|
|
761
|
-
out += char;
|
|
762
|
-
used += size;
|
|
763
|
-
}
|
|
764
|
-
return out;
|
|
765
|
-
}
|
|
766
|
-
function fitDefaultName(params) {
|
|
767
|
-
const limit = IDENTIFIER_LIMITS[params.dialect];
|
|
768
|
-
if (limit === undefined)
|
|
769
|
-
return `${params.prefix}${params.suffix}`;
|
|
770
|
-
const room = limit.max - identifierLength({ name: params.suffix, dialect: params.dialect });
|
|
771
|
-
if (identifierLength({ name: params.prefix, dialect: params.dialect }) <= room)
|
|
772
|
-
return `${params.prefix}${params.suffix}`;
|
|
773
|
-
return `${cutIdentifier({ name: params.prefix, length: room, unit: limit.unit })}${params.suffix}`;
|
|
774
|
-
}
|
|
775
|
-
function assertIdentifierFits(params) {
|
|
776
|
-
const limit = IDENTIFIER_LIMITS[params.dialect];
|
|
777
|
-
if (limit === undefined)
|
|
778
|
-
return;
|
|
779
|
-
const length = identifierLength({ name: params.name, dialect: params.dialect });
|
|
780
|
-
if (length <= limit.max)
|
|
781
|
-
return;
|
|
782
|
-
throw new VibeError({
|
|
783
|
-
code: "VIBE_SCHEMA_INVALID",
|
|
784
|
-
message: `${params.what} "${params.name}" is ${length} ${limit.unit} \u2014 ${params.dialect} keeps at most ${limit.max}; the created object would never carry this name and migrations could not converge. ${params.remedy}`,
|
|
785
|
-
meta: { name: params.name, dialect: params.dialect, length, max: limit.max }
|
|
786
|
-
});
|
|
787
|
-
}
|
|
788
|
-
function defaultIndexName(params) {
|
|
789
|
-
return fitDefaultName({
|
|
790
|
-
prefix: `${params.table}_${params.columns.join("_")}`,
|
|
791
|
-
suffix: params.unique ? "_key" : "_idx",
|
|
792
|
-
dialect: params.dialect
|
|
793
|
-
});
|
|
794
|
-
}
|
|
795
|
-
function defaultForeignKeyName(params) {
|
|
796
|
-
return fitDefaultName({ prefix: `${params.table}_${params.columns.join("_")}`, suffix: "_fkey", dialect: params.dialect });
|
|
797
|
-
}
|
|
798
|
-
function defaultPrimaryKeyName(params) {
|
|
799
|
-
return fitDefaultName({ prefix: params.table, suffix: "_pkey", dialect: params.dialect });
|
|
800
|
-
}
|
|
801
|
-
function implicitJoinTableObjectNames(params) {
|
|
802
|
-
return {
|
|
803
|
-
uniqueIndex: `${params.table}_AB_unique`,
|
|
804
|
-
indexB: `${params.table}_B_index`,
|
|
805
|
-
foreignKeyA: `${params.table}_A_fkey`,
|
|
806
|
-
foreignKeyB: `${params.table}_B_fkey`
|
|
807
|
-
};
|
|
808
|
-
}
|
|
809
|
-
function postgresGeneratedObjectName(params) {
|
|
810
|
-
const limit = IDENTIFIER_LIMITS.postgres;
|
|
811
|
-
const overhead = params.label.length + 2;
|
|
812
|
-
let chars1 = [...params.name1];
|
|
813
|
-
let chars2 = [...params.name2];
|
|
814
|
-
const bytes = (chars) => ENCODER.encode(chars.join("")).length;
|
|
815
|
-
while (bytes(chars1) + bytes(chars2) + overhead > limit.max) {
|
|
816
|
-
if (bytes(chars1) > bytes(chars2))
|
|
817
|
-
chars1 = chars1.slice(0, -1);
|
|
818
|
-
else
|
|
819
|
-
chars2 = chars2.slice(0, -1);
|
|
820
|
-
}
|
|
821
|
-
return `${chars1.join("")}_${chars2.join("")}_${params.label}`;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
// packages/schema/src/capabilities.ts
|
|
825
|
-
var POSTGRES_CAPABILITIES = Object.freeze({
|
|
826
|
-
exclusionConstraints: true,
|
|
827
|
-
exclusionExtensions: Object.freeze(["btree_gist"]),
|
|
828
|
-
checkNormalization: "rollback",
|
|
829
|
-
nativeRowLevelSecurity: true,
|
|
830
|
-
returning: true,
|
|
831
|
-
scalarArrays: true,
|
|
832
|
-
nativeEnums: true,
|
|
833
|
-
ilike: true,
|
|
834
|
-
lateralJoin: true,
|
|
835
|
-
distinctOn: true,
|
|
836
|
-
partialIndexes: true,
|
|
837
|
-
conflictTargetConstraint: true,
|
|
838
|
-
transactionalDdl: true,
|
|
839
|
-
createManyAndReturn: true,
|
|
840
|
-
jsonColumnType: true,
|
|
841
|
-
orderByNulls: true,
|
|
842
|
-
fullTextEngines: Object.freeze(["bm25", "tsvector"]),
|
|
843
|
-
rowLockStrengths: Object.freeze(["update", "noKeyUpdate", "share", "keyShare"]),
|
|
844
|
-
rowLockWaitPolicies: Object.freeze(["wait", "nowait", "skipLocked"]),
|
|
845
|
-
advisoryLocks: true
|
|
846
|
-
});
|
|
847
|
-
var SQLITE_CAPABILITIES = Object.freeze({
|
|
848
|
-
exclusionConstraints: false,
|
|
849
|
-
exclusionExtensions: Object.freeze([]),
|
|
850
|
-
checkNormalization: "sourceText",
|
|
851
|
-
nativeRowLevelSecurity: false,
|
|
852
|
-
returning: true,
|
|
853
|
-
scalarArrays: false,
|
|
854
|
-
nativeEnums: false,
|
|
855
|
-
ilike: false,
|
|
856
|
-
lateralJoin: false,
|
|
857
|
-
distinctOn: false,
|
|
858
|
-
partialIndexes: true,
|
|
859
|
-
conflictTargetConstraint: false,
|
|
860
|
-
transactionalDdl: true,
|
|
861
|
-
createManyAndReturn: true,
|
|
862
|
-
jsonColumnType: false,
|
|
863
|
-
orderByNulls: true,
|
|
864
|
-
fullTextEngines: Object.freeze(["fts5"]),
|
|
865
|
-
rowLockStrengths: Object.freeze([]),
|
|
866
|
-
rowLockWaitPolicies: Object.freeze([]),
|
|
867
|
-
advisoryLocks: false
|
|
868
|
-
});
|
|
869
|
-
var MYSQL_CAPABILITIES = Object.freeze({
|
|
870
|
-
exclusionConstraints: false,
|
|
871
|
-
exclusionExtensions: Object.freeze([]),
|
|
872
|
-
checkNormalization: "temporary",
|
|
873
|
-
nativeRowLevelSecurity: false,
|
|
874
|
-
returning: false,
|
|
875
|
-
scalarArrays: false,
|
|
876
|
-
nativeEnums: true,
|
|
877
|
-
ilike: false,
|
|
878
|
-
lateralJoin: false,
|
|
879
|
-
distinctOn: false,
|
|
880
|
-
partialIndexes: false,
|
|
881
|
-
conflictTargetConstraint: false,
|
|
882
|
-
transactionalDdl: false,
|
|
883
|
-
createManyAndReturn: false,
|
|
884
|
-
jsonColumnType: true,
|
|
885
|
-
orderByNulls: false,
|
|
886
|
-
fullTextEngines: Object.freeze(["fulltext"]),
|
|
887
|
-
rowLockStrengths: Object.freeze(["update", "share"]),
|
|
888
|
-
rowLockWaitPolicies: Object.freeze(["wait", "nowait", "skipLocked"]),
|
|
889
|
-
advisoryLocks: false
|
|
890
|
-
});
|
|
891
|
-
var DIALECT_CAPABILITIES = Object.freeze({
|
|
892
|
-
postgres: POSTGRES_CAPABILITIES,
|
|
893
|
-
sqlite: SQLITE_CAPABILITIES,
|
|
894
|
-
mysql: MYSQL_CAPABILITIES
|
|
895
|
-
});
|
|
896
|
-
|
|
897
1115
|
// packages/schema/src/validate.ts
|
|
898
1116
|
var RESERVED_ENUM_NAMES = [
|
|
899
1117
|
"sql",
|
|
@@ -913,6 +1131,11 @@ var RESERVED_ENUM_NAMES = [
|
|
|
913
1131
|
"UncertainKeys",
|
|
914
1132
|
"HasSelectedField",
|
|
915
1133
|
"Provided",
|
|
1134
|
+
"SelectionMember",
|
|
1135
|
+
"SelectionValue",
|
|
1136
|
+
"BagKeys",
|
|
1137
|
+
"IncludedRelation",
|
|
1138
|
+
"IncludedCount",
|
|
916
1139
|
"ViewDelegateMap",
|
|
917
1140
|
"extensionManifest",
|
|
918
1141
|
"extensionMounts",
|
|
@@ -948,6 +1171,7 @@ var RESERVED_ENUM_NAMES = [
|
|
|
948
1171
|
"SortOrder",
|
|
949
1172
|
"SortOrderWithNulls",
|
|
950
1173
|
"KeysetToken",
|
|
1174
|
+
"KeysetPage",
|
|
951
1175
|
"KeysetOptions",
|
|
952
1176
|
"RowLockRequest",
|
|
953
1177
|
"JsonValue",
|
|
@@ -1025,7 +1249,20 @@ var GENERATED_TYPE_NAMES = {
|
|
|
1025
1249
|
"SumAggregateInput",
|
|
1026
1250
|
"MinAggregateInput",
|
|
1027
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",
|
|
1028
1264
|
"FindManyArgs",
|
|
1265
|
+
"FindPageArgs",
|
|
1029
1266
|
"FindFirstArgs",
|
|
1030
1267
|
"FindUniqueArgs",
|
|
1031
1268
|
"CreateArgs",
|
|
@@ -1052,6 +1289,7 @@ var GENERATED_TYPE_NAMES = {
|
|
|
1052
1289
|
"AggregateSelection",
|
|
1053
1290
|
"CountOutput",
|
|
1054
1291
|
"RelationPayload",
|
|
1292
|
+
"ResolvedSelection",
|
|
1055
1293
|
"IncludePayload",
|
|
1056
1294
|
"GetPayload",
|
|
1057
1295
|
"AggregateResult",
|
|
@@ -1193,9 +1431,69 @@ function validateGeneratedNames(params) {
|
|
|
1193
1431
|
register({ name: `${declared.name}${suffix}`, owner: `enum "${declared.name}"` });
|
|
1194
1432
|
}
|
|
1195
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
|
+
}
|
|
1196
1485
|
function validateSchemaIR(params) {
|
|
1197
1486
|
const { schema } = params;
|
|
1198
1487
|
const issues = [];
|
|
1488
|
+
for (const entry of [...schema.models, ...schema.enums]) {
|
|
1489
|
+
if (entry.namespace === undefined)
|
|
1490
|
+
continue;
|
|
1491
|
+
const issue = sqlNamespaceIssue({ namespace: entry.namespace });
|
|
1492
|
+
if (issue !== undefined)
|
|
1493
|
+
issues.push({ rule: "namespace", message: issue });
|
|
1494
|
+
if (providerToDialect({ provider: schema.datasource.provider }) !== "postgres")
|
|
1495
|
+
issues.push({ rule: "namespace-dialect", message: "Explicit fixed namespaces require PostgreSQL or PGlite" });
|
|
1496
|
+
}
|
|
1199
1497
|
const modelByName = new Map;
|
|
1200
1498
|
for (const model of schema.models) {
|
|
1201
1499
|
if (modelByName.has(model.name)) {
|
|
@@ -1254,8 +1552,18 @@ function validateSchemaIR(params) {
|
|
|
1254
1552
|
});
|
|
1255
1553
|
}
|
|
1256
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";
|
|
1257
1565
|
for (const model of schema.models) {
|
|
1258
|
-
validateModel({ model, modelByName, enumByName, issues });
|
|
1566
|
+
validateModel({ model, modelByName, enumByName, issues, schema, lookups, postgres });
|
|
1259
1567
|
}
|
|
1260
1568
|
for (const model of schema.models) {
|
|
1261
1569
|
for (const relation of model.relations) {
|
|
@@ -1300,19 +1608,11 @@ function validateSchemaIR(params) {
|
|
|
1300
1608
|
}
|
|
1301
1609
|
}
|
|
1302
1610
|
for (const model of schema.models) {
|
|
1303
|
-
const fieldNames =
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1306
|
-
defaultKeys.add(model.primaryKey.fields.join("_"));
|
|
1307
|
-
}
|
|
1308
|
-
for (const index of model.indexes) {
|
|
1309
|
-
if (index.kind === "unique" && index.where === undefined && index.fields.length > 1 && index.clientName === undefined) {
|
|
1310
|
-
defaultKeys.add(index.fields.map((field) => field.name).join("_"));
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
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);
|
|
1314
1614
|
const checkSelectorName = (params) => {
|
|
1315
|
-
const { clientName,
|
|
1615
|
+
const { clientName, fields, where, isUnique, site } = params;
|
|
1316
1616
|
if (!isUnique) {
|
|
1317
1617
|
issues.push({
|
|
1318
1618
|
rule: "client-name-non-unique",
|
|
@@ -1329,7 +1629,7 @@ function validateSchemaIR(params) {
|
|
|
1329
1629
|
});
|
|
1330
1630
|
return;
|
|
1331
1631
|
}
|
|
1332
|
-
if (
|
|
1632
|
+
if (fields.length < 2) {
|
|
1333
1633
|
issues.push({
|
|
1334
1634
|
rule: "client-name-single-field",
|
|
1335
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`,
|
|
@@ -1337,7 +1637,9 @@ function validateSchemaIR(params) {
|
|
|
1337
1637
|
});
|
|
1338
1638
|
return;
|
|
1339
1639
|
}
|
|
1340
|
-
|
|
1640
|
+
const tuple = JSON.stringify(fields);
|
|
1641
|
+
const previous = seenSelectorNames.get(clientName);
|
|
1642
|
+
if (fieldNames.has(clientName) || previous !== undefined && previous !== tuple) {
|
|
1341
1643
|
issues.push({
|
|
1342
1644
|
rule: "client-name-collision",
|
|
1343
1645
|
message: `name: ("${clientName}") on "${model.name}" collides with a field or another unique selector \u2014 WhereUniqueInput keys must be unambiguous`,
|
|
@@ -1345,22 +1647,22 @@ function validateSchemaIR(params) {
|
|
|
1345
1647
|
});
|
|
1346
1648
|
return;
|
|
1347
1649
|
}
|
|
1348
|
-
seenSelectorNames.
|
|
1650
|
+
seenSelectorNames.set(clientName, tuple);
|
|
1349
1651
|
};
|
|
1350
|
-
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))) {
|
|
1351
1653
|
checkSelectorName({
|
|
1352
|
-
clientName: model.primaryKey.clientName,
|
|
1353
|
-
|
|
1654
|
+
clientName: model.primaryKey.clientName ?? model.primaryKey.fields.join("_"),
|
|
1655
|
+
fields: model.primaryKey.fields,
|
|
1354
1656
|
isUnique: true,
|
|
1355
1657
|
site: "@@id"
|
|
1356
1658
|
});
|
|
1357
1659
|
}
|
|
1358
1660
|
for (const index of model.indexes) {
|
|
1359
|
-
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))))
|
|
1360
1662
|
continue;
|
|
1361
1663
|
checkSelectorName({
|
|
1362
|
-
clientName: index.clientName,
|
|
1363
|
-
|
|
1664
|
+
clientName: index.clientName ?? index.fields.map((field) => field.name).join("_"),
|
|
1665
|
+
fields: index.fields.map((field) => field.name),
|
|
1364
1666
|
...index.where === undefined ? {} : { where: index.where },
|
|
1365
1667
|
isUnique: index.kind === "unique",
|
|
1366
1668
|
site: "@@unique"
|
|
@@ -1368,17 +1670,17 @@ function validateSchemaIR(params) {
|
|
|
1368
1670
|
}
|
|
1369
1671
|
}
|
|
1370
1672
|
validateIdentifierLengths({ schema, issues });
|
|
1371
|
-
validatePhysicalNameUniqueness({ schema, issues });
|
|
1673
|
+
validatePhysicalNameUniqueness({ schema, issues, modelByName, fieldsByModel });
|
|
1372
1674
|
validateGeneratedNames({ schema, issues });
|
|
1373
1675
|
validateNativeRls({ schema, issues });
|
|
1374
1676
|
return issues;
|
|
1375
1677
|
}
|
|
1376
1678
|
function validatePhysicalNameUniqueness(params) {
|
|
1377
|
-
const { schema, issues } = params;
|
|
1679
|
+
const { schema, issues, modelByName, fieldsByModel } = params;
|
|
1378
1680
|
const dialect = providerToDialect({ provider: schema.datasource.provider });
|
|
1379
1681
|
const owners = new Map;
|
|
1380
1682
|
const claim = (entry) => {
|
|
1381
|
-
const key =
|
|
1683
|
+
const key = JSON.stringify([entry.physicalNamespace ?? "public", entry.namespace, entry.name]);
|
|
1382
1684
|
const previous = owners.get(key);
|
|
1383
1685
|
if (previous === undefined) {
|
|
1384
1686
|
owners.set(key, `${entry.what} of ${entry.owner}`);
|
|
@@ -1396,14 +1698,16 @@ function validatePhysicalNameUniqueness(params) {
|
|
|
1396
1698
|
const constraintNamespace = (table) => dialect === "mysql" ? "constraint" : `constraint:${table}`;
|
|
1397
1699
|
for (const model of schema.models) {
|
|
1398
1700
|
const table = model.dbName ?? model.name;
|
|
1399
|
-
|
|
1701
|
+
const physicalNamespace = qualifiedSqlName({ name: table, namespace: model.namespace }).namespace;
|
|
1702
|
+
if (!claim({ namespace: "relation", physicalNamespace, name: table, what: model.isView ? "view" : "table", owner: `model "${model.name}"`, model: model.name }))
|
|
1400
1703
|
continue;
|
|
1401
1704
|
for (const fieldIR of model.fields) {
|
|
1402
|
-
claim({ namespace: `column:${table}`, name: fieldIR.dbName ?? fieldIR.name, what: "column", owner: `field "${model.name}.${fieldIR.name}"`, model: model.name, field: fieldIR.name });
|
|
1705
|
+
claim({ namespace: `column:${table}`, physicalNamespace, name: fieldIR.dbName ?? fieldIR.name, what: "column", owner: `field "${model.name}.${fieldIR.name}"`, model: model.name, field: fieldIR.name });
|
|
1403
1706
|
}
|
|
1404
1707
|
if (model.isView)
|
|
1405
1708
|
continue;
|
|
1406
|
-
const
|
|
1709
|
+
const fields = fieldsByModel.get(model);
|
|
1710
|
+
const columnOf = (name) => fields.get(name)?.dbName ?? name;
|
|
1407
1711
|
const seenIndexKeys = new Set;
|
|
1408
1712
|
const declarations = [
|
|
1409
1713
|
...model.indexes.map((index) => ({ index, site: index.kind === "unique" ? "@@unique" : "@@index" })),
|
|
@@ -1411,45 +1715,45 @@ function validatePhysicalNameUniqueness(params) {
|
|
|
1411
1715
|
];
|
|
1412
1716
|
for (const { index, site } of declarations) {
|
|
1413
1717
|
const unique = index.kind === "unique";
|
|
1414
|
-
const columns = index.fields.map((entry) =>
|
|
1415
|
-
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() ?? ""]);
|
|
1416
1720
|
if (seenIndexKeys.has(key))
|
|
1417
1721
|
continue;
|
|
1418
1722
|
seenIndexKeys.add(key);
|
|
1419
1723
|
const name = index.name ?? defaultIndexName({ table, columns: index.fields.map((entry) => columnOf(entry.name)), unique, dialect });
|
|
1420
|
-
claim({ namespace: indexNamespace(table), name, what: unique ? "unique index" : "index", owner: `${site} on "${model.name}"`, model: model.name });
|
|
1724
|
+
claim({ namespace: indexNamespace(table), physicalNamespace, name, what: unique ? "unique index" : "index", owner: `${site} on "${model.name}"`, model: model.name });
|
|
1421
1725
|
if (index.constraintName !== undefined) {
|
|
1422
|
-
claim({ namespace: constraintNamespace(table), name: index.constraintName, what: "unique constraint", owner: `${site} on "${model.name}"`, model: model.name });
|
|
1726
|
+
claim({ namespace: constraintNamespace(table), physicalNamespace, name: index.constraintName, what: "unique constraint", owner: `${site} on "${model.name}"`, model: model.name });
|
|
1423
1727
|
}
|
|
1424
1728
|
}
|
|
1425
1729
|
if (dialect === "postgres" && (model.primaryKey !== undefined || model.fields.some((fieldIR) => fieldIR.isId))) {
|
|
1426
1730
|
const name = model.primaryKeyConstraintName ?? defaultPrimaryKeyName({ table, dialect });
|
|
1427
|
-
claim({ namespace: "relation", name, what: "primary key", owner: `model "${model.name}"`, model: model.name });
|
|
1428
|
-
claim({ namespace: constraintNamespace(table), name, what: "primary key", owner: `model "${model.name}"`, model: model.name });
|
|
1731
|
+
claim({ namespace: "relation", physicalNamespace, name, what: "primary key", owner: `model "${model.name}"`, model: model.name });
|
|
1732
|
+
claim({ namespace: constraintNamespace(table), physicalNamespace, name, what: "primary key", owner: `model "${model.name}"`, model: model.name });
|
|
1429
1733
|
}
|
|
1430
1734
|
for (const relation of model.relations) {
|
|
1431
1735
|
if (relation.fields === undefined || relation.fields.length === 0)
|
|
1432
1736
|
continue;
|
|
1433
|
-
const target =
|
|
1737
|
+
const target = modelByName.get(relation.targetModel);
|
|
1434
1738
|
if (target !== undefined && target.isView)
|
|
1435
1739
|
continue;
|
|
1436
1740
|
const name = relation.constraintName ?? defaultForeignKeyName({ table, columns: relation.fields.map((entry) => columnOf(entry)), dialect });
|
|
1437
|
-
claim({ namespace: constraintNamespace(table), name, what: "foreign key", owner: `relation "${model.name}.${relation.name}"`, model: model.name });
|
|
1741
|
+
claim({ namespace: constraintNamespace(table), physicalNamespace, name, what: "foreign key", owner: `relation "${model.name}.${relation.name}"`, model: model.name });
|
|
1438
1742
|
}
|
|
1439
1743
|
for (const checkIR of model.checks ?? []) {
|
|
1440
|
-
claim({ namespace: constraintNamespace(table), name: checkIR.name, what: "CHECK constraint", owner: `model "${model.name}"`, model: model.name });
|
|
1744
|
+
claim({ namespace: constraintNamespace(table), physicalNamespace, name: checkIR.name, what: "CHECK constraint", owner: `model "${model.name}"`, model: model.name });
|
|
1441
1745
|
}
|
|
1442
1746
|
for (const exclusion of model.exclusions ?? []) {
|
|
1443
|
-
claim({ namespace: constraintNamespace(table), name: exclusion.name, what: "exclusion constraint", owner: `model "${model.name}"`, model: model.name });
|
|
1747
|
+
claim({ namespace: constraintNamespace(table), physicalNamespace, name: exclusion.name, what: "exclusion constraint", owner: `model "${model.name}"`, model: model.name });
|
|
1444
1748
|
if (dialect === "postgres")
|
|
1445
|
-
claim({ namespace: "relation", name: exclusion.name, what: "exclusion constraint", owner: `model "${model.name}"`, model: model.name });
|
|
1749
|
+
claim({ namespace: "relation", physicalNamespace, name: exclusion.name, what: "exclusion constraint", owner: `model "${model.name}"`, model: model.name });
|
|
1446
1750
|
}
|
|
1447
1751
|
}
|
|
1448
1752
|
if (dialect === "postgres") {
|
|
1449
1753
|
for (const enumDef of schema.enums) {
|
|
1450
1754
|
if (enumStorageOf({ enumDef }) !== "database")
|
|
1451
1755
|
continue;
|
|
1452
|
-
claim({ namespace: "relation", name: enumDef.dbName ?? enumDef.name, what: "enum type", owner: `enum "${enumDef.name}"` });
|
|
1756
|
+
claim({ namespace: "relation", physicalNamespace: qualifiedSqlName({ name: enumDef.dbName ?? enumDef.name, namespace: enumDef.namespace }).namespace, name: enumDef.dbName ?? enumDef.name, what: "enum type", owner: `enum "${enumDef.name}"` });
|
|
1453
1757
|
}
|
|
1454
1758
|
}
|
|
1455
1759
|
const seenJoinTables = new Set;
|
|
@@ -1459,17 +1763,24 @@ function validatePhysicalNameUniqueness(params) {
|
|
|
1459
1763
|
continue;
|
|
1460
1764
|
if (!isImplicitManyToManyRelation({ model, relation, models: schema.models }))
|
|
1461
1765
|
continue;
|
|
1766
|
+
const target = modelByName.get(relation.targetModel);
|
|
1767
|
+
const physicalNamespace = model.namespace ?? "public";
|
|
1768
|
+
if ((target?.namespace ?? "public") !== physicalNamespace) {
|
|
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
|
+
continue;
|
|
1771
|
+
}
|
|
1462
1772
|
const join = implicitJoinTable({ modelA: model.name, modelB: relation.targetModel, ...relation.relationName === undefined ? {} : { relationName: relation.relationName } });
|
|
1463
|
-
|
|
1773
|
+
const joinKey = JSON.stringify([physicalNamespace, join.tableName]);
|
|
1774
|
+
if (seenJoinTables.has(joinKey))
|
|
1464
1775
|
continue;
|
|
1465
|
-
seenJoinTables.add(
|
|
1776
|
+
seenJoinTables.add(joinKey);
|
|
1466
1777
|
const owner = `implicit relation "${model.name}.${relation.name}"`;
|
|
1467
|
-
claim({ namespace: "relation", name: join.tableName, what: "join table", owner, model: model.name });
|
|
1778
|
+
claim({ namespace: "relation", physicalNamespace, name: join.tableName, what: "join table", owner, model: model.name });
|
|
1468
1779
|
const derived = implicitJoinTableObjectNames({ table: join.tableName });
|
|
1469
|
-
claim({ namespace: indexNamespace(join.tableName), name: derived.uniqueIndex, what: "unique index", owner, model: model.name });
|
|
1470
|
-
claim({ namespace: indexNamespace(join.tableName), name: derived.indexB, what: "index", owner, model: model.name });
|
|
1471
|
-
claim({ namespace: constraintNamespace(join.tableName), name: derived.foreignKeyA, what: "foreign key", owner, model: model.name });
|
|
1472
|
-
claim({ namespace: constraintNamespace(join.tableName), name: derived.foreignKeyB, what: "foreign key", owner, model: model.name });
|
|
1780
|
+
claim({ namespace: indexNamespace(join.tableName), physicalNamespace, name: derived.uniqueIndex, what: "unique index", owner, model: model.name });
|
|
1781
|
+
claim({ namespace: indexNamespace(join.tableName), physicalNamespace, name: derived.indexB, what: "index", owner, model: model.name });
|
|
1782
|
+
claim({ namespace: constraintNamespace(join.tableName), physicalNamespace, name: derived.foreignKeyA, what: "foreign key", owner, model: model.name });
|
|
1783
|
+
claim({ namespace: constraintNamespace(join.tableName), physicalNamespace, name: derived.foreignKeyB, what: "foreign key", owner, model: model.name });
|
|
1473
1784
|
}
|
|
1474
1785
|
}
|
|
1475
1786
|
}
|
|
@@ -1551,7 +1862,7 @@ function validateIdentifierLengths(params) {
|
|
|
1551
1862
|
}
|
|
1552
1863
|
}
|
|
1553
1864
|
function validateModel(params) {
|
|
1554
|
-
const { model, modelByName, enumByName, issues } = params;
|
|
1865
|
+
const { model, modelByName, enumByName, issues, lookups, postgres } = params;
|
|
1555
1866
|
const memberNames = new Set;
|
|
1556
1867
|
for (const field of model.fields) {
|
|
1557
1868
|
if (memberNames.has(field.name)) {
|
|
@@ -1653,8 +1964,23 @@ function validateModel(params) {
|
|
|
1653
1964
|
}
|
|
1654
1965
|
memberNames.add(relation.name);
|
|
1655
1966
|
}
|
|
1656
|
-
const fieldNames = new Set(
|
|
1657
|
-
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
|
+
}
|
|
1658
1984
|
if (!model.isView && !hasIdField && model.primaryKey === undefined) {
|
|
1659
1985
|
issues.push({
|
|
1660
1986
|
rule: "missing-id",
|
|
@@ -1713,8 +2039,14 @@ function validateModel(params) {
|
|
|
1713
2039
|
});
|
|
1714
2040
|
}
|
|
1715
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
|
+
}
|
|
1716
2045
|
if (target !== undefined) {
|
|
1717
|
-
|
|
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);
|
|
1718
2050
|
for (const refField of relation.references) {
|
|
1719
2051
|
if (!targetFieldNames.has(refField)) {
|
|
1720
2052
|
issues.push({
|
|
@@ -1835,6 +2167,20 @@ function validatePredicateFields(params) {
|
|
|
1835
2167
|
}
|
|
1836
2168
|
function assertValidSchemaIR(params) {
|
|
1837
2169
|
const dialect = providerToDialect({ provider: params.schema.datasource.provider });
|
|
2170
|
+
assertIndexOperatorClasses({ schema: params.schema, dialect });
|
|
2171
|
+
for (const entry of [...params.schema.models, ...params.schema.enums])
|
|
2172
|
+
if (entry.namespace !== undefined)
|
|
2173
|
+
assertSqlNamespace({ namespace: entry.namespace, dialect });
|
|
2174
|
+
for (const model of params.schema.models)
|
|
2175
|
+
for (const field of model.fields) {
|
|
2176
|
+
if (!isEnumType(field.type) || field.type.namespace === undefined)
|
|
2177
|
+
continue;
|
|
2178
|
+
assertSqlNamespace({ namespace: field.type.namespace, dialect });
|
|
2179
|
+
const enumType = field.type;
|
|
2180
|
+
const declared = params.schema.enums.find((entry) => entry.name === enumType.enum || entry.dbName === enumType.enum);
|
|
2181
|
+
if (declared !== undefined && (declared.namespace ?? "public") !== enumType.namespace)
|
|
2182
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Enum reference namespace differs from its declared native type", meta: { rule: "enum-namespace", model: model.name, field: field.name } });
|
|
2183
|
+
}
|
|
1838
2184
|
for (const model of params.schema.models) {
|
|
1839
2185
|
if (model.exclusions !== undefined && !Array.isArray(model.exclusions))
|
|
1840
2186
|
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Exclusions must be an array", meta: { rule: "exclusion-definition" } });
|
|
@@ -1868,6 +2214,16 @@ function assertValidSchemaIR(params) {
|
|
|
1868
2214
|
if (dialect === "mysql" && counters.length > 1)
|
|
1869
2215
|
throw unsupportedCapability({ dialect, feature: "multiple auto-increment columns" });
|
|
1870
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
|
+
}
|
|
1871
2227
|
if (field.default?.kind === "list") {
|
|
1872
2228
|
if (dialect !== "postgres")
|
|
1873
2229
|
throw unsupportedCapability({ dialect, feature: "scalar-list defaults" });
|
|
@@ -2094,11 +2450,14 @@ export {
|
|
|
2094
2450
|
RlsContextError,
|
|
2095
2451
|
SCOPED_REFUSAL_REASONS,
|
|
2096
2452
|
SQLITE_CAPABILITIES,
|
|
2453
|
+
SQL_NAMESPACES,
|
|
2097
2454
|
ScopedQueryError,
|
|
2098
2455
|
VIBE_ERROR_CODES,
|
|
2099
2456
|
VibeError,
|
|
2100
2457
|
assertIdentifierFits,
|
|
2458
|
+
assertIndexOperatorClasses,
|
|
2101
2459
|
assertMigrationObjects,
|
|
2460
|
+
assertSqlNamespace,
|
|
2102
2461
|
assertValidSchemaIR,
|
|
2103
2462
|
buildComputedManifest,
|
|
2104
2463
|
buildExtensionRegistry,
|
|
@@ -2132,14 +2491,19 @@ export {
|
|
|
2132
2491
|
postgresGeneratedObjectName,
|
|
2133
2492
|
predicateFieldNames,
|
|
2134
2493
|
predicateKey,
|
|
2494
|
+
prepareDatabaseModuleSchema,
|
|
2135
2495
|
providerToDialect,
|
|
2496
|
+
qualifiedSqlName,
|
|
2136
2497
|
scalarListDefault,
|
|
2137
2498
|
serializeJsonDefault,
|
|
2499
|
+
sqlNamespaceIssue,
|
|
2138
2500
|
sqlObjectKey,
|
|
2501
|
+
sqlObjectNamespace,
|
|
2502
|
+
timeOfDayType,
|
|
2139
2503
|
unsupportedCapability,
|
|
2140
2504
|
validateComputedConfig,
|
|
2141
2505
|
validateRlsContextIR,
|
|
2142
2506
|
validateSchemaIR
|
|
2143
2507
|
};
|
|
2144
2508
|
|
|
2145
|
-
//# debugId=
|
|
2509
|
+
//# debugId=99BBBD6C3E58D9DD64756E2164756E21
|