@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/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/reserved.ts
398
- var RESERVED_DELEGATE_NAMES = [
399
- "findMany",
400
- "findPage",
401
- "findFirst",
402
- "findUnique",
403
- "findFirstOrThrow",
404
- "findUniqueOrThrow",
405
- "create",
406
- "createMany",
407
- "createManyAndReturn",
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
- function isReservedClientName(params) {
448
- const withPrefix = params.name.startsWith("$") ? params.name : `$${params.name}`;
449
- return RESERVED_CLIENT_NAMES.includes(withPrefix);
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/extension.ts
453
- function stableSerialize(value) {
454
- if (value === null || typeof value !== "object")
455
- return JSON.stringify(value) ?? "undefined";
456
- if (Array.isArray(value))
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 extensionOptionsHash(params) {
462
- const text = stableSerialize(params.options);
463
- let hash = 2166136261;
464
- for (let index = 0;index < text.length; index += 1) {
465
- hash ^= text.charCodeAt(index);
466
- hash = Math.imul(hash, 16777619);
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 extensionManifestEntry(params) {
471
- const { extension } = params;
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
- name: extension.name,
474
- version: extension.version,
475
- optionsHash: extensionOptionsHash({ options: extension.options })
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
- var EXTENSION_NAME_PATTERN = /^[a-z][a-zA-Z0-9]*$/;
479
- function defineExtension(extension) {
480
- const { name } = extension;
481
- if (!EXTENSION_NAME_PATTERN.test(name)) {
482
- throw new VibeError({
483
- code: "VIBE_CONFIG",
484
- message: `extension name "${name}" is invalid \u2014 must match ${EXTENSION_NAME_PATTERN} (it becomes a client identifier)`,
485
- meta: { name }
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
- if (RESERVED_DELEGATE_NAMES.includes(name)) {
489
- throw new VibeError({
490
- code: "VIBE_CONFIG",
491
- message: `extension name "${name}" collides with a core delegate method`,
492
- meta: { name }
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 (RESERVED_CLIENT_NAMES.includes(`$${name}`)) {
496
- throw new VibeError({
497
- code: "VIBE_CONFIG",
498
- message: `extension name "${name}" collides with the core client method "$${name}"`,
499
- meta: { name }
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
- return Object.freeze(extension);
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 buildExtensionRegistry(params) {
505
- const byName = new Map;
506
- const modelAttributes = new Map;
507
- const fieldTypes = new Map;
508
- for (const extension of params.extensions) {
509
- if (byName.has(extension.name)) {
510
- throw new VibeError({
511
- code: "VIBE_CONFIG",
512
- message: `duplicate extension name "${extension.name}" in the configured extension list`,
513
- meta: { name: extension.name }
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
- byName.set(extension.name, extension);
517
- for (const attribute of extension.schema?.modelAttributes ?? []) {
518
- const existing = modelAttributes.get(attribute.name);
519
- if (existing !== undefined) {
520
- throw new VibeError({
521
- code: "VIBE_CONFIG",
522
- message: `model attribute "@@${attribute.name}" is claimed by both "${existing.extension.name}" and "${extension.name}"`,
523
- meta: { attribute: attribute.name }
524
- });
525
- }
526
- modelAttributes.set(attribute.name, { extension, attribute });
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
- for (const fieldType of extension.schema?.fieldTypes ?? []) {
529
- const existing = fieldTypes.get(fieldType.name);
530
- if (existing !== undefined) {
531
- throw new VibeError({
532
- code: "VIBE_CONFIG",
533
- message: `field type "${fieldType.name}" is claimed by both "${existing.extension.name}" and "${extension.name}"`,
534
- meta: { fieldType: fieldType.name }
535
- });
536
- }
537
- fieldTypes.set(fieldType.name, { extension, fieldType });
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
- return { extensions: params.extensions, byName, modelAttributes, fieldTypes };
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
- var EMPTY_EXTENSION_REGISTRY = buildExtensionRegistry({
543
- extensions: []
544
- });
545
- var PUBLIC_ONLY_EXTENSIONS = ["search", "vector", "trgm"];
546
- function hasPublicExtensionDeclaration(params) {
547
- return PUBLIC_ONLY_EXTENSIONS.some((name) => Object.hasOwn(extensionDeclarations({ schema: params.schema, name }), params.model));
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 assertExtensionModelPlacement(params) {
550
- if (params.namespace !== undefined && params.namespace !== "public") {
551
- throw unsupportedCapability({ dialect: params.dialect ?? "postgres", feature: "fixedNamespaceExtensions", hint: "Search, vector and trigram extensions currently require public table placement" });
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 assertExtensionPlacement(params) {
2052
+ function assertRowRuleReferentialActions(params) {
555
2053
  const dialect = params.dialect ?? providerToDialect({ provider: params.schema.datasource.provider });
556
- for (const extension of params.schema.extensions) {
557
- if (!PUBLIC_ONLY_EXTENSIONS.includes(extension.name))
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
- const declarations = extensionDeclarations({ schema: params.schema, name: extension.name });
560
- for (const model of params.schema.models) {
561
- if (Object.hasOwn(declarations, model.name))
562
- assertExtensionModelPlacement({ namespace: model.namespace, dialect });
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 isPlainObject(value) {
567
- return typeof value === "object" && value !== null && !Array.isArray(value);
568
- }
569
- function mergeExtensionDeclarations(params) {
570
- if (params.extension !== undefined && PUBLIC_ONLY_EXTENSIONS.includes(params.extension) && Array.isArray(params.base.fields) && Array.isArray(params.next.fields)) {
571
- 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 } });
572
- }
573
- const out = { ...params.base };
574
- for (const [key, value] of Object.entries(params.next)) {
575
- const existing = out[key];
576
- out[key] = isPlainObject(existing) && isPlainObject(value) ? { ...existing, ...value } : value;
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 record(value) {
2126
+ function record3(value) {
634
2127
  return value !== null && typeof value === "object" && !Array.isArray(value);
635
2128
  }
636
2129
  function name(value) {
637
- return record(value) && typeof value.namespace === "string" && value.namespace.length > 0 && typeof value.name === "string" && value.name.length > 0;
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 (!record(value))
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 record(value) && (value.format === "catalog-v1" || value.format === "catalog-v2") && isSqlObjectIdentity(value.identity) && record(value.facts) && Object.keys(value.facts).length > 0 && Object.values(value.facts).every((fact) => fact === null || typeof fact === "string" || typeof fact === "boolean" || typeof fact === "number" && Number.isFinite(fact) || Array.isArray(fact) && fact.every((entry) => typeof entry === "string")) && Array.isArray(value.dependencies) && value.dependencies.every(isSqlObjectIdentity);
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 (!record(params.value))
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 (!record(value))
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 (!record(value))
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 (!record(value.facts) || Object.keys(value.facts).length === 0)
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 (!record(object))
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 field of index.fields) {
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/predicate.ts
837
- var PREDICATE_COMPARISON_OPS = [
838
- "eq",
839
- "ne",
840
- "gt",
841
- "gte",
842
- "lt",
843
- "lte"
844
- ];
845
- function isPredicateFieldRef(operand) {
846
- return operand.kind === "field";
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 predicateKey(params) {
921
- if (inspectPredicate(params).checkOnly) {
922
- const scalar = (node) => {
923
- switch (node.kind) {
924
- case "field":
925
- return ["field", node.name];
926
- case "jsonText":
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
- const expression = (node) => {
951
- switch (node.kind) {
952
- case "field":
953
- return `f:${node.name}`;
954
- case "jsonText":
955
- return `jsonText(${JSON.stringify([node.field.name, node.path])})`;
956
- case "length":
957
- return `length(${expression(node.expression)})`;
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
- const encode = (node) => {
961
- switch (node.kind) {
962
- case "compare":
963
- 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)})`;
964
- case "inList":
965
- return `in${node.negate === true ? "!" : ""}(${node.field.name},[${node.values.map((entry) => `${typeof entry.value}:${String(entry.value)}`).join("|")}])`;
966
- case "isNull":
967
- return `null${node.negate === true ? "!" : ""}(${node.field.name})`;
968
- case "and":
969
- case "or":
970
- return `${node.kind}(${node.operands.map(encode).join(",")})`;
971
- case "not":
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
- const { schema } = params;
1723
- const issues = [];
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 key = JSON.stringify([entry.physicalNamespace ?? "public", entry.namespace, entry.name]);
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 inspection = inspectPredicate({ predicate });
2411
- if (inspection.checkOnly && (params.usage !== "check" || !DIALECT_CAPABILITIES[params.dialect].schemaCheckExpressions)) {
2412
- issues.push({ rule: "predicate-check-capability", message: `${site}: computed schema predicates require a PostgreSQL CHECK`, model: model.name });
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
- if (expression.kind === "field")
2416
- return model.fields.find((field) => field.name === expression.name)?.type;
2417
- if (expression.kind === "jsonText") {
2418
- const field = model.fields.find((candidate) => candidate.name === expression.field.name);
2419
- if (field !== undefined && (field.type !== "Json" || field.isUnsupported === true))
2420
- issues.push({ rule: "predicate-type-mismatch", message: `${site}: jsonText requires a Json field`, model: model.name, field: field.name });
2421
- if (!Array.isArray(expression.path) || Array.from(expression.path).some((segment) => typeof segment !== "string" || segment.includes("\x00")))
2422
- issues.push({ rule: "predicate-json-path", message: `${site}: jsonText requires a string-only path without NUL bytes`, model: model.name });
2423
- return "String";
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
- if (node.left.kind !== "field" || node.right.kind !== "value" && node.right.kind !== "field") {
2432
- const left = expressionType(node.left);
2433
- const right = node.right.kind === "value" ? undefined : expressionType(node.right);
2434
- if (left !== undefined && right !== undefined && left !== right)
2435
- issues.push({ rule: "predicate-type-mismatch", message: `${site}: computed comparison operands must have the same scalar type`, model: model.name });
2436
- if (node.right.kind === "value" && left === "String" && typeof node.right.value !== "string")
2437
- issues.push({ rule: "predicate-type-mismatch", message: `${site}: a text expression requires a string literal`, model: model.name });
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 (inspection.checkOnly && referenced?.isUnsupported === true)
2457
- issues.push({ rule: "predicate-unsupported-type", message: `${site}: computed predicates cannot reference unsupported fields`, model: model.name, field: name });
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) => index.fields[0]?.name === field.name))
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 = validateSchemaIR(params);
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.type === ref.type || isEnumType(field.type) && isEnumType(ref.type) && field.type.enum === ref.type.enum);
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
- validateSchemaIR
4494
+ validateRowRules,
4495
+ validateSchemaIR,
4496
+ validatorRefIssue
2830
4497
  };
2831
4498
 
2832
- //# debugId=315C4C10E208512464756E2164756E21
4499
+ //# debugId=2B926380FFCA323B64756E2164756E21