@vibeorm/schema 4.1.0 → 4.3.0

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