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