@vibeorm/schema 4.1.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capabilities.d.ts +29 -0
- package/dist/check-rules.d.ts +92 -0
- package/dist/generated-columns.d.ts +39 -0
- package/dist/index-expressions.d.ts +73 -0
- package/dist/index.d.ts +15 -4
- package/dist/index.js +2260 -586
- package/dist/index.js.map +19 -12
- package/dist/ir.d.ts +31 -1
- package/dist/local-date.d.ts +21 -0
- package/dist/physical-names.d.ts +8 -1
- package/dist/predicate.d.ts +65 -13
- package/dist/referential-actions.d.ts +35 -0
- package/dist/row-rules.d.ts +198 -0
- package/dist/validator-ref.d.ts +22 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -144,202 +144,27 @@ class ReadOnlyClientError extends VibeError {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
// packages/schema/src/
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
sqlite: undefined
|
|
152
|
-
});
|
|
153
|
-
var ENCODER = new TextEncoder;
|
|
154
|
-
function identifierLength(params) {
|
|
155
|
-
const limit = IDENTIFIER_LIMITS[params.dialect];
|
|
156
|
-
return limit?.unit === "bytes" ? ENCODER.encode(params.name).length : [...params.name].length;
|
|
157
|
-
}
|
|
158
|
-
function cutIdentifier(params) {
|
|
159
|
-
let out = "";
|
|
160
|
-
let used = 0;
|
|
161
|
-
for (const char of params.name) {
|
|
162
|
-
const size = params.unit === "bytes" ? ENCODER.encode(char).length : 1;
|
|
163
|
-
if (used + size > params.length)
|
|
164
|
-
break;
|
|
165
|
-
out += char;
|
|
166
|
-
used += size;
|
|
167
|
-
}
|
|
168
|
-
return out;
|
|
169
|
-
}
|
|
170
|
-
function fitDefaultName(params) {
|
|
171
|
-
const limit = IDENTIFIER_LIMITS[params.dialect];
|
|
172
|
-
if (limit === undefined)
|
|
173
|
-
return `${params.prefix}${params.suffix}`;
|
|
174
|
-
const room = limit.max - identifierLength({ name: params.suffix, dialect: params.dialect });
|
|
175
|
-
if (identifierLength({ name: params.prefix, dialect: params.dialect }) <= room)
|
|
176
|
-
return `${params.prefix}${params.suffix}`;
|
|
177
|
-
return `${cutIdentifier({ name: params.prefix, length: room, unit: limit.unit })}${params.suffix}`;
|
|
178
|
-
}
|
|
179
|
-
function assertIdentifierFits(params) {
|
|
180
|
-
const limit = IDENTIFIER_LIMITS[params.dialect];
|
|
181
|
-
if (limit === undefined)
|
|
182
|
-
return;
|
|
183
|
-
const length = identifierLength({ name: params.name, dialect: params.dialect });
|
|
184
|
-
if (length <= limit.max)
|
|
185
|
-
return;
|
|
186
|
-
throw new VibeError({
|
|
187
|
-
code: "VIBE_SCHEMA_INVALID",
|
|
188
|
-
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}`,
|
|
189
|
-
meta: { name: params.name, dialect: params.dialect, length, max: limit.max }
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
function defaultIndexName(params) {
|
|
193
|
-
return fitDefaultName({
|
|
194
|
-
prefix: `${params.table}_${params.columns.join("_")}`,
|
|
195
|
-
suffix: params.unique ? "_key" : "_idx",
|
|
196
|
-
dialect: params.dialect
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
function defaultForeignKeyName(params) {
|
|
200
|
-
return fitDefaultName({ prefix: `${params.table}_${params.columns.join("_")}`, suffix: "_fkey", dialect: params.dialect });
|
|
201
|
-
}
|
|
202
|
-
function defaultPrimaryKeyName(params) {
|
|
203
|
-
return fitDefaultName({ prefix: params.table, suffix: "_pkey", dialect: params.dialect });
|
|
204
|
-
}
|
|
205
|
-
function implicitJoinTableObjectNames(params) {
|
|
206
|
-
return {
|
|
207
|
-
uniqueIndex: `${params.table}_AB_unique`,
|
|
208
|
-
indexB: `${params.table}_B_index`,
|
|
209
|
-
foreignKeyA: `${params.table}_A_fkey`,
|
|
210
|
-
foreignKeyB: `${params.table}_B_fkey`
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
function postgresGeneratedObjectName(params) {
|
|
214
|
-
const limit = IDENTIFIER_LIMITS.postgres;
|
|
215
|
-
const overhead = params.label.length + 2;
|
|
216
|
-
let chars1 = [...params.name1];
|
|
217
|
-
let chars2 = [...params.name2];
|
|
218
|
-
const bytes = (chars) => ENCODER.encode(chars.join("")).length;
|
|
219
|
-
while (bytes(chars1) + bytes(chars2) + overhead > limit.max) {
|
|
220
|
-
if (bytes(chars1) > bytes(chars2))
|
|
221
|
-
chars1 = chars1.slice(0, -1);
|
|
222
|
-
else
|
|
223
|
-
chars2 = chars2.slice(0, -1);
|
|
224
|
-
}
|
|
225
|
-
return `${chars1.join("")}_${chars2.join("")}_${params.label}`;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// packages/schema/src/namespaces.ts
|
|
229
|
-
var SQL_NAMESPACES = Object.freeze({
|
|
230
|
-
postgres: "public",
|
|
231
|
-
sqlite: "main",
|
|
232
|
-
private: Object.freeze(["_vibeorm_shadow", "_vibe_shadow"])
|
|
233
|
-
});
|
|
234
|
-
function sqlNamespaceIssue(params) {
|
|
235
|
-
const { namespace } = params;
|
|
236
|
-
if (typeof namespace !== "string" || namespace.trim().length === 0 || namespace.includes("\x00"))
|
|
237
|
-
return "Namespace must be a non-empty identifier without NUL bytes";
|
|
238
|
-
if (identifierLength({ name: namespace, dialect: "postgres" }) > IDENTIFIER_LIMITS.postgres.max)
|
|
239
|
-
return "PostgreSQL namespace exceeds its identifier byte limit";
|
|
240
|
-
if (namespace === "information_schema" || namespace.startsWith("pg_") || SQL_NAMESPACES.private.includes(namespace))
|
|
241
|
-
return "System and private VibeORM namespaces cannot be declared";
|
|
242
|
-
if (params.allowPublic === false && namespace === SQL_NAMESPACES.postgres)
|
|
243
|
-
return "A fixed package namespace must be non-public; omit namespace for a legacy public artifact";
|
|
244
|
-
return;
|
|
147
|
+
// packages/schema/src/local-date.ts
|
|
148
|
+
function isLocalDateField(params) {
|
|
149
|
+
const { field } = params;
|
|
150
|
+
return field.type === "String" && field.isUnsupported !== true && field.nativeType?.trim().toLowerCase() === "date";
|
|
245
151
|
}
|
|
246
|
-
function
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
152
|
+
function isCanonicalLocalDate(params) {
|
|
153
|
+
const { value } = params;
|
|
154
|
+
if (typeof value !== "string" || value.length !== 10 || !/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
155
|
+
return false;
|
|
156
|
+
const year = Number(value.slice(0, 4));
|
|
157
|
+
const month = Number(value.slice(5, 7));
|
|
158
|
+
const day = Number(value.slice(8, 10));
|
|
159
|
+
if (year === 0 || month < 1 || month > 12 || day < 1)
|
|
160
|
+
return false;
|
|
161
|
+
const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
162
|
+
const daysInMonth = month === 2 ? leapYear ? 29 : 28 : [4, 6, 9, 11].includes(month) ? 30 : 31;
|
|
163
|
+
return day <= daysInMonth;
|
|
252
164
|
}
|
|
253
|
-
function
|
|
254
|
-
return {
|
|
165
|
+
function localDateCheckName(params) {
|
|
166
|
+
return `${params.table}_${params.column}_localDate_check`;
|
|
255
167
|
}
|
|
256
|
-
// packages/schema/src/capabilities.ts
|
|
257
|
-
var POSTGRES_CAPABILITIES = Object.freeze({
|
|
258
|
-
fixedNamespaces: true,
|
|
259
|
-
externalObjects: true,
|
|
260
|
-
indexOperatorClasses: true,
|
|
261
|
-
exclusionConstraints: true,
|
|
262
|
-
exclusionExtensions: Object.freeze(["btree_gist"]),
|
|
263
|
-
checkNormalization: "rollback",
|
|
264
|
-
schemaCheckExpressions: true,
|
|
265
|
-
nativeRowLevelSecurity: true,
|
|
266
|
-
returning: true,
|
|
267
|
-
scalarArrays: true,
|
|
268
|
-
nativeEnums: true,
|
|
269
|
-
ilike: true,
|
|
270
|
-
lateralJoin: true,
|
|
271
|
-
distinctOn: true,
|
|
272
|
-
partialIndexes: true,
|
|
273
|
-
conflictTargetConstraint: true,
|
|
274
|
-
transactionalDdl: true,
|
|
275
|
-
createManyAndReturn: true,
|
|
276
|
-
jsonColumnType: true,
|
|
277
|
-
orderByNulls: true,
|
|
278
|
-
fullTextEngines: Object.freeze(["bm25", "tsvector"]),
|
|
279
|
-
rowLockStrengths: Object.freeze(["update", "noKeyUpdate", "share", "keyShare"]),
|
|
280
|
-
rowLockWaitPolicies: Object.freeze(["wait", "nowait", "skipLocked"]),
|
|
281
|
-
advisoryLocks: true,
|
|
282
|
-
timeOfDayTypes: Object.freeze(["time", "timetz"])
|
|
283
|
-
});
|
|
284
|
-
var SQLITE_CAPABILITIES = Object.freeze({
|
|
285
|
-
fixedNamespaces: false,
|
|
286
|
-
externalObjects: false,
|
|
287
|
-
indexOperatorClasses: false,
|
|
288
|
-
exclusionConstraints: false,
|
|
289
|
-
exclusionExtensions: Object.freeze([]),
|
|
290
|
-
checkNormalization: "sourceText",
|
|
291
|
-
schemaCheckExpressions: false,
|
|
292
|
-
nativeRowLevelSecurity: false,
|
|
293
|
-
returning: true,
|
|
294
|
-
scalarArrays: false,
|
|
295
|
-
nativeEnums: false,
|
|
296
|
-
ilike: false,
|
|
297
|
-
lateralJoin: false,
|
|
298
|
-
distinctOn: false,
|
|
299
|
-
partialIndexes: true,
|
|
300
|
-
conflictTargetConstraint: false,
|
|
301
|
-
transactionalDdl: true,
|
|
302
|
-
createManyAndReturn: true,
|
|
303
|
-
jsonColumnType: false,
|
|
304
|
-
orderByNulls: true,
|
|
305
|
-
fullTextEngines: Object.freeze(["fts5"]),
|
|
306
|
-
rowLockStrengths: Object.freeze([]),
|
|
307
|
-
rowLockWaitPolicies: Object.freeze([]),
|
|
308
|
-
advisoryLocks: false,
|
|
309
|
-
timeOfDayTypes: Object.freeze([])
|
|
310
|
-
});
|
|
311
|
-
var MYSQL_CAPABILITIES = Object.freeze({
|
|
312
|
-
fixedNamespaces: false,
|
|
313
|
-
externalObjects: false,
|
|
314
|
-
indexOperatorClasses: false,
|
|
315
|
-
exclusionConstraints: false,
|
|
316
|
-
exclusionExtensions: Object.freeze([]),
|
|
317
|
-
checkNormalization: "temporary",
|
|
318
|
-
schemaCheckExpressions: false,
|
|
319
|
-
nativeRowLevelSecurity: false,
|
|
320
|
-
returning: false,
|
|
321
|
-
scalarArrays: false,
|
|
322
|
-
nativeEnums: true,
|
|
323
|
-
ilike: false,
|
|
324
|
-
lateralJoin: false,
|
|
325
|
-
distinctOn: false,
|
|
326
|
-
partialIndexes: false,
|
|
327
|
-
conflictTargetConstraint: false,
|
|
328
|
-
transactionalDdl: false,
|
|
329
|
-
createManyAndReturn: false,
|
|
330
|
-
jsonColumnType: true,
|
|
331
|
-
orderByNulls: false,
|
|
332
|
-
fullTextEngines: Object.freeze(["fulltext"]),
|
|
333
|
-
rowLockStrengths: Object.freeze(["update", "share"]),
|
|
334
|
-
rowLockWaitPolicies: Object.freeze(["wait", "nowait", "skipLocked"]),
|
|
335
|
-
advisoryLocks: false,
|
|
336
|
-
timeOfDayTypes: Object.freeze(["time"])
|
|
337
|
-
});
|
|
338
|
-
var DIALECT_CAPABILITIES = Object.freeze({
|
|
339
|
-
postgres: POSTGRES_CAPABILITIES,
|
|
340
|
-
sqlite: SQLITE_CAPABILITIES,
|
|
341
|
-
mysql: MYSQL_CAPABILITIES
|
|
342
|
-
});
|
|
343
168
|
|
|
344
169
|
// packages/schema/src/ir.ts
|
|
345
170
|
var DIALECTS = ["postgres", "sqlite", "mysql"];
|
|
@@ -359,6 +184,8 @@ function isEnumType(fieldType) {
|
|
|
359
184
|
return typeof fieldType === "object" && "enum" in fieldType;
|
|
360
185
|
}
|
|
361
186
|
function fieldTypeIdentity(params) {
|
|
187
|
+
if (isLocalDateField({ field: params }))
|
|
188
|
+
return "localDate";
|
|
362
189
|
const { type } = params;
|
|
363
190
|
return isEnumType(type) ? type.namespace === undefined || type.namespace === "public" ? `enum:${type.enum}` : JSON.stringify(["enum", type.namespace, type.enum]) : type;
|
|
364
191
|
}
|
|
@@ -367,30 +194,6 @@ function enumStorageOf(params) {
|
|
|
367
194
|
return params.enumDef.storage ?? "database";
|
|
368
195
|
}
|
|
369
196
|
|
|
370
|
-
// packages/schema/src/native-rls-admission.ts
|
|
371
|
-
function assertNativeRlsAdmission(params) {
|
|
372
|
-
const { schema } = params;
|
|
373
|
-
if (schema.rls === undefined && !schema.models.some((model) => model.rls !== undefined))
|
|
374
|
-
return;
|
|
375
|
-
const dialect = params.dialect ?? providerToDialect({ provider: schema.datasource.provider });
|
|
376
|
-
if (!DIALECT_CAPABILITIES[dialect].nativeRowLevelSecurity) {
|
|
377
|
-
throw unsupportedCapability({ dialect, feature: "nativeRowLevelSecurity" });
|
|
378
|
-
}
|
|
379
|
-
for (const model of schema.models) {
|
|
380
|
-
const rule = model.rls;
|
|
381
|
-
if (rule === undefined || rule === "unprotected")
|
|
382
|
-
continue;
|
|
383
|
-
const parentName = rule !== null && typeof rule === "object" && "through" in rule ? model.relations.find((relation) => relation.name === rule.through)?.targetModel : undefined;
|
|
384
|
-
const parent = parentName === undefined ? undefined : schema.models.find((entry) => entry.name === parentName);
|
|
385
|
-
if (model.namespace !== undefined && model.namespace !== "public" || parent?.namespace !== undefined && parent.namespace !== "public") {
|
|
386
|
-
throw unsupportedCapability({
|
|
387
|
-
dialect,
|
|
388
|
-
feature: "fixedNamespaceNativeRls",
|
|
389
|
-
hint: "Native RLS protects public tables only, including deny-protected models and through-policy parents"
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
197
|
// packages/schema/src/reserved.ts
|
|
395
198
|
var RESERVED_DELEGATE_NAMES = [
|
|
396
199
|
"findMany",
|
|
@@ -418,6 +221,7 @@ var RESERVED_CLIENT_NAMES = [
|
|
|
418
221
|
"$connect",
|
|
419
222
|
"$disconnect",
|
|
420
223
|
"$transaction",
|
|
224
|
+
"$all",
|
|
421
225
|
"$querySql",
|
|
422
226
|
"$querySqlResult",
|
|
423
227
|
"$executeSql",
|
|
@@ -512,6 +316,12 @@ function buildExtensionRegistry(params) {
|
|
|
512
316
|
}
|
|
513
317
|
byName.set(extension.name, extension);
|
|
514
318
|
for (const attribute of extension.schema?.modelAttributes ?? []) {
|
|
319
|
+
if (["appendOnly", "fixed", "rowFixed", "frozen"].includes(attribute.name))
|
|
320
|
+
throw new VibeError({
|
|
321
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
322
|
+
message: `Model attribute @@${attribute.name} is reserved for core row rules; choose a different extension attribute name`,
|
|
323
|
+
meta: { rule: "reserved-row-rule-attribute", attribute: attribute.name, extension: extension.name }
|
|
324
|
+
});
|
|
515
325
|
const existing = modelAttributes.get(attribute.name);
|
|
516
326
|
if (existing !== undefined) {
|
|
517
327
|
throw new VibeError({
|
|
@@ -584,57 +394,1743 @@ function extensionDeclarations(params) {
|
|
|
584
394
|
return models;
|
|
585
395
|
}
|
|
586
396
|
|
|
587
|
-
// packages/schema/src/
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
397
|
+
// packages/schema/src/capabilities.ts
|
|
398
|
+
var scalarUsages = Object.freeze(["check", "indexPredicate", "exclusionPredicate", "indexExpression"]);
|
|
399
|
+
var booleanUsages = Object.freeze(["check", "indexPredicate", "exclusionPredicate"]);
|
|
400
|
+
var checkUsage = Object.freeze(["check"]);
|
|
401
|
+
var noUsages = Object.freeze([]);
|
|
402
|
+
function predicateExpressions(params) {
|
|
403
|
+
return Object.freeze({
|
|
404
|
+
field: scalarUsages,
|
|
405
|
+
value: scalarUsages,
|
|
406
|
+
compare: booleanUsages,
|
|
407
|
+
inList: booleanUsages,
|
|
408
|
+
isNull: booleanUsages,
|
|
409
|
+
and: booleanUsages,
|
|
410
|
+
or: booleanUsages,
|
|
411
|
+
not: booleanUsages,
|
|
412
|
+
jsonText: params.legacy,
|
|
413
|
+
length: params.legacy,
|
|
414
|
+
isTrue: params.legacy,
|
|
415
|
+
add: scalarUsages,
|
|
416
|
+
sub: scalarUsages,
|
|
417
|
+
mul: scalarUsages,
|
|
418
|
+
lower: scalarUsages,
|
|
419
|
+
upper: scalarUsages
|
|
596
420
|
});
|
|
597
|
-
return protectedModel || throughParent || hasPublicExtensionDeclaration({ schema, model: model.name }) ? SQL_NAMESPACES.postgres : undefined;
|
|
598
|
-
}
|
|
599
|
-
// packages/schema/src/migration-objects.ts
|
|
600
|
-
function sqlObjectKey(params) {
|
|
601
|
-
const object = params.identity;
|
|
602
|
-
switch (object.kind) {
|
|
603
|
-
case "namespace":
|
|
604
|
-
return JSON.stringify([object.kind, object.name]);
|
|
605
|
-
case "table":
|
|
606
|
-
case "view":
|
|
607
|
-
case "sequence":
|
|
608
|
-
case "postgresExtension":
|
|
609
|
-
case "enum":
|
|
610
|
-
return JSON.stringify([object.kind, object.name.namespace, object.name.name]);
|
|
611
|
-
case "function":
|
|
612
|
-
return JSON.stringify([object.kind, object.name.namespace, object.name.name, object.argumentTypes.map((type) => [type.namespace, type.name])]);
|
|
613
|
-
case "index":
|
|
614
|
-
case "trigger":
|
|
615
|
-
return JSON.stringify([object.kind, object.table.namespace, object.table.name, object.name]);
|
|
616
|
-
case "constraint":
|
|
617
|
-
return JSON.stringify([object.kind, object.table.namespace, object.table.name, object.name, object.constraintType]);
|
|
618
|
-
case "column":
|
|
619
|
-
case "columnDefault":
|
|
620
|
-
case "generatedColumn":
|
|
621
|
-
return JSON.stringify([object.kind, object.table.namespace, object.table.name, object.column]);
|
|
622
|
-
}
|
|
623
421
|
}
|
|
624
|
-
|
|
422
|
+
var POSTGRES_CAPABILITIES = Object.freeze({
|
|
423
|
+
rowRuleAppendOnly: true,
|
|
424
|
+
rowRuleFixed: true,
|
|
425
|
+
rowRuleRowFixed: true,
|
|
426
|
+
rowRuleFrozen: true,
|
|
427
|
+
fixedNamespaces: true,
|
|
428
|
+
externalObjects: true,
|
|
429
|
+
indexOperatorClasses: true,
|
|
430
|
+
exclusionConstraints: true,
|
|
431
|
+
exclusionExtensions: Object.freeze(["btree_gist"]),
|
|
432
|
+
checkNormalization: "rollback",
|
|
433
|
+
expressionIndexes: true,
|
|
434
|
+
generatedColumns: true,
|
|
435
|
+
generatedColumnNormalization: "rollback",
|
|
436
|
+
schemaCheckExpressions: true,
|
|
437
|
+
predicateExpressions: predicateExpressions({ legacy: checkUsage }),
|
|
438
|
+
predicateArithmeticTypes: Object.freeze(["Int", "BigInt", "Float", "Decimal"]),
|
|
439
|
+
nativeRowLevelSecurity: true,
|
|
440
|
+
returning: true,
|
|
441
|
+
scalarArrays: true,
|
|
442
|
+
nativeEnums: true,
|
|
443
|
+
ilike: true,
|
|
444
|
+
lateralJoin: true,
|
|
445
|
+
distinctOn: true,
|
|
446
|
+
partialIndexes: true,
|
|
447
|
+
conflictTargetConstraint: true,
|
|
448
|
+
transactionalDdl: true,
|
|
449
|
+
createManyAndReturn: true,
|
|
450
|
+
jsonColumnType: true,
|
|
451
|
+
orderByNulls: true,
|
|
452
|
+
fullTextEngines: Object.freeze(["bm25", "tsvector"]),
|
|
453
|
+
rowLockStrengths: Object.freeze(["update", "noKeyUpdate", "share", "keyShare"]),
|
|
454
|
+
rowLockWaitPolicies: Object.freeze(["wait", "nowait", "skipLocked"]),
|
|
455
|
+
advisoryLocks: true,
|
|
456
|
+
timeOfDayTypes: Object.freeze(["time", "timetz"]),
|
|
457
|
+
uuidColumns: "native",
|
|
458
|
+
localDateColumns: true
|
|
459
|
+
});
|
|
460
|
+
var SQLITE_CAPABILITIES = Object.freeze({
|
|
461
|
+
rowRuleAppendOnly: true,
|
|
462
|
+
rowRuleFixed: true,
|
|
463
|
+
rowRuleRowFixed: true,
|
|
464
|
+
rowRuleFrozen: true,
|
|
465
|
+
fixedNamespaces: false,
|
|
466
|
+
externalObjects: false,
|
|
467
|
+
indexOperatorClasses: false,
|
|
468
|
+
exclusionConstraints: false,
|
|
469
|
+
exclusionExtensions: Object.freeze([]),
|
|
470
|
+
checkNormalization: "sourceText",
|
|
471
|
+
expressionIndexes: true,
|
|
472
|
+
generatedColumns: true,
|
|
473
|
+
generatedColumnNormalization: "sourceText",
|
|
474
|
+
schemaCheckExpressions: false,
|
|
475
|
+
predicateExpressions: predicateExpressions({ legacy: noUsages }),
|
|
476
|
+
predicateArithmeticTypes: Object.freeze(["Int", "BigInt", "Float"]),
|
|
477
|
+
nativeRowLevelSecurity: false,
|
|
478
|
+
returning: true,
|
|
479
|
+
scalarArrays: false,
|
|
480
|
+
nativeEnums: false,
|
|
481
|
+
ilike: false,
|
|
482
|
+
lateralJoin: false,
|
|
483
|
+
distinctOn: false,
|
|
484
|
+
partialIndexes: true,
|
|
485
|
+
conflictTargetConstraint: false,
|
|
486
|
+
transactionalDdl: true,
|
|
487
|
+
createManyAndReturn: true,
|
|
488
|
+
jsonColumnType: false,
|
|
489
|
+
orderByNulls: true,
|
|
490
|
+
fullTextEngines: Object.freeze(["fts5"]),
|
|
491
|
+
rowLockStrengths: Object.freeze([]),
|
|
492
|
+
rowLockWaitPolicies: Object.freeze([]),
|
|
493
|
+
advisoryLocks: false,
|
|
494
|
+
timeOfDayTypes: Object.freeze([]),
|
|
495
|
+
uuidColumns: "text",
|
|
496
|
+
localDateColumns: true
|
|
497
|
+
});
|
|
498
|
+
var MYSQL_CAPABILITIES = Object.freeze({
|
|
499
|
+
rowRuleAppendOnly: true,
|
|
500
|
+
rowRuleFixed: true,
|
|
501
|
+
rowRuleRowFixed: true,
|
|
502
|
+
rowRuleFrozen: true,
|
|
503
|
+
fixedNamespaces: false,
|
|
504
|
+
externalObjects: false,
|
|
505
|
+
indexOperatorClasses: false,
|
|
506
|
+
exclusionConstraints: false,
|
|
507
|
+
exclusionExtensions: Object.freeze([]),
|
|
508
|
+
checkNormalization: "temporary",
|
|
509
|
+
expressionIndexes: true,
|
|
510
|
+
generatedColumns: true,
|
|
511
|
+
generatedColumnNormalization: "temporary",
|
|
512
|
+
schemaCheckExpressions: false,
|
|
513
|
+
predicateExpressions: predicateExpressions({ legacy: noUsages }),
|
|
514
|
+
predicateArithmeticTypes: Object.freeze(["Int", "BigInt", "Float", "Decimal"]),
|
|
515
|
+
nativeRowLevelSecurity: false,
|
|
516
|
+
returning: false,
|
|
517
|
+
scalarArrays: false,
|
|
518
|
+
nativeEnums: true,
|
|
519
|
+
ilike: false,
|
|
520
|
+
lateralJoin: false,
|
|
521
|
+
distinctOn: false,
|
|
522
|
+
partialIndexes: false,
|
|
523
|
+
conflictTargetConstraint: false,
|
|
524
|
+
transactionalDdl: false,
|
|
525
|
+
createManyAndReturn: false,
|
|
526
|
+
jsonColumnType: true,
|
|
527
|
+
orderByNulls: false,
|
|
528
|
+
fullTextEngines: Object.freeze(["fulltext"]),
|
|
529
|
+
rowLockStrengths: Object.freeze(["update", "share"]),
|
|
530
|
+
rowLockWaitPolicies: Object.freeze(["wait", "nowait", "skipLocked"]),
|
|
531
|
+
advisoryLocks: false,
|
|
532
|
+
timeOfDayTypes: Object.freeze(["time"]),
|
|
533
|
+
uuidColumns: "unsupported",
|
|
534
|
+
localDateColumns: true
|
|
535
|
+
});
|
|
536
|
+
var DIALECT_CAPABILITIES = Object.freeze({
|
|
537
|
+
postgres: POSTGRES_CAPABILITIES,
|
|
538
|
+
sqlite: SQLITE_CAPABILITIES,
|
|
539
|
+
mysql: MYSQL_CAPABILITIES
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// packages/schema/src/predicate.ts
|
|
543
|
+
var PREDICATE_COMPARISON_OPS = [
|
|
544
|
+
"eq",
|
|
545
|
+
"ne",
|
|
546
|
+
"gt",
|
|
547
|
+
"gte",
|
|
548
|
+
"lt",
|
|
549
|
+
"lte"
|
|
550
|
+
];
|
|
551
|
+
function isPredicateFieldRef(operand) {
|
|
552
|
+
return operand.kind === "field";
|
|
553
|
+
}
|
|
554
|
+
function predicateFieldNames(params) {
|
|
555
|
+
return [...new Set(inspectPredicate(params).expressions.flatMap((expression) => expression.kind === "field" ? [expression.name] : []))];
|
|
556
|
+
}
|
|
557
|
+
function inspectPredicate(params) {
|
|
558
|
+
return inspectInput(params);
|
|
559
|
+
}
|
|
560
|
+
function inspectPredicateExpression(params) {
|
|
561
|
+
return inspectInput(params);
|
|
562
|
+
}
|
|
563
|
+
function inspectInput(params) {
|
|
564
|
+
if (params.predicate === undefined === (params.expression === undefined))
|
|
565
|
+
throw invalidOperand();
|
|
566
|
+
const expressions = [];
|
|
567
|
+
const kinds = new Set;
|
|
568
|
+
const object = (node) => {
|
|
569
|
+
if (typeof node !== "object" || node === null || Array.isArray(node))
|
|
570
|
+
throw invalidOperand();
|
|
571
|
+
};
|
|
572
|
+
const value = (node) => {
|
|
573
|
+
object(node);
|
|
574
|
+
if (node.kind !== "value" || !["string", "number", "boolean"].includes(typeof node.value))
|
|
575
|
+
throw invalidOperand();
|
|
576
|
+
kinds.add("value");
|
|
577
|
+
};
|
|
578
|
+
const operand = (node) => {
|
|
579
|
+
object(node);
|
|
580
|
+
if (node.kind === "value")
|
|
581
|
+
value(node);
|
|
582
|
+
else
|
|
583
|
+
expression(node);
|
|
584
|
+
};
|
|
585
|
+
const field = (node) => {
|
|
586
|
+
object(node);
|
|
587
|
+
if (node.kind !== "field")
|
|
588
|
+
throw invalidOperand();
|
|
589
|
+
expression(node);
|
|
590
|
+
};
|
|
591
|
+
const expression = (node) => {
|
|
592
|
+
object(node);
|
|
593
|
+
switch (node.kind) {
|
|
594
|
+
case "field":
|
|
595
|
+
if (typeof node.name !== "string")
|
|
596
|
+
throw invalidOperand();
|
|
597
|
+
expressions.push(node);
|
|
598
|
+
kinds.add(node.kind);
|
|
599
|
+
return;
|
|
600
|
+
case "jsonText":
|
|
601
|
+
expressions.push(node);
|
|
602
|
+
kinds.add(node.kind);
|
|
603
|
+
field(node.field);
|
|
604
|
+
return;
|
|
605
|
+
case "length":
|
|
606
|
+
case "lower":
|
|
607
|
+
case "upper":
|
|
608
|
+
expressions.push(node);
|
|
609
|
+
kinds.add(node.kind);
|
|
610
|
+
expression(node.expression);
|
|
611
|
+
return;
|
|
612
|
+
case "add":
|
|
613
|
+
case "sub":
|
|
614
|
+
case "mul":
|
|
615
|
+
expressions.push(node);
|
|
616
|
+
kinds.add(node.kind);
|
|
617
|
+
expression(node.left);
|
|
618
|
+
operand(node.right);
|
|
619
|
+
return;
|
|
620
|
+
default:
|
|
621
|
+
throw invalidOperand();
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
const walk = (node) => {
|
|
625
|
+
object(node);
|
|
626
|
+
switch (node.kind) {
|
|
627
|
+
case "compare":
|
|
628
|
+
if (!PREDICATE_COMPARISON_OPS.includes(node.op))
|
|
629
|
+
throw invalidOperand();
|
|
630
|
+
kinds.add(node.kind);
|
|
631
|
+
expression(node.left);
|
|
632
|
+
operand(node.right);
|
|
633
|
+
return;
|
|
634
|
+
case "inList":
|
|
635
|
+
if (!Array.isArray(node.values) || node.negate !== undefined && node.negate !== true)
|
|
636
|
+
throw invalidOperand();
|
|
637
|
+
kinds.add(node.kind);
|
|
638
|
+
field(node.field);
|
|
639
|
+
for (const entry of node.values)
|
|
640
|
+
value(entry);
|
|
641
|
+
return;
|
|
642
|
+
case "isNull":
|
|
643
|
+
if (node.negate !== undefined && node.negate !== true)
|
|
644
|
+
throw invalidOperand();
|
|
645
|
+
kinds.add(node.kind);
|
|
646
|
+
field(node.field);
|
|
647
|
+
return;
|
|
648
|
+
case "and":
|
|
649
|
+
case "or":
|
|
650
|
+
if (!Array.isArray(node.operands))
|
|
651
|
+
throw invalidOperand();
|
|
652
|
+
kinds.add(node.kind);
|
|
653
|
+
for (const child of node.operands)
|
|
654
|
+
walk(child);
|
|
655
|
+
return;
|
|
656
|
+
case "not":
|
|
657
|
+
case "isTrue":
|
|
658
|
+
kinds.add(node.kind);
|
|
659
|
+
walk(node.operand);
|
|
660
|
+
return;
|
|
661
|
+
default:
|
|
662
|
+
throw invalidOperand();
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
if (params.predicate !== undefined)
|
|
666
|
+
walk(params.predicate);
|
|
667
|
+
else
|
|
668
|
+
expression(params.expression);
|
|
669
|
+
return { expressions, nodeKinds: [...kinds] };
|
|
670
|
+
}
|
|
671
|
+
function invalidOperand() {
|
|
672
|
+
return new VibeError({
|
|
673
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
674
|
+
message: "A schema predicate requires valid Boolean nodes, scalar expression children and JSON-safe literals only on operand right sides",
|
|
675
|
+
meta: { rule: "predicate-operand-invalid" }
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
var POSTGRES_PREDICATE_NATIVE_TYPES = Object.freeze({
|
|
679
|
+
String: Object.freeze(["text", "varchar", "charactervarying", "char", "character", "bpchar", "citext"]),
|
|
680
|
+
Int: Object.freeze(["integer", "int", "int4", "smallint", "int2"]),
|
|
681
|
+
BigInt: Object.freeze(["bigint", "int8"]),
|
|
682
|
+
Float: Object.freeze(["real", "float4", "doubleprecision", "float8"]),
|
|
683
|
+
Decimal: Object.freeze(["decimal", "numeric"])
|
|
684
|
+
});
|
|
685
|
+
function postgresNativeOperandIssue(params) {
|
|
686
|
+
for (const expression of params.expressions) {
|
|
687
|
+
let operands;
|
|
688
|
+
switch (expression.kind) {
|
|
689
|
+
case "lower":
|
|
690
|
+
case "upper":
|
|
691
|
+
operands = [expression.expression];
|
|
692
|
+
break;
|
|
693
|
+
case "add":
|
|
694
|
+
case "sub":
|
|
695
|
+
case "mul":
|
|
696
|
+
operands = [expression.left, expression.right];
|
|
697
|
+
break;
|
|
698
|
+
default:
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
for (const operand of operands) {
|
|
702
|
+
if (operand.kind !== "field")
|
|
703
|
+
continue;
|
|
704
|
+
const field = params.model.fields.find((candidate) => candidate.name === operand.name);
|
|
705
|
+
if (field === undefined || field.nativeType === undefined || field.isList || field.isUnsupported === true || isLocalDateField({ field }))
|
|
706
|
+
continue;
|
|
707
|
+
const text = expression.kind === "lower" || expression.kind === "upper";
|
|
708
|
+
const numeric = field.type === "Int" || field.type === "BigInt" || field.type === "Float" || field.type === "Decimal";
|
|
709
|
+
const family = text ? field.type === "String" ? "String" : undefined : numeric ? field.type : undefined;
|
|
710
|
+
if (family === undefined)
|
|
711
|
+
continue;
|
|
712
|
+
const base = field.nativeType.trim().toLowerCase().replace(/\(.*$/s, "").replace(/[\s_]+/g, "");
|
|
713
|
+
if (POSTGRES_PREDICATE_NATIVE_TYPES[family].includes(base))
|
|
714
|
+
continue;
|
|
715
|
+
return {
|
|
716
|
+
node: expression.kind,
|
|
717
|
+
feature: `predicateNativeTypes.${family}.${base}`,
|
|
718
|
+
hint: `${params.model.name}.${field.name} is PostgreSQL ${field.nativeType.trim()}, which has no ${text ? `${expression.kind}()` : `same-family ${expression.kind} operator`}; use a standard ${family} native type or reviewed raw SQL`
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
function predicateCapabilityIssue(params) {
|
|
725
|
+
const inspection = inspectInput(params);
|
|
726
|
+
const capabilities = DIALECT_CAPABILITIES[params.dialect];
|
|
727
|
+
for (const node of inspection.nodeKinds) {
|
|
728
|
+
if (capabilities.predicateExpressions[node].includes(params.usage))
|
|
729
|
+
continue;
|
|
730
|
+
const legacy = node === "jsonText" || node === "length" || node === "isTrue";
|
|
731
|
+
return { node, feature: legacy ? params.usage === "check" ? "schemaCheckExpressions" : "schema CHECK expressions outside CHECK constraints" : `predicateExpressions.${node}.${params.usage}` };
|
|
732
|
+
}
|
|
733
|
+
if (params.model === undefined)
|
|
734
|
+
return;
|
|
735
|
+
if (params.dialect === "postgres") {
|
|
736
|
+
const native = postgresNativeOperandIssue({ expressions: inspection.expressions, model: params.model });
|
|
737
|
+
if (native !== undefined)
|
|
738
|
+
return native;
|
|
739
|
+
}
|
|
740
|
+
const type = (expression) => {
|
|
741
|
+
switch (expression.kind) {
|
|
742
|
+
case "field": {
|
|
743
|
+
const field = params.model?.fields.find((candidate) => candidate.name === expression.name);
|
|
744
|
+
return field === undefined || field.isList || field.isUnsupported ? undefined : field.type;
|
|
745
|
+
}
|
|
746
|
+
case "jsonText":
|
|
747
|
+
case "lower":
|
|
748
|
+
case "upper":
|
|
749
|
+
return "String";
|
|
750
|
+
case "length":
|
|
751
|
+
return "Int";
|
|
752
|
+
case "add":
|
|
753
|
+
case "sub":
|
|
754
|
+
case "mul": {
|
|
755
|
+
const left = type(expression.left);
|
|
756
|
+
if (left !== "Int" && left !== "BigInt" && left !== "Float" && left !== "Decimal")
|
|
757
|
+
return;
|
|
758
|
+
return expression.right.kind === "value" || type(expression.right) === left ? left : undefined;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
for (const expression of inspection.expressions) {
|
|
763
|
+
if (expression.kind !== "add" && expression.kind !== "sub" && expression.kind !== "mul")
|
|
764
|
+
continue;
|
|
765
|
+
const family = type(expression);
|
|
766
|
+
if (family === "Int" || family === "BigInt" || family === "Float" || family === "Decimal") {
|
|
767
|
+
if (!capabilities.predicateArithmeticTypes.includes(family))
|
|
768
|
+
return { node: expression.kind, feature: `predicateArithmeticTypes.${family}` };
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
function assertPredicateCapabilities(params) {
|
|
774
|
+
const issue = predicateCapabilityIssue(params);
|
|
775
|
+
if (issue !== undefined)
|
|
776
|
+
throw unsupportedCapability({ dialect: params.dialect, feature: issue.feature, ...issue.hint === undefined ? {} : { hint: issue.hint } });
|
|
777
|
+
}
|
|
778
|
+
function predicateKey(params) {
|
|
779
|
+
const structuredKinds = ["jsonText", "length", "isTrue", "add", "sub", "mul", "lower", "upper"];
|
|
780
|
+
if (inspectPredicate(params).nodeKinds.some((kind) => structuredKinds.includes(kind))) {
|
|
781
|
+
const scalar = (node) => {
|
|
782
|
+
switch (node.kind) {
|
|
783
|
+
case "field":
|
|
784
|
+
return ["field", node.name];
|
|
785
|
+
case "jsonText":
|
|
786
|
+
return ["jsonText", node.field.name, node.path];
|
|
787
|
+
case "length":
|
|
788
|
+
case "lower":
|
|
789
|
+
case "upper":
|
|
790
|
+
return [node.kind, scalar(node.expression)];
|
|
791
|
+
case "add":
|
|
792
|
+
case "sub":
|
|
793
|
+
case "mul":
|
|
794
|
+
return [node.kind, scalar(node.left), node.right.kind === "value" ? ["value", node.right.value] : scalar(node.right)];
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
const key = (node) => {
|
|
798
|
+
switch (node.kind) {
|
|
799
|
+
case "compare":
|
|
800
|
+
return [node.kind, node.op, scalar(node.left), node.right.kind === "value" ? ["value", node.right.value] : scalar(node.right)];
|
|
801
|
+
case "inList":
|
|
802
|
+
return [node.kind, node.field.name, node.negate === true, node.values.map((value) => value.value)];
|
|
803
|
+
case "isNull":
|
|
804
|
+
return [node.kind, node.field.name, node.negate === true];
|
|
805
|
+
case "and":
|
|
806
|
+
case "or":
|
|
807
|
+
return [node.kind, node.operands.map(key)];
|
|
808
|
+
case "not":
|
|
809
|
+
case "isTrue":
|
|
810
|
+
return [node.kind, key(node.operand)];
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
return `check:${JSON.stringify(key(params.predicate))}`;
|
|
814
|
+
}
|
|
815
|
+
const expression = (node) => {
|
|
816
|
+
switch (node.kind) {
|
|
817
|
+
case "field":
|
|
818
|
+
return `f:${node.name}`;
|
|
819
|
+
case "jsonText":
|
|
820
|
+
return `jsonText(${JSON.stringify([node.field.name, node.path])})`;
|
|
821
|
+
case "length":
|
|
822
|
+
case "lower":
|
|
823
|
+
case "upper":
|
|
824
|
+
return `${node.kind}(${expression(node.expression)})`;
|
|
825
|
+
case "add":
|
|
826
|
+
case "sub":
|
|
827
|
+
case "mul":
|
|
828
|
+
return `${node.kind}(${expression(node.left)},${node.right.kind === "value" ? `v:${typeof node.right.value}:${String(node.right.value)}` : expression(node.right)})`;
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
const encode = (node) => {
|
|
832
|
+
switch (node.kind) {
|
|
833
|
+
case "compare":
|
|
834
|
+
return `cmp(${node.op},${node.left.kind === "field" ? node.left.name : expression(node.left)},${node.right.kind === "value" ? `v:${typeof node.right.value}:${String(node.right.value)}` : expression(node.right)})`;
|
|
835
|
+
case "inList":
|
|
836
|
+
return `in${node.negate === true ? "!" : ""}(${node.field.name},[${node.values.map((entry) => `${typeof entry.value}:${String(entry.value)}`).join("|")}])`;
|
|
837
|
+
case "isNull":
|
|
838
|
+
return `null${node.negate === true ? "!" : ""}(${node.field.name})`;
|
|
839
|
+
case "and":
|
|
840
|
+
case "or":
|
|
841
|
+
return `${node.kind}(${node.operands.map(encode).join(",")})`;
|
|
842
|
+
case "not":
|
|
843
|
+
return `not(${encode(node.operand)})`;
|
|
844
|
+
case "isTrue":
|
|
845
|
+
return `isTrue(${encode(node.operand)})`;
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
return encode(params.predicate);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// packages/schema/src/prisma-literal.ts
|
|
852
|
+
var STRING_ESCAPES = {
|
|
853
|
+
'"': '"',
|
|
854
|
+
"'": "'",
|
|
855
|
+
"\\": "\\",
|
|
856
|
+
"/": "/",
|
|
857
|
+
n: `
|
|
858
|
+
`,
|
|
859
|
+
r: "\r",
|
|
860
|
+
t: "\t",
|
|
861
|
+
b: "\b",
|
|
862
|
+
f: "\f"
|
|
863
|
+
};
|
|
864
|
+
function malformed(params) {
|
|
865
|
+
return new VibeError({
|
|
866
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
867
|
+
message: `Malformed string literal ${JSON.stringify(params.raw)}: ${params.detail}`,
|
|
868
|
+
meta: { issues: [{ rule: "malformed-string-literal", message: params.detail }], literal: params.raw }
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
function decodePrismaString(params) {
|
|
872
|
+
const { raw } = params;
|
|
873
|
+
const quoted = raw.length >= 2 && raw.startsWith('"') && raw.endsWith('"');
|
|
874
|
+
const body = quoted ? raw.slice(1, -1) : raw;
|
|
875
|
+
if (!body.includes("\\"))
|
|
876
|
+
return body;
|
|
877
|
+
let out = "";
|
|
878
|
+
for (let index = 0;index < body.length; index += 1) {
|
|
879
|
+
const char = body[index];
|
|
880
|
+
if (char !== "\\") {
|
|
881
|
+
out += char;
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
const next = body[index + 1];
|
|
885
|
+
if (next === undefined)
|
|
886
|
+
throw malformed({ raw, detail: "it ends with a lone backslash" });
|
|
887
|
+
if (next === "u") {
|
|
888
|
+
const digits = body.slice(index + 2, index + 6);
|
|
889
|
+
if (!/^[0-9a-fA-F]{4}$/.test(digits)) {
|
|
890
|
+
throw malformed({ raw, detail: `\\u must be followed by four hex digits, found "${digits}"` });
|
|
891
|
+
}
|
|
892
|
+
out += String.fromCharCode(Number.parseInt(digits, 16));
|
|
893
|
+
index += 5;
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
const decoded = STRING_ESCAPES[next];
|
|
897
|
+
if (decoded === undefined)
|
|
898
|
+
throw malformed({ raw, detail: `unknown escape "\\${next}"` });
|
|
899
|
+
out += decoded;
|
|
900
|
+
index += 1;
|
|
901
|
+
}
|
|
902
|
+
return out;
|
|
903
|
+
}
|
|
904
|
+
function unicodeEscape(params) {
|
|
905
|
+
return `\\u${params.char.charCodeAt(0).toString(16).padStart(4, "0")}`;
|
|
906
|
+
}
|
|
907
|
+
function encodePrismaString(params) {
|
|
908
|
+
const { value } = params;
|
|
909
|
+
let out = '"';
|
|
910
|
+
for (const char of value) {
|
|
911
|
+
if (char === "\\")
|
|
912
|
+
out += "\\\\";
|
|
913
|
+
else if (char === '"')
|
|
914
|
+
out += "\\\"";
|
|
915
|
+
else if (char === `
|
|
916
|
+
`)
|
|
917
|
+
out += "\\n";
|
|
918
|
+
else if (char === "\r")
|
|
919
|
+
out += "\\r";
|
|
920
|
+
else if (char === "\t")
|
|
921
|
+
out += "\\t";
|
|
922
|
+
else if (char < " " || char === "\x7F")
|
|
923
|
+
out += unicodeEscape({ char });
|
|
924
|
+
else
|
|
925
|
+
out += char;
|
|
926
|
+
}
|
|
927
|
+
return `${out}"`;
|
|
928
|
+
}
|
|
929
|
+
var INTEGER_LITERAL = /^-?\d+$/;
|
|
930
|
+
function exactIntegerLiteral(params) {
|
|
931
|
+
const { text } = params;
|
|
932
|
+
if (!INTEGER_LITERAL.test(text)) {
|
|
933
|
+
throw new VibeError({
|
|
934
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
935
|
+
message: `Not an integer literal: ${JSON.stringify(text)}`,
|
|
936
|
+
meta: { issues: [{ rule: "malformed-integer-literal", message: text }], literal: text }
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
const asNumber = Number(text);
|
|
940
|
+
if (Number.isSafeInteger(asNumber))
|
|
941
|
+
return asNumber;
|
|
942
|
+
return BigInt(text).toString();
|
|
943
|
+
}
|
|
944
|
+
var DECIMAL_LITERAL = /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/;
|
|
945
|
+
function exactDecimalLiteral(params) {
|
|
946
|
+
const { text } = params;
|
|
947
|
+
if (!DECIMAL_LITERAL.test(text)) {
|
|
948
|
+
throw new VibeError({
|
|
949
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
950
|
+
message: `Not a numeric literal: ${JSON.stringify(text)}`,
|
|
951
|
+
meta: { issues: [{ rule: "malformed-numeric-literal", message: text }], literal: text }
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
if (INTEGER_LITERAL.test(text))
|
|
955
|
+
return exactIntegerLiteral({ text });
|
|
956
|
+
const mantissa = text.replace(/[eE].*$/, "").replace("-", "").replace(".", "").replace(/^0+/, "");
|
|
957
|
+
return mantissa.length <= 15 ? Number(text) : text;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// packages/schema/src/index-expressions.ts
|
|
961
|
+
function isExpressionIndex(params) {
|
|
962
|
+
return "expressions" in params.index;
|
|
963
|
+
}
|
|
964
|
+
function indexKeyTerms(params) {
|
|
965
|
+
if (isExpressionIndex(params)) {
|
|
966
|
+
const descriptor = Object.getOwnPropertyDescriptor(params.index, "expressions");
|
|
967
|
+
if (descriptor?.enumerable !== true || !("value" in descriptor) || !Array.isArray(descriptor.value))
|
|
968
|
+
throw invalid({ message: "Expression keys must be an enumerable data array" });
|
|
969
|
+
return descriptor.value;
|
|
970
|
+
}
|
|
971
|
+
return params.index.fields.map((field) => ({ kind: "field", field }));
|
|
972
|
+
}
|
|
973
|
+
function invalid(params) {
|
|
974
|
+
return new VibeError({ code: "VIBE_SCHEMA_INVALID", message: params.message, meta: { rule: "expression-index-shape" } });
|
|
975
|
+
}
|
|
976
|
+
function record(params) {
|
|
977
|
+
const { value, required, optional = [] } = params;
|
|
978
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
|
|
979
|
+
return false;
|
|
980
|
+
const keys = Reflect.ownKeys(value);
|
|
981
|
+
if (required.some((key) => !Object.hasOwn(value, key)) || keys.some((key) => typeof key !== "string" || !required.includes(key) && !optional.includes(key)))
|
|
982
|
+
return false;
|
|
983
|
+
return keys.every((key) => {
|
|
984
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
985
|
+
return descriptor?.enumerable === true && "value" in descriptor;
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
function text(params) {
|
|
989
|
+
return typeof params.value === "string" && params.value.trim().length > 0 && !params.value.includes("\x00");
|
|
990
|
+
}
|
|
991
|
+
function sort(params) {
|
|
992
|
+
return params.value === undefined || params.value === "asc" || params.value === "desc";
|
|
993
|
+
}
|
|
994
|
+
function expressionShape(params) {
|
|
995
|
+
const { value, parents = new Set, operand = false } = params;
|
|
996
|
+
if (!record({ value, required: ["kind"], optional: ["name", "expression", "left", "right", "value", "field", "path"] }))
|
|
997
|
+
return false;
|
|
998
|
+
const node = value;
|
|
999
|
+
if (parents.has(node))
|
|
1000
|
+
return false;
|
|
1001
|
+
const next = new Set(parents).add(node);
|
|
1002
|
+
const child = (value, operand = false) => expressionShape({ value, parents: next, operand });
|
|
1003
|
+
switch (node.kind) {
|
|
1004
|
+
case "field":
|
|
1005
|
+
return record({ value, required: ["kind", "name"] }) && text({ value: node.name });
|
|
1006
|
+
case "value":
|
|
1007
|
+
return operand && record({ value, required: ["kind", "value"] }) && (typeof node.value === "string" && !node.value.includes("\x00") || typeof node.value === "number" && Number.isFinite(node.value) || typeof node.value === "boolean");
|
|
1008
|
+
case "lower":
|
|
1009
|
+
case "upper":
|
|
1010
|
+
case "length":
|
|
1011
|
+
return record({ value, required: ["kind", "expression"] }) && child(node.expression);
|
|
1012
|
+
case "add":
|
|
1013
|
+
case "sub":
|
|
1014
|
+
case "mul":
|
|
1015
|
+
return record({ value, required: ["kind", "left", "right"] }) && child(node.left) && child(node.right, true);
|
|
1016
|
+
case "jsonText":
|
|
1017
|
+
return record({ value, required: ["kind", "field", "path"] }) && child(node.field) && node.field.kind === "field" && Array.isArray(node.path) && node.path.every((part) => typeof part === "string" && !part.includes("\x00"));
|
|
1018
|
+
default:
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function dataOnly(params) {
|
|
1023
|
+
const { value, parents = new Set } = params;
|
|
1024
|
+
if (value === null || value === undefined || typeof value === "string" || typeof value === "boolean")
|
|
1025
|
+
return true;
|
|
1026
|
+
if (typeof value === "number")
|
|
1027
|
+
return Number.isFinite(value);
|
|
1028
|
+
if (typeof value !== "object" || parents.has(value))
|
|
1029
|
+
return false;
|
|
1030
|
+
if (!Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
|
|
1031
|
+
return false;
|
|
1032
|
+
const next = new Set(parents).add(value);
|
|
1033
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
1034
|
+
if (Array.isArray(value) && key === "length")
|
|
1035
|
+
continue;
|
|
1036
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1037
|
+
if (typeof key !== "string" || descriptor?.enumerable !== true || !("value" in descriptor) || !dataOnly({ value: descriptor.value, parents: next }))
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
return !Array.isArray(value) || Object.keys(value).length === value.length;
|
|
1041
|
+
}
|
|
1042
|
+
function shapeIssue(params) {
|
|
1043
|
+
const { index } = params;
|
|
1044
|
+
if (!dataOnly({ value: index }))
|
|
1045
|
+
return "Expression indexes accept JSON-safe plain data only, not accessors, callbacks or cycles";
|
|
1046
|
+
if (!record({ value: index, required: ["kind", "fields", "expressions"], optional: ["name", "clientName", "constraintName", "where", "wherePredicate", "nullsNotDistinct"] }))
|
|
1047
|
+
return "Expression indexes require canonical index data properties";
|
|
1048
|
+
if (typeof index.kind !== "string")
|
|
1049
|
+
return "Index kind must be a string";
|
|
1050
|
+
if (index.nullsNotDistinct !== undefined && typeof index.nullsNotDistinct !== "boolean" || index.name !== undefined && !text({ value: index.name }) || index.where !== undefined && !text({ value: index.where }))
|
|
1051
|
+
return "Expression index names, predicates and flags must have their canonical data types";
|
|
1052
|
+
const descriptor = Object.getOwnPropertyDescriptor(index, "expressions");
|
|
1053
|
+
if (descriptor?.enumerable !== true || !("value" in descriptor) || !Array.isArray(descriptor.value) || descriptor.value.length === 0)
|
|
1054
|
+
return "Expression keys require a nonempty enumerable data array";
|
|
1055
|
+
if (!Array.isArray(index.fields) || index.fields.length !== 0)
|
|
1056
|
+
return "Expression keys must stay outside fields; fields must be empty";
|
|
1057
|
+
const terms = descriptor.value;
|
|
1058
|
+
let computed = false;
|
|
1059
|
+
for (let ordinal = 0;ordinal < terms.length; ordinal++) {
|
|
1060
|
+
const entry = Object.getOwnPropertyDescriptor(terms, String(ordinal));
|
|
1061
|
+
if (entry?.enumerable !== true || !("value" in entry))
|
|
1062
|
+
return "Expression key arrays cannot contain holes or accessors";
|
|
1063
|
+
const term = entry.value;
|
|
1064
|
+
if (!record({ value: term, required: ["kind"], optional: ["field", "expression", "sql", "sort"] }))
|
|
1065
|
+
return "Index key terms must be plain enumerable data objects";
|
|
1066
|
+
const key = term;
|
|
1067
|
+
if (key.kind === "field") {
|
|
1068
|
+
if (!record({ value: key, required: ["kind", "field"] }) || !record({ value: key.field, required: ["name"], optional: ["sort", "operatorClass"] }) || !text({ value: key.field.name }) || !sort({ value: key.field.sort }))
|
|
1069
|
+
return "Malformed named field in expression keys";
|
|
1070
|
+
} else if (key.kind === "expression") {
|
|
1071
|
+
if (!record({ value: key, required: ["kind", "expression"], optional: ["sort"] }) || !sort({ value: key.sort }) || !expressionShape({ value: key.expression }))
|
|
1072
|
+
return "Malformed scalar expression key";
|
|
1073
|
+
if (!["lower", "upper", "add", "sub", "mul", "length", "jsonText"].includes(key.expression.kind))
|
|
1074
|
+
return "A typed index root must be a computed scalar node, not a wrapped field";
|
|
1075
|
+
computed = true;
|
|
1076
|
+
} else if (key.kind === "raw") {
|
|
1077
|
+
if (!record({ value: key, required: ["kind", "sql"], optional: ["sort"] }) || !text({ value: key.sql }) || !sort({ value: key.sort }))
|
|
1078
|
+
return "A raw key requires nonempty NUL-free scalar SQL and a valid direction";
|
|
1079
|
+
if (!text({ value: index.name }))
|
|
1080
|
+
return "A raw expression index requires an explicit physical name (map)";
|
|
1081
|
+
computed = true;
|
|
1082
|
+
} else
|
|
1083
|
+
return "Unknown index key term kind";
|
|
1084
|
+
}
|
|
1085
|
+
return computed ? undefined : "Plain-only indexes use fields and omit expressions";
|
|
1086
|
+
}
|
|
1087
|
+
function expressionFields(params) {
|
|
1088
|
+
return inspectPredicateExpression(params).expressions.flatMap((node) => node.kind === "field" ? [node.name] : []);
|
|
1089
|
+
}
|
|
1090
|
+
function indexKeyFieldNames(params) {
|
|
1091
|
+
const terms = indexKeyTerms(params);
|
|
1092
|
+
if (terms.some((term) => term.kind === "raw"))
|
|
1093
|
+
return;
|
|
1094
|
+
return [...new Set(terms.flatMap((term) => term.kind === "field" ? [term.field.name] : term.kind === "expression" ? expressionFields(term) : []))];
|
|
1095
|
+
}
|
|
1096
|
+
function structuralData(params) {
|
|
1097
|
+
const { value, model } = params;
|
|
1098
|
+
if (Array.isArray(value))
|
|
1099
|
+
return value.map((child) => structuralData({ value: child, model }));
|
|
1100
|
+
if (value === null || typeof value !== "object")
|
|
1101
|
+
return value;
|
|
1102
|
+
const node = value;
|
|
1103
|
+
const result = {};
|
|
1104
|
+
for (const key of Object.keys(node).sort()) {
|
|
1105
|
+
const child = node[key];
|
|
1106
|
+
if (key === "name" && (node.kind === "field" || node.kind === undefined && ("name" in node) && !("namespace" in node)))
|
|
1107
|
+
result[key] = model?.fields.find((field) => field.name === child)?.dbName ?? child;
|
|
1108
|
+
else
|
|
1109
|
+
result[key] = structuralData({ value: child, model });
|
|
1110
|
+
}
|
|
1111
|
+
return result;
|
|
1112
|
+
}
|
|
1113
|
+
function indexKeyDefinitionKey(params) {
|
|
1114
|
+
const { index, model } = params;
|
|
1115
|
+
if (!isExpressionIndex({ index })) {
|
|
1116
|
+
const columns = index.fields.map((field) => [model?.fields.find((entry) => entry.name === field.name)?.dbName ?? field.name, field.sort === "desc", field.operatorClass === undefined ? null : [field.operatorClass.namespace, field.operatorClass.name]]);
|
|
1117
|
+
return JSON.stringify([index.kind === "unique", index.nullsNotDistinct === true, index.kind === "unique" ? "btree" : index.kind, columns, index.where?.trim() ?? ""]);
|
|
1118
|
+
}
|
|
1119
|
+
const issue = shapeIssue({ index });
|
|
1120
|
+
if (issue !== undefined)
|
|
1121
|
+
throw invalid({ message: issue });
|
|
1122
|
+
return JSON.stringify(["expression", index.kind, structuralData({ value: index.expressions, model }), index.nullsNotDistinct === true, index.where?.trim() ?? "", structuralData({ value: index.wherePredicate, model }) ?? null]);
|
|
1123
|
+
}
|
|
1124
|
+
function scalarType(params) {
|
|
1125
|
+
const { expression, model, dialect, add } = params;
|
|
1126
|
+
const recurse = (expression) => scalarType({ ...params, expression });
|
|
1127
|
+
switch (expression.kind) {
|
|
1128
|
+
case "field": {
|
|
1129
|
+
const field = model.fields.find((entry) => entry.name === expression.name);
|
|
1130
|
+
if (field === undefined || field.isList || field.isUnsupported === true) {
|
|
1131
|
+
add({ rule: "expression-index-field", message: `${expression.name} must be a declared scalar, supported column` });
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (isLocalDateField({ field })) {
|
|
1135
|
+
add({ rule: "expression-index-type", message: "Local dates are not text or arithmetic operands" });
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
if (dialect === "mysql" && field.default?.kind === "autoincrement")
|
|
1139
|
+
add({ rule: "expression-index-mysql-autoincrement", feature: "expressionIndexAutoIncrement", message: "MySQL functional keys cannot reference an auto-increment column" });
|
|
1140
|
+
return field.type;
|
|
1141
|
+
}
|
|
1142
|
+
case "lower":
|
|
1143
|
+
case "upper": {
|
|
1144
|
+
const type = recurse(expression.expression);
|
|
1145
|
+
if (type !== undefined && type !== "String")
|
|
1146
|
+
add({ rule: "expression-index-type", message: `${expression.kind} requires a text operand` });
|
|
1147
|
+
if (dialect === "mysql")
|
|
1148
|
+
for (const name of expressionFields({ expression })) {
|
|
1149
|
+
const field = model.fields.find((entry) => entry.name === name);
|
|
1150
|
+
if (field?.nativeType !== undefined && /^(tinytext|text|mediumtext|longtext)\b/i.test(field.nativeType.trim()))
|
|
1151
|
+
add({ rule: "expression-index-mysql-text", feature: "expressionIndexTextResult", message: "MySQL lower/upper over unbounded TEXT requires a bounded VARCHAR source or an explicit raw cast; no automatic width is chosen" });
|
|
1152
|
+
}
|
|
1153
|
+
return "String";
|
|
1154
|
+
}
|
|
1155
|
+
case "add":
|
|
1156
|
+
case "sub":
|
|
1157
|
+
case "mul": {
|
|
1158
|
+
const left = recurse(expression.left);
|
|
1159
|
+
const right = expression.right.kind === "value" ? undefined : recurse(expression.right);
|
|
1160
|
+
if (left !== "Int" && left !== "BigInt" && left !== "Float" && left !== "Decimal") {
|
|
1161
|
+
if (left !== undefined)
|
|
1162
|
+
add({ rule: "expression-index-type", message: "Arithmetic requires a numeric scalar family" });
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
if (right !== undefined && right !== left)
|
|
1166
|
+
add({ rule: "expression-index-type", message: "Arithmetic operands require the same numeric family" });
|
|
1167
|
+
if (expression.right.kind === "value") {
|
|
1168
|
+
const value = expression.right.value;
|
|
1169
|
+
if (left === "Int" || left === "BigInt") {
|
|
1170
|
+
if (!(typeof value === "number" && Number.isSafeInteger(value) || typeof value === "string" && /^-?(0|[1-9]\d*)$/.test(value)))
|
|
1171
|
+
add({ rule: "expression-index-literal", message: "Integer arithmetic requires exact integer literals" });
|
|
1172
|
+
else {
|
|
1173
|
+
const number = BigInt(value);
|
|
1174
|
+
const min = left === "Int" ? -2147483648n : -(2n ** 63n);
|
|
1175
|
+
const max = left === "Int" ? 2147483647n : 2n ** 63n - 1n;
|
|
1176
|
+
if (number < min || number > max)
|
|
1177
|
+
add({ rule: "expression-index-literal", message: "Integer literal is outside the scalar's signed range" });
|
|
1178
|
+
}
|
|
1179
|
+
} else if (left === "Float" ? typeof value !== "number" || !Number.isFinite(value) : typeof value !== "string" || !/^-?(0|[1-9]\d*)(\.\d+)?$/.test(value))
|
|
1180
|
+
add({ rule: "expression-index-literal", message: `${left} literal has the wrong carrier or numeric format` });
|
|
1181
|
+
}
|
|
1182
|
+
return left;
|
|
1183
|
+
}
|
|
1184
|
+
case "length":
|
|
1185
|
+
return "Int";
|
|
1186
|
+
case "jsonText":
|
|
1187
|
+
return "String";
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
var MYSQL_FUNCTIONAL_KEY_LIMITS = Object.freeze({ bytes: 3072, bytesPerCharacter: 4, defaultStringLength: 191, arithmeticBytes: 8, intBytes: 4, wideBytes: 8, decimalBytes: 32, enumBytes: 2, booleanBytes: 1 });
|
|
1191
|
+
function mysqlIndexKeyWidth(params) {
|
|
1192
|
+
const limits = MYSQL_FUNCTIONAL_KEY_LIMITS;
|
|
1193
|
+
let refusal;
|
|
1194
|
+
const refuse = (params) => {
|
|
1195
|
+
refusal ??= params;
|
|
1196
|
+
return 0;
|
|
1197
|
+
};
|
|
1198
|
+
const width = ({ name, functional }) => {
|
|
1199
|
+
const field = params.model.fields.find((candidate) => candidate.name === name);
|
|
1200
|
+
if (field === undefined)
|
|
1201
|
+
return 0;
|
|
1202
|
+
if (functional && field.default?.kind === "autoincrement")
|
|
1203
|
+
return refuse({ field: name, feature: "mysqlFunctionalAutoIncrementSource" });
|
|
1204
|
+
if (field.type === "String") {
|
|
1205
|
+
const native = field.nativeType?.trim();
|
|
1206
|
+
const length = native === undefined ? limits.defaultStringLength : Number(/^(?:varchar|char)\s*\(\s*(\d+)\s*\)$/i.exec(native)?.[1]);
|
|
1207
|
+
if (!Number.isSafeInteger(length) || length <= 0)
|
|
1208
|
+
return refuse({ field: name, feature: "mysqlFunctionalTextResult" });
|
|
1209
|
+
return length * limits.bytesPerCharacter;
|
|
1210
|
+
}
|
|
1211
|
+
if (functional && field.nativeType !== undefined) {
|
|
1212
|
+
const base = field.nativeType.toLowerCase().replace(/\(.*$/s, "").replace(/\s+/g, "");
|
|
1213
|
+
const allowed = field.type === "Int" ? ["tinyint", "smallint", "mediumint", "int", "integer"] : field.type === "BigInt" ? ["bigint"] : field.type === "Float" ? ["float", "double", "real"] : field.type === "Decimal" ? ["decimal", "numeric", "dec"] : [];
|
|
1214
|
+
if (!allowed.includes(base))
|
|
1215
|
+
return refuse({ field: name, feature: "mysqlFunctionalNativeType" });
|
|
1216
|
+
}
|
|
1217
|
+
return isEnumType(field.type) ? limits.enumBytes : field.type === "Int" ? limits.intBytes : field.type === "Boolean" ? limits.booleanBytes : field.type === "Decimal" ? limits.decimalBytes : limits.wideBytes;
|
|
1218
|
+
};
|
|
1219
|
+
let bytes = 0;
|
|
1220
|
+
let raw = false;
|
|
1221
|
+
for (const term of params.terms) {
|
|
1222
|
+
if (term.kind === "field")
|
|
1223
|
+
bytes += width({ name: term.field.name, functional: false });
|
|
1224
|
+
else if (term.kind === "expression") {
|
|
1225
|
+
const widths = (indexKeyFieldNames({ index: { fields: [], expressions: [term] } }) ?? []).map((name) => width({ name, functional: true }));
|
|
1226
|
+
bytes += Math.max(term.expression.kind === "lower" || term.expression.kind === "upper" ? 0 : limits.arithmeticBytes, ...widths);
|
|
1227
|
+
} else
|
|
1228
|
+
raw = true;
|
|
1229
|
+
if (refusal !== undefined)
|
|
1230
|
+
return { kind: "refused", ...refusal };
|
|
1231
|
+
}
|
|
1232
|
+
return raw ? { kind: "raw" } : { kind: "bytes", bytes };
|
|
1233
|
+
}
|
|
1234
|
+
function validateIndexExpressions(params) {
|
|
1235
|
+
const issues = [];
|
|
1236
|
+
let expressionUniquesByModel;
|
|
1237
|
+
for (const model of params.schema.models)
|
|
1238
|
+
for (const index of model.indexes) {
|
|
1239
|
+
if (!isExpressionIndex({ index }))
|
|
1240
|
+
continue;
|
|
1241
|
+
const add = (issue) => {
|
|
1242
|
+
issues.push({ ...issue, model: model.name });
|
|
1243
|
+
};
|
|
1244
|
+
const before = issues.length;
|
|
1245
|
+
const shape = shapeIssue({ index });
|
|
1246
|
+
if (shape !== undefined) {
|
|
1247
|
+
add({ rule: "expression-index-shape", message: shape });
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
if (index.kind === "unique") {
|
|
1251
|
+
expressionUniquesByModel ??= new Map;
|
|
1252
|
+
const indexes = expressionUniquesByModel.get(model);
|
|
1253
|
+
if (indexes === undefined)
|
|
1254
|
+
expressionUniquesByModel.set(model, [index]);
|
|
1255
|
+
else
|
|
1256
|
+
indexes.push(index);
|
|
1257
|
+
}
|
|
1258
|
+
if (index.clientName !== undefined || index.constraintName !== undefined)
|
|
1259
|
+
add({ rule: "expression-index-selector", message: "Expression indexes cannot name a client selector or a unique constraint; use map for the physical index" });
|
|
1260
|
+
if ((index.where !== undefined || index.wherePredicate !== undefined) && !DIALECT_CAPABILITIES[params.dialect].partialIndexes)
|
|
1261
|
+
add({ rule: "expression-index-partial", feature: "partialIndexes", message: "This dialect cannot admit a partial expression index" });
|
|
1262
|
+
if (index.nullsNotDistinct === true && index.kind !== "unique")
|
|
1263
|
+
add({ rule: "expression-index-null-semantics", message: "NULLS NOT DISTINCT only applies to a unique expression index" });
|
|
1264
|
+
if (index.nullsNotDistinct === true && params.dialect !== "postgres")
|
|
1265
|
+
add({ rule: "expression-index-null-semantics", feature: "nullsNotDistinct", message: "NULLS NOT DISTINCT expression indexes require PostgreSQL/PGlite" });
|
|
1266
|
+
if (!DIALECT_CAPABILITIES[params.dialect].expressionIndexes)
|
|
1267
|
+
add({ rule: "expression-index-capability", feature: "expressionIndexes", message: "Expression indexes are unsupported on this dialect" });
|
|
1268
|
+
if (index.kind !== "unique" && index.kind !== "btree")
|
|
1269
|
+
add({ rule: "expression-index-capability", feature: "expressionIndexAccessMethod", message: "Expression keys admit ordinary BTree indexes only" });
|
|
1270
|
+
for (const term of indexKeyTerms({ index })) {
|
|
1271
|
+
if (term.kind === "raw")
|
|
1272
|
+
continue;
|
|
1273
|
+
if (term.kind === "field") {
|
|
1274
|
+
const field = model.fields.find((entry) => entry.name === term.field.name);
|
|
1275
|
+
if (field === undefined || field.isList || field.isUnsupported === true)
|
|
1276
|
+
add({ rule: "expression-index-field", message: `${term.field.name} must be a declared scalar supported column` });
|
|
1277
|
+
if (term.field.operatorClass !== undefined)
|
|
1278
|
+
add({ rule: "expression-index-capability", feature: "expressionIndexOperatorClasses", message: "Custom operator classes are not admitted on expression-bearing keys" });
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
const capability = predicateCapabilityIssue({ expression: term.expression, model, dialect: params.dialect, usage: "indexExpression" });
|
|
1282
|
+
if (capability !== undefined)
|
|
1283
|
+
add({ rule: "expression-index-capability", feature: capability.feature, message: capability.hint ?? `Scalar node ${capability.node} is not admitted in an index key` });
|
|
1284
|
+
scalarType({ expression: term.expression, model, dialect: params.dialect, add });
|
|
1285
|
+
}
|
|
1286
|
+
if (params.dialect === "mysql" && issues.length === before) {
|
|
1287
|
+
const width = mysqlIndexKeyWidth({ terms: indexKeyTerms({ index }), model });
|
|
1288
|
+
if (width.kind === "refused")
|
|
1289
|
+
add({ rule: "expression-index-mysql-storage", feature: width.feature, message: `${model.name}.${width.field}: use bounded VARCHAR/CHAR, an explicit raw cast with intentional width/collation, or a generated column; no automatic cast is inserted` });
|
|
1290
|
+
else if (width.kind === "bytes" && width.bytes > MYSQL_FUNCTIONAL_KEY_LIMITS.bytes)
|
|
1291
|
+
add({ rule: "expression-index-mysql-width", feature: "mysqlFunctionalKeyWidth", message: `The complete utf8mb4 key requires up to ${width.bytes} bytes; modern InnoDB admits at most ${MYSQL_FUNCTIONAL_KEY_LIMITS.bytes}. Use narrower storage or an explicit reviewed raw key` });
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
if (expressionUniquesByModel === undefined || issues.some((issue) => issue.rule === "expression-index-shape"))
|
|
1295
|
+
return issues;
|
|
1296
|
+
const targets = new Map;
|
|
1297
|
+
for (const [target, indexes] of expressionUniquesByModel) {
|
|
1298
|
+
const sources = indexes.map((index) => {
|
|
1299
|
+
const names = indexKeyFieldNames({ index });
|
|
1300
|
+
return names === undefined ? undefined : new Set(names);
|
|
1301
|
+
});
|
|
1302
|
+
const plain = [
|
|
1303
|
+
...target.fields.filter((field) => field.isId || field.isUnique).map((field) => [field.name]),
|
|
1304
|
+
...target.primaryKey === undefined ? [] : [target.primaryKey.fields],
|
|
1305
|
+
...target.indexes.filter((index) => index.kind === "unique" && !isExpressionIndex({ index }) && index.where === undefined && index.wherePredicate === undefined).map((index) => index.fields.map((field) => field.name))
|
|
1306
|
+
];
|
|
1307
|
+
const distinctPlainKeys = new Set;
|
|
1308
|
+
const repeatedPlainKeys = [];
|
|
1309
|
+
for (const key of plain) {
|
|
1310
|
+
if (new Set(key).size === key.length)
|
|
1311
|
+
distinctPlainKeys.add(JSON.stringify([...key].sort()));
|
|
1312
|
+
else
|
|
1313
|
+
repeatedPlainKeys.push(key);
|
|
1314
|
+
}
|
|
1315
|
+
targets.set(target.name, { sources, distinctPlainKeys, repeatedPlainKeys });
|
|
1316
|
+
}
|
|
1317
|
+
for (const model of params.schema.models)
|
|
1318
|
+
for (const relation of model.relations) {
|
|
1319
|
+
if (relation.references === undefined || relation.fields === undefined || relation.fields.length === 0)
|
|
1320
|
+
continue;
|
|
1321
|
+
const target = targets.get(relation.targetModel);
|
|
1322
|
+
if (target === undefined)
|
|
1323
|
+
continue;
|
|
1324
|
+
const references = relation.references;
|
|
1325
|
+
if (!target.sources.some((names) => names === undefined || references.every((name) => names.has(name))))
|
|
1326
|
+
continue;
|
|
1327
|
+
const plain = target.distinctPlainKeys.has(JSON.stringify([...references].sort())) || target.repeatedPlainKeys.some((key) => key.length === references.length && key.every((name) => references.includes(name)));
|
|
1328
|
+
if (!plain)
|
|
1329
|
+
issues.push({ rule: "expression-index-foreign-key", model: model.name, message: `Relation ${model.name}.${relation.name} cannot reference expression-index source fields without a separate plain unique key` });
|
|
1330
|
+
}
|
|
1331
|
+
return issues;
|
|
1332
|
+
}
|
|
1333
|
+
function assertIndexExpressions(params) {
|
|
1334
|
+
const issues = validateIndexExpressions(params);
|
|
1335
|
+
const structural = issues.filter((issue) => issue.feature === undefined);
|
|
1336
|
+
if (structural.length > 0)
|
|
1337
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: structural.map((issue) => `${issue.model}: ${issue.message}`).join("; "), meta: { issues: structural } });
|
|
1338
|
+
const denied = issues.find((issue) => issue.feature !== undefined);
|
|
1339
|
+
if (denied !== undefined) {
|
|
1340
|
+
const error = unsupportedCapability({ dialect: params.dialect, feature: denied.feature, hint: denied.message });
|
|
1341
|
+
throw new VibeError({ code: error.code, message: error.message, meta: { ...error.meta, rule: denied.rule, model: denied.model, issues: [denied] } });
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
function scalarPrisma(params) {
|
|
1345
|
+
const { operand, compact = false } = params;
|
|
1346
|
+
switch (operand.kind) {
|
|
1347
|
+
case "field":
|
|
1348
|
+
return compact ? operand.name : `field(${encodePrismaString({ value: operand.name })})`;
|
|
1349
|
+
case "value":
|
|
1350
|
+
return `value(${typeof operand.value === "string" ? encodePrismaString({ value: operand.value }) : String(operand.value)})`;
|
|
1351
|
+
case "lower":
|
|
1352
|
+
case "upper":
|
|
1353
|
+
case "length":
|
|
1354
|
+
return `${operand.kind}(${scalarPrisma({ operand: operand.expression, compact: operand.expression.kind === "field" })})`;
|
|
1355
|
+
case "add":
|
|
1356
|
+
case "sub":
|
|
1357
|
+
case "mul":
|
|
1358
|
+
return `${operand.kind}(${scalarPrisma({ operand: operand.left })}, ${scalarPrisma({ operand: operand.right })})`;
|
|
1359
|
+
case "jsonText":
|
|
1360
|
+
throw invalid({ message: "CHECK-only JSON expressions cannot be spelled as index keys" });
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
function scalarDsl(params) {
|
|
1364
|
+
const { expression, model } = params;
|
|
1365
|
+
const family = (node) => node.kind === "field" ? model.fields.find((field) => field.name === node.name)?.type : node.kind === "lower" || node.kind === "upper" ? "String" : node.kind === "add" || node.kind === "sub" || node.kind === "mul" ? family(node.left) : undefined;
|
|
1366
|
+
const render = (node) => scalarDsl({ expression: node, model });
|
|
1367
|
+
switch (expression.kind) {
|
|
1368
|
+
case "field":
|
|
1369
|
+
return `i.field(${JSON.stringify(expression.name)})`;
|
|
1370
|
+
case "lower":
|
|
1371
|
+
case "upper":
|
|
1372
|
+
return `i.${expression.kind}(${expression.expression.kind === "field" ? JSON.stringify(expression.expression.name) : render(expression.expression)})`;
|
|
1373
|
+
case "add":
|
|
1374
|
+
case "sub":
|
|
1375
|
+
case "mul": {
|
|
1376
|
+
const type = family(expression);
|
|
1377
|
+
if (expression.right.kind === "value" && typeof expression.right.value === "string" && (type === "Int" || type === "BigInt" && expression.right.value === "-0"))
|
|
1378
|
+
return JSON.stringify({ ...expression, __kind: type });
|
|
1379
|
+
const right = expression.right.kind !== "value" ? render(expression.right) : typeof expression.right.value === "string" && type === "BigInt" ? `${expression.right.value}n` : JSON.stringify(expression.right.value);
|
|
1380
|
+
return `i.${expression.kind}({ left: ${render(expression.left)}, right: ${right} })`;
|
|
1381
|
+
}
|
|
1382
|
+
case "jsonText":
|
|
1383
|
+
case "length":
|
|
1384
|
+
throw invalid({ message: "CHECK-only scalar helpers cannot be printed as index keys" });
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
function indexKeyDsl(params) {
|
|
1388
|
+
const { term, model } = params;
|
|
1389
|
+
const source = term.kind === "field" ? JSON.stringify(term.field.name) : term.kind === "raw" ? `i.sql({ text: ${JSON.stringify(term.sql)} })` : scalarDsl({ expression: term.expression, model });
|
|
1390
|
+
const direction = term.kind === "field" ? term.field.sort : term.sort;
|
|
1391
|
+
return direction === undefined ? source : `i.${direction}(${source})`;
|
|
1392
|
+
}
|
|
1393
|
+
function indexKeyPrisma(params) {
|
|
1394
|
+
const { term } = params;
|
|
1395
|
+
const direction = term.kind === "field" ? term.field.sort : term.sort;
|
|
1396
|
+
const suffix = direction === undefined ? "" : `sort: ${direction === "desc" ? "Desc" : "Asc"}`;
|
|
1397
|
+
if (term.kind === "field")
|
|
1398
|
+
return `${term.field.name}${suffix === "" ? "" : `(${suffix})`}`;
|
|
1399
|
+
const source = term.kind === "raw" ? `sql(${encodePrismaString({ value: term.sql })})` : scalarPrisma({ operand: term.expression });
|
|
1400
|
+
return suffix === "" ? source : `${source.slice(0, -1)}, ${suffix})`;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// packages/schema/src/physical-names.ts
|
|
1404
|
+
var IDENTIFIER_LIMITS = Object.freeze({
|
|
1405
|
+
postgres: { max: 63, unit: "bytes" },
|
|
1406
|
+
mysql: { max: 64, unit: "characters" },
|
|
1407
|
+
sqlite: undefined
|
|
1408
|
+
});
|
|
1409
|
+
var ENCODER = new TextEncoder;
|
|
1410
|
+
function identifierLength(params) {
|
|
1411
|
+
const limit = IDENTIFIER_LIMITS[params.dialect];
|
|
1412
|
+
return limit?.unit === "bytes" ? ENCODER.encode(params.name).length : [...params.name].length;
|
|
1413
|
+
}
|
|
1414
|
+
function cutIdentifier(params) {
|
|
1415
|
+
let out = "";
|
|
1416
|
+
let used = 0;
|
|
1417
|
+
for (const char of params.name) {
|
|
1418
|
+
const size = params.unit === "bytes" ? ENCODER.encode(char).length : 1;
|
|
1419
|
+
if (used + size > params.length)
|
|
1420
|
+
break;
|
|
1421
|
+
out += char;
|
|
1422
|
+
used += size;
|
|
1423
|
+
}
|
|
1424
|
+
return out;
|
|
1425
|
+
}
|
|
1426
|
+
function fitDefaultName(params) {
|
|
1427
|
+
const limit = IDENTIFIER_LIMITS[params.dialect];
|
|
1428
|
+
if (limit === undefined)
|
|
1429
|
+
return `${params.prefix}${params.suffix}`;
|
|
1430
|
+
const room = limit.max - identifierLength({ name: params.suffix, dialect: params.dialect });
|
|
1431
|
+
if (identifierLength({ name: params.prefix, dialect: params.dialect }) <= room)
|
|
1432
|
+
return `${params.prefix}${params.suffix}`;
|
|
1433
|
+
return `${cutIdentifier({ name: params.prefix, length: room, unit: limit.unit })}${params.suffix}`;
|
|
1434
|
+
}
|
|
1435
|
+
function assertIdentifierFits(params) {
|
|
1436
|
+
const limit = IDENTIFIER_LIMITS[params.dialect];
|
|
1437
|
+
if (limit === undefined)
|
|
1438
|
+
return;
|
|
1439
|
+
const length = identifierLength({ name: params.name, dialect: params.dialect });
|
|
1440
|
+
if (length <= limit.max)
|
|
1441
|
+
return;
|
|
1442
|
+
throw new VibeError({
|
|
1443
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
1444
|
+
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}`,
|
|
1445
|
+
meta: { name: params.name, dialect: params.dialect, length, max: limit.max }
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
function defaultIndexName(params) {
|
|
1449
|
+
return fitDefaultName({
|
|
1450
|
+
prefix: `${params.table}_${params.columns.join("_")}`,
|
|
1451
|
+
suffix: params.unique ? "_key" : "_idx",
|
|
1452
|
+
dialect: params.dialect
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
function defaultExpressionIndexName(params) {
|
|
1456
|
+
const { table, model, index, dialect } = params;
|
|
1457
|
+
if (!isExpressionIndex({ index }))
|
|
1458
|
+
return defaultIndexName({ table, columns: index.fields.map((entry) => model.fields.find((field) => field.name === entry.name)?.dbName ?? entry.name), unique: index.kind === "unique", dialect });
|
|
1459
|
+
const names = indexKeyFieldNames({ index });
|
|
1460
|
+
if (names === undefined)
|
|
1461
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "A raw expression index requires an explicit physical name (map)", meta: { rule: "expression-index-raw-name" } });
|
|
1462
|
+
const columns = names.map((name) => model.fields.find((field) => field.name === name)?.dbName ?? name);
|
|
1463
|
+
const keyIndex = { kind: index.kind, fields: [], expressions: index.expressions };
|
|
1464
|
+
const digest = extensionOptionsHash({ options: { key: indexKeyDefinitionKey({ index: keyIndex, model }) } });
|
|
1465
|
+
return fitDefaultName({ prefix: `${table}_${columns.join("_")}`, suffix: `_expr_${digest}${index.kind === "unique" ? "_key" : "_idx"}`, dialect });
|
|
1466
|
+
}
|
|
1467
|
+
function defaultForeignKeyName(params) {
|
|
1468
|
+
return fitDefaultName({ prefix: `${params.table}_${params.columns.join("_")}`, suffix: "_fkey", dialect: params.dialect });
|
|
1469
|
+
}
|
|
1470
|
+
function defaultPrimaryKeyName(params) {
|
|
1471
|
+
return fitDefaultName({ prefix: params.table, suffix: "_pkey", dialect: params.dialect });
|
|
1472
|
+
}
|
|
1473
|
+
function implicitJoinTableObjectNames(params) {
|
|
1474
|
+
return {
|
|
1475
|
+
uniqueIndex: `${params.table}_AB_unique`,
|
|
1476
|
+
indexB: `${params.table}_B_index`,
|
|
1477
|
+
foreignKeyA: `${params.table}_A_fkey`,
|
|
1478
|
+
foreignKeyB: `${params.table}_B_fkey`
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
function postgresGeneratedObjectName(params) {
|
|
1482
|
+
const limit = IDENTIFIER_LIMITS.postgres;
|
|
1483
|
+
const overhead = params.label.length + 2;
|
|
1484
|
+
let chars1 = [...params.name1];
|
|
1485
|
+
let chars2 = [...params.name2];
|
|
1486
|
+
const bytes = (chars) => ENCODER.encode(chars.join("")).length;
|
|
1487
|
+
while (bytes(chars1) + bytes(chars2) + overhead > limit.max) {
|
|
1488
|
+
if (bytes(chars1) > bytes(chars2))
|
|
1489
|
+
chars1 = chars1.slice(0, -1);
|
|
1490
|
+
else
|
|
1491
|
+
chars2 = chars2.slice(0, -1);
|
|
1492
|
+
}
|
|
1493
|
+
return `${chars1.join("")}_${chars2.join("")}_${params.label}`;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// packages/schema/src/namespaces.ts
|
|
1497
|
+
var SQL_NAMESPACES = Object.freeze({
|
|
1498
|
+
postgres: "public",
|
|
1499
|
+
sqlite: "main",
|
|
1500
|
+
private: Object.freeze(["_vibeorm_shadow", "_vibe_shadow"])
|
|
1501
|
+
});
|
|
1502
|
+
function sqlNamespaceIssue(params) {
|
|
1503
|
+
const { namespace } = params;
|
|
1504
|
+
if (typeof namespace !== "string" || namespace.trim().length === 0 || namespace.includes("\x00"))
|
|
1505
|
+
return "Namespace must be a non-empty identifier without NUL bytes";
|
|
1506
|
+
if (identifierLength({ name: namespace, dialect: "postgres" }) > IDENTIFIER_LIMITS.postgres.max)
|
|
1507
|
+
return "PostgreSQL namespace exceeds its identifier byte limit";
|
|
1508
|
+
if (namespace === "information_schema" || namespace.startsWith("pg_") || SQL_NAMESPACES.private.includes(namespace))
|
|
1509
|
+
return "System and private VibeORM namespaces cannot be declared";
|
|
1510
|
+
if (params.allowPublic === false && namespace === SQL_NAMESPACES.postgres)
|
|
1511
|
+
return "A fixed package namespace must be non-public; omit namespace for a legacy public artifact";
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
function assertSqlNamespace(params) {
|
|
1515
|
+
if (params.dialect !== "postgres")
|
|
1516
|
+
throw unsupportedCapability({ dialect: params.dialect, feature: "fixedNamespaces" });
|
|
1517
|
+
const issue = sqlNamespaceIssue(params);
|
|
1518
|
+
if (issue !== undefined)
|
|
1519
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: issue, meta: { rule: "namespace", namespace: params.namespace } });
|
|
1520
|
+
}
|
|
1521
|
+
function qualifiedSqlName(params) {
|
|
1522
|
+
return { namespace: params.namespace ?? params.defaultNamespace ?? SQL_NAMESPACES.postgres, name: params.name };
|
|
1523
|
+
}
|
|
1524
|
+
// packages/schema/src/native-rls-admission.ts
|
|
1525
|
+
function assertNativeRlsAdmission(params) {
|
|
1526
|
+
const { schema } = params;
|
|
1527
|
+
if (schema.rls === undefined && !schema.models.some((model) => model.rls !== undefined))
|
|
1528
|
+
return;
|
|
1529
|
+
const dialect = params.dialect ?? providerToDialect({ provider: schema.datasource.provider });
|
|
1530
|
+
if (!DIALECT_CAPABILITIES[dialect].nativeRowLevelSecurity) {
|
|
1531
|
+
throw unsupportedCapability({ dialect, feature: "nativeRowLevelSecurity" });
|
|
1532
|
+
}
|
|
1533
|
+
for (const model of schema.models) {
|
|
1534
|
+
const rule = model.rls;
|
|
1535
|
+
if (rule === undefined || rule === "unprotected")
|
|
1536
|
+
continue;
|
|
1537
|
+
const parentName = rule !== null && typeof rule === "object" && "through" in rule ? model.relations.find((relation) => relation.name === rule.through)?.targetModel : undefined;
|
|
1538
|
+
const parent = parentName === undefined ? undefined : schema.models.find((entry) => entry.name === parentName);
|
|
1539
|
+
if (model.namespace !== undefined && model.namespace !== "public" || parent?.namespace !== undefined && parent.namespace !== "public") {
|
|
1540
|
+
throw unsupportedCapability({
|
|
1541
|
+
dialect,
|
|
1542
|
+
feature: "fixedNamespaceNativeRls",
|
|
1543
|
+
hint: "Native RLS protects public tables only, including deny-protected models and through-policy parents"
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
// packages/schema/src/model-sql-namespace.ts
|
|
1549
|
+
function modelSqlNamespace(params) {
|
|
1550
|
+
const { schema, model } = params;
|
|
1551
|
+
if (model.namespace !== undefined || (params.dialect ?? providerToDialect({ provider: schema.datasource.provider })) !== "postgres")
|
|
1552
|
+
return model.namespace;
|
|
1553
|
+
const protectedModel = model.rls !== undefined && model.rls !== "unprotected";
|
|
1554
|
+
const throughParent = schema.models.some((child) => {
|
|
1555
|
+
const rule = child.rls;
|
|
1556
|
+
return rule !== null && typeof rule === "object" && "through" in rule && child.relations.some((relation) => relation.name === rule.through && relation.targetModel === model.name);
|
|
1557
|
+
});
|
|
1558
|
+
return protectedModel || throughParent || hasPublicExtensionDeclaration({ schema, model: model.name }) ? SQL_NAMESPACES.postgres : undefined;
|
|
1559
|
+
}
|
|
1560
|
+
// packages/schema/src/referential-actions.ts
|
|
1561
|
+
function resolveOnDelete(params) {
|
|
1562
|
+
if (params.explicit !== undefined)
|
|
1563
|
+
return params.explicit;
|
|
1564
|
+
return params.required ? "Restrict" : "SetNull";
|
|
1565
|
+
}
|
|
1566
|
+
function resolveOnUpdate(params) {
|
|
1567
|
+
return params.explicit ?? "Cascade";
|
|
1568
|
+
}
|
|
1569
|
+
function resolveReferentialActions(params) {
|
|
1570
|
+
const { relation } = params;
|
|
1571
|
+
const required = (relation.fields ?? []).every((name) => params.field(name)?.isOptional !== true);
|
|
1572
|
+
return { onDelete: resolveOnDelete({ explicit: relation.onDelete, required }), onUpdate: resolveOnUpdate({ explicit: relation.onUpdate }) };
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// packages/schema/src/generated-columns.ts
|
|
1576
|
+
function declarationIssue(params) {
|
|
1577
|
+
const { value, authoring } = params;
|
|
1578
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
|
1579
|
+
return "A generated column requires a plain data object";
|
|
1580
|
+
const expected = authoring ? ["sql"] : ["expression", "storage"];
|
|
1581
|
+
const keys = Reflect.ownKeys(value);
|
|
1582
|
+
if (keys.length !== expected.length || keys.some((key) => typeof key !== "string" || !expected.includes(key)))
|
|
1583
|
+
return authoring ? "generated accepts exactly sql; storage is always stored" : "A generated declaration accepts exactly expression and storage";
|
|
1584
|
+
for (const key of expected) {
|
|
1585
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1586
|
+
if (descriptor?.enumerable !== true || !("value" in descriptor))
|
|
1587
|
+
return "A generated declaration requires enumerable data properties";
|
|
1588
|
+
}
|
|
1589
|
+
const expression = Object.getOwnPropertyDescriptor(value, authoring ? "sql" : "expression")?.value;
|
|
1590
|
+
if (typeof expression !== "string" || expression.trim().length === 0 || expression.includes("\x00"))
|
|
1591
|
+
return "A generated expression must be a nonempty, NUL-free SQL string";
|
|
1592
|
+
if (!authoring && Object.getOwnPropertyDescriptor(value, "storage")?.value !== "stored")
|
|
1593
|
+
return "Generated storage must be stored; virtual columns are not supported";
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
function generatedColumnShapeIssue(params) {
|
|
1597
|
+
return declarationIssue({ ...params, authoring: false });
|
|
1598
|
+
}
|
|
1599
|
+
function assertGeneratedColumn(params) {
|
|
1600
|
+
const issue = generatedColumnShapeIssue(params);
|
|
1601
|
+
if (issue !== undefined)
|
|
1602
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: issue, meta: { issues: [{ rule: "generated-column-shape", message: issue }] } });
|
|
1603
|
+
const value = params.value;
|
|
1604
|
+
return Object.freeze({ expression: value.expression, storage: "stored" });
|
|
1605
|
+
}
|
|
1606
|
+
function generatedColumnFromSql(params) {
|
|
1607
|
+
const issue = declarationIssue({ ...params, authoring: true });
|
|
1608
|
+
if (issue !== undefined)
|
|
1609
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: issue, meta: { issues: [{ rule: "generated-column-shape", message: issue }] } });
|
|
1610
|
+
const value = params.value;
|
|
1611
|
+
return assertGeneratedColumn({ value: { expression: value.sql, storage: "stored" } });
|
|
1612
|
+
}
|
|
1613
|
+
function validateGeneratedColumns(params) {
|
|
1614
|
+
const issues = [];
|
|
1615
|
+
for (const model of params.schema.models) {
|
|
1616
|
+
const owning = new Set(model.relations.flatMap((relation) => relation.fields ?? []));
|
|
1617
|
+
for (const field of model.fields) {
|
|
1618
|
+
if (!("generated" in field))
|
|
1619
|
+
continue;
|
|
1620
|
+
const add = ({ rule, message }) => {
|
|
1621
|
+
issues.push({ rule, message, model: model.name, field: field.name });
|
|
1622
|
+
};
|
|
1623
|
+
const shape = generatedColumnShapeIssue({ value: field.generated });
|
|
1624
|
+
if (shape !== undefined) {
|
|
1625
|
+
add({ rule: "generated-column-shape", message: shape });
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
if (field.default !== undefined)
|
|
1629
|
+
add({ rule: "generated-column-default", message: "A generated field cannot have a default" });
|
|
1630
|
+
if (field.isUpdatedAt)
|
|
1631
|
+
add({ rule: "generated-column-updated-at", message: "A generated field cannot use @updatedAt" });
|
|
1632
|
+
if (field.isId || model.primaryKey?.fields.includes(field.name))
|
|
1633
|
+
add({ rule: "generated-column-id", message: "A generated field cannot belong to a primary key" });
|
|
1634
|
+
if (field.isList)
|
|
1635
|
+
add({ rule: "generated-column-list", message: "A generated field cannot be a list" });
|
|
1636
|
+
if (owning.has(field.name))
|
|
1637
|
+
add({ rule: "generated-column-foreign-key", message: "A generated field cannot own a foreign key" });
|
|
1638
|
+
if (model.policy?.field === field.name)
|
|
1639
|
+
add({ rule: "generated-column-policy", message: "A generated field cannot be an application-policy context column" });
|
|
1640
|
+
if (model.rls !== null && typeof model.rls === "object" && "where" in model.rls && model.rls.where !== null && typeof model.rls.where === "object" && Object.hasOwn(model.rls.where, field.name))
|
|
1641
|
+
add({ rule: "generated-column-rls", message: "A generated field cannot be a native RLS context column" });
|
|
1642
|
+
if (model.isView)
|
|
1643
|
+
add({ rule: "generated-column-view", message: "A view cannot declare a core-owned generated column" });
|
|
1644
|
+
if (field.validator !== undefined)
|
|
1645
|
+
add({ rule: "generated-column-validator", message: "A generated field cannot carry write validation because the client never writes it" });
|
|
1646
|
+
}
|
|
1647
|
+
for (const relation of model.relations)
|
|
1648
|
+
if ("generated" in relation)
|
|
1649
|
+
issues.push({ rule: "generated-column-relation", message: "Generation belongs on scalar or enum columns, not relation properties", model: model.name, field: relation.name });
|
|
1650
|
+
}
|
|
1651
|
+
return issues;
|
|
1652
|
+
}
|
|
1653
|
+
function assertGeneratedColumns(params) {
|
|
1654
|
+
const issues = validateGeneratedColumns(params);
|
|
1655
|
+
if (issues.length > 0)
|
|
1656
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: `Invalid generated columns: ${issues.map((issue) => issue.message).join("; ")}`, meta: { issues } });
|
|
1657
|
+
const dialect = params.dialect ?? providerToDialect({ provider: params.schema.datasource.provider });
|
|
1658
|
+
const generatedModels = new Set(params.schema.models.filter((model) => model.fields.some((field) => field.generated !== undefined)).map((model) => model.name));
|
|
1659
|
+
if (generatedModels.size === 0)
|
|
1660
|
+
return;
|
|
1661
|
+
if (!DIALECT_CAPABILITIES[dialect].generatedColumns)
|
|
1662
|
+
throw unsupportedCapability({ dialect, feature: "generatedColumns" });
|
|
1663
|
+
if (dialect !== "mysql")
|
|
1664
|
+
return;
|
|
1665
|
+
for (const model of params.schema.models)
|
|
1666
|
+
for (const relation of model.relations) {
|
|
1667
|
+
if ((relation.fields?.length ?? 0) === 0)
|
|
1668
|
+
continue;
|
|
1669
|
+
const target = params.schema.models.find((candidate) => candidate.name === relation.targetModel);
|
|
1670
|
+
const referencedGenerated = target?.fields.some((field) => field.generated !== undefined && relation.references?.includes(field.name)) === true;
|
|
1671
|
+
if (!generatedModels.has(model.name) && !referencedGenerated)
|
|
1672
|
+
continue;
|
|
1673
|
+
const actions = resolveReferentialActions({ relation, field: (name) => model.fields.find((field) => field.name === name) });
|
|
1674
|
+
const unsafeUpdate = actions.onUpdate === "Cascade" || actions.onUpdate === "SetNull" || actions.onUpdate === "SetDefault";
|
|
1675
|
+
const unsafeDelete = actions.onDelete === "SetNull" || actions.onDelete === "SetDefault" || generatedModels.has(model.name) && actions.onDelete === "Cascade";
|
|
1676
|
+
if (unsafeUpdate || unsafeDelete)
|
|
1677
|
+
throw new VibeError({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "MySQL generated-column foreign-key dependencies require safe referential actions; opaque expressions cannot prove a base column is unaffected", meta: { feature: "generatedColumnReferentialActions", dialect, model: model.name, relation: relation.name } });
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// packages/schema/src/row-rules.ts
|
|
1682
|
+
var SQLITE_ROW_RULE_ERROR_PREFIX = "VIBEORM_ROW_RULE:";
|
|
1683
|
+
var MYSQL_ROW_RULE_MESSAGE_MAX_BYTES = 128;
|
|
1684
|
+
function mysqlRowRuleMessageByteLength(params) {
|
|
1685
|
+
return new TextEncoder().encode(`Check constraint '${params.triggerName}' is violated.`).length;
|
|
1686
|
+
}
|
|
1687
|
+
function sqliteRowRuleConstraint(params) {
|
|
1688
|
+
if (!params.message.startsWith(SQLITE_ROW_RULE_ERROR_PREFIX))
|
|
1689
|
+
return;
|
|
1690
|
+
const name = params.message.slice(SQLITE_ROW_RULE_ERROR_PREFIX.length);
|
|
1691
|
+
return name.length === 0 ? undefined : name;
|
|
1692
|
+
}
|
|
1693
|
+
var CAPABILITIES = Object.freeze({
|
|
1694
|
+
appendOnly: "rowRuleAppendOnly",
|
|
1695
|
+
fixed: "rowRuleFixed",
|
|
1696
|
+
rowFixed: "rowRuleRowFixed",
|
|
1697
|
+
frozen: "rowRuleFrozen"
|
|
1698
|
+
});
|
|
1699
|
+
function record2(params) {
|
|
1700
|
+
const { value } = params;
|
|
1701
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null) && Reflect.ownKeys(value).every((key) => {
|
|
1702
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1703
|
+
return typeof key === "string" && descriptor?.enumerable === true && "value" in descriptor;
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
function keys(params) {
|
|
1707
|
+
return params.required.every((key) => Object.hasOwn(params.value, key)) && Object.keys(params.value).every((key) => params.required.includes(key) || params.optional?.includes(key));
|
|
1708
|
+
}
|
|
1709
|
+
function strings(params) {
|
|
1710
|
+
return Array.isArray(params.value) && Object.keys(params.value).length === params.value.length && params.value.every((entry) => typeof entry === "string" && entry.length > 0);
|
|
1711
|
+
}
|
|
1712
|
+
function shapeIssue2(params) {
|
|
1713
|
+
const candidate = { value: params.value };
|
|
1714
|
+
if (!record2(candidate))
|
|
1715
|
+
return "a row rule must be a plain data object";
|
|
1716
|
+
const rule = candidate.value;
|
|
1717
|
+
const required = params.resolved ? ["kind", "name"] : ["kind"];
|
|
1718
|
+
const optional = params.resolved ? [] : ["name"];
|
|
1719
|
+
if ((params.resolved || rule.name !== undefined) && (typeof rule.name !== "string" || rule.name.trim().length === 0 || rule.name.includes("\x00")))
|
|
1720
|
+
return "name must be a nonempty physical stem without NUL bytes";
|
|
1721
|
+
switch (rule.kind) {
|
|
1722
|
+
case "appendOnly":
|
|
1723
|
+
return keys({ value: rule, required, optional }) ? undefined : "appendOnly accepts only kind and name";
|
|
1724
|
+
case "fixed": {
|
|
1725
|
+
if (!keys({ value: rule, required: [...required, "columns", "when"], optional }) || !strings({ value: rule.columns }))
|
|
1726
|
+
return "fixed requires columns and exactly one when gate; unknown keys are not accepted";
|
|
1727
|
+
const when = { value: rule.when };
|
|
1728
|
+
if (!record2(when) || !(keys({ value: when.value, required: ["fromCreation"] }) && when.value.fromCreation === true || keys({ value: when.value, required: ["onceSet"] }) && typeof when.value.onceSet === "string" && when.value.onceSet.length > 0))
|
|
1729
|
+
return "fixed.when requires exactly fromCreation: true or onceSet: a logical field name";
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
case "rowFixed":
|
|
1733
|
+
return keys({ value: rule, required: [...required, "except"], optional }) && strings({ value: rule.except }) ? undefined : "rowFixed requires an except field array and accepts no unknown keys";
|
|
1734
|
+
case "frozen": {
|
|
1735
|
+
if (!keys({ value: rule, required: [...required, "from", "mutable"], optional }) || !strings({ value: rule.mutable }))
|
|
1736
|
+
return "frozen requires from and a mutable field array; unknown keys are not accepted";
|
|
1737
|
+
const from = { value: rule.from };
|
|
1738
|
+
if (!record2(from) || !(keys({ value: from.value, required: ["create"] }) && from.value.create === true || keys({ value: from.value, required: ["field"] }) && typeof from.value.field === "string" && from.value.field.length > 0))
|
|
1739
|
+
return "frozen.from requires exactly create: true or field: a logical field name";
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
default:
|
|
1743
|
+
return "kind must be appendOnly, fixed, rowFixed or frozen";
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
function rowRuleShapeIssue(params) {
|
|
1747
|
+
return shapeIssue2({ ...params, resolved: true });
|
|
1748
|
+
}
|
|
1749
|
+
function rowRuleCapability(params) {
|
|
1750
|
+
return CAPABILITIES[params.kind];
|
|
1751
|
+
}
|
|
1752
|
+
function assertRowRuleCapability(params) {
|
|
1753
|
+
const feature = rowRuleCapability({ kind: params.rule.kind });
|
|
1754
|
+
if (!DIALECT_CAPABILITIES[params.dialect][feature])
|
|
1755
|
+
throw new VibeError({
|
|
1756
|
+
code: "VIBE_UNSUPPORTED_CAPABILITY",
|
|
1757
|
+
message: `${params.dialect} does not support declared ${params.rule.kind} row rules on ${params.model}`,
|
|
1758
|
+
meta: { dialect: params.dialect, feature, model: params.model, rule: params.rule.kind, ...params.rule.name === undefined ? {} : { name: params.rule.name } }
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
function defaultRowRuleName(params) {
|
|
1762
|
+
const table = params.model.dbName ?? params.model.name;
|
|
1763
|
+
if (params.rule.kind !== "fixed")
|
|
1764
|
+
return `vibe_rr_${table}_${params.rule.kind}`;
|
|
1765
|
+
const columns = params.rule.columns.map((name) => params.model.fields.find((field) => field.name === name)?.dbName ?? name);
|
|
1766
|
+
return `vibe_rr_${table}_${columns.join("_")}_fixed`;
|
|
1767
|
+
}
|
|
1768
|
+
function rowRuleObjectNames(params) {
|
|
1769
|
+
return { functionName: `${params.rule.name}_fn`, triggerName: `${params.rule.name}_trigger` };
|
|
1770
|
+
}
|
|
1771
|
+
function rowRuleTriggerName(params) {
|
|
1772
|
+
return `${params.rule.name}_${params.event}_trigger`;
|
|
1773
|
+
}
|
|
1774
|
+
function rowRuleGateField(params) {
|
|
1775
|
+
const { rule } = params;
|
|
1776
|
+
if (rule.kind === "fixed" && "onceSet" in rule.when)
|
|
1777
|
+
return rule.when.onceSet;
|
|
1778
|
+
if (rule.kind === "frozen" && "field" in rule.from)
|
|
1779
|
+
return rule.from.field;
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
function createRowRuleValidationContext(params) {
|
|
1783
|
+
const { schema } = params;
|
|
1784
|
+
const dialect = params.dialect ?? providerToDialect({ provider: schema.datasource.provider });
|
|
1785
|
+
let ownedObjects;
|
|
1786
|
+
let columnsByTable;
|
|
1787
|
+
const fieldsByModel = new Map;
|
|
1788
|
+
const tableKey = (table) => JSON.stringify(dialect === "mysql" ? [table.name] : [table.namespace, table.name]);
|
|
1789
|
+
const objects = () => {
|
|
1790
|
+
if (ownedObjects === undefined)
|
|
1791
|
+
ownedObjects = (schema.migrationObjects ?? []).map((object) => ({ id: object.id, identity: object.identity }));
|
|
1792
|
+
return ownedObjects;
|
|
1793
|
+
};
|
|
1794
|
+
const generatedFields = ({ model }) => {
|
|
1795
|
+
const previous = fieldsByModel.get(model);
|
|
1796
|
+
if (previous !== undefined)
|
|
1797
|
+
return previous;
|
|
1798
|
+
if (columnsByTable === undefined) {
|
|
1799
|
+
const columns = new Map;
|
|
1800
|
+
for (const object of objects()) {
|
|
1801
|
+
const identity = object.identity;
|
|
1802
|
+
if (identity.kind !== "generatedColumn")
|
|
1803
|
+
continue;
|
|
1804
|
+
const key = tableKey(identity.table);
|
|
1805
|
+
const members = columns.get(key) ?? new Set;
|
|
1806
|
+
members.add(identity.column);
|
|
1807
|
+
columns.set(key, members);
|
|
1808
|
+
}
|
|
1809
|
+
columnsByTable = columns;
|
|
1810
|
+
}
|
|
1811
|
+
const namespace = modelSqlNamespace({ schema, model, dialect }) ?? (dialect === "sqlite" ? "main" : "public");
|
|
1812
|
+
const columns = columnsByTable.get(tableKey({ namespace, name: model.dbName ?? model.name }));
|
|
1813
|
+
const fields = new Set(model.fields.filter((field) => field.generated !== undefined || columns?.has(field.dbName ?? field.name)).map((field) => field.name));
|
|
1814
|
+
for (const field of model.fields)
|
|
1815
|
+
if (field.generated !== undefined && generatedColumnShapeIssue({ value: field.generated }) !== undefined && !columns?.has(field.dbName ?? field.name))
|
|
1816
|
+
fields.delete(field.name);
|
|
1817
|
+
fieldsByModel.set(model, fields);
|
|
1818
|
+
return fields;
|
|
1819
|
+
};
|
|
1820
|
+
return {
|
|
1821
|
+
objects,
|
|
1822
|
+
generatedFields,
|
|
1823
|
+
validate: ({ model }) => validateRowRulesWithLookup({ model, dialect, generatedFields }),
|
|
1824
|
+
assertEquality: ({ model, rule }) => assertRowRuleEqualityWithGenerated({ model, rule, dialect, generated: rule.kind === "appendOnly" ? new Set : generatedFields({ model }) })
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
function rowRuleGeneratedFields(params) {
|
|
1828
|
+
const generated = createRowRuleValidationContext(params).generatedFields({ model: params.model });
|
|
1829
|
+
return params.model.fields.filter((field) => generated.has(field.name)).map((field) => field.name);
|
|
1830
|
+
}
|
|
1831
|
+
function rowRuleProtectedFields(params) {
|
|
1832
|
+
if (params.rule.kind === "appendOnly")
|
|
1833
|
+
return [];
|
|
1834
|
+
const generated = createRowRuleValidationContext(params).generatedFields({ model: params.model });
|
|
1835
|
+
return protectedFieldsWithGenerated({ model: params.model, rule: params.rule, generated });
|
|
1836
|
+
}
|
|
1837
|
+
function protectedFieldsWithGenerated(params) {
|
|
1838
|
+
const { model, rule, generated } = params;
|
|
1839
|
+
if (rule.kind === "appendOnly")
|
|
1840
|
+
return [];
|
|
1841
|
+
const gate = rowRuleGateField({ rule });
|
|
1842
|
+
if (rule.kind === "fixed") {
|
|
1843
|
+
const names = [...rule.columns, ...gate === undefined || rule.columns.includes(gate) ? [] : [gate]];
|
|
1844
|
+
return names.flatMap((name) => {
|
|
1845
|
+
const field = model.fields.find((field) => field.name === name);
|
|
1846
|
+
return field === undefined || generated.has(name) ? [] : [field];
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
return model.fields.filter((field) => {
|
|
1850
|
+
if (generated.has(field.name))
|
|
1851
|
+
return false;
|
|
1852
|
+
const except = rule.kind === "rowFixed" ? rule.except : rule.mutable;
|
|
1853
|
+
return !except.includes(field.name);
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
function rowRuleNamespace(params) {
|
|
1857
|
+
const declared = modelSqlNamespace(params);
|
|
1858
|
+
if (declared !== undefined)
|
|
1859
|
+
assertSqlNamespace({ namespace: declared, dialect: params.dialect });
|
|
1860
|
+
const canonical = declared ?? (params.dialect === "sqlite" ? "main" : "public");
|
|
1861
|
+
if (params.namespace === undefined)
|
|
1862
|
+
return canonical;
|
|
1863
|
+
const namespace = params.namespace;
|
|
1864
|
+
if (params.dialect === "sqlite" && namespace !== "main")
|
|
1865
|
+
throw new VibeError({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "SQLite row rules require the main database; attached-schema placement is not supported", meta: { dialect: params.dialect, feature: "fixedNamespaces", namespace } });
|
|
1866
|
+
if (params.dialect === "postgres") {
|
|
1867
|
+
assertSqlNamespace({ namespace, dialect: params.dialect });
|
|
1868
|
+
if (namespace !== canonical)
|
|
1869
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "A row-rule namespace binding cannot override PostgreSQL model placement", meta: { namespace, expected: canonical, model: params.model.name } });
|
|
1870
|
+
}
|
|
1871
|
+
if (namespace.trim().length === 0 || namespace.includes("\x00") || !wellFormedName({ name: namespace }) || params.dialect === "mysql" && (identifierLength({ name: namespace, dialect: "mysql" }) > 64 || /[\u{10000}-\u{10FFFF}]/u.test(namespace) || namespace.endsWith(" ")))
|
|
1872
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "A row-rule executor database binding requires a valid database identifier", meta: { dialect: params.dialect, namespace } });
|
|
1873
|
+
return namespace;
|
|
1874
|
+
}
|
|
1875
|
+
function rowRuleIdentities(params) {
|
|
1876
|
+
const dialect = params.dialect ?? providerToDialect({ provider: params.schema.datasource.provider });
|
|
1877
|
+
assertRowRuleCapability({ dialect, model: params.model.name, rule: params.rule });
|
|
1878
|
+
const namespace = rowRuleNamespace({ ...params, dialect });
|
|
1879
|
+
const table = { namespace, name: params.model.dbName ?? params.model.name };
|
|
1880
|
+
const { functionName, triggerName } = rowRuleObjectNames(params);
|
|
1881
|
+
const objects = dialect === "postgres" ? [
|
|
1882
|
+
{ role: "function", identity: { kind: "function", name: { namespace, name: functionName }, argumentTypes: [] } },
|
|
1883
|
+
{ role: "trigger", identity: { kind: "trigger", table, name: triggerName } }
|
|
1884
|
+
] : (params.rule.kind === "appendOnly" ? ["update", "delete"] : ["update"]).map((event) => ({ role: "trigger", identity: { kind: "trigger", table, name: rowRuleTriggerName({ rule: params.rule, event }) } }));
|
|
1885
|
+
return Object.freeze(objects.map((object) => Object.freeze(object)));
|
|
1886
|
+
}
|
|
1887
|
+
function schemaRowRuleIdentities(params) {
|
|
1888
|
+
return params.schema.models.flatMap((model) => (model.rowRules ?? []).flatMap((rule) => rowRuleIdentities({ ...params, model, rule }).map((object) => object.identity)));
|
|
1889
|
+
}
|
|
1890
|
+
var NATIVE_EQUALITY = Object.freeze({
|
|
1891
|
+
String: ["text", "varchar", "character varying", "char", "character", "bpchar", "uuid", "bit", "varbit", "bit varying", "inet"],
|
|
1892
|
+
Boolean: ["boolean", "bool"],
|
|
1893
|
+
Int: ["smallint", "integer", "int", "int2", "int4", "oid", "serial", "smallserial", "serial2", "serial4"],
|
|
1894
|
+
BigInt: ["bigint", "int8", "bigserial", "serial8"],
|
|
1895
|
+
Float: ["real", "float", "float4", "float8", "double precision"],
|
|
1896
|
+
Decimal: ["decimal", "numeric", "money"],
|
|
1897
|
+
DateTime: ["timestamp", "timestamptz", "timestamp with time zone", "timestamp without time zone", "date", "time", "timetz", "time with time zone", "time without time zone"],
|
|
1898
|
+
Json: ["json", "jsonb"],
|
|
1899
|
+
Bytes: ["bytea"]
|
|
1900
|
+
});
|
|
1901
|
+
var MYSQL_NATIVE_EQUALITY = Object.freeze({
|
|
1902
|
+
String: ["text", "tinytext", "mediumtext", "longtext", "varchar", "char", "character", "date"],
|
|
1903
|
+
Boolean: ["boolean", "bool", "tinyint"],
|
|
1904
|
+
Int: ["tinyint", "smallint", "mediumint", "int", "integer"],
|
|
1905
|
+
BigInt: ["bigint"],
|
|
1906
|
+
Float: ["float", "double", "double precision", "real"],
|
|
1907
|
+
Decimal: ["decimal", "dec", "numeric", "fixed"],
|
|
1908
|
+
DateTime: ["datetime", "timestamp", "date", "time"],
|
|
1909
|
+
Json: ["json"],
|
|
1910
|
+
Bytes: ["binary", "varbinary", "tinyblob", "blob", "mediumblob", "longblob"]
|
|
1911
|
+
});
|
|
1912
|
+
function equalitySupported(params) {
|
|
1913
|
+
const { field } = params;
|
|
1914
|
+
if (field.isUnsupported === true)
|
|
1915
|
+
return false;
|
|
1916
|
+
if (params.dialect === "sqlite")
|
|
1917
|
+
return true;
|
|
1918
|
+
if (field.nativeType === undefined)
|
|
1919
|
+
return true;
|
|
1920
|
+
const native = field.nativeType.trim().toLowerCase().replace(/\(\s*\d+(?:\s*,\s*\d+)*\s*\)/g, "").replace(/\s+/g, "");
|
|
1921
|
+
if (params.dialect === "mysql") {
|
|
1922
|
+
const base = native.replace(/(?:unsigned|zerofill)+$/, "");
|
|
1923
|
+
if (isEnumType(field.type))
|
|
1924
|
+
return ["text", "tinytext", "mediumtext", "longtext", "varchar", "char", "character"].includes(base);
|
|
1925
|
+
return MYSQL_NATIVE_EQUALITY[field.type]?.some((type) => type.replace(/\s+/g, "") === base) === true;
|
|
1926
|
+
}
|
|
1927
|
+
if (isEnumType(field.type))
|
|
1928
|
+
return ["text", "varchar", "char"].includes(native);
|
|
1929
|
+
return NATIVE_EQUALITY[field.type]?.some((type) => type.replace(/\s+/g, "") === native) === true || field.type === "String" && native === "date" && !field.isList;
|
|
1930
|
+
}
|
|
1931
|
+
function validateRowRules(params) {
|
|
1932
|
+
return createRowRuleValidationContext(params).validate({ model: params.model });
|
|
1933
|
+
}
|
|
1934
|
+
function wellFormedName(params) {
|
|
1935
|
+
return [...params.name].every((character) => character.length !== 1 || character.charCodeAt(0) < 55296 || character.charCodeAt(0) > 57343);
|
|
1936
|
+
}
|
|
1937
|
+
function validateRowRulesWithLookup(params) {
|
|
1938
|
+
const { model, dialect } = params;
|
|
1939
|
+
const rules = model.rowRules;
|
|
1940
|
+
if (rules === undefined)
|
|
1941
|
+
return [];
|
|
1942
|
+
if (!Array.isArray(rules))
|
|
1943
|
+
return [{ rule: "row-rule-shape", model: model.name, message: "rowRules must be an array of canonical declarations" }];
|
|
1944
|
+
const issues = [];
|
|
1945
|
+
const add = (params) => {
|
|
1946
|
+
issues.push({ rule: params.rule ?? "row-rule-invalid", model: model.name, ...params });
|
|
1947
|
+
};
|
|
1948
|
+
const seenKinds = new Set;
|
|
1949
|
+
const seenDefinitions = new Set;
|
|
1950
|
+
const seenNames = new Set;
|
|
1951
|
+
for (const value of rules) {
|
|
1952
|
+
const shape = rowRuleShapeIssue({ value });
|
|
1953
|
+
if (shape !== undefined) {
|
|
1954
|
+
add({ rule: "row-rule-shape", message: shape });
|
|
1955
|
+
continue;
|
|
1956
|
+
}
|
|
1957
|
+
const rule = value;
|
|
1958
|
+
const generated = rule.kind === "appendOnly" ? new Set : params.generatedFields({ model });
|
|
1959
|
+
if (seenNames.has(rule.name))
|
|
1960
|
+
add({ rule: "row-rule-name-collision", message: `Row rules share the physical stem ${rule.name}; supply an explicit, distinct name stem` });
|
|
1961
|
+
seenNames.add(rule.name);
|
|
1962
|
+
if (model.isView)
|
|
1963
|
+
add({ message: "Row rules require a stored table, not a view" });
|
|
1964
|
+
if (rule.kind !== "fixed" && seenKinds.has(rule.kind))
|
|
1965
|
+
add({ message: `Only one ${rule.kind} row rule is allowed per model` });
|
|
1966
|
+
seenKinds.add(rule.kind);
|
|
1967
|
+
const definition = rule.kind === "fixed" ? JSON.stringify([rule.kind, [...rule.columns].sort(), rule.when]) : rule.kind;
|
|
1968
|
+
if (seenDefinitions.has(definition))
|
|
1969
|
+
add({ message: `Duplicate ${rule.kind} row-rule declaration` });
|
|
1970
|
+
seenDefinitions.add(definition);
|
|
1971
|
+
const pair = rowRuleObjectNames({ rule });
|
|
1972
|
+
const names = dialect === "postgres" ? [pair.functionName, pair.triggerName] : (rule.kind === "appendOnly" ? ["update", "delete"] : ["update"]).map((event) => rowRuleTriggerName({ rule, event }));
|
|
1973
|
+
for (const name of names) {
|
|
1974
|
+
const limit = IDENTIFIER_LIMITS[dialect];
|
|
1975
|
+
if (limit !== undefined && identifierLength({ name, dialect }) > limit.max)
|
|
1976
|
+
add({ rule: "row-rule-name-length", message: `Row-rule object ${name} exceeds ${limit.max} ${limit.unit}; supply a shorter name stem` });
|
|
1977
|
+
if (!wellFormedName({ name }))
|
|
1978
|
+
add({ rule: "row-rule-name-encoding", message: "Row-rule names require valid Unicode without unpaired surrogates" });
|
|
1979
|
+
if (dialect === "mysql" && /[^\x00-\x7F]/.test(name))
|
|
1980
|
+
add({ rule: "row-rule-name-collation", message: "MySQL row-rule stems require ASCII for exact trigger-name collision admission; supply an explicit ASCII name stem" });
|
|
1981
|
+
if (dialect === "mysql" && mysqlRowRuleMessageByteLength({ triggerName: name }) > MYSQL_ROW_RULE_MESSAGE_MAX_BYTES)
|
|
1982
|
+
add({ rule: "row-rule-diagnostic-length", message: `MySQL row-rule MESSAGE_TEXT exceeds ${MYSQL_ROW_RULE_MESSAGE_MAX_BYTES} UTF-8 bytes; supply a shorter name stem` });
|
|
1983
|
+
}
|
|
1984
|
+
const references = rule.kind === "fixed" ? rule.columns : rule.kind === "rowFixed" ? rule.except : rule.kind === "frozen" ? rule.mutable : [];
|
|
1985
|
+
if (new Set(references).size !== references.length)
|
|
1986
|
+
add({ message: `${rule.kind} field arrays require distinct members` });
|
|
1987
|
+
for (const name of references) {
|
|
1988
|
+
const field = model.fields.find((field) => field.name === name);
|
|
1989
|
+
if (field === undefined)
|
|
1990
|
+
add({ message: `Unknown or relation field ${name} in ${rule.kind}; use stored logical field names`, field: name });
|
|
1991
|
+
else if (generated.has(name))
|
|
1992
|
+
add({ message: `Generated field ${name} cannot be listed in ${rule.kind}; generated values are already excluded`, field: name });
|
|
1993
|
+
}
|
|
1994
|
+
const gate = rowRuleGateField({ rule });
|
|
1995
|
+
if (gate !== undefined) {
|
|
1996
|
+
const field = model.fields.find((field) => field.name === gate);
|
|
1997
|
+
if (field === undefined || field.isList || !field.isOptional || generated.has(gate))
|
|
1998
|
+
add({ message: `${rule.kind} gate ${gate} must be a nullable, non-generated scalar field`, field: gate });
|
|
1999
|
+
if (rule.kind === "frozen" && rule.mutable.includes(gate))
|
|
2000
|
+
add({ message: `Frozen gate ${gate} cannot be mutable; it cannot be cleared after activation`, field: gate });
|
|
2001
|
+
}
|
|
2002
|
+
if (rule.kind !== "appendOnly" && (rule.kind === "fixed" && rule.columns.length === 0 || protectedFieldsWithGenerated({ model, rule, generated }).length === 0))
|
|
2003
|
+
add({ message: `${rule.kind} requires at least one protected stored field` });
|
|
2004
|
+
}
|
|
2005
|
+
if (seenKinds.has("appendOnly") && rules.length > 1)
|
|
2006
|
+
add({ message: "appendOnly cannot combine with other row rules; it already refuses every UPDATE and DELETE" });
|
|
2007
|
+
return issues;
|
|
2008
|
+
}
|
|
2009
|
+
function normalizeRowRules(params) {
|
|
2010
|
+
if (!Array.isArray(params.rules))
|
|
2011
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "rowRules must produce an array of declarations", meta: { model: params.model.name } });
|
|
2012
|
+
const modelOrder = (fields) => [...fields].sort((a, b) => params.model.fields.findIndex((field) => field.name === a) - params.model.fields.findIndex((field) => field.name === b));
|
|
2013
|
+
const rules = params.rules.map((value) => {
|
|
2014
|
+
const issue = shapeIssue2({ value, resolved: false });
|
|
2015
|
+
if (issue !== undefined)
|
|
2016
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: issue, meta: { model: params.model.name } });
|
|
2017
|
+
const rule = value;
|
|
2018
|
+
const name = rule.name ?? defaultRowRuleName({ model: params.model, rule });
|
|
2019
|
+
switch (rule.kind) {
|
|
2020
|
+
case "appendOnly":
|
|
2021
|
+
return Object.freeze({ kind: rule.kind, name });
|
|
2022
|
+
case "fixed":
|
|
2023
|
+
return Object.freeze({ kind: rule.kind, name, columns: Object.freeze([...rule.columns]), when: Object.freeze({ ...rule.when }) });
|
|
2024
|
+
case "rowFixed":
|
|
2025
|
+
return Object.freeze({ kind: rule.kind, name, except: Object.freeze(modelOrder(rule.except)) });
|
|
2026
|
+
case "frozen":
|
|
2027
|
+
return Object.freeze({ kind: rule.kind, name, from: Object.freeze({ ...rule.from }), mutable: Object.freeze(modelOrder(rule.mutable)) });
|
|
2028
|
+
}
|
|
2029
|
+
});
|
|
2030
|
+
const model = { ...params.model, rowRules: rules };
|
|
2031
|
+
const schema = params.schema ?? { datasource: { provider: params.dialect === "postgres" ? "postgresql" : params.dialect }, models: [model], enums: [], extensions: [] };
|
|
2032
|
+
const context = createRowRuleValidationContext({ schema, dialect: params.dialect });
|
|
2033
|
+
const issues = context.validate({ model });
|
|
2034
|
+
if (issues.length > 0)
|
|
2035
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: issues.map((issue) => issue.message).join("; "), meta: { issues } });
|
|
2036
|
+
for (const rule of rules) {
|
|
2037
|
+
assertRowRuleCapability({ dialect: params.dialect, model: model.name, rule });
|
|
2038
|
+
context.assertEquality({ model, rule });
|
|
2039
|
+
}
|
|
2040
|
+
assertRowRuleReferentialActions({ schema: { ...schema, models: [...schema.models.filter((entry) => entry.name !== model.name), model] }, dialect: params.dialect });
|
|
2041
|
+
return Object.freeze(rules);
|
|
2042
|
+
}
|
|
2043
|
+
function assertRowRuleEquality(params) {
|
|
2044
|
+
createRowRuleValidationContext(params).assertEquality({ model: params.model, rule: params.rule });
|
|
2045
|
+
}
|
|
2046
|
+
function assertRowRuleEqualityWithGenerated(params) {
|
|
2047
|
+
for (const field of protectedFieldsWithGenerated(params)) {
|
|
2048
|
+
if (!equalitySupported({ field, dialect: params.dialect }))
|
|
2049
|
+
throw new VibeError({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `Row rule ${params.rule.name} cannot compare ${params.model.name}.${field.name}; exempt it explicitly or use an admitted built-in type`, meta: { dialect: params.dialect, feature: "rowRuleEquality", model: params.model.name, field: field.name, rule: params.rule.kind, nativeType: field.nativeType } });
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
function assertRowRuleReferentialActions(params) {
|
|
2053
|
+
const dialect = params.dialect ?? providerToDialect({ provider: params.schema.datasource.provider });
|
|
2054
|
+
if (dialect !== "mysql")
|
|
2055
|
+
return;
|
|
2056
|
+
const context = createRowRuleValidationContext({ schema: params.schema, dialect });
|
|
2057
|
+
const mutates = (action) => action === "Cascade" || action === "SetNull" || action === "SetDefault";
|
|
2058
|
+
for (const model of params.schema.models) {
|
|
2059
|
+
if (!Array.isArray(model.rowRules) || model.rowRules.length === 0)
|
|
2060
|
+
continue;
|
|
2061
|
+
for (const relation of model.relations) {
|
|
2062
|
+
const columns = relation.fields;
|
|
2063
|
+
if (columns === undefined || columns.length === 0 || params.schema.models.some((target) => target.name === relation.targetModel && target.isView))
|
|
2064
|
+
continue;
|
|
2065
|
+
const actions = resolveReferentialActions({ relation, field: (name) => model.fields.find((field) => field.name === name) });
|
|
2066
|
+
for (const rule of model.rowRules) {
|
|
2067
|
+
if (rowRuleShapeIssue({ value: rule }) !== undefined)
|
|
2068
|
+
continue;
|
|
2069
|
+
const protectedColumns = rule.kind === "appendOnly" ? [] : protectedFieldsWithGenerated({ model, rule, generated: context.generatedFields({ model }) }).map((field) => field.name);
|
|
2070
|
+
const field = columns.find((name) => protectedColumns.includes(name));
|
|
2071
|
+
const conservative = rule.kind === "appendOnly" || rule.kind === "fixed" && (actions.onUpdate === "Cascade" || actions.onDelete === "Cascade");
|
|
2072
|
+
const unsafeUpdate = mutates(actions.onUpdate) && (conservative || field !== undefined);
|
|
2073
|
+
const unsafeDelete = (rule.kind === "appendOnly" || rule.kind === "fixed") && actions.onDelete === "Cascade" || (actions.onDelete === "SetNull" || actions.onDelete === "SetDefault") && (rule.kind === "appendOnly" || field !== undefined);
|
|
2074
|
+
if (!unsafeUpdate && !unsafeDelete)
|
|
2075
|
+
continue;
|
|
2076
|
+
throw new VibeError({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `MySQL relation ${model.name}.${relation.name} can bypass row rule ${rule.name} through a foreign-key action; declare safe Restrict/NoAction actions or change the protected fields`, meta: { feature: "rowRuleReferentialActions", dialect, model: model.name, relation: relation.name, rule: rule.kind, name: rule.name, onUpdate: actions.onUpdate, onDelete: actions.onDelete, action: unsafeUpdate ? "onUpdate" : "onDelete", ...field === undefined ? {} : { field } } });
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
function rowRulePrismaAttribute(params) {
|
|
2082
|
+
const { rule } = params;
|
|
2083
|
+
const name = `name: ${encodePrismaString({ value: rule.name })}`;
|
|
2084
|
+
switch (rule.kind) {
|
|
2085
|
+
case "appendOnly":
|
|
2086
|
+
return `@@appendOnly(${name})`;
|
|
2087
|
+
case "fixed":
|
|
2088
|
+
return `@@fixed([${rule.columns.join(", ")}], ${"fromCreation" in rule.when ? "fromCreation: true" : `onceSet: ${rule.when.onceSet}`}, ${name})`;
|
|
2089
|
+
case "rowFixed":
|
|
2090
|
+
return `@@rowFixed(except: [${rule.except.join(", ")}], ${name})`;
|
|
2091
|
+
case "frozen":
|
|
2092
|
+
return `@@frozen(${"create" in rule.from ? "fromCreation: true" : `from: ${rule.from.field}`}, mutable: [${rule.mutable.join(", ")}], ${name})`;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
// packages/schema/src/migration-objects.ts
|
|
2096
|
+
function sqlObjectKey(params) {
|
|
2097
|
+
const object = params.identity;
|
|
2098
|
+
switch (object.kind) {
|
|
2099
|
+
case "namespace":
|
|
2100
|
+
return JSON.stringify([object.kind, object.name]);
|
|
2101
|
+
case "table":
|
|
2102
|
+
case "view":
|
|
2103
|
+
case "sequence":
|
|
2104
|
+
case "postgresExtension":
|
|
2105
|
+
case "enum":
|
|
2106
|
+
return JSON.stringify([object.kind, object.name.namespace, object.name.name]);
|
|
2107
|
+
case "function":
|
|
2108
|
+
return JSON.stringify([object.kind, object.name.namespace, object.name.name, object.argumentTypes.map((type) => [type.namespace, type.name])]);
|
|
2109
|
+
case "index":
|
|
2110
|
+
case "trigger":
|
|
2111
|
+
return JSON.stringify([object.kind, object.table.namespace, object.table.name, object.name]);
|
|
2112
|
+
case "constraint":
|
|
2113
|
+
return JSON.stringify([object.kind, object.table.namespace, object.table.name, object.name, object.constraintType]);
|
|
2114
|
+
case "column":
|
|
2115
|
+
case "columnDefault":
|
|
2116
|
+
case "generatedColumn":
|
|
2117
|
+
return JSON.stringify([object.kind, object.table.namespace, object.table.name, object.column]);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
function sqlObjectNamespace(params) {
|
|
625
2121
|
const identity = params.identity;
|
|
626
2122
|
if (identity.kind === "namespace")
|
|
627
2123
|
return identity.name;
|
|
628
2124
|
return "table" in identity ? identity.table.namespace : identity.name.namespace;
|
|
629
2125
|
}
|
|
630
|
-
function
|
|
2126
|
+
function record3(value) {
|
|
631
2127
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
632
2128
|
}
|
|
633
2129
|
function name(value) {
|
|
634
|
-
return
|
|
2130
|
+
return record3(value) && typeof value.namespace === "string" && value.namespace.length > 0 && typeof value.name === "string" && value.name.length > 0;
|
|
635
2131
|
}
|
|
636
2132
|
function isSqlObjectIdentity(value) {
|
|
637
|
-
if (!
|
|
2133
|
+
if (!record3(value))
|
|
638
2134
|
return false;
|
|
639
2135
|
switch (value.kind) {
|
|
640
2136
|
case "namespace":
|
|
@@ -661,17 +2157,17 @@ function isSqlObjectIdentity(value) {
|
|
|
661
2157
|
}
|
|
662
2158
|
}
|
|
663
2159
|
function isSqlObjectDefinition(value) {
|
|
664
|
-
return
|
|
2160
|
+
return record3(value) && (value.format === "catalog-v1" || value.format === "catalog-v2") && isSqlObjectIdentity(value.identity) && record3(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);
|
|
665
2161
|
}
|
|
666
2162
|
function qualifiedNameIssue(params) {
|
|
667
|
-
if (!
|
|
2163
|
+
if (!record3(params.value))
|
|
668
2164
|
return { path: params.path, expected: "qualified SQL name { namespace, name }" };
|
|
669
2165
|
const member = typeof params.value.namespace !== "string" || params.value.namespace.length === 0 ? "namespace" : "name";
|
|
670
2166
|
return { path: `${params.path}.${member}`, expected: "non-empty string" };
|
|
671
2167
|
}
|
|
672
2168
|
function identityIssue(params) {
|
|
673
2169
|
const { value, path } = params;
|
|
674
|
-
if (!
|
|
2170
|
+
if (!record3(value))
|
|
675
2171
|
return { path, expected: "SQL object identity" };
|
|
676
2172
|
switch (value.kind) {
|
|
677
2173
|
case "namespace":
|
|
@@ -710,13 +2206,13 @@ function identityIssue(params) {
|
|
|
710
2206
|
}
|
|
711
2207
|
function definitionIssue(params) {
|
|
712
2208
|
const { value, path } = params;
|
|
713
|
-
if (!
|
|
2209
|
+
if (!record3(value))
|
|
714
2210
|
return { path, expected: "independent SQL object definition" };
|
|
715
2211
|
if (value.format !== "catalog-v1" && value.format !== "catalog-v2")
|
|
716
2212
|
return { path: `${path}.format`, expected: ["catalog-v1", "catalog-v2"] };
|
|
717
2213
|
if (!isSqlObjectIdentity(value.identity))
|
|
718
2214
|
return identityIssue({ value: value.identity, path: `${path}.identity` });
|
|
719
|
-
if (!
|
|
2215
|
+
if (!record3(value.facts) || Object.keys(value.facts).length === 0)
|
|
720
2216
|
return { path: `${path}.facts`, expected: "non-empty serializable catalog facts" };
|
|
721
2217
|
for (const [key, fact] of Object.entries(value.facts)) {
|
|
722
2218
|
if (!(fact === null || typeof fact === "string" || typeof fact === "boolean" || typeof fact === "number" && Number.isFinite(fact) || Array.isArray(fact) && fact.every((entry) => typeof entry === "string")))
|
|
@@ -743,7 +2239,7 @@ function assertMigrationObjects(params) {
|
|
|
743
2239
|
for (const [index, value] of params.objects.entries()) {
|
|
744
2240
|
const object = value;
|
|
745
2241
|
const path = `migrationObjects[${index}]`;
|
|
746
|
-
if (!
|
|
2242
|
+
if (!record3(object))
|
|
747
2243
|
invalidDeclaration({ path, expected: "object" });
|
|
748
2244
|
if (typeof object.id !== "string" || object.id.length === 0)
|
|
749
2245
|
invalidDeclaration({ path: `${path}.id`, expected: "non-empty string" });
|
|
@@ -784,9 +2280,14 @@ function assertMigrationObjects(params) {
|
|
|
784
2280
|
function assertIndexOperatorClasses(params) {
|
|
785
2281
|
for (const model of params.schema.models)
|
|
786
2282
|
for (const index of model.indexes)
|
|
787
|
-
for (const
|
|
2283
|
+
for (const term of indexKeyTerms({ index })) {
|
|
2284
|
+
if (term.kind !== "field")
|
|
2285
|
+
continue;
|
|
2286
|
+
const field = term.field;
|
|
788
2287
|
if (field.operatorClass === undefined)
|
|
789
2288
|
continue;
|
|
2289
|
+
if (isExpressionIndex({ index }))
|
|
2290
|
+
throw unsupportedCapability({ dialect: params.dialect, feature: "expressionIndexOperatorClasses" });
|
|
790
2291
|
if (!DIALECT_CAPABILITIES[params.dialect].indexOperatorClasses)
|
|
791
2292
|
throw unsupportedCapability({ dialect: params.dialect, feature: "indexOperatorClasses" });
|
|
792
2293
|
if (index.kind === "unique" && params.physical !== true)
|
|
@@ -802,176 +2303,100 @@ function prepareDatabaseModuleSchema(params) {
|
|
|
802
2303
|
const dialect = providerToDialect({ provider: schema.datasource.provider });
|
|
803
2304
|
if (namespace !== undefined)
|
|
804
2305
|
assertSqlNamespace({ namespace, dialect, allowPublic: false });
|
|
805
|
-
const expected = namespace ?? SQL_NAMESPACES.postgres;
|
|
806
|
-
for (const entry of [...schema.models, ...schema.enums]) {
|
|
807
|
-
if (entry.namespace === undefined)
|
|
808
|
-
continue;
|
|
809
|
-
assertSqlNamespace({ namespace: entry.namespace, dialect });
|
|
810
|
-
if (qualifiedSqlName({ name: entry.dbName ?? entry.name, namespace: entry.namespace }).namespace !== expected)
|
|
811
|
-
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 } });
|
|
812
|
-
}
|
|
813
|
-
for (const model of schema.models)
|
|
814
|
-
for (const field of model.fields) {
|
|
815
|
-
if (!isEnumType(field.type) || field.type.namespace === undefined)
|
|
816
|
-
continue;
|
|
817
|
-
assertSqlNamespace({ namespace: field.type.namespace, dialect });
|
|
818
|
-
if (field.type.namespace !== expected)
|
|
819
|
-
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 } });
|
|
820
|
-
}
|
|
821
|
-
if (namespace === undefined)
|
|
822
|
-
return schema;
|
|
823
|
-
const nativeEnums = new Set(schema.enums.filter((entry) => enumStorageOf({ enumDef: entry }) === "database").map((entry) => entry.name));
|
|
824
|
-
return {
|
|
825
|
-
...schema,
|
|
826
|
-
models: schema.models.map((model) => ({ ...model, namespace, fields: model.fields.map((field) => {
|
|
827
|
-
const type = field.type;
|
|
828
|
-
return isEnumType(type) && nativeEnums.has(type.enum) ? { ...field, type: { ...type, namespace } } : field;
|
|
829
|
-
}) })),
|
|
830
|
-
enums: schema.enums.map((entry) => enumStorageOf({ enumDef: entry }) === "database" ? { ...entry, namespace } : entry)
|
|
831
|
-
};
|
|
832
|
-
}
|
|
833
|
-
// packages/schema/src/predicate.ts
|
|
834
|
-
var PREDICATE_COMPARISON_OPS = [
|
|
835
|
-
"eq",
|
|
836
|
-
"ne",
|
|
837
|
-
"gt",
|
|
838
|
-
"gte",
|
|
839
|
-
"lt",
|
|
840
|
-
"lte"
|
|
841
|
-
];
|
|
842
|
-
function isPredicateFieldRef(operand) {
|
|
843
|
-
return operand.kind === "field";
|
|
844
|
-
}
|
|
845
|
-
function predicateFieldNames(params) {
|
|
846
|
-
return [...new Set(inspectPredicate(params).expressions.flatMap((expression) => expression.kind === "field" ? [expression.name] : []))];
|
|
847
|
-
}
|
|
848
|
-
function inspectPredicate(params) {
|
|
849
|
-
const expressions = [];
|
|
850
|
-
let checkOnly = false;
|
|
851
|
-
const expression = (node) => {
|
|
852
|
-
if (typeof node !== "object" || node === null)
|
|
853
|
-
throw invalidOperand();
|
|
854
|
-
switch (node.kind) {
|
|
855
|
-
case "field":
|
|
856
|
-
expressions.push(node);
|
|
857
|
-
return;
|
|
858
|
-
case "jsonText":
|
|
859
|
-
if (typeof node.field !== "object" || node.field === null || node.field.kind !== "field")
|
|
860
|
-
throw invalidOperand();
|
|
861
|
-
expressions.push(node);
|
|
862
|
-
checkOnly = true;
|
|
863
|
-
expression(node.field);
|
|
864
|
-
return;
|
|
865
|
-
case "length":
|
|
866
|
-
expressions.push(node);
|
|
867
|
-
checkOnly = true;
|
|
868
|
-
expression(node.expression);
|
|
869
|
-
return;
|
|
870
|
-
default:
|
|
871
|
-
throw invalidOperand();
|
|
872
|
-
}
|
|
873
|
-
};
|
|
874
|
-
const walk = (node) => {
|
|
875
|
-
switch (node.kind) {
|
|
876
|
-
case "compare":
|
|
877
|
-
expression(node.left);
|
|
878
|
-
if (typeof node.right !== "object" || node.right === null || node.right.kind !== "value")
|
|
879
|
-
expression(node.right);
|
|
880
|
-
return;
|
|
881
|
-
case "inList":
|
|
882
|
-
case "isNull":
|
|
883
|
-
expression(node.field);
|
|
884
|
-
return;
|
|
885
|
-
case "and":
|
|
886
|
-
case "or":
|
|
887
|
-
for (const operand of node.operands)
|
|
888
|
-
walk(operand);
|
|
889
|
-
return;
|
|
890
|
-
case "isTrue":
|
|
891
|
-
checkOnly = true;
|
|
892
|
-
walk(node.operand);
|
|
893
|
-
return;
|
|
894
|
-
case "not":
|
|
895
|
-
walk(node.operand);
|
|
896
|
-
return;
|
|
897
|
-
}
|
|
898
|
-
};
|
|
899
|
-
walk(params.predicate);
|
|
900
|
-
return { expressions, checkOnly };
|
|
901
|
-
}
|
|
902
|
-
function invalidOperand() {
|
|
903
|
-
return new VibeError({
|
|
904
|
-
code: "VIBE_SCHEMA_INVALID",
|
|
905
|
-
message: "A schema predicate operand must be a field, jsonText or length expression, or a literal on the right of a comparison",
|
|
906
|
-
meta: { rule: "predicate-operand-invalid" }
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
function assertPredicateCapabilities(params) {
|
|
910
|
-
if (!inspectPredicate(params).checkOnly)
|
|
911
|
-
return;
|
|
912
|
-
if (params.usage !== "check")
|
|
913
|
-
throw unsupportedCapability({ dialect: params.dialect, feature: "schema CHECK expressions outside CHECK constraints" });
|
|
914
|
-
if (!DIALECT_CAPABILITIES[params.dialect].schemaCheckExpressions)
|
|
915
|
-
throw unsupportedCapability({ dialect: params.dialect, feature: "schemaCheckExpressions" });
|
|
916
|
-
}
|
|
917
|
-
function predicateKey(params) {
|
|
918
|
-
if (inspectPredicate(params).checkOnly) {
|
|
919
|
-
const scalar = (node) => {
|
|
920
|
-
switch (node.kind) {
|
|
921
|
-
case "field":
|
|
922
|
-
return ["field", node.name];
|
|
923
|
-
case "jsonText":
|
|
924
|
-
return ["jsonText", node.field.name, node.path];
|
|
925
|
-
case "length":
|
|
926
|
-
return ["length", scalar(node.expression)];
|
|
927
|
-
}
|
|
928
|
-
};
|
|
929
|
-
const key = (node) => {
|
|
930
|
-
switch (node.kind) {
|
|
931
|
-
case "compare":
|
|
932
|
-
return [node.kind, node.op, scalar(node.left), node.right.kind === "value" ? ["value", node.right.value] : scalar(node.right)];
|
|
933
|
-
case "inList":
|
|
934
|
-
return [node.kind, node.field.name, node.negate === true, node.values.map((value) => value.value)];
|
|
935
|
-
case "isNull":
|
|
936
|
-
return [node.kind, node.field.name, node.negate === true];
|
|
937
|
-
case "and":
|
|
938
|
-
case "or":
|
|
939
|
-
return [node.kind, node.operands.map(key)];
|
|
940
|
-
case "not":
|
|
941
|
-
case "isTrue":
|
|
942
|
-
return [node.kind, key(node.operand)];
|
|
943
|
-
}
|
|
944
|
-
};
|
|
945
|
-
return `check:${JSON.stringify(key(params.predicate))}`;
|
|
2306
|
+
const expected = namespace ?? SQL_NAMESPACES.postgres;
|
|
2307
|
+
for (const entry of [...schema.models, ...schema.enums]) {
|
|
2308
|
+
if (entry.namespace === undefined)
|
|
2309
|
+
continue;
|
|
2310
|
+
assertSqlNamespace({ namespace: entry.namespace, dialect });
|
|
2311
|
+
if (qualifiedSqlName({ name: entry.dbName ?? entry.name, namespace: entry.namespace }).namespace !== expected)
|
|
2312
|
+
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 } });
|
|
946
2313
|
}
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
return `length(${expression(node.expression)})`;
|
|
2314
|
+
for (const model of schema.models)
|
|
2315
|
+
for (const field of model.fields) {
|
|
2316
|
+
if (!isEnumType(field.type) || field.type.namespace === undefined)
|
|
2317
|
+
continue;
|
|
2318
|
+
assertSqlNamespace({ namespace: field.type.namespace, dialect });
|
|
2319
|
+
if (field.type.namespace !== expected)
|
|
2320
|
+
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 } });
|
|
955
2321
|
}
|
|
2322
|
+
if (namespace === undefined)
|
|
2323
|
+
return schema;
|
|
2324
|
+
const nativeEnums = new Set(schema.enums.filter((entry) => enumStorageOf({ enumDef: entry }) === "database").map((entry) => entry.name));
|
|
2325
|
+
return {
|
|
2326
|
+
...schema,
|
|
2327
|
+
models: schema.models.map((model) => ({ ...model, namespace, fields: model.fields.map((field) => {
|
|
2328
|
+
const type = field.type;
|
|
2329
|
+
return isEnumType(type) && nativeEnums.has(type.enum) ? { ...field, type: { ...type, namespace } } : field;
|
|
2330
|
+
}) })),
|
|
2331
|
+
enums: schema.enums.map((entry) => enumStorageOf({ enumDef: entry }) === "database" ? { ...entry, namespace } : entry)
|
|
956
2332
|
};
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
2333
|
+
}
|
|
2334
|
+
// packages/schema/src/check-rules.ts
|
|
2335
|
+
var CHECK_RULE_GROUP_MAX = 8;
|
|
2336
|
+
var field = (name) => ({ kind: "field", name });
|
|
2337
|
+
var value = (literal) => ({ kind: "value", value: literal });
|
|
2338
|
+
var isNull = (name) => ({ kind: "isNull", field: field(name) });
|
|
2339
|
+
var isNotNull = (name) => ({ kind: "isNull", field: field(name), negate: true });
|
|
2340
|
+
var and = (operands) => operands.length === 1 ? operands[0] : { kind: "and", operands };
|
|
2341
|
+
var or = (operands) => operands.length === 1 ? operands[0] : { kind: "or", operands };
|
|
2342
|
+
function notMember(params) {
|
|
2343
|
+
const { by, members } = params;
|
|
2344
|
+
return members.length === 1 ? { kind: "compare", op: "ne", left: field(by), right: value(members[0]) } : { kind: "inList", field: field(by), values: members.map((member) => value(member)), negate: true };
|
|
2345
|
+
}
|
|
2346
|
+
function invalid2(params) {
|
|
2347
|
+
return new VibeError({ code: "VIBE_SCHEMA_INVALID", message: params.message, meta: { rule: params.rule, ...params.meta ?? {} } });
|
|
2348
|
+
}
|
|
2349
|
+
function assertGroup(params) {
|
|
2350
|
+
const { kind, fields } = params;
|
|
2351
|
+
if (fields.length < 2 || fields.length > CHECK_RULE_GROUP_MAX) {
|
|
2352
|
+
throw invalid2({ rule: "check-rule-group-arity", message: `${kind}() takes 2 to ${CHECK_RULE_GROUP_MAX} columns, not ${fields.length}`, meta: { kind, fields: [...fields] } });
|
|
2353
|
+
}
|
|
2354
|
+
if (new Set(fields).size !== fields.length) {
|
|
2355
|
+
throw invalid2({ rule: "check-rule-duplicate-field", message: `${kind}() names a column twice (${fields.join(", ")}) \u2014 the expansion would contradict itself`, meta: { kind, fields: [...fields] } });
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
function checkRulePredicate(params) {
|
|
2359
|
+
const { rule } = params;
|
|
2360
|
+
switch (rule.kind) {
|
|
2361
|
+
case "compare":
|
|
2362
|
+
return { kind: "compare", op: rule.op, left: field(rule.field), right: value(rule.value) };
|
|
2363
|
+
case "range":
|
|
2364
|
+
return and([
|
|
2365
|
+
{ kind: "compare", op: "gte", left: field(rule.field), right: value(rule.min) },
|
|
2366
|
+
{ kind: "compare", op: "lte", left: field(rule.field), right: value(rule.max) }
|
|
2367
|
+
]);
|
|
2368
|
+
case "oneOf":
|
|
2369
|
+
if (rule.values.length === 0)
|
|
2370
|
+
throw invalid2({ rule: "check-rule-empty", message: `oneOf() on "${rule.field}" has no values \u2014 an empty IN list is never what an author means`, meta: { field: rule.field } });
|
|
2371
|
+
return { kind: "inList", field: field(rule.field), values: rule.values.map((entry) => value(entry)) };
|
|
2372
|
+
case "lengthAtMost":
|
|
2373
|
+
return { kind: "compare", op: "lte", left: { kind: "length", expression: field(rule.field) }, right: value(rule.max) };
|
|
2374
|
+
case "order":
|
|
2375
|
+
return { kind: "compare", op: rule.op, left: field(rule.left), right: field(rule.right) };
|
|
2376
|
+
case "allOrNone":
|
|
2377
|
+
assertGroup({ kind: rule.kind, fields: rule.fields });
|
|
2378
|
+
return or([and(rule.fields.map(isNull)), and(rule.fields.map(isNotNull))]);
|
|
2379
|
+
case "exactlyOne":
|
|
2380
|
+
case "atMostOne": {
|
|
2381
|
+
assertGroup({ kind: rule.kind, fields: rule.fields });
|
|
2382
|
+
const one = rule.fields.map((name) => and([isNotNull(name), ...rule.fields.filter((other) => other !== name).map(isNull)]));
|
|
2383
|
+
return or(rule.kind === "exactlyOne" ? one : [...one, and(rule.fields.map(isNull))]);
|
|
972
2384
|
}
|
|
973
|
-
|
|
974
|
-
|
|
2385
|
+
case "anySet":
|
|
2386
|
+
assertGroup({ kind: rule.kind, fields: rule.fields });
|
|
2387
|
+
return or(rule.fields.map(isNotNull));
|
|
2388
|
+
case "presence": {
|
|
2389
|
+
if (rule.set.length === 0 && rule.unset.length === 0) {
|
|
2390
|
+
throw invalid2({ rule: "presence-empty", message: `presence rule for "${rule.field}" names no member \u2014 it would hold nothing`, meta: { field: rule.field, by: rule.by } });
|
|
2391
|
+
}
|
|
2392
|
+
const halves = [];
|
|
2393
|
+
if (rule.set.length > 0)
|
|
2394
|
+
halves.push(or([notMember({ by: rule.by, members: rule.set }), isNotNull(rule.field)]));
|
|
2395
|
+
if (rule.unset.length > 0)
|
|
2396
|
+
halves.push(or([notMember({ by: rule.by, members: rule.unset }), isNull(rule.field)]));
|
|
2397
|
+
return and(halves);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
975
2400
|
}
|
|
976
2401
|
// packages/schema/src/time-of-day.ts
|
|
977
2402
|
var TIME_OF_DAY_NATIVE_TYPE = /^time(tz)?(?:\(\d+\))?$|^time(?:\(\d+\))?with(out)?timezone$/;
|
|
@@ -987,114 +2412,6 @@ function timeOfDayType(params) {
|
|
|
987
2412
|
return "timetz";
|
|
988
2413
|
return "time";
|
|
989
2414
|
}
|
|
990
|
-
// packages/schema/src/prisma-literal.ts
|
|
991
|
-
var STRING_ESCAPES = {
|
|
992
|
-
'"': '"',
|
|
993
|
-
"'": "'",
|
|
994
|
-
"\\": "\\",
|
|
995
|
-
"/": "/",
|
|
996
|
-
n: `
|
|
997
|
-
`,
|
|
998
|
-
r: "\r",
|
|
999
|
-
t: "\t",
|
|
1000
|
-
b: "\b",
|
|
1001
|
-
f: "\f"
|
|
1002
|
-
};
|
|
1003
|
-
function malformed(params) {
|
|
1004
|
-
return new VibeError({
|
|
1005
|
-
code: "VIBE_SCHEMA_INVALID",
|
|
1006
|
-
message: `Malformed string literal ${JSON.stringify(params.raw)}: ${params.detail}`,
|
|
1007
|
-
meta: { issues: [{ rule: "malformed-string-literal", message: params.detail }], literal: params.raw }
|
|
1008
|
-
});
|
|
1009
|
-
}
|
|
1010
|
-
function decodePrismaString(params) {
|
|
1011
|
-
const { raw } = params;
|
|
1012
|
-
const quoted = raw.length >= 2 && raw.startsWith('"') && raw.endsWith('"');
|
|
1013
|
-
const body = quoted ? raw.slice(1, -1) : raw;
|
|
1014
|
-
if (!body.includes("\\"))
|
|
1015
|
-
return body;
|
|
1016
|
-
let out = "";
|
|
1017
|
-
for (let index = 0;index < body.length; index += 1) {
|
|
1018
|
-
const char = body[index];
|
|
1019
|
-
if (char !== "\\") {
|
|
1020
|
-
out += char;
|
|
1021
|
-
continue;
|
|
1022
|
-
}
|
|
1023
|
-
const next = body[index + 1];
|
|
1024
|
-
if (next === undefined)
|
|
1025
|
-
throw malformed({ raw, detail: "it ends with a lone backslash" });
|
|
1026
|
-
if (next === "u") {
|
|
1027
|
-
const digits = body.slice(index + 2, index + 6);
|
|
1028
|
-
if (!/^[0-9a-fA-F]{4}$/.test(digits)) {
|
|
1029
|
-
throw malformed({ raw, detail: `\\u must be followed by four hex digits, found "${digits}"` });
|
|
1030
|
-
}
|
|
1031
|
-
out += String.fromCharCode(Number.parseInt(digits, 16));
|
|
1032
|
-
index += 5;
|
|
1033
|
-
continue;
|
|
1034
|
-
}
|
|
1035
|
-
const decoded = STRING_ESCAPES[next];
|
|
1036
|
-
if (decoded === undefined)
|
|
1037
|
-
throw malformed({ raw, detail: `unknown escape "\\${next}"` });
|
|
1038
|
-
out += decoded;
|
|
1039
|
-
index += 1;
|
|
1040
|
-
}
|
|
1041
|
-
return out;
|
|
1042
|
-
}
|
|
1043
|
-
function unicodeEscape(params) {
|
|
1044
|
-
return `\\u${params.char.charCodeAt(0).toString(16).padStart(4, "0")}`;
|
|
1045
|
-
}
|
|
1046
|
-
function encodePrismaString(params) {
|
|
1047
|
-
const { value } = params;
|
|
1048
|
-
let out = '"';
|
|
1049
|
-
for (const char of value) {
|
|
1050
|
-
if (char === "\\")
|
|
1051
|
-
out += "\\\\";
|
|
1052
|
-
else if (char === '"')
|
|
1053
|
-
out += "\\\"";
|
|
1054
|
-
else if (char === `
|
|
1055
|
-
`)
|
|
1056
|
-
out += "\\n";
|
|
1057
|
-
else if (char === "\r")
|
|
1058
|
-
out += "\\r";
|
|
1059
|
-
else if (char === "\t")
|
|
1060
|
-
out += "\\t";
|
|
1061
|
-
else if (char < " " || char === "\x7F")
|
|
1062
|
-
out += unicodeEscape({ char });
|
|
1063
|
-
else
|
|
1064
|
-
out += char;
|
|
1065
|
-
}
|
|
1066
|
-
return `${out}"`;
|
|
1067
|
-
}
|
|
1068
|
-
var INTEGER_LITERAL = /^-?\d+$/;
|
|
1069
|
-
function exactIntegerLiteral(params) {
|
|
1070
|
-
const { text } = params;
|
|
1071
|
-
if (!INTEGER_LITERAL.test(text)) {
|
|
1072
|
-
throw new VibeError({
|
|
1073
|
-
code: "VIBE_SCHEMA_INVALID",
|
|
1074
|
-
message: `Not an integer literal: ${JSON.stringify(text)}`,
|
|
1075
|
-
meta: { issues: [{ rule: "malformed-integer-literal", message: text }], literal: text }
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
|
-
const asNumber = Number(text);
|
|
1079
|
-
if (Number.isSafeInteger(asNumber))
|
|
1080
|
-
return asNumber;
|
|
1081
|
-
return BigInt(text).toString();
|
|
1082
|
-
}
|
|
1083
|
-
var DECIMAL_LITERAL = /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/;
|
|
1084
|
-
function exactDecimalLiteral(params) {
|
|
1085
|
-
const { text } = params;
|
|
1086
|
-
if (!DECIMAL_LITERAL.test(text)) {
|
|
1087
|
-
throw new VibeError({
|
|
1088
|
-
code: "VIBE_SCHEMA_INVALID",
|
|
1089
|
-
message: `Not a numeric literal: ${JSON.stringify(text)}`,
|
|
1090
|
-
meta: { issues: [{ rule: "malformed-numeric-literal", message: text }], literal: text }
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
|
-
if (INTEGER_LITERAL.test(text))
|
|
1094
|
-
return exactIntegerLiteral({ text });
|
|
1095
|
-
const mantissa = text.replace(/[eE].*$/, "").replace("-", "").replace(".", "").replace(/^0+/, "");
|
|
1096
|
-
return mantissa.length <= 15 ? Number(text) : text;
|
|
1097
|
-
}
|
|
1098
2415
|
// packages/schema/src/computed.ts
|
|
1099
2416
|
function buildComputedManifest(params) {
|
|
1100
2417
|
const { computed } = params;
|
|
@@ -1325,6 +2642,61 @@ function normalizeJsonSchemaRef(params) {
|
|
|
1325
2642
|
return { ...remaining === undefined ? {} : { documentation: remaining }, ...jsonSchema === undefined ? {} : { jsonSchema } };
|
|
1326
2643
|
}
|
|
1327
2644
|
|
|
2645
|
+
// packages/schema/src/validator-ref.ts
|
|
2646
|
+
var EXPORT_NAME2 = /^[$_\p{ID_Start}][$_\u200c\u200d\p{ID_Continue}]*$/u;
|
|
2647
|
+
var ANNOTATION_START = /^@validate\s*\(/u;
|
|
2648
|
+
var ANNOTATION_LINE = /^@validate\(\s*([$_\p{ID_Start}][$_\u200c\u200d\p{ID_Continue}]*)\s+from\s+("(?:[^"\\\r\n]|\\.)*")\s*\)$/u;
|
|
2649
|
+
function annotations2(params) {
|
|
2650
|
+
const lines = params.documentation.split(`
|
|
2651
|
+
`);
|
|
2652
|
+
return { lines, indexes: lines.flatMap((line, index) => ANNOTATION_START.test(line.trim()) ? [index] : []) };
|
|
2653
|
+
}
|
|
2654
|
+
function validatorRefIssue(params) {
|
|
2655
|
+
const { value } = params;
|
|
2656
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
2657
|
+
return "Validator reference must be { name, from }; validators belong in the referenced export";
|
|
2658
|
+
const keys = Reflect.ownKeys(value);
|
|
2659
|
+
if (keys.length !== 2 || !keys.includes("name") || !keys.includes("from"))
|
|
2660
|
+
return "Validator reference accepts only name and from";
|
|
2661
|
+
const name = Object.getOwnPropertyDescriptor(value, "name");
|
|
2662
|
+
const from = Object.getOwnPropertyDescriptor(value, "from");
|
|
2663
|
+
if (name?.enumerable !== true || !("value" in name) || typeof name.value !== "string" || !EXPORT_NAME2.test(name.value) || name.value === "default")
|
|
2664
|
+
return "Validator name must be a valid named export, not default";
|
|
2665
|
+
if (from?.enumerable !== true || !("value" in from) || typeof from.value !== "string" || from.value.trim().length === 0 || /[\u0000-\u001f\u007f]/u.test(from.value))
|
|
2666
|
+
return "Validator from must be a non-empty import specifier without control characters";
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
function assertValidatorRef(params) {
|
|
2670
|
+
const issue = validatorRefIssue(params);
|
|
2671
|
+
if (issue !== undefined)
|
|
2672
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: issue, meta: { issues: [{ rule: "invalid-validator-ref", message: issue }] } });
|
|
2673
|
+
const value = params.value;
|
|
2674
|
+
return Object.freeze({ name: value.name, from: value.from });
|
|
2675
|
+
}
|
|
2676
|
+
function hasValidatorAnnotation(params) {
|
|
2677
|
+
return annotations2({ documentation: params.documentation ?? "" }).indexes.length > 0;
|
|
2678
|
+
}
|
|
2679
|
+
function normalizeValidatorRef(params) {
|
|
2680
|
+
const { documentation, reference } = params;
|
|
2681
|
+
const refs = annotations2({ documentation: documentation ?? "" });
|
|
2682
|
+
let validator = reference === undefined ? undefined : assertValidatorRef({ value: reference });
|
|
2683
|
+
let remaining = documentation;
|
|
2684
|
+
if (refs.indexes.length > 1 || refs.indexes.length > 0 && validator !== undefined)
|
|
2685
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "A field may declare only one validator reference", meta: { issues: [{ rule: "duplicate-validator-ref" }] } });
|
|
2686
|
+
const index = refs.indexes[0];
|
|
2687
|
+
if (index !== undefined) {
|
|
2688
|
+
const match = ANNOTATION_LINE.exec(refs.lines[index].trim());
|
|
2689
|
+
if (match === null)
|
|
2690
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: 'Expected a documentation line @validate(Name from "specifier")', meta: { issues: [{ rule: "invalid-validator-ref" }] } });
|
|
2691
|
+
validator = assertValidatorRef({ value: { name: match[1], from: decodePrismaString({ raw: match[2] }) } });
|
|
2692
|
+
remaining = refs.lines.map((line, position) => position === index ? "" : line).join(`
|
|
2693
|
+
`).trim();
|
|
2694
|
+
if (remaining === "")
|
|
2695
|
+
remaining = undefined;
|
|
2696
|
+
}
|
|
2697
|
+
return { ...remaining === undefined ? {} : { documentation: remaining }, ...validator === undefined ? {} : { validator } };
|
|
2698
|
+
}
|
|
2699
|
+
|
|
1328
2700
|
// packages/schema/src/m2m.ts
|
|
1329
2701
|
function implicitJoinTable(params) {
|
|
1330
2702
|
const sorted = [params.modelA, params.modelB].sort();
|
|
@@ -1468,6 +2840,7 @@ var GENERATED_TYPE_NAMES = {
|
|
|
1468
2840
|
"WhereInput",
|
|
1469
2841
|
"WhereUniqueInput",
|
|
1470
2842
|
"OrderByInput",
|
|
2843
|
+
"KeyRow",
|
|
1471
2844
|
"ScalarOrderByInput",
|
|
1472
2845
|
"RelationOrderByInput",
|
|
1473
2846
|
"RelationFindManyArgs",
|
|
@@ -1626,6 +2999,10 @@ function validateGeneratedNames(params) {
|
|
|
1626
2999
|
owners.set("BoundVibeClientInstance", "native RLS client");
|
|
1627
3000
|
owners.set("BoundVibeTransactionClientInstance", "native RLS client");
|
|
1628
3001
|
}
|
|
3002
|
+
if (schema.models.some((model) => model.fields.some((field) => isLocalDateField({ field })))) {
|
|
3003
|
+
for (const name of ["LocalDate", "LocalDateFilter", "LocalDateNullableFilter"])
|
|
3004
|
+
owners.set(name, "local-date filters");
|
|
3005
|
+
}
|
|
1629
3006
|
const register = (params) => {
|
|
1630
3007
|
if (!/^[$_\p{ID_Start}][$_\u200c\u200d\p{ID_Continue}]*$/u.test(params.name) || INVALID_BINDING_NAMES.has(params.name)) {
|
|
1631
3008
|
issues.push({ rule: "invalid-generated-identifier", message: `Generated name "${params.name}" from ${params.owner} is not a legal TypeScript identifier` });
|
|
@@ -1684,7 +3061,7 @@ function referencesPartialOnly(params) {
|
|
|
1684
3061
|
const { target, references, schema, lookups } = params;
|
|
1685
3062
|
let partial = lookups.partialKeys.get(target);
|
|
1686
3063
|
if (partial === undefined) {
|
|
1687
|
-
partial = foreignKeyKeys({ keys: target.indexes.filter((index) => index.kind === "unique" && (index.where !== undefined || index.wherePredicate !== undefined)).map((index) => index.fields.map((field) => field.name)) });
|
|
3064
|
+
partial = foreignKeyKeys({ keys: target.indexes.filter((index) => index.kind === "unique" && !isExpressionIndex({ index }) && (index.where !== undefined || index.wherePredicate !== undefined)).map((index) => index.fields.map((field) => field.name)) });
|
|
1688
3065
|
lookups.partialKeys.set(target, partial);
|
|
1689
3066
|
}
|
|
1690
3067
|
if (partial.distinct.size === 0 && partial.repeated.length === 0)
|
|
@@ -1696,7 +3073,7 @@ function referencesPartialOnly(params) {
|
|
|
1696
3073
|
total = foreignKeyKeys({ keys: [
|
|
1697
3074
|
...target.fields.filter((field) => field.isId || field.isUnique).map((field) => [field.name]),
|
|
1698
3075
|
...target.primaryKey === undefined ? [] : [target.primaryKey.fields],
|
|
1699
|
-
...target.indexes.filter((index) => index.kind === "unique" && index.where === undefined && index.wherePredicate === undefined).map((index) => index.fields.map((field) => field.name))
|
|
3076
|
+
...target.indexes.filter((index) => index.kind === "unique" && !isExpressionIndex({ index }) && index.where === undefined && index.wherePredicate === undefined).map((index) => index.fields.map((field) => field.name))
|
|
1700
3077
|
] });
|
|
1701
3078
|
lookups.totalKeys.set(target, total);
|
|
1702
3079
|
}
|
|
@@ -1716,8 +3093,16 @@ function referencesPartialOnly(params) {
|
|
|
1716
3093
|
return !lookups.independentTables.has(JSON.stringify([placement.namespace, placement.name]));
|
|
1717
3094
|
}
|
|
1718
3095
|
function validateSchemaIR(params) {
|
|
1719
|
-
|
|
1720
|
-
|
|
3096
|
+
return validateSchemaWithRowRules({ ...params, rowRules: createRowRuleValidationContext(params) });
|
|
3097
|
+
}
|
|
3098
|
+
function validateSchemaWithRowRules(params) {
|
|
3099
|
+
const { schema, rowRules } = params;
|
|
3100
|
+
const issues = [...validateGeneratedColumns({ schema })];
|
|
3101
|
+
const expressionIssues = validateIndexExpressions({ schema, dialect: providerToDialect({ provider: schema.datasource.provider }) });
|
|
3102
|
+
if (expressionIssues.length > 0)
|
|
3103
|
+
return [...issues, ...expressionIssues];
|
|
3104
|
+
for (const model of schema.models)
|
|
3105
|
+
issues.push(...rowRules.validate({ model }));
|
|
1721
3106
|
for (const entry of [...schema.models, ...schema.enums]) {
|
|
1722
3107
|
if (entry.namespace === undefined)
|
|
1723
3108
|
continue;
|
|
@@ -1891,6 +3276,8 @@ function validateSchemaIR(params) {
|
|
|
1891
3276
|
});
|
|
1892
3277
|
}
|
|
1893
3278
|
for (const index of model.indexes) {
|
|
3279
|
+
if (isExpressionIndex({ index }))
|
|
3280
|
+
continue;
|
|
1894
3281
|
if (index.clientName === undefined && (index.kind !== "unique" || index.where !== undefined || index.fields.length < 2 || !addressable(index.fields.map((field) => field.name))))
|
|
1895
3282
|
continue;
|
|
1896
3283
|
checkSelectorName({
|
|
@@ -1903,7 +3290,7 @@ function validateSchemaIR(params) {
|
|
|
1903
3290
|
}
|
|
1904
3291
|
}
|
|
1905
3292
|
validateIdentifierLengths({ schema, issues });
|
|
1906
|
-
validatePhysicalNameUniqueness({ schema, issues, modelByName, fieldsByModel });
|
|
3293
|
+
validatePhysicalNameUniqueness({ schema, issues, modelByName, fieldsByModel, rowRules });
|
|
1907
3294
|
validateGeneratedNames({ schema, issues });
|
|
1908
3295
|
validateNativeRls({ schema, issues });
|
|
1909
3296
|
return issues;
|
|
@@ -1912,8 +3299,14 @@ function validatePhysicalNameUniqueness(params) {
|
|
|
1912
3299
|
const { schema, issues, modelByName, fieldsByModel } = params;
|
|
1913
3300
|
const dialect = providerToDialect({ provider: schema.datasource.provider });
|
|
1914
3301
|
const owners = new Map;
|
|
3302
|
+
let hasRowRuleClaims = false;
|
|
3303
|
+
const ruleDiagnostics = new Set;
|
|
3304
|
+
const triggerNamespace = (table) => dialect === "postgres" ? `trigger:${table}` : "trigger";
|
|
3305
|
+
const collisionName = (name) => dialect === "postgres" ? name : dialect === "sqlite" ? name.replace(/[A-Z]/g, (character) => character.toLowerCase()) : name.toLowerCase();
|
|
3306
|
+
const diagnosticKey = (entry) => JSON.stringify([entry.namespace, entry.table, collisionName(entry.name)]);
|
|
1915
3307
|
const claim = (entry) => {
|
|
1916
|
-
const
|
|
3308
|
+
const rowRuleScope = entry.namespace === "trigger" || entry.namespace.startsWith("row-rule-diagnostic:");
|
|
3309
|
+
const key = JSON.stringify([entry.physicalNamespace ?? "public", entry.namespace, rowRuleScope ? collisionName(entry.name) : entry.name]);
|
|
1917
3310
|
const previous = owners.get(key);
|
|
1918
3311
|
if (previous === undefined) {
|
|
1919
3312
|
owners.set(key, `${entry.what} of ${entry.owner}`);
|
|
@@ -1921,7 +3314,7 @@ function validatePhysicalNameUniqueness(params) {
|
|
|
1921
3314
|
}
|
|
1922
3315
|
issues.push({
|
|
1923
3316
|
rule: "duplicate-physical-name",
|
|
1924
|
-
message: `${entry.what} "${entry.name}" of ${entry.owner} collides with the ${previous} \u2014 both resolve to one physical ${entry.namespace === "relation" ? "object" : "name"}, and only one can exist`,
|
|
3317
|
+
message: `${entry.what} "${entry.name}" of ${entry.owner} collides with the ${previous} \u2014 both resolve to one physical ${entry.namespace === "relation" ? "object" : "name"}, and only one can exist${entry.remedy ?? (previous.includes("expression index") ? "; set an explicit map on the expression index to choose a distinct physical name" : "")}`,
|
|
1925
3318
|
...entry.model === undefined ? {} : { model: entry.model },
|
|
1926
3319
|
...entry.field === undefined ? {} : { field: entry.field }
|
|
1927
3320
|
});
|
|
@@ -1949,12 +3342,12 @@ function validatePhysicalNameUniqueness(params) {
|
|
|
1949
3342
|
for (const { index, site } of declarations) {
|
|
1950
3343
|
const unique = index.kind === "unique";
|
|
1951
3344
|
const columns = index.fields.map((entry) => [columnOf(entry.name), entry.sort === "desc", entry.operatorClass === undefined ? null : [entry.operatorClass.namespace, entry.operatorClass.name]]);
|
|
1952
|
-
const key = JSON.stringify([unique, index.nullsNotDistinct === true, unique ? "btree" : index.kind, columns, index.where?.trim() ?? ""]);
|
|
3345
|
+
const key = isExpressionIndex({ index }) ? indexKeyDefinitionKey({ index, model }) : JSON.stringify([unique, index.nullsNotDistinct === true, unique ? "btree" : index.kind, columns, index.where?.trim() ?? ""]);
|
|
1953
3346
|
if (seenIndexKeys.has(key))
|
|
1954
3347
|
continue;
|
|
1955
3348
|
seenIndexKeys.add(key);
|
|
1956
|
-
const name = index.name ?? defaultIndexName({ table, columns: index.fields.map((entry) => columnOf(entry.name)), unique, dialect });
|
|
1957
|
-
claim({ namespace: indexNamespace(table), physicalNamespace, name, what: unique ? "unique index" : "index", owner: `${site} on "${model.name}"`, model: model.name });
|
|
3349
|
+
const name = index.name ?? (isExpressionIndex({ index }) ? defaultExpressionIndexName({ table, model, index, dialect }) : defaultIndexName({ table, columns: index.fields.map((entry) => columnOf(entry.name)), unique, dialect }));
|
|
3350
|
+
claim({ namespace: indexNamespace(table), physicalNamespace, name, what: isExpressionIndex({ index }) ? unique ? "unique expression index" : "expression index" : unique ? "unique index" : "index", owner: `${site} on "${model.name}"`, model: model.name, ...isExpressionIndex({ index }) ? { remedy: "; set an explicit map on the expression index to choose a distinct physical name" } : {} });
|
|
1958
3351
|
if (index.constraintName !== undefined) {
|
|
1959
3352
|
claim({ namespace: constraintNamespace(table), physicalNamespace, name: index.constraintName, what: "unique constraint", owner: `${site} on "${model.name}"`, model: model.name });
|
|
1960
3353
|
}
|
|
@@ -1973,15 +3366,64 @@ function validatePhysicalNameUniqueness(params) {
|
|
|
1973
3366
|
const name = relation.constraintName ?? defaultForeignKeyName({ table, columns: relation.fields.map((entry) => columnOf(entry)), dialect });
|
|
1974
3367
|
claim({ namespace: constraintNamespace(table), physicalNamespace, name, what: "foreign key", owner: `relation "${model.name}.${relation.name}"`, model: model.name });
|
|
1975
3368
|
}
|
|
3369
|
+
if (dialect === "sqlite") {
|
|
3370
|
+
for (const field of model.fields) {
|
|
3371
|
+
if (!isLocalDateField({ field }))
|
|
3372
|
+
continue;
|
|
3373
|
+
claim({ namespace: constraintNamespace(table), physicalNamespace, name: localDateCheckName({ table, column: field.dbName ?? field.name }), what: "owned local-date CHECK", owner: `field "${model.name}.${field.name}"`, model: model.name, field: field.name });
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
1976
3376
|
for (const checkIR of model.checks ?? []) {
|
|
1977
3377
|
claim({ namespace: constraintNamespace(table), physicalNamespace, name: checkIR.name, what: "CHECK constraint", owner: `model "${model.name}"`, model: model.name });
|
|
1978
3378
|
}
|
|
3379
|
+
if (Array.isArray(model.rowRules))
|
|
3380
|
+
for (const rule of model.rowRules) {
|
|
3381
|
+
if (rowRuleShapeIssue({ value: rule }) !== undefined)
|
|
3382
|
+
continue;
|
|
3383
|
+
hasRowRuleClaims = true;
|
|
3384
|
+
const pair = rowRuleObjectNames({ rule });
|
|
3385
|
+
const owner = `row rule "${model.name}.${rule.name}" (supply an explicit, distinct name stem)`;
|
|
3386
|
+
const ruleNamespace = dialect === "sqlite" ? "main" : physicalNamespace;
|
|
3387
|
+
if (dialect === "postgres")
|
|
3388
|
+
claim({ namespace: "function:[]", physicalNamespace, name: pair.functionName, what: "row-rule function", owner, model: model.name });
|
|
3389
|
+
const names = dialect === "postgres" ? [pair.triggerName] : (rule.kind === "appendOnly" ? ["update", "delete"] : ["update"]).map((event) => rowRuleTriggerName({ rule, event }));
|
|
3390
|
+
for (const name of names) {
|
|
3391
|
+
claim({ namespace: triggerNamespace(table), physicalNamespace: ruleNamespace, name, what: "row-rule trigger", owner, model: model.name });
|
|
3392
|
+
if (dialect === "postgres")
|
|
3393
|
+
claim({ namespace: constraintNamespace(table), physicalNamespace, name, what: "row-rule diagnostic name", owner, model: model.name });
|
|
3394
|
+
else {
|
|
3395
|
+
claim({ namespace: `row-rule-diagnostic:${table}`, physicalNamespace: ruleNamespace, name, what: "row-rule diagnostic name", owner, model: model.name });
|
|
3396
|
+
const constraints = [
|
|
3397
|
+
...(model.checks ?? []).map((check) => check.name),
|
|
3398
|
+
...model.indexes.flatMap((index) => index.constraintName === undefined ? [] : [index.constraintName]),
|
|
3399
|
+
...model.relations.filter((relation) => (relation.fields?.length ?? 0) > 0).map((relation) => relation.constraintName ?? defaultForeignKeyName({ table, columns: relation.fields.map(columnOf), dialect })),
|
|
3400
|
+
...dialect === "sqlite" ? model.fields.filter((field) => isLocalDateField({ field })).map((field) => localDateCheckName({ table, column: field.dbName ?? field.name })) : []
|
|
3401
|
+
];
|
|
3402
|
+
for (const constraint of constraints)
|
|
3403
|
+
if (collisionName(constraint) === collisionName(name))
|
|
3404
|
+
claim({ namespace: `row-rule-diagnostic:${table}`, physicalNamespace: ruleNamespace, name: constraint, what: "constraint diagnostic name", owner: `model "${model.name}"`, model: model.name });
|
|
3405
|
+
}
|
|
3406
|
+
ruleDiagnostics.add(diagnosticKey({ namespace: ruleNamespace, table, name }));
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
1979
3409
|
for (const exclusion of model.exclusions ?? []) {
|
|
1980
3410
|
claim({ namespace: constraintNamespace(table), physicalNamespace, name: exclusion.name, what: "exclusion constraint", owner: `model "${model.name}"`, model: model.name });
|
|
1981
3411
|
if (dialect === "postgres")
|
|
1982
3412
|
claim({ namespace: "relation", physicalNamespace, name: exclusion.name, what: "exclusion constraint", owner: `model "${model.name}"`, model: model.name });
|
|
1983
3413
|
}
|
|
1984
3414
|
}
|
|
3415
|
+
for (const object of hasRowRuleClaims ? params.rowRules.objects() : []) {
|
|
3416
|
+
const identity = object.identity;
|
|
3417
|
+
if (dialect === "postgres" && identity.kind === "function")
|
|
3418
|
+
claim({ namespace: `function:${JSON.stringify(identity.argumentTypes.map((type) => [type.namespace, type.name]))}`, physicalNamespace: identity.name.namespace, name: identity.name.name, what: "migration function", owner: object.id });
|
|
3419
|
+
if (identity.kind === "trigger") {
|
|
3420
|
+
if (dialect === "mysql" && /[^\x00-\x7F]/.test(identity.name))
|
|
3421
|
+
issues.push({ rule: "row-rule-name-collation", message: `MySQL migration trigger ${identity.name} cannot be qualified against declared row rules without engine trigger-name collation evidence; use an ASCII name` });
|
|
3422
|
+
claim({ namespace: triggerNamespace(identity.table.name), physicalNamespace: identity.table.namespace, name: identity.name, what: "migration trigger", owner: object.id });
|
|
3423
|
+
}
|
|
3424
|
+
if (identity.kind === "constraint" && ruleDiagnostics.has(diagnosticKey({ namespace: identity.table.namespace, table: identity.table.name, name: identity.name })))
|
|
3425
|
+
claim({ namespace: dialect === "postgres" ? constraintNamespace(identity.table.name) : `row-rule-diagnostic:${identity.table.name}`, physicalNamespace: identity.table.namespace, name: identity.name, what: "migration constraint", owner: object.id });
|
|
3426
|
+
}
|
|
1985
3427
|
if (dialect === "postgres") {
|
|
1986
3428
|
for (const enumDef of schema.enums) {
|
|
1987
3429
|
if (enumStorageOf({ enumDef }) !== "database")
|
|
@@ -2124,11 +3566,31 @@ function validateModel(params) {
|
|
|
2124
3566
|
}
|
|
2125
3567
|
if (hasJsonSchemaAnnotation({ documentation: field.documentation }))
|
|
2126
3568
|
issues.push({ rule: "unnormalized-json-schema-ref", message: "Normalize @schema documentation into FieldIR.jsonSchema before consuming SchemaIR", model: model.name, field: field.name });
|
|
3569
|
+
if (field.validator !== undefined) {
|
|
3570
|
+
const issue = validatorRefIssue({ value: field.validator });
|
|
3571
|
+
if (issue !== undefined)
|
|
3572
|
+
issues.push({ rule: "invalid-validator-ref", message: issue, model: model.name, field: field.name });
|
|
3573
|
+
if (field.type === "Json" || field.isUnsupported === true)
|
|
3574
|
+
issues.push({ rule: "validator-non-scalar", message: "Validators are only valid on queryable non-JSON scalar or enum fields; use .schema() for Json", model: model.name, field: field.name });
|
|
3575
|
+
if (field.jsonSchema !== undefined || hasType)
|
|
3576
|
+
issues.push({ rule: "conflicting-validator-ref", message: "A field cannot combine a scalar validator with a JSON schema or type assertion", model: model.name, field: field.name });
|
|
3577
|
+
}
|
|
3578
|
+
if (hasValidatorAnnotation({ documentation: field.documentation }))
|
|
3579
|
+
issues.push({ rule: "unnormalized-validator-ref", message: "Normalize @validate documentation into FieldIR.validator before consuming SchemaIR", model: model.name, field: field.name });
|
|
2127
3580
|
const typeIdentity = field.nativeTypeIdentity;
|
|
2128
3581
|
if (typeIdentity !== undefined && (typeIdentity === null || typeof typeIdentity !== "object" || field.isUnsupported !== true || typeof typeIdentity.source !== "string" || typeIdentity.source !== field.nativeType || typeof typeIdentity.namespace !== "string" || typeIdentity.namespace.length === 0 || typeIdentity.namespace.includes("\x00"))) {
|
|
2129
3582
|
issues.push({ rule: "invalid-native-type-identity", message: `PostgreSQL type identity on "${model.name}.${field.name}" must be bound to its unsupported declaration`, model: model.name, field: field.name });
|
|
2130
3583
|
}
|
|
2131
3584
|
const defaultValue = field.default;
|
|
3585
|
+
if (isLocalDateField({ field })) {
|
|
3586
|
+
if (field.isList)
|
|
3587
|
+
issues.push({ rule: "local-date-list", message: `Local-date field "${model.name}.${field.name}" cannot be a list`, model: model.name, field: field.name });
|
|
3588
|
+
if (field.isUpdatedAt)
|
|
3589
|
+
issues.push({ rule: "local-date-updated-at", message: `Local-date field "${model.name}.${field.name}" cannot use @updatedAt`, model: model.name, field: field.name });
|
|
3590
|
+
if (defaultValue !== undefined && (defaultValue.kind !== "literal" || !isCanonicalLocalDate({ value: defaultValue.value }))) {
|
|
3591
|
+
issues.push({ rule: "local-date-default", message: `Default on "${model.name}.${field.name}" must be a canonical YYYY-MM-DD calendar-day literal in years 0001\u20139999; generated defaults are not supported`, model: model.name, field: field.name });
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
2132
3594
|
if (defaultValue !== undefined && defaultValue.kind !== "dbgenerated" && defaultValue.kind !== "list") {
|
|
2133
3595
|
let valid = !field.isList;
|
|
2134
3596
|
if (defaultValue.kind === "now")
|
|
@@ -2251,6 +3713,8 @@ function validateModel(params) {
|
|
|
2251
3713
|
}
|
|
2252
3714
|
}
|
|
2253
3715
|
for (const relation of model.relations) {
|
|
3716
|
+
if ("validator" in relation || "documentation" in relation && typeof relation.documentation === "string" && hasValidatorAnnotation({ documentation: relation.documentation }))
|
|
3717
|
+
issues.push({ rule: "validator-non-scalar", message: "Validators cannot be declared on relations", model: model.name, field: relation.name });
|
|
2254
3718
|
const target = modelByName.get(relation.targetModel);
|
|
2255
3719
|
if (target === undefined) {
|
|
2256
3720
|
issues.push({
|
|
@@ -2297,6 +3761,13 @@ function validateModel(params) {
|
|
|
2297
3761
|
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 });
|
|
2298
3762
|
}
|
|
2299
3763
|
const targetFieldNames = lookups.fieldsByModel.get(target);
|
|
3764
|
+
for (const [index, fkName] of relation.fields.entries()) {
|
|
3765
|
+
const own = lookups.fieldsByModel.get(model)?.get(fkName);
|
|
3766
|
+
const referenced = targetFieldNames.get(relation.references[index] ?? "");
|
|
3767
|
+
if (own !== undefined && referenced !== undefined && (isLocalDateField({ field: own }) || isLocalDateField({ field: referenced })) && fieldTypeIdentity(own) !== fieldTypeIdentity(referenced)) {
|
|
3768
|
+
issues.push({ rule: "relation-type-mismatch", message: `Relation "${model.name}.${relation.name}" pairs a local date with a different logical type; both foreign-key columns must be local dates`, model: model.name, field: fkName });
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
2300
3771
|
for (const refField of relation.references) {
|
|
2301
3772
|
if (!targetFieldNames.has(refField)) {
|
|
2302
3773
|
issues.push({
|
|
@@ -2311,10 +3782,10 @@ function validateModel(params) {
|
|
|
2311
3782
|
}
|
|
2312
3783
|
}
|
|
2313
3784
|
for (const field of model.fields) {
|
|
2314
|
-
if (field.collation !== undefined && field.type !== "String") {
|
|
3785
|
+
if (field.collation !== undefined && (field.type !== "String" || isLocalDateField({ field }))) {
|
|
2315
3786
|
issues.push({
|
|
2316
3787
|
rule: "collation-non-string",
|
|
2317
|
-
message: `@collation on "${model.name}.${field.name}" \u2014 collation is only valid on String fields`,
|
|
3788
|
+
message: `@collation on "${model.name}.${field.name}" \u2014 collation is only valid on text String fields, not local dates`,
|
|
2318
3789
|
model: model.name,
|
|
2319
3790
|
field: field.name
|
|
2320
3791
|
});
|
|
@@ -2368,6 +3839,7 @@ function validateModel(params) {
|
|
|
2368
3839
|
fieldNames,
|
|
2369
3840
|
site: `CHECK "${check.name}"`,
|
|
2370
3841
|
usage: "check",
|
|
3842
|
+
enums: params.schema.enums,
|
|
2371
3843
|
dialect: providerToDialect({ provider: params.schema.datasource.provider }),
|
|
2372
3844
|
issues
|
|
2373
3845
|
});
|
|
@@ -2391,6 +3863,7 @@ function validateModel(params) {
|
|
|
2391
3863
|
fieldNames,
|
|
2392
3864
|
site: `partial index on [${index.fields.map((entry) => entry.name).join(", ")}]`,
|
|
2393
3865
|
usage: "indexPredicate",
|
|
3866
|
+
enums: params.schema.enums,
|
|
2394
3867
|
dialect: providerToDialect({ provider: params.schema.datasource.provider }),
|
|
2395
3868
|
issues
|
|
2396
3869
|
});
|
|
@@ -2399,40 +3872,129 @@ function validateModel(params) {
|
|
|
2399
3872
|
for (const exclusion of model.exclusions ?? []) {
|
|
2400
3873
|
const predicate = exclusion.source?.where;
|
|
2401
3874
|
if (predicate !== undefined)
|
|
2402
|
-
validatePredicateFields({ predicate, model, fieldNames, site: `exclusion "${exclusion.name}"`, usage: "exclusionPredicate", dialect: providerToDialect({ provider: params.schema.datasource.provider }), issues });
|
|
3875
|
+
validatePredicateFields({ predicate, model, fieldNames, site: `exclusion "${exclusion.name}"`, usage: "exclusionPredicate", enums: params.schema.enums, dialect: providerToDialect({ provider: params.schema.datasource.provider }), issues });
|
|
2403
3876
|
}
|
|
2404
3877
|
}
|
|
2405
3878
|
function validatePredicateFields(params) {
|
|
2406
|
-
const { predicate, model, fieldNames, site, issues } = params;
|
|
2407
|
-
const
|
|
2408
|
-
if (
|
|
2409
|
-
issues.push({ rule: "predicate-check-capability", message: `${site}:
|
|
3879
|
+
const { predicate, model, fieldNames, site, issues, enums } = params;
|
|
3880
|
+
const capability = predicateCapabilityIssue({ predicate, model, dialect: params.dialect, usage: params.usage });
|
|
3881
|
+
if (capability !== undefined) {
|
|
3882
|
+
issues.push({ rule: "predicate-check-capability", message: `${site}: ${capability.feature} is not supported for ${params.usage} on ${params.dialect}${capability.hint === undefined ? "" : ` (${capability.hint})`}`, model: model.name });
|
|
2410
3883
|
}
|
|
3884
|
+
const issue = (params) => {
|
|
3885
|
+
issues.push({ ...params, message: `${site}: ${params.message}`, model: model.name });
|
|
3886
|
+
};
|
|
3887
|
+
const identity = (params) => fieldTypeIdentity({ type: params.result.type, nativeType: params.result.refinement === "localDate" ? "Date" : undefined });
|
|
3888
|
+
const literal = (params) => {
|
|
3889
|
+
const { value } = params.value;
|
|
3890
|
+
const { type, refinement } = params.result;
|
|
3891
|
+
if (refinement === "localDate") {
|
|
3892
|
+
if (!isCanonicalLocalDate({ value }))
|
|
3893
|
+
issue({ rule: "predicate-local-date-literal", message: "a local-date predicate requires a canonical YYYY-MM-DD calendar-day literal" });
|
|
3894
|
+
return;
|
|
3895
|
+
}
|
|
3896
|
+
if (isEnumType(type)) {
|
|
3897
|
+
const declared = enums.find((entry) => entry.name === type.enum);
|
|
3898
|
+
if (typeof value !== "string")
|
|
3899
|
+
issue({ rule: "predicate-type-mismatch", message: "an enum predicate requires a member name as a string" });
|
|
3900
|
+
else if (declared !== undefined && !declared.values.includes(value))
|
|
3901
|
+
issue({ rule: "predicate-enum-member", message: `literal ${JSON.stringify(value)} is not a member of enum ${type.enum}` });
|
|
3902
|
+
return;
|
|
3903
|
+
}
|
|
3904
|
+
if (type === "Int" || type === "BigInt") {
|
|
3905
|
+
if (typeof value !== "number" && typeof value !== "string") {
|
|
3906
|
+
issue({ rule: "predicate-type-mismatch", message: `${type} requires an exact integer literal` });
|
|
3907
|
+
return;
|
|
3908
|
+
}
|
|
3909
|
+
if (typeof value === "number" ? !Number.isSafeInteger(value) : !/^-?(0|[1-9]\d*)$/.test(value)) {
|
|
3910
|
+
issue({ rule: "predicate-literal-invalid", message: `${type} requires an exact integer literal without rounding or exponent notation` });
|
|
3911
|
+
return;
|
|
3912
|
+
}
|
|
3913
|
+
const parsed = BigInt(value);
|
|
3914
|
+
const min = type === "Int" ? -2147483648n : -(2n ** 63n);
|
|
3915
|
+
const max = type === "Int" ? 2147483647n : 2n ** 63n - 1n;
|
|
3916
|
+
if (parsed < min || parsed > max)
|
|
3917
|
+
issue({ rule: "predicate-literal-invalid", message: `${type} literal is outside its signed integer range` });
|
|
3918
|
+
return;
|
|
3919
|
+
}
|
|
3920
|
+
if (type === "Float") {
|
|
3921
|
+
if (typeof value !== "number")
|
|
3922
|
+
issue({ rule: "predicate-type-mismatch", message: "Float requires a number literal" });
|
|
3923
|
+
else if (!Number.isFinite(value))
|
|
3924
|
+
issue({ rule: "predicate-literal-invalid", message: "Float requires a finite number literal" });
|
|
3925
|
+
return;
|
|
3926
|
+
}
|
|
3927
|
+
if (type === "Decimal") {
|
|
3928
|
+
if (typeof value !== "string")
|
|
3929
|
+
issue({ rule: "predicate-type-mismatch", message: "Decimal requires exact fixed-point text, not a JavaScript number" });
|
|
3930
|
+
else if (!/^-?(0|[1-9]\d*)(\.\d+)?$/.test(value))
|
|
3931
|
+
issue({ rule: "predicate-literal-invalid", message: "Decimal requires exact fixed-point text without exponent notation" });
|
|
3932
|
+
return;
|
|
3933
|
+
}
|
|
3934
|
+
if (type === "String") {
|
|
3935
|
+
if (typeof value !== "string")
|
|
3936
|
+
issue({ rule: "predicate-type-mismatch", message: "a text expression requires a string literal" });
|
|
3937
|
+
else if (value.includes("\x00"))
|
|
3938
|
+
issue({ rule: "predicate-literal-invalid", message: "a text literal cannot contain NUL bytes" });
|
|
3939
|
+
} else if (type === "Boolean" && typeof value !== "boolean")
|
|
3940
|
+
issue({ rule: "predicate-type-mismatch", message: "Boolean requires true or false" });
|
|
3941
|
+
};
|
|
2411
3942
|
const expressionType = (expression) => {
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
3943
|
+
switch (expression.kind) {
|
|
3944
|
+
case "field": {
|
|
3945
|
+
const field = model.fields.find((candidate) => candidate.name === expression.name);
|
|
3946
|
+
if (field === undefined || field.isList || field.isUnsupported)
|
|
3947
|
+
return;
|
|
3948
|
+
return isLocalDateField({ field }) ? { type: field.type, refinement: "localDate" } : { type: field.type };
|
|
3949
|
+
}
|
|
3950
|
+
case "jsonText": {
|
|
3951
|
+
const field = model.fields.find((candidate) => candidate.name === expression.field.name);
|
|
3952
|
+
if (field !== undefined && (field.type !== "Json" || field.isUnsupported === true))
|
|
3953
|
+
issue({ rule: "predicate-type-mismatch", message: "jsonText requires a Json field", field: field.name });
|
|
3954
|
+
if (!Array.isArray(expression.path) || Array.from(expression.path).some((segment) => typeof segment !== "string" || segment.includes("\x00")))
|
|
3955
|
+
issue({ rule: "predicate-json-path", message: "jsonText requires a string-only path without NUL bytes" });
|
|
3956
|
+
return { type: "String" };
|
|
3957
|
+
}
|
|
3958
|
+
case "length":
|
|
3959
|
+
case "lower":
|
|
3960
|
+
case "upper": {
|
|
3961
|
+
const child = expressionType(expression.expression);
|
|
3962
|
+
if (child !== undefined && (child.type !== "String" || child.refinement === "localDate"))
|
|
3963
|
+
issue({ rule: "predicate-type-mismatch", message: `${expression.kind} requires a text expression, not a local date or another scalar` });
|
|
3964
|
+
return { type: expression.kind === "length" ? "Int" : "String" };
|
|
3965
|
+
}
|
|
3966
|
+
case "add":
|
|
3967
|
+
case "sub":
|
|
3968
|
+
case "mul": {
|
|
3969
|
+
const left = expressionType(expression.left);
|
|
3970
|
+
const right = expression.right.kind === "value" ? undefined : expressionType(expression.right);
|
|
3971
|
+
if (left === undefined)
|
|
3972
|
+
return;
|
|
3973
|
+
if (left.type !== "Int" && left.type !== "BigInt" && left.type !== "Float" && left.type !== "Decimal") {
|
|
3974
|
+
issue({ rule: "predicate-type-mismatch", message: `${expression.kind} requires Int, BigInt, Float or Decimal operands` });
|
|
3975
|
+
return;
|
|
3976
|
+
}
|
|
3977
|
+
if (right !== undefined && right.type !== left.type)
|
|
3978
|
+
issue({ rule: "predicate-type-mismatch", message: `${expression.kind} operands must have the same numeric family` });
|
|
3979
|
+
if (expression.right.kind === "value")
|
|
3980
|
+
literal({ value: expression.right, result: left });
|
|
3981
|
+
return { type: left.type };
|
|
3982
|
+
}
|
|
2421
3983
|
}
|
|
2422
|
-
if (expressionType(expression.expression) !== "String")
|
|
2423
|
-
issues.push({ rule: "predicate-type-mismatch", message: `${site}: length requires a text expression`, model: model.name });
|
|
2424
|
-
return "Int";
|
|
2425
3984
|
};
|
|
2426
3985
|
const validateComputedComparisons = (node) => {
|
|
2427
3986
|
if (node.kind === "compare") {
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
3987
|
+
const left = expressionType(node.left);
|
|
3988
|
+
const right = node.right.kind === "value" ? undefined : expressionType(node.right);
|
|
3989
|
+
if (left !== undefined && right !== undefined && identity({ result: left }) !== identity({ result: right }))
|
|
3990
|
+
issue({ rule: "predicate-type-mismatch", message: "comparison operands must have the same logical type" });
|
|
3991
|
+
if (node.right.kind === "value" && left !== undefined)
|
|
3992
|
+
literal({ value: node.right, result: left });
|
|
3993
|
+
} else if (node.kind === "inList") {
|
|
3994
|
+
const result = expressionType(node.field);
|
|
3995
|
+
if (result !== undefined)
|
|
3996
|
+
for (const value of node.values)
|
|
3997
|
+
literal({ value, result });
|
|
2436
3998
|
} else if (node.kind === "and" || node.kind === "or")
|
|
2437
3999
|
node.operands.forEach(validateComputedComparisons);
|
|
2438
4000
|
else if (node.kind === "not" || node.kind === "isTrue")
|
|
@@ -2450,8 +4012,8 @@ function validatePredicateFields(params) {
|
|
|
2450
4012
|
continue;
|
|
2451
4013
|
}
|
|
2452
4014
|
const referenced = model.fields.find((field) => field.name === name);
|
|
2453
|
-
if (
|
|
2454
|
-
issues.push({ rule: "predicate-unsupported-type", message: `${site}:
|
|
4015
|
+
if (referenced?.isUnsupported === true)
|
|
4016
|
+
issues.push({ rule: "predicate-unsupported-type", message: `${site}: predicates cannot reference unsupported fields`, model: model.name, field: name });
|
|
2455
4017
|
if (referenced?.isList === true) {
|
|
2456
4018
|
issues.push({
|
|
2457
4019
|
rule: "predicate-list-field",
|
|
@@ -2462,9 +4024,49 @@ function validatePredicateFields(params) {
|
|
|
2462
4024
|
}
|
|
2463
4025
|
}
|
|
2464
4026
|
}
|
|
4027
|
+
function assertMysqlCheckReferentialActions(params) {
|
|
4028
|
+
const views = new Set(params.schema.models.filter((model) => model.isView).map((model) => model.name));
|
|
4029
|
+
for (const model of params.schema.models) {
|
|
4030
|
+
const typed = (model.checks ?? []).filter((check) => check.predicate !== undefined);
|
|
4031
|
+
if (typed.length === 0)
|
|
4032
|
+
continue;
|
|
4033
|
+
for (const relation of model.relations) {
|
|
4034
|
+
const columns = relation.fields;
|
|
4035
|
+
if (columns === undefined || columns.length === 0 || views.has(relation.targetModel))
|
|
4036
|
+
continue;
|
|
4037
|
+
const { onDelete, onUpdate } = resolveReferentialActions({ relation, field: (name) => model.fields.find((field) => field.name === name) });
|
|
4038
|
+
const writes = (action) => action === "Cascade" || action === "SetNull" || action === "SetDefault";
|
|
4039
|
+
if (!writes(onUpdate) && onDelete !== "SetNull" && onDelete !== "SetDefault")
|
|
4040
|
+
continue;
|
|
4041
|
+
for (const check of typed) {
|
|
4042
|
+
const column = predicateFieldNames({ predicate: check.predicate }).find((name) => columns.includes(name));
|
|
4043
|
+
if (column === undefined)
|
|
4044
|
+
continue;
|
|
4045
|
+
const error = unsupportedCapability({
|
|
4046
|
+
dialect: "mysql",
|
|
4047
|
+
feature: "CHECK constraint on a foreign-key column that a referential action writes",
|
|
4048
|
+
hint: `CHECK "${check.name}" reads "${model.name}.${column}", which relation "${model.name}.${relation.name}" writes through ON DELETE ${onDelete} / ON UPDATE ${onUpdate}; MySQL refuses this (error 3823). Declare onUpdate "NoAction" (or "Restrict") and an onDelete of "Restrict", "NoAction" or "Cascade" on the relation, or remove the CHECK`
|
|
4049
|
+
});
|
|
4050
|
+
throw new VibeError({ code: error.code, message: error.message, meta: { ...error.meta, model: model.name, check: check.name, field: column, relation: relation.name, onDelete, onUpdate } });
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
2465
4055
|
function assertValidSchemaIR(params) {
|
|
4056
|
+
const rowRules = createRowRuleValidationContext(params);
|
|
2466
4057
|
const dialect = providerToDialect({ provider: params.schema.datasource.provider });
|
|
4058
|
+
assertIndexExpressions({ schema: params.schema, dialect });
|
|
4059
|
+
assertGeneratedColumns({ schema: params.schema, dialect });
|
|
2467
4060
|
assertNativeRlsAdmission({ schema: params.schema, dialect });
|
|
4061
|
+
for (const model of params.schema.models) {
|
|
4062
|
+
const rule = model.rls;
|
|
4063
|
+
if (rule === undefined || rule === null || typeof rule !== "object" || !("where" in rule) || rule.where === null || typeof rule.where !== "object")
|
|
4064
|
+
continue;
|
|
4065
|
+
for (const field of model.fields) {
|
|
4066
|
+
if (Object.hasOwn(rule.where, field.name) && isLocalDateField({ field }))
|
|
4067
|
+
throw unsupportedCapability({ dialect, feature: "local-date native RLS ownership", hint: `${model.name}.${field.name} is a calendar day, not a supported RLS identity column` });
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
2468
4070
|
assertExtensionPlacement({ schema: params.schema, dialect });
|
|
2469
4071
|
assertIndexOperatorClasses({ schema: params.schema, dialect });
|
|
2470
4072
|
for (const entry of [...params.schema.models, ...params.schema.enums])
|
|
@@ -2480,13 +4082,15 @@ function assertValidSchemaIR(params) {
|
|
|
2480
4082
|
if (declared !== undefined && (declared.namespace ?? "public") !== enumType.namespace)
|
|
2481
4083
|
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 } });
|
|
2482
4084
|
}
|
|
4085
|
+
if (dialect === "mysql")
|
|
4086
|
+
assertMysqlCheckReferentialActions({ schema: params.schema });
|
|
2483
4087
|
for (const model of params.schema.models) {
|
|
2484
4088
|
for (const check of model.checks ?? [])
|
|
2485
4089
|
if (check.predicate !== undefined)
|
|
2486
|
-
assertPredicateCapabilities({ predicate: check.predicate, dialect, usage: "check" });
|
|
4090
|
+
assertPredicateCapabilities({ predicate: check.predicate, model, dialect, usage: "check" });
|
|
2487
4091
|
for (const index of model.indexes)
|
|
2488
4092
|
if (index.wherePredicate !== undefined)
|
|
2489
|
-
assertPredicateCapabilities({ predicate: index.wherePredicate, dialect, usage: "indexPredicate" });
|
|
4093
|
+
assertPredicateCapabilities({ predicate: index.wherePredicate, model, dialect, usage: "indexPredicate" });
|
|
2490
4094
|
if (model.exclusions !== undefined && !Array.isArray(model.exclusions))
|
|
2491
4095
|
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Exclusions must be an array", meta: { rule: "exclusion-definition" } });
|
|
2492
4096
|
if ((model.exclusions?.length ?? 0) === 0)
|
|
@@ -2501,7 +4105,7 @@ function assertValidSchemaIR(params) {
|
|
|
2501
4105
|
const source = exclusion.source;
|
|
2502
4106
|
if (source !== undefined) {
|
|
2503
4107
|
if (source.where !== undefined)
|
|
2504
|
-
assertPredicateCapabilities({ predicate: source.where, dialect, usage: "exclusionPredicate" });
|
|
4108
|
+
assertPredicateCapabilities({ predicate: source.where, model, dialect, usage: "exclusionPredicate" });
|
|
2505
4109
|
const references = [...source.keys.flatMap((key) => ("field" in key) ? [key.field] : key.fields), ...[source.range.lower, source.range.upper].flatMap((endpoint) => ("field" in endpoint) ? [endpoint.field] : []), ...source.where === undefined ? [] : predicateFieldNames({ predicate: source.where })];
|
|
2506
4110
|
if (references.some((name) => !model.fields.some((field) => field.name === name && !field.isList)))
|
|
2507
4111
|
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Exclusion references an unknown or list field", meta: { rule: "exclusion-field", model: model.name } });
|
|
@@ -2518,12 +4122,23 @@ function assertValidSchemaIR(params) {
|
|
|
2518
4122
|
if (dialect === "mysql" && counters.length > 1)
|
|
2519
4123
|
throw unsupportedCapability({ dialect, feature: "multiple auto-increment columns" });
|
|
2520
4124
|
for (const field of model.fields) {
|
|
4125
|
+
if (isLocalDateField({ field })) {
|
|
4126
|
+
if (!DIALECT_CAPABILITIES[dialect].localDateColumns)
|
|
4127
|
+
throw unsupportedCapability({ dialect, feature: "localDateColumns", hint: `${model.name}.${field.name}` });
|
|
4128
|
+
if (field.isList || field.isUpdatedAt || field.default !== undefined && (field.default.kind !== "literal" || !isCanonicalLocalDate({ value: field.default.value }))) {
|
|
4129
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: `Local-date field ${model.name}.${field.name} requires a scalar calendar day and an optional canonical YYYY-MM-DD literal default; lists, @updatedAt and generated defaults are not supported`, meta: { issues: [{ rule: "local-date-declaration", model: model.name, field: field.name }] } });
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
2521
4132
|
const timeOfDay = field.isUnsupported === true || field.type !== "DateTime" ? undefined : timeOfDayType({ nativeType: field.nativeType });
|
|
2522
4133
|
if (timeOfDay !== undefined && !DIALECT_CAPABILITIES[dialect].timeOfDayTypes.includes(timeOfDay)) {
|
|
2523
4134
|
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";
|
|
2524
4135
|
const error = unsupportedCapability({ dialect, feature: "timeOfDayTypes", hint: `${model.name}.${field.name} declares @db.${field.nativeType ?? ""} (${timeOfDay}) \u2014 ${instead}` });
|
|
2525
4136
|
throw new VibeError({ code: error.code, message: error.message, meta: { ...error.meta, model: model.name, field: field.name, nativeType: field.nativeType } });
|
|
2526
4137
|
}
|
|
4138
|
+
if (field.type === "String" && field.isUnsupported !== true && field.nativeType?.trim().toLowerCase() === "uuid" && DIALECT_CAPABILITIES[dialect].uuidColumns === "unsupported") {
|
|
4139
|
+
const error = unsupportedCapability({ dialect, feature: "uuidColumns", hint: `${model.name}.${field.name} declares @db.Uuid \u2014 ${dialect} has no uuid column type; store it as a plain String (or @db.Char(36))` });
|
|
4140
|
+
throw new VibeError({ code: error.code, message: error.message, meta: { ...error.meta, model: model.name, field: field.name, nativeType: field.nativeType } });
|
|
4141
|
+
}
|
|
2527
4142
|
if (timeOfDay !== undefined && dialect === "mysql" && field.default?.kind === "now") {
|
|
2528
4143
|
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)` });
|
|
2529
4144
|
throw new VibeError({ code: error.code, message: error.message, meta: { ...error.meta, model: model.name, field: field.name, nativeType: field.nativeType } });
|
|
@@ -2548,11 +4163,14 @@ function assertValidSchemaIR(params) {
|
|
|
2548
4163
|
const pk = model.primaryKey?.fields ?? model.fields.filter((entry) => entry.isId).map((entry) => entry.name);
|
|
2549
4164
|
if (dialect === "sqlite" && (pk.length !== 1 || pk[0] !== field.name))
|
|
2550
4165
|
throw unsupportedCapability({ dialect, feature: `non-primary auto-increment column ${model.name}.${field.name}` });
|
|
2551
|
-
if (dialect === "mysql" && pk[0] !== field.name && !field.isUnique && !model.indexes.some((index) =>
|
|
4166
|
+
if (dialect === "mysql" && pk[0] !== field.name && !field.isUnique && !model.indexes.some((index) => {
|
|
4167
|
+
const first = indexKeyTerms({ index })[0];
|
|
4168
|
+
return first?.kind === "field" && first.field.name === field.name;
|
|
4169
|
+
}))
|
|
2552
4170
|
throw unsupportedCapability({ dialect, feature: `unindexed auto-increment column ${model.name}.${field.name}` });
|
|
2553
4171
|
}
|
|
2554
4172
|
}
|
|
2555
|
-
const issues =
|
|
4173
|
+
const issues = validateSchemaWithRowRules({ ...params, rowRules });
|
|
2556
4174
|
if (issues.length > 0) {
|
|
2557
4175
|
throw new VibeError({
|
|
2558
4176
|
code: "VIBE_SCHEMA_INVALID",
|
|
@@ -2560,6 +4178,12 @@ function assertValidSchemaIR(params) {
|
|
|
2560
4178
|
meta: { issues }
|
|
2561
4179
|
});
|
|
2562
4180
|
}
|
|
4181
|
+
for (const model of params.schema.models)
|
|
4182
|
+
for (const rule of model.rowRules ?? []) {
|
|
4183
|
+
assertRowRuleCapability({ dialect, model: model.name, rule });
|
|
4184
|
+
rowRules.assertEquality({ model, rule });
|
|
4185
|
+
}
|
|
4186
|
+
assertRowRuleReferentialActions(params);
|
|
2563
4187
|
}
|
|
2564
4188
|
function rlsRecord(value) {
|
|
2565
4189
|
return typeof value === "object" && value !== null && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null) && Reflect.ownKeys(value).every((key) => {
|
|
@@ -2673,7 +4297,7 @@ function validateNativeRls(params) {
|
|
|
2673
4297
|
const field = model.fields.find((f) => f.name === fieldName);
|
|
2674
4298
|
if (field === undefined) {
|
|
2675
4299
|
add({ rule: "rls-unknown-field", message: `Native RLS requires a direct scalar field, not path ${String(fieldName)}` });
|
|
2676
|
-
} else if (field.isList || field.isUnsupported || isEnumType(field.type) || field.type === "Json" || field.type === "Bytes") {
|
|
4300
|
+
} else if (field.isList || field.isUnsupported || isLocalDateField({ field }) || isEnumType(field.type) || field.type === "Json" || field.type === "Bytes") {
|
|
2677
4301
|
add({ rule: "rls-unsupported-field", message: "Native RLS ownership requires a supported non-list scalar", field: field.name });
|
|
2678
4302
|
} else if (context !== undefined) {
|
|
2679
4303
|
const slot = Object.hasOwn(context, ref.context) ? context[ref.context] : undefined;
|
|
@@ -2727,22 +4351,25 @@ function validRlsForeignKey(params) {
|
|
|
2727
4351
|
const validFields = fields.every((name, index) => {
|
|
2728
4352
|
const field = model.fields.find((f) => f.name === name);
|
|
2729
4353
|
const ref = target.fields.find((f) => f.name === refs[index]);
|
|
2730
|
-
return field !== undefined && ref !== undefined && !field.isList && !ref.isList && !field.isUnsupported && !ref.isUnsupported && (field
|
|
4354
|
+
return field !== undefined && ref !== undefined && !field.isList && !ref.isList && !field.isUnsupported && !ref.isUnsupported && fieldTypeIdentity(field) === fieldTypeIdentity(ref);
|
|
2731
4355
|
});
|
|
2732
4356
|
const uniqueSets = [
|
|
2733
4357
|
...target.fields.filter((f) => f.isId || f.isUnique).map((f) => [f.name]),
|
|
2734
4358
|
...target.primaryKey === undefined ? [] : [target.primaryKey.fields],
|
|
2735
|
-
...target.indexes.filter((index) => index.kind === "unique" && index.where === undefined).map((index) => index.fields.map((f) => f.name))
|
|
4359
|
+
...target.indexes.filter((index) => index.kind === "unique" && !isExpressionIndex({ index }) && index.where === undefined).map((index) => index.fields.map((f) => f.name))
|
|
2736
4360
|
];
|
|
2737
4361
|
return validFields && uniqueSets.some((set) => set.length === refs.length && set.every((name) => refs.includes(name)));
|
|
2738
4362
|
}
|
|
2739
4363
|
export {
|
|
4364
|
+
CHECK_RULE_GROUP_MAX,
|
|
2740
4365
|
DIALECTS,
|
|
2741
4366
|
DIALECT_CAPABILITIES,
|
|
2742
4367
|
EMPTY_EXTENSION_REGISTRY,
|
|
2743
4368
|
ENUM_STORAGES,
|
|
2744
4369
|
IDENTIFIER_LIMITS,
|
|
2745
4370
|
MYSQL_CAPABILITIES,
|
|
4371
|
+
MYSQL_FUNCTIONAL_KEY_LIMITS,
|
|
4372
|
+
MYSQL_ROW_RULE_MESSAGE_MAX_BYTES,
|
|
2746
4373
|
POSTGRES_CAPABILITIES,
|
|
2747
4374
|
PREDICATE_COMPARISON_OPS,
|
|
2748
4375
|
READ_ONLY_REFUSAL_REASONS,
|
|
@@ -2754,26 +4381,37 @@ export {
|
|
|
2754
4381
|
RlsContextError,
|
|
2755
4382
|
SCOPED_REFUSAL_REASONS,
|
|
2756
4383
|
SQLITE_CAPABILITIES,
|
|
4384
|
+
SQLITE_ROW_RULE_ERROR_PREFIX,
|
|
2757
4385
|
SQL_NAMESPACES,
|
|
2758
4386
|
ScopedQueryError,
|
|
2759
4387
|
VIBE_ERROR_CODES,
|
|
2760
4388
|
VibeError,
|
|
2761
4389
|
assertExtensionModelPlacement,
|
|
2762
4390
|
assertExtensionPlacement,
|
|
4391
|
+
assertGeneratedColumn,
|
|
4392
|
+
assertGeneratedColumns,
|
|
2763
4393
|
assertIdentifierFits,
|
|
4394
|
+
assertIndexExpressions,
|
|
2764
4395
|
assertIndexOperatorClasses,
|
|
2765
4396
|
assertJsonSchemaRef,
|
|
2766
4397
|
assertMigrationObjects,
|
|
2767
4398
|
assertNativeRlsAdmission,
|
|
2768
4399
|
assertPredicateCapabilities,
|
|
4400
|
+
assertRowRuleCapability,
|
|
4401
|
+
assertRowRuleEquality,
|
|
4402
|
+
assertRowRuleReferentialActions,
|
|
2769
4403
|
assertSqlNamespace,
|
|
2770
4404
|
assertValidSchemaIR,
|
|
4405
|
+
assertValidatorRef,
|
|
2771
4406
|
buildComputedManifest,
|
|
2772
4407
|
buildExtensionRegistry,
|
|
4408
|
+
checkRulePredicate,
|
|
2773
4409
|
decodePrismaString,
|
|
4410
|
+
defaultExpressionIndexName,
|
|
2774
4411
|
defaultForeignKeyName,
|
|
2775
4412
|
defaultIndexName,
|
|
2776
4413
|
defaultPrimaryKeyName,
|
|
4414
|
+
defaultRowRuleName,
|
|
2777
4415
|
defineExtension,
|
|
2778
4416
|
encodePrismaString,
|
|
2779
4417
|
enumStorageOf,
|
|
@@ -2784,15 +4422,27 @@ export {
|
|
|
2784
4422
|
extensionOptionsHash,
|
|
2785
4423
|
fieldTypeIdentity,
|
|
2786
4424
|
fitDefaultName,
|
|
4425
|
+
generatedColumnFromSql,
|
|
4426
|
+
generatedColumnShapeIssue,
|
|
2787
4427
|
hasJsonSchemaAnnotation,
|
|
4428
|
+
hasValidatorAnnotation,
|
|
2788
4429
|
identifierLength,
|
|
2789
4430
|
implicitJoinTable,
|
|
2790
4431
|
implicitJoinTableObjectNames,
|
|
4432
|
+
indexKeyDefinitionKey,
|
|
4433
|
+
indexKeyDsl,
|
|
4434
|
+
indexKeyFieldNames,
|
|
4435
|
+
indexKeyPrisma,
|
|
4436
|
+
indexKeyTerms,
|
|
2791
4437
|
inspectPredicate,
|
|
4438
|
+
inspectPredicateExpression,
|
|
2792
4439
|
internalError,
|
|
2793
4440
|
invalidStoredData,
|
|
4441
|
+
isCanonicalLocalDate,
|
|
2794
4442
|
isEnumType,
|
|
4443
|
+
isExpressionIndex,
|
|
2795
4444
|
isImplicitManyToManyRelation,
|
|
4445
|
+
isLocalDateField,
|
|
2796
4446
|
isPredicateFieldRef,
|
|
2797
4447
|
isReservedClientName,
|
|
2798
4448
|
isReservedDelegateName,
|
|
@@ -2801,25 +4451,49 @@ export {
|
|
|
2801
4451
|
jsonSchemaRefIssue,
|
|
2802
4452
|
jsonTypeAnnotationIssue,
|
|
2803
4453
|
jsonTypeAnnotationRef,
|
|
4454
|
+
localDateCheckName,
|
|
2804
4455
|
mergeExtensionDeclarations,
|
|
2805
4456
|
modelSqlNamespace,
|
|
4457
|
+
mysqlIndexKeyWidth,
|
|
4458
|
+
mysqlRowRuleMessageByteLength,
|
|
2806
4459
|
normalizeJsonSchemaRef,
|
|
4460
|
+
normalizeRowRules,
|
|
4461
|
+
normalizeValidatorRef,
|
|
2807
4462
|
postgresGeneratedObjectName,
|
|
4463
|
+
predicateCapabilityIssue,
|
|
2808
4464
|
predicateFieldNames,
|
|
2809
4465
|
predicateKey,
|
|
2810
4466
|
prepareDatabaseModuleSchema,
|
|
2811
4467
|
providerToDialect,
|
|
2812
4468
|
qualifiedSqlName,
|
|
4469
|
+
resolveOnDelete,
|
|
4470
|
+
resolveOnUpdate,
|
|
4471
|
+
resolveReferentialActions,
|
|
4472
|
+
rowRuleCapability,
|
|
4473
|
+
rowRuleGateField,
|
|
4474
|
+
rowRuleGeneratedFields,
|
|
4475
|
+
rowRuleIdentities,
|
|
4476
|
+
rowRuleObjectNames,
|
|
4477
|
+
rowRulePrismaAttribute,
|
|
4478
|
+
rowRuleProtectedFields,
|
|
4479
|
+
rowRuleShapeIssue,
|
|
4480
|
+
rowRuleTriggerName,
|
|
2813
4481
|
scalarListDefault,
|
|
4482
|
+
schemaRowRuleIdentities,
|
|
2814
4483
|
serializeJsonDefault,
|
|
2815
4484
|
sqlNamespaceIssue,
|
|
2816
4485
|
sqlObjectKey,
|
|
2817
4486
|
sqlObjectNamespace,
|
|
4487
|
+
sqliteRowRuleConstraint,
|
|
2818
4488
|
timeOfDayType,
|
|
2819
4489
|
unsupportedCapability,
|
|
2820
4490
|
validateComputedConfig,
|
|
4491
|
+
validateGeneratedColumns,
|
|
4492
|
+
validateIndexExpressions,
|
|
2821
4493
|
validateRlsContextIR,
|
|
2822
|
-
|
|
4494
|
+
validateRowRules,
|
|
4495
|
+
validateSchemaIR,
|
|
4496
|
+
validatorRefIssue
|
|
2823
4497
|
};
|
|
2824
4498
|
|
|
2825
|
-
//# debugId=
|
|
4499
|
+
//# debugId=2B926380FFCA323B64756E2164756E21
|