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