@vibeorm/schema 2.4.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,85 +1,4 @@
1
1
  // @bun
2
- // packages/schema/src/ir.ts
3
- var DIALECTS = ["postgres", "sqlite", "mysql"];
4
- function providerToDialect(params) {
5
- const { provider } = params;
6
- switch (provider) {
7
- case "postgresql":
8
- case "pglite":
9
- return "postgres";
10
- case "sqlite":
11
- return "sqlite";
12
- case "mysql":
13
- return "mysql";
14
- }
15
- }
16
- function isEnumType(fieldType) {
17
- return typeof fieldType === "object" && "enum" in fieldType;
18
- }
19
- function fieldTypeIdentity(params) {
20
- const { type } = params;
21
- return isEnumType(type) ? `enum:${type.enum}` : type;
22
- }
23
- var ENUM_STORAGES = ["database", "string"];
24
- function enumStorageOf(params) {
25
- return params.enumDef.storage ?? "database";
26
- }
27
- // packages/schema/src/predicate.ts
28
- var PREDICATE_COMPARISON_OPS = [
29
- "eq",
30
- "ne",
31
- "gt",
32
- "gte",
33
- "lt",
34
- "lte"
35
- ];
36
- function isPredicateFieldRef(operand) {
37
- return operand.kind === "field";
38
- }
39
- function predicateFieldNames(params) {
40
- const seen = new Set;
41
- const walk = (node) => {
42
- switch (node.kind) {
43
- case "compare":
44
- seen.add(node.left.name);
45
- if (isPredicateFieldRef(node.right))
46
- seen.add(node.right.name);
47
- return;
48
- case "inList":
49
- case "isNull":
50
- seen.add(node.field.name);
51
- return;
52
- case "and":
53
- case "or":
54
- for (const operand of node.operands)
55
- walk(operand);
56
- return;
57
- case "not":
58
- walk(node.operand);
59
- return;
60
- }
61
- };
62
- walk(params.predicate);
63
- return [...seen];
64
- }
65
- function predicateKey(params) {
66
- const encode = (node) => {
67
- switch (node.kind) {
68
- case "compare":
69
- return `cmp(${node.op},${node.left.name},${isPredicateFieldRef(node.right) ? `f:${node.right.name}` : `v:${typeof node.right.value}:${String(node.right.value)}`})`;
70
- case "inList":
71
- return `in${node.negate === true ? "!" : ""}(${node.field.name},[${node.values.map((entry) => `${typeof entry.value}:${String(entry.value)}`).join("|")}])`;
72
- case "isNull":
73
- return `null${node.negate === true ? "!" : ""}(${node.field.name})`;
74
- case "and":
75
- case "or":
76
- return `${node.kind}(${node.operands.map(encode).join(",")})`;
77
- case "not":
78
- return `not(${encode(node.operand)})`;
79
- }
80
- };
81
- return encode(params.predicate);
82
- }
83
2
  // packages/schema/src/errors.ts
84
3
  var VIBE_ERROR_CODES = [
85
4
  "VIBE_NOT_FOUND",
@@ -100,6 +19,7 @@ var VIBE_ERROR_CODES = [
100
19
  "VIBE_RLS_CONTEXT",
101
20
  "VIBE_PERMISSION_DENIED",
102
21
  "VIBE_ADAPTER",
22
+ "VIBE_ADAPTER_CLOSED",
103
23
  "VIBE_INTERNAL"
104
24
  ];
105
25
 
@@ -223,10 +143,206 @@ class ReadOnlyClientError extends VibeError {
223
143
  }
224
144
  }
225
145
 
146
+ // packages/schema/src/physical-names.ts
147
+ var IDENTIFIER_LIMITS = Object.freeze({
148
+ postgres: { max: 63, unit: "bytes" },
149
+ mysql: { max: 64, unit: "characters" },
150
+ sqlite: undefined
151
+ });
152
+ var ENCODER = new TextEncoder;
153
+ function identifierLength(params) {
154
+ const limit = IDENTIFIER_LIMITS[params.dialect];
155
+ return limit?.unit === "bytes" ? ENCODER.encode(params.name).length : [...params.name].length;
156
+ }
157
+ function cutIdentifier(params) {
158
+ let out = "";
159
+ let used = 0;
160
+ for (const char of params.name) {
161
+ const size = params.unit === "bytes" ? ENCODER.encode(char).length : 1;
162
+ if (used + size > params.length)
163
+ break;
164
+ out += char;
165
+ used += size;
166
+ }
167
+ return out;
168
+ }
169
+ function fitDefaultName(params) {
170
+ const limit = IDENTIFIER_LIMITS[params.dialect];
171
+ if (limit === undefined)
172
+ return `${params.prefix}${params.suffix}`;
173
+ const room = limit.max - identifierLength({ name: params.suffix, dialect: params.dialect });
174
+ if (identifierLength({ name: params.prefix, dialect: params.dialect }) <= room)
175
+ return `${params.prefix}${params.suffix}`;
176
+ return `${cutIdentifier({ name: params.prefix, length: room, unit: limit.unit })}${params.suffix}`;
177
+ }
178
+ function assertIdentifierFits(params) {
179
+ const limit = IDENTIFIER_LIMITS[params.dialect];
180
+ if (limit === undefined)
181
+ return;
182
+ const length = identifierLength({ name: params.name, dialect: params.dialect });
183
+ if (length <= limit.max)
184
+ return;
185
+ throw new VibeError({
186
+ code: "VIBE_SCHEMA_INVALID",
187
+ 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}`,
188
+ meta: { name: params.name, dialect: params.dialect, length, max: limit.max }
189
+ });
190
+ }
191
+ function defaultIndexName(params) {
192
+ return fitDefaultName({
193
+ prefix: `${params.table}_${params.columns.join("_")}`,
194
+ suffix: params.unique ? "_key" : "_idx",
195
+ dialect: params.dialect
196
+ });
197
+ }
198
+ function defaultForeignKeyName(params) {
199
+ return fitDefaultName({ prefix: `${params.table}_${params.columns.join("_")}`, suffix: "_fkey", dialect: params.dialect });
200
+ }
201
+ function defaultPrimaryKeyName(params) {
202
+ return fitDefaultName({ prefix: params.table, suffix: "_pkey", dialect: params.dialect });
203
+ }
204
+ function implicitJoinTableObjectNames(params) {
205
+ return {
206
+ uniqueIndex: `${params.table}_AB_unique`,
207
+ indexB: `${params.table}_B_index`,
208
+ foreignKeyA: `${params.table}_A_fkey`,
209
+ foreignKeyB: `${params.table}_B_fkey`
210
+ };
211
+ }
212
+ function postgresGeneratedObjectName(params) {
213
+ const limit = IDENTIFIER_LIMITS.postgres;
214
+ const overhead = params.label.length + 2;
215
+ let chars1 = [...params.name1];
216
+ let chars2 = [...params.name2];
217
+ const bytes = (chars) => ENCODER.encode(chars.join("")).length;
218
+ while (bytes(chars1) + bytes(chars2) + overhead > limit.max) {
219
+ if (bytes(chars1) > bytes(chars2))
220
+ chars1 = chars1.slice(0, -1);
221
+ else
222
+ chars2 = chars2.slice(0, -1);
223
+ }
224
+ return `${chars1.join("")}_${chars2.join("")}_${params.label}`;
225
+ }
226
+
227
+ // packages/schema/src/namespaces.ts
228
+ var SQL_NAMESPACES = Object.freeze({
229
+ postgres: "public",
230
+ sqlite: "main",
231
+ private: Object.freeze(["_vibeorm_shadow", "_vibe_shadow"])
232
+ });
233
+ function sqlNamespaceIssue(params) {
234
+ const { namespace } = params;
235
+ if (typeof namespace !== "string" || namespace.trim().length === 0 || namespace.includes("\x00"))
236
+ return "Namespace must be a non-empty identifier without NUL bytes";
237
+ if (identifierLength({ name: namespace, dialect: "postgres" }) > IDENTIFIER_LIMITS.postgres.max)
238
+ return "PostgreSQL namespace exceeds its identifier byte limit";
239
+ if (namespace === "information_schema" || namespace.startsWith("pg_") || SQL_NAMESPACES.private.includes(namespace))
240
+ return "System and private VibeORM namespaces cannot be declared";
241
+ if (params.allowPublic === false && namespace === SQL_NAMESPACES.postgres)
242
+ return "A fixed package namespace must be non-public; omit namespace for a legacy public artifact";
243
+ return;
244
+ }
245
+ function assertSqlNamespace(params) {
246
+ if (params.dialect !== "postgres")
247
+ throw unsupportedCapability({ dialect: params.dialect, feature: "fixedNamespaces" });
248
+ const issue = sqlNamespaceIssue(params);
249
+ if (issue !== undefined)
250
+ throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: issue, meta: { rule: "namespace", namespace: params.namespace } });
251
+ }
252
+ function qualifiedSqlName(params) {
253
+ return { namespace: params.namespace ?? params.defaultNamespace ?? SQL_NAMESPACES.postgres, name: params.name };
254
+ }
255
+ // packages/schema/src/capabilities.ts
256
+ var POSTGRES_CAPABILITIES = Object.freeze({
257
+ fixedNamespaces: true,
258
+ externalObjects: true,
259
+ indexOperatorClasses: true,
260
+ exclusionConstraints: true,
261
+ exclusionExtensions: Object.freeze(["btree_gist"]),
262
+ checkNormalization: "rollback",
263
+ nativeRowLevelSecurity: true,
264
+ returning: true,
265
+ scalarArrays: true,
266
+ nativeEnums: true,
267
+ ilike: true,
268
+ lateralJoin: true,
269
+ distinctOn: true,
270
+ partialIndexes: true,
271
+ conflictTargetConstraint: true,
272
+ transactionalDdl: true,
273
+ createManyAndReturn: true,
274
+ jsonColumnType: true,
275
+ orderByNulls: true,
276
+ fullTextEngines: Object.freeze(["bm25", "tsvector"]),
277
+ rowLockStrengths: Object.freeze(["update", "noKeyUpdate", "share", "keyShare"]),
278
+ rowLockWaitPolicies: Object.freeze(["wait", "nowait", "skipLocked"]),
279
+ advisoryLocks: true,
280
+ timeOfDayTypes: Object.freeze(["time", "timetz"])
281
+ });
282
+ var SQLITE_CAPABILITIES = Object.freeze({
283
+ fixedNamespaces: false,
284
+ externalObjects: false,
285
+ indexOperatorClasses: false,
286
+ exclusionConstraints: false,
287
+ exclusionExtensions: Object.freeze([]),
288
+ checkNormalization: "sourceText",
289
+ nativeRowLevelSecurity: false,
290
+ returning: true,
291
+ scalarArrays: false,
292
+ nativeEnums: false,
293
+ ilike: false,
294
+ lateralJoin: false,
295
+ distinctOn: false,
296
+ partialIndexes: true,
297
+ conflictTargetConstraint: false,
298
+ transactionalDdl: true,
299
+ createManyAndReturn: true,
300
+ jsonColumnType: false,
301
+ orderByNulls: true,
302
+ fullTextEngines: Object.freeze(["fts5"]),
303
+ rowLockStrengths: Object.freeze([]),
304
+ rowLockWaitPolicies: Object.freeze([]),
305
+ advisoryLocks: false,
306
+ timeOfDayTypes: Object.freeze([])
307
+ });
308
+ var MYSQL_CAPABILITIES = Object.freeze({
309
+ fixedNamespaces: false,
310
+ externalObjects: false,
311
+ indexOperatorClasses: false,
312
+ exclusionConstraints: false,
313
+ exclusionExtensions: Object.freeze([]),
314
+ checkNormalization: "temporary",
315
+ nativeRowLevelSecurity: false,
316
+ returning: false,
317
+ scalarArrays: false,
318
+ nativeEnums: true,
319
+ ilike: false,
320
+ lateralJoin: false,
321
+ distinctOn: false,
322
+ partialIndexes: false,
323
+ conflictTargetConstraint: false,
324
+ transactionalDdl: false,
325
+ createManyAndReturn: false,
326
+ jsonColumnType: true,
327
+ orderByNulls: false,
328
+ fullTextEngines: Object.freeze(["fulltext"]),
329
+ rowLockStrengths: Object.freeze(["update", "share"]),
330
+ rowLockWaitPolicies: Object.freeze(["wait", "nowait", "skipLocked"]),
331
+ advisoryLocks: false,
332
+ timeOfDayTypes: Object.freeze(["time"])
333
+ });
334
+ var DIALECT_CAPABILITIES = Object.freeze({
335
+ postgres: POSTGRES_CAPABILITIES,
336
+ sqlite: SQLITE_CAPABILITIES,
337
+ mysql: MYSQL_CAPABILITIES
338
+ });
339
+
226
340
  // packages/schema/src/migration-objects.ts
227
341
  function sqlObjectKey(params) {
228
342
  const object = params.identity;
229
343
  switch (object.kind) {
344
+ case "namespace":
345
+ return JSON.stringify([object.kind, object.name]);
230
346
  case "table":
231
347
  case "view":
232
348
  case "sequence":
@@ -246,6 +362,12 @@ function sqlObjectKey(params) {
246
362
  return JSON.stringify([object.kind, object.table.namespace, object.table.name, object.column]);
247
363
  }
248
364
  }
365
+ function sqlObjectNamespace(params) {
366
+ const identity = params.identity;
367
+ if (identity.kind === "namespace")
368
+ return identity.name;
369
+ return "table" in identity ? identity.table.namespace : identity.name.namespace;
370
+ }
249
371
  function record(value) {
250
372
  return value !== null && typeof value === "object" && !Array.isArray(value);
251
373
  }
@@ -256,6 +378,8 @@ function isSqlObjectIdentity(value) {
256
378
  if (!record(value))
257
379
  return false;
258
380
  switch (value.kind) {
381
+ case "namespace":
382
+ return typeof value.name === "string" && value.name.length > 0;
259
383
  case "table":
260
384
  case "view":
261
385
  case "sequence":
@@ -277,24 +401,271 @@ function isSqlObjectIdentity(value) {
277
401
  return false;
278
402
  }
279
403
  }
280
- function isSqlObjectDefinition(value) {
281
- 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);
404
+ function isSqlObjectDefinition(value) {
405
+ 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);
406
+ }
407
+ function qualifiedNameIssue(params) {
408
+ if (!record(params.value))
409
+ return { path: params.path, expected: "qualified SQL name { namespace, name }" };
410
+ const member = typeof params.value.namespace !== "string" || params.value.namespace.length === 0 ? "namespace" : "name";
411
+ return { path: `${params.path}.${member}`, expected: "non-empty string" };
412
+ }
413
+ function identityIssue(params) {
414
+ const { value, path } = params;
415
+ if (!record(value))
416
+ return { path, expected: "SQL object identity" };
417
+ switch (value.kind) {
418
+ case "namespace":
419
+ return { path: `${path}.name`, expected: "non-empty namespace string" };
420
+ case "table":
421
+ case "view":
422
+ case "sequence":
423
+ case "postgresExtension":
424
+ case "enum":
425
+ return qualifiedNameIssue({ value: value.name, path: `${path}.name` });
426
+ case "function": {
427
+ if (!name(value.name))
428
+ return qualifiedNameIssue({ value: value.name, path: `${path}.name` });
429
+ if (!Array.isArray(value.argumentTypes))
430
+ return { path: `${path}.argumentTypes`, expected: "array of qualified SQL type names" };
431
+ const index = value.argumentTypes.findIndex((entry) => !name(entry));
432
+ return qualifiedNameIssue({ value: value.argumentTypes[index], path: `${path}.argumentTypes[${index}]` });
433
+ }
434
+ case "index":
435
+ case "trigger":
436
+ case "constraint":
437
+ case "column":
438
+ case "columnDefault":
439
+ case "generatedColumn": {
440
+ if (!name(value.table))
441
+ return qualifiedNameIssue({ value: value.table, path: `${path}.table` });
442
+ if (value.kind === "column" || value.kind === "columnDefault" || value.kind === "generatedColumn")
443
+ return { path: `${path}.column`, expected: "non-empty column name" };
444
+ if (typeof value.name !== "string" || value.name.length === 0)
445
+ return { path: `${path}.name`, expected: "non-empty object name" };
446
+ return { path: `${path}.constraintType`, expected: ["check", "foreignKey", "exclusion", "unique", "primaryKey"] };
447
+ }
448
+ default:
449
+ return { path: `${path}.kind`, expected: ["namespace", "table", "view", "sequence", "postgresExtension", "enum", "function", "index", "trigger", "constraint", "column", "columnDefault", "generatedColumn"] };
450
+ }
451
+ }
452
+ function definitionIssue(params) {
453
+ const { value, path } = params;
454
+ if (!record(value))
455
+ return { path, expected: "independent SQL object definition" };
456
+ if (value.format !== "catalog-v1" && value.format !== "catalog-v2")
457
+ return { path: `${path}.format`, expected: ["catalog-v1", "catalog-v2"] };
458
+ if (!isSqlObjectIdentity(value.identity))
459
+ return identityIssue({ value: value.identity, path: `${path}.identity` });
460
+ if (!record(value.facts) || Object.keys(value.facts).length === 0)
461
+ return { path: `${path}.facts`, expected: "non-empty serializable catalog facts" };
462
+ for (const [key, fact] of Object.entries(value.facts)) {
463
+ if (!(fact === null || typeof fact === "string" || typeof fact === "boolean" || typeof fact === "number" && Number.isFinite(fact) || Array.isArray(fact) && fact.every((entry) => typeof entry === "string")))
464
+ return { path: `${path}.facts.${key}`, expected: "string, finite number, boolean, null or string array" };
465
+ }
466
+ if (!Array.isArray(value.dependencies))
467
+ return { path: `${path}.dependencies`, expected: "array of SQL object identities" };
468
+ const index = value.dependencies.findIndex((entry) => !isSqlObjectIdentity(entry));
469
+ return identityIssue({ value: value.dependencies[index], path: `${path}.dependencies[${index}]` });
470
+ }
471
+ function invalidDeclaration(params) {
472
+ const expected = typeof params.expected === "string" ? params.expected : params.expected.join(", ");
473
+ throw new VibeError({
474
+ code: "VIBE_SCHEMA_INVALID",
475
+ message: `Invalid migration-only object declaration at ${params.path}; expected ${expected}`,
476
+ meta: { path: params.path, expected: params.expected, ...params.id === undefined ? {} : { id: params.id } }
477
+ });
478
+ }
479
+ function assertMigrationObjects(params) {
480
+ if (!Array.isArray(params.objects))
481
+ invalidDeclaration({ path: "migrationObjects", expected: "array" });
482
+ const ids = new Set;
483
+ const owners = new Set;
484
+ for (const [index, value] of params.objects.entries()) {
485
+ const object = value;
486
+ const path = `migrationObjects[${index}]`;
487
+ if (!record(object))
488
+ invalidDeclaration({ path, expected: "object" });
489
+ if (typeof object.id !== "string" || object.id.length === 0)
490
+ invalidDeclaration({ path: `${path}.id`, expected: "non-empty string" });
491
+ const id = object.id;
492
+ if (!["postgres", "sqlite", "mysql"].includes(String(object.dialect)))
493
+ invalidDeclaration({ path: `${path}.dialect`, expected: ["postgres", "sqlite", "mysql"], id });
494
+ const identity = object.identity;
495
+ if (!isSqlObjectIdentity(identity))
496
+ invalidDeclaration({ ...identityIssue({ value: identity, path: `${path}.identity` }), id });
497
+ for (const member of ["create", "drop"]) {
498
+ const statements = object[member];
499
+ if (!Array.isArray(statements))
500
+ invalidDeclaration({ path: `${path}.${member}`, expected: "array of SQL strings", id });
501
+ const invalid = statements.findIndex((entry) => typeof entry !== "string");
502
+ if (invalid !== -1)
503
+ invalidDeclaration({ path: `${path}.${member}[${invalid}]`, expected: "SQL string", id });
504
+ }
505
+ if (!Array.isArray(object.dependsOn))
506
+ invalidDeclaration({ path: `${path}.dependsOn`, expected: "array of SQL object identities", id });
507
+ for (const [dependencyIndex, dependency] of object.dependsOn.entries())
508
+ if (!isSqlObjectIdentity(dependency))
509
+ invalidDeclaration({ ...identityIssue({ value: dependency, path: `${path}.dependsOn[${dependencyIndex}]` }), id });
510
+ if (object.expected !== undefined) {
511
+ if (!isSqlObjectDefinition(object.expected))
512
+ invalidDeclaration({ ...definitionIssue({ value: object.expected, path: `${path}.expected` }), id });
513
+ if (sqlObjectKey({ identity: object.expected.identity }) !== sqlObjectKey({ identity }))
514
+ invalidDeclaration({ path: `${path}.expected.identity`, expected: "the declaration's SQL object identity", id });
515
+ }
516
+ const key = sqlObjectKey({ identity });
517
+ if (ids.has(id) || owners.has(key))
518
+ throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Duplicate migration object identity or id", meta: { id, key, path: `${path}.${ids.has(id) ? "id" : "identity"}` } });
519
+ ids.add(id);
520
+ owners.add(key);
521
+ }
522
+ }
523
+
524
+ // packages/schema/src/index-operator-classes.ts
525
+ function assertIndexOperatorClasses(params) {
526
+ for (const model of params.schema.models)
527
+ for (const index of model.indexes)
528
+ for (const field of index.fields) {
529
+ if (field.operatorClass === undefined)
530
+ continue;
531
+ if (!DIALECT_CAPABILITIES[params.dialect].indexOperatorClasses)
532
+ throw unsupportedCapability({ dialect: params.dialect, feature: "indexOperatorClasses" });
533
+ if (index.kind === "unique" && params.physical !== true)
534
+ throw unsupportedCapability({ dialect: params.dialect, feature: "uniqueIndexOperatorClasses", hint: "custom unique operator classes can change client selector equality" });
535
+ if (!isSqlObjectIdentity({ kind: "table", name: field.operatorClass }))
536
+ throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Index operator class must be a qualified SQL name { namespace, name }", meta: { rule: "index-operator-class-identity", model: model.name, field: field.name } });
537
+ }
538
+ }
539
+ // packages/schema/src/ir.ts
540
+ var DIALECTS = ["postgres", "sqlite", "mysql"];
541
+ function providerToDialect(params) {
542
+ const { provider } = params;
543
+ switch (provider) {
544
+ case "postgresql":
545
+ case "pglite":
546
+ return "postgres";
547
+ case "sqlite":
548
+ return "sqlite";
549
+ case "mysql":
550
+ return "mysql";
551
+ }
552
+ }
553
+ function isEnumType(fieldType) {
554
+ return typeof fieldType === "object" && "enum" in fieldType;
555
+ }
556
+ function fieldTypeIdentity(params) {
557
+ const { type } = params;
558
+ return isEnumType(type) ? type.namespace === undefined || type.namespace === "public" ? `enum:${type.enum}` : JSON.stringify(["enum", type.namespace, type.enum]) : type;
559
+ }
560
+ var ENUM_STORAGES = ["database", "string"];
561
+ function enumStorageOf(params) {
562
+ return params.enumDef.storage ?? "database";
563
+ }
564
+
565
+ // packages/schema/src/module-schema.ts
566
+ function prepareDatabaseModuleSchema(params) {
567
+ const { schema, module } = params;
568
+ const namespace = module.namespace;
569
+ const dialect = providerToDialect({ provider: schema.datasource.provider });
570
+ if (namespace !== undefined)
571
+ assertSqlNamespace({ namespace, dialect, allowPublic: false });
572
+ const expected = namespace ?? SQL_NAMESPACES.postgres;
573
+ for (const entry of [...schema.models, ...schema.enums]) {
574
+ if (entry.namespace === undefined)
575
+ continue;
576
+ assertSqlNamespace({ namespace: entry.namespace, dialect });
577
+ if (qualifiedSqlName({ name: entry.dbName ?? entry.name, namespace: entry.namespace }).namespace !== expected)
578
+ 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 } });
579
+ }
580
+ for (const model of schema.models)
581
+ for (const field of model.fields) {
582
+ if (!isEnumType(field.type) || field.type.namespace === undefined)
583
+ continue;
584
+ assertSqlNamespace({ namespace: field.type.namespace, dialect });
585
+ if (field.type.namespace !== expected)
586
+ 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 } });
587
+ }
588
+ if (namespace === undefined)
589
+ return schema;
590
+ const nativeEnums = new Set(schema.enums.filter((entry) => enumStorageOf({ enumDef: entry }) === "database").map((entry) => entry.name));
591
+ return {
592
+ ...schema,
593
+ models: schema.models.map((model) => ({ ...model, namespace, fields: model.fields.map((field) => {
594
+ const type = field.type;
595
+ return isEnumType(type) && nativeEnums.has(type.enum) ? { ...field, type: { ...type, namespace } } : field;
596
+ }) })),
597
+ enums: schema.enums.map((entry) => enumStorageOf({ enumDef: entry }) === "database" ? { ...entry, namespace } : entry)
598
+ };
599
+ }
600
+ // packages/schema/src/predicate.ts
601
+ var PREDICATE_COMPARISON_OPS = [
602
+ "eq",
603
+ "ne",
604
+ "gt",
605
+ "gte",
606
+ "lt",
607
+ "lte"
608
+ ];
609
+ function isPredicateFieldRef(operand) {
610
+ return operand.kind === "field";
282
611
  }
283
- function assertMigrationObjects(params) {
284
- if (!Array.isArray(params.objects))
285
- throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "migrationObjects must be an array" });
286
- const ids = new Set;
287
- const owners = new Set;
288
- for (const object of params.objects) {
289
- if (!record(object) || typeof object.id !== "string" || object.id.length === 0 || !["postgres", "sqlite", "mysql"].includes(String(object.dialect)) || !isSqlObjectIdentity(object.identity) || !Array.isArray(object.create) || !object.create.every((sql) => typeof sql === "string") || !Array.isArray(object.drop) || !object.drop.every((sql) => typeof sql === "string") || !Array.isArray(object.dependsOn) || !object.dependsOn.every(isSqlObjectIdentity) || object.expected !== undefined && (!isSqlObjectDefinition(object.expected) || sqlObjectKey({ identity: object.expected.identity }) !== sqlObjectKey({ identity: object.identity }))) {
290
- throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Invalid migration-only object declaration" });
291
- }
292
- const key = sqlObjectKey({ identity: object.identity });
293
- if (ids.has(object.id) || owners.has(key))
294
- throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Duplicate migration object identity or id", meta: { id: object.id, key } });
295
- ids.add(object.id);
296
- owners.add(key);
297
- }
612
+ function predicateFieldNames(params) {
613
+ const seen = new Set;
614
+ const walk = (node) => {
615
+ switch (node.kind) {
616
+ case "compare":
617
+ seen.add(node.left.name);
618
+ if (isPredicateFieldRef(node.right))
619
+ seen.add(node.right.name);
620
+ return;
621
+ case "inList":
622
+ case "isNull":
623
+ seen.add(node.field.name);
624
+ return;
625
+ case "and":
626
+ case "or":
627
+ for (const operand of node.operands)
628
+ walk(operand);
629
+ return;
630
+ case "not":
631
+ walk(node.operand);
632
+ return;
633
+ }
634
+ };
635
+ walk(params.predicate);
636
+ return [...seen];
637
+ }
638
+ function predicateKey(params) {
639
+ const encode = (node) => {
640
+ switch (node.kind) {
641
+ case "compare":
642
+ return `cmp(${node.op},${node.left.name},${isPredicateFieldRef(node.right) ? `f:${node.right.name}` : `v:${typeof node.right.value}:${String(node.right.value)}`})`;
643
+ case "inList":
644
+ return `in${node.negate === true ? "!" : ""}(${node.field.name},[${node.values.map((entry) => `${typeof entry.value}:${String(entry.value)}`).join("|")}])`;
645
+ case "isNull":
646
+ return `null${node.negate === true ? "!" : ""}(${node.field.name})`;
647
+ case "and":
648
+ case "or":
649
+ return `${node.kind}(${node.operands.map(encode).join(",")})`;
650
+ case "not":
651
+ return `not(${encode(node.operand)})`;
652
+ }
653
+ };
654
+ return encode(params.predicate);
655
+ }
656
+ // packages/schema/src/time-of-day.ts
657
+ var TIME_OF_DAY_NATIVE_TYPE = /^time(tz)?(?:\(\d+\))?$|^time(?:\(\d+\))?with(out)?timezone$/;
658
+ function timeOfDayType(params) {
659
+ if (params.nativeType === undefined)
660
+ return;
661
+ const match = TIME_OF_DAY_NATIVE_TYPE.exec(params.nativeType.toLowerCase().replace(/\s+/g, ""));
662
+ if (match === null)
663
+ return;
664
+ if (match[1] !== undefined)
665
+ return "timetz";
666
+ if (match[0].endsWith("withtimezone") && match[2] === undefined)
667
+ return "timetz";
668
+ return "time";
298
669
  }
299
670
  // packages/schema/src/prisma-literal.ts
300
671
  var STRING_ESCAPES = {
@@ -407,6 +778,7 @@ function exactDecimalLiteral(params) {
407
778
  // packages/schema/src/reserved.ts
408
779
  var RESERVED_DELEGATE_NAMES = [
409
780
  "findMany",
781
+ "findPage",
410
782
  "findFirst",
411
783
  "findUnique",
412
784
  "findFirstOrThrow",
@@ -740,160 +1112,6 @@ function implicitJoinTable(params) {
740
1112
  };
741
1113
  }
742
1114
 
743
- // packages/schema/src/physical-names.ts
744
- var IDENTIFIER_LIMITS = Object.freeze({
745
- postgres: { max: 63, unit: "bytes" },
746
- mysql: { max: 64, unit: "characters" },
747
- sqlite: undefined
748
- });
749
- var ENCODER = new TextEncoder;
750
- function identifierLength(params) {
751
- const limit = IDENTIFIER_LIMITS[params.dialect];
752
- return limit?.unit === "bytes" ? ENCODER.encode(params.name).length : [...params.name].length;
753
- }
754
- function cutIdentifier(params) {
755
- let out = "";
756
- let used = 0;
757
- for (const char of params.name) {
758
- const size = params.unit === "bytes" ? ENCODER.encode(char).length : 1;
759
- if (used + size > params.length)
760
- break;
761
- out += char;
762
- used += size;
763
- }
764
- return out;
765
- }
766
- function fitDefaultName(params) {
767
- const limit = IDENTIFIER_LIMITS[params.dialect];
768
- if (limit === undefined)
769
- return `${params.prefix}${params.suffix}`;
770
- const room = limit.max - identifierLength({ name: params.suffix, dialect: params.dialect });
771
- if (identifierLength({ name: params.prefix, dialect: params.dialect }) <= room)
772
- return `${params.prefix}${params.suffix}`;
773
- return `${cutIdentifier({ name: params.prefix, length: room, unit: limit.unit })}${params.suffix}`;
774
- }
775
- function assertIdentifierFits(params) {
776
- const limit = IDENTIFIER_LIMITS[params.dialect];
777
- if (limit === undefined)
778
- return;
779
- const length = identifierLength({ name: params.name, dialect: params.dialect });
780
- if (length <= limit.max)
781
- return;
782
- throw new VibeError({
783
- code: "VIBE_SCHEMA_INVALID",
784
- 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}`,
785
- meta: { name: params.name, dialect: params.dialect, length, max: limit.max }
786
- });
787
- }
788
- function defaultIndexName(params) {
789
- return fitDefaultName({
790
- prefix: `${params.table}_${params.columns.join("_")}`,
791
- suffix: params.unique ? "_key" : "_idx",
792
- dialect: params.dialect
793
- });
794
- }
795
- function defaultForeignKeyName(params) {
796
- return fitDefaultName({ prefix: `${params.table}_${params.columns.join("_")}`, suffix: "_fkey", dialect: params.dialect });
797
- }
798
- function defaultPrimaryKeyName(params) {
799
- return fitDefaultName({ prefix: params.table, suffix: "_pkey", dialect: params.dialect });
800
- }
801
- function implicitJoinTableObjectNames(params) {
802
- return {
803
- uniqueIndex: `${params.table}_AB_unique`,
804
- indexB: `${params.table}_B_index`,
805
- foreignKeyA: `${params.table}_A_fkey`,
806
- foreignKeyB: `${params.table}_B_fkey`
807
- };
808
- }
809
- function postgresGeneratedObjectName(params) {
810
- const limit = IDENTIFIER_LIMITS.postgres;
811
- const overhead = params.label.length + 2;
812
- let chars1 = [...params.name1];
813
- let chars2 = [...params.name2];
814
- const bytes = (chars) => ENCODER.encode(chars.join("")).length;
815
- while (bytes(chars1) + bytes(chars2) + overhead > limit.max) {
816
- if (bytes(chars1) > bytes(chars2))
817
- chars1 = chars1.slice(0, -1);
818
- else
819
- chars2 = chars2.slice(0, -1);
820
- }
821
- return `${chars1.join("")}_${chars2.join("")}_${params.label}`;
822
- }
823
-
824
- // packages/schema/src/capabilities.ts
825
- var POSTGRES_CAPABILITIES = Object.freeze({
826
- exclusionConstraints: true,
827
- exclusionExtensions: Object.freeze(["btree_gist"]),
828
- checkNormalization: "rollback",
829
- nativeRowLevelSecurity: true,
830
- returning: true,
831
- scalarArrays: true,
832
- nativeEnums: true,
833
- ilike: true,
834
- lateralJoin: true,
835
- distinctOn: true,
836
- partialIndexes: true,
837
- conflictTargetConstraint: true,
838
- transactionalDdl: true,
839
- createManyAndReturn: true,
840
- jsonColumnType: true,
841
- orderByNulls: true,
842
- fullTextEngines: Object.freeze(["bm25", "tsvector"]),
843
- rowLockStrengths: Object.freeze(["update", "noKeyUpdate", "share", "keyShare"]),
844
- rowLockWaitPolicies: Object.freeze(["wait", "nowait", "skipLocked"]),
845
- advisoryLocks: true
846
- });
847
- var SQLITE_CAPABILITIES = Object.freeze({
848
- exclusionConstraints: false,
849
- exclusionExtensions: Object.freeze([]),
850
- checkNormalization: "sourceText",
851
- nativeRowLevelSecurity: false,
852
- returning: true,
853
- scalarArrays: false,
854
- nativeEnums: false,
855
- ilike: false,
856
- lateralJoin: false,
857
- distinctOn: false,
858
- partialIndexes: true,
859
- conflictTargetConstraint: false,
860
- transactionalDdl: true,
861
- createManyAndReturn: true,
862
- jsonColumnType: false,
863
- orderByNulls: true,
864
- fullTextEngines: Object.freeze(["fts5"]),
865
- rowLockStrengths: Object.freeze([]),
866
- rowLockWaitPolicies: Object.freeze([]),
867
- advisoryLocks: false
868
- });
869
- var MYSQL_CAPABILITIES = Object.freeze({
870
- exclusionConstraints: false,
871
- exclusionExtensions: Object.freeze([]),
872
- checkNormalization: "temporary",
873
- nativeRowLevelSecurity: false,
874
- returning: false,
875
- scalarArrays: false,
876
- nativeEnums: true,
877
- ilike: false,
878
- lateralJoin: false,
879
- distinctOn: false,
880
- partialIndexes: false,
881
- conflictTargetConstraint: false,
882
- transactionalDdl: false,
883
- createManyAndReturn: false,
884
- jsonColumnType: true,
885
- orderByNulls: false,
886
- fullTextEngines: Object.freeze(["fulltext"]),
887
- rowLockStrengths: Object.freeze(["update", "share"]),
888
- rowLockWaitPolicies: Object.freeze(["wait", "nowait", "skipLocked"]),
889
- advisoryLocks: false
890
- });
891
- var DIALECT_CAPABILITIES = Object.freeze({
892
- postgres: POSTGRES_CAPABILITIES,
893
- sqlite: SQLITE_CAPABILITIES,
894
- mysql: MYSQL_CAPABILITIES
895
- });
896
-
897
1115
  // packages/schema/src/validate.ts
898
1116
  var RESERVED_ENUM_NAMES = [
899
1117
  "sql",
@@ -913,6 +1131,11 @@ var RESERVED_ENUM_NAMES = [
913
1131
  "UncertainKeys",
914
1132
  "HasSelectedField",
915
1133
  "Provided",
1134
+ "SelectionMember",
1135
+ "SelectionValue",
1136
+ "BagKeys",
1137
+ "IncludedRelation",
1138
+ "IncludedCount",
916
1139
  "ViewDelegateMap",
917
1140
  "extensionManifest",
918
1141
  "extensionMounts",
@@ -948,6 +1171,7 @@ var RESERVED_ENUM_NAMES = [
948
1171
  "SortOrder",
949
1172
  "SortOrderWithNulls",
950
1173
  "KeysetToken",
1174
+ "KeysetPage",
951
1175
  "KeysetOptions",
952
1176
  "RowLockRequest",
953
1177
  "JsonValue",
@@ -1025,7 +1249,20 @@ var GENERATED_TYPE_NAMES = {
1025
1249
  "SumAggregateInput",
1026
1250
  "MinAggregateInput",
1027
1251
  "MaxAggregateInput",
1252
+ "CountAggregateHavingInput",
1253
+ "AvgAggregateHavingInput",
1254
+ "SumAggregateHavingInput",
1255
+ "MinAggregateHavingInput",
1256
+ "MaxAggregateHavingInput",
1257
+ "CountAggregateOrderByInput",
1258
+ "AvgAggregateOrderByInput",
1259
+ "SumAggregateOrderByInput",
1260
+ "MinAggregateOrderByInput",
1261
+ "MaxAggregateOrderByInput",
1262
+ "HavingInput",
1263
+ "GroupByOrderByInput",
1028
1264
  "FindManyArgs",
1265
+ "FindPageArgs",
1029
1266
  "FindFirstArgs",
1030
1267
  "FindUniqueArgs",
1031
1268
  "CreateArgs",
@@ -1052,6 +1289,7 @@ var GENERATED_TYPE_NAMES = {
1052
1289
  "AggregateSelection",
1053
1290
  "CountOutput",
1054
1291
  "RelationPayload",
1292
+ "ResolvedSelection",
1055
1293
  "IncludePayload",
1056
1294
  "GetPayload",
1057
1295
  "AggregateResult",
@@ -1193,9 +1431,69 @@ function validateGeneratedNames(params) {
1193
1431
  register({ name: `${declared.name}${suffix}`, owner: `enum "${declared.name}"` });
1194
1432
  }
1195
1433
  }
1434
+ function foreignKeyKeys(params) {
1435
+ const distinct = new Set;
1436
+ const repeated = [];
1437
+ for (const fields of params.keys) {
1438
+ if (new Set(fields).size === fields.length)
1439
+ distinct.add(JSON.stringify([...fields].sort()));
1440
+ else
1441
+ repeated.push(fields);
1442
+ }
1443
+ return { distinct, repeated };
1444
+ }
1445
+ function matchesForeignKey(params) {
1446
+ if (params.keys.distinct.has(JSON.stringify([...params.references].sort())))
1447
+ return true;
1448
+ return params.keys.repeated.some((fields) => fields.length === params.references.length && fields.every((name) => params.references.includes(name)));
1449
+ }
1450
+ function referencesPartialOnly(params) {
1451
+ const { target, references, schema, lookups } = params;
1452
+ let partial = lookups.partialKeys.get(target);
1453
+ if (partial === undefined) {
1454
+ partial = foreignKeyKeys({ keys: target.indexes.filter((index) => index.kind === "unique" && (index.where !== undefined || index.wherePredicate !== undefined)).map((index) => index.fields.map((field) => field.name)) });
1455
+ lookups.partialKeys.set(target, partial);
1456
+ }
1457
+ if (partial.distinct.size === 0 && partial.repeated.length === 0)
1458
+ return false;
1459
+ if (!matchesForeignKey({ keys: partial, references }))
1460
+ return false;
1461
+ let total = lookups.totalKeys.get(target);
1462
+ if (total === undefined) {
1463
+ total = foreignKeyKeys({ keys: [
1464
+ ...target.fields.filter((field) => field.isId || field.isUnique).map((field) => [field.name]),
1465
+ ...target.primaryKey === undefined ? [] : [target.primaryKey.fields],
1466
+ ...target.indexes.filter((index) => index.kind === "unique" && index.where === undefined && index.wherePredicate === undefined).map((index) => index.fields.map((field) => field.name))
1467
+ ] });
1468
+ lookups.totalKeys.set(target, total);
1469
+ }
1470
+ if (matchesForeignKey({ keys: total, references }))
1471
+ return false;
1472
+ if (lookups.independentTables === undefined) {
1473
+ const tables = new Set;
1474
+ for (const object of schema.migrationObjects ?? []) {
1475
+ const identity = object.identity;
1476
+ if (identity.kind === "index" || identity.kind === "constraint" && (identity.constraintType === "unique" || identity.constraintType === "primaryKey")) {
1477
+ tables.add(JSON.stringify([identity.table.namespace, identity.table.name]));
1478
+ }
1479
+ }
1480
+ lookups.independentTables = tables;
1481
+ }
1482
+ const placement = qualifiedSqlName({ name: target.dbName ?? target.name, namespace: target.namespace });
1483
+ return !lookups.independentTables.has(JSON.stringify([placement.namespace, placement.name]));
1484
+ }
1196
1485
  function validateSchemaIR(params) {
1197
1486
  const { schema } = params;
1198
1487
  const issues = [];
1488
+ for (const entry of [...schema.models, ...schema.enums]) {
1489
+ if (entry.namespace === undefined)
1490
+ continue;
1491
+ const issue = sqlNamespaceIssue({ namespace: entry.namespace });
1492
+ if (issue !== undefined)
1493
+ issues.push({ rule: "namespace", message: issue });
1494
+ if (providerToDialect({ provider: schema.datasource.provider }) !== "postgres")
1495
+ issues.push({ rule: "namespace-dialect", message: "Explicit fixed namespaces require PostgreSQL or PGlite" });
1496
+ }
1199
1497
  const modelByName = new Map;
1200
1498
  for (const model of schema.models) {
1201
1499
  if (modelByName.has(model.name)) {
@@ -1254,8 +1552,18 @@ function validateSchemaIR(params) {
1254
1552
  });
1255
1553
  }
1256
1554
  }
1555
+ const fieldsByModel = new Map;
1556
+ for (const model of schema.models) {
1557
+ const fields = new Map;
1558
+ for (const field of model.fields)
1559
+ if (!fields.has(field.name))
1560
+ fields.set(field.name, field);
1561
+ fieldsByModel.set(model, fields);
1562
+ }
1563
+ const lookups = { fieldsByModel, partialKeys: new Map, totalKeys: new Map };
1564
+ const postgres = providerToDialect({ provider: schema.datasource.provider }) === "postgres";
1257
1565
  for (const model of schema.models) {
1258
- validateModel({ model, modelByName, enumByName, issues });
1566
+ validateModel({ model, modelByName, enumByName, issues, schema, lookups, postgres });
1259
1567
  }
1260
1568
  for (const model of schema.models) {
1261
1569
  for (const relation of model.relations) {
@@ -1300,19 +1608,11 @@ function validateSchemaIR(params) {
1300
1608
  }
1301
1609
  }
1302
1610
  for (const model of schema.models) {
1303
- const fieldNames = new Set(model.fields.map((field) => field.name));
1304
- const defaultKeys = new Set;
1305
- if (model.primaryKey !== undefined && model.primaryKey.fields.length > 1 && model.primaryKey.clientName === undefined) {
1306
- defaultKeys.add(model.primaryKey.fields.join("_"));
1307
- }
1308
- for (const index of model.indexes) {
1309
- if (index.kind === "unique" && index.where === undefined && index.fields.length > 1 && index.clientName === undefined) {
1310
- defaultKeys.add(index.fields.map((field) => field.name).join("_"));
1311
- }
1312
- }
1313
- const seenSelectorNames = new Set;
1611
+ const fieldNames = fieldsByModel.get(model);
1612
+ const seenSelectorNames = new Map;
1613
+ const addressable = (names) => names.every((name) => fieldNames.get(name)?.isUnsupported !== true);
1314
1614
  const checkSelectorName = (params) => {
1315
- const { clientName, fieldCount, where, isUnique, site } = params;
1615
+ const { clientName, fields, where, isUnique, site } = params;
1316
1616
  if (!isUnique) {
1317
1617
  issues.push({
1318
1618
  rule: "client-name-non-unique",
@@ -1329,7 +1629,7 @@ function validateSchemaIR(params) {
1329
1629
  });
1330
1630
  return;
1331
1631
  }
1332
- if (fieldCount < 2) {
1632
+ if (fields.length < 2) {
1333
1633
  issues.push({
1334
1634
  rule: "client-name-single-field",
1335
1635
  message: `name: ("${clientName}") on a single-field ${site} of "${model.name}" \u2014 the field itself is the selector; name: applies to compound groups only`,
@@ -1337,7 +1637,9 @@ function validateSchemaIR(params) {
1337
1637
  });
1338
1638
  return;
1339
1639
  }
1340
- if (fieldNames.has(clientName) || defaultKeys.has(clientName) || seenSelectorNames.has(clientName)) {
1640
+ const tuple = JSON.stringify(fields);
1641
+ const previous = seenSelectorNames.get(clientName);
1642
+ if (fieldNames.has(clientName) || previous !== undefined && previous !== tuple) {
1341
1643
  issues.push({
1342
1644
  rule: "client-name-collision",
1343
1645
  message: `name: ("${clientName}") on "${model.name}" collides with a field or another unique selector \u2014 WhereUniqueInput keys must be unambiguous`,
@@ -1345,22 +1647,22 @@ function validateSchemaIR(params) {
1345
1647
  });
1346
1648
  return;
1347
1649
  }
1348
- seenSelectorNames.add(clientName);
1650
+ seenSelectorNames.set(clientName, tuple);
1349
1651
  };
1350
- if (model.primaryKey !== undefined && model.primaryKey.clientName !== undefined) {
1652
+ if (model.primaryKey !== undefined && (model.primaryKey.clientName !== undefined || model.primaryKey.fields.length > 1 && addressable(model.primaryKey.fields))) {
1351
1653
  checkSelectorName({
1352
- clientName: model.primaryKey.clientName,
1353
- fieldCount: model.primaryKey.fields.length,
1654
+ clientName: model.primaryKey.clientName ?? model.primaryKey.fields.join("_"),
1655
+ fields: model.primaryKey.fields,
1354
1656
  isUnique: true,
1355
1657
  site: "@@id"
1356
1658
  });
1357
1659
  }
1358
1660
  for (const index of model.indexes) {
1359
- if (index.clientName === undefined)
1661
+ if (index.clientName === undefined && (index.kind !== "unique" || index.where !== undefined || index.fields.length < 2 || !addressable(index.fields.map((field) => field.name))))
1360
1662
  continue;
1361
1663
  checkSelectorName({
1362
- clientName: index.clientName,
1363
- fieldCount: index.fields.length,
1664
+ clientName: index.clientName ?? index.fields.map((field) => field.name).join("_"),
1665
+ fields: index.fields.map((field) => field.name),
1364
1666
  ...index.where === undefined ? {} : { where: index.where },
1365
1667
  isUnique: index.kind === "unique",
1366
1668
  site: "@@unique"
@@ -1368,17 +1670,17 @@ function validateSchemaIR(params) {
1368
1670
  }
1369
1671
  }
1370
1672
  validateIdentifierLengths({ schema, issues });
1371
- validatePhysicalNameUniqueness({ schema, issues });
1673
+ validatePhysicalNameUniqueness({ schema, issues, modelByName, fieldsByModel });
1372
1674
  validateGeneratedNames({ schema, issues });
1373
1675
  validateNativeRls({ schema, issues });
1374
1676
  return issues;
1375
1677
  }
1376
1678
  function validatePhysicalNameUniqueness(params) {
1377
- const { schema, issues } = params;
1679
+ const { schema, issues, modelByName, fieldsByModel } = params;
1378
1680
  const dialect = providerToDialect({ provider: schema.datasource.provider });
1379
1681
  const owners = new Map;
1380
1682
  const claim = (entry) => {
1381
- const key = `${entry.namespace}\x00${entry.name}`;
1683
+ const key = JSON.stringify([entry.physicalNamespace ?? "public", entry.namespace, entry.name]);
1382
1684
  const previous = owners.get(key);
1383
1685
  if (previous === undefined) {
1384
1686
  owners.set(key, `${entry.what} of ${entry.owner}`);
@@ -1396,14 +1698,16 @@ function validatePhysicalNameUniqueness(params) {
1396
1698
  const constraintNamespace = (table) => dialect === "mysql" ? "constraint" : `constraint:${table}`;
1397
1699
  for (const model of schema.models) {
1398
1700
  const table = model.dbName ?? model.name;
1399
- if (!claim({ namespace: "relation", name: table, what: model.isView ? "view" : "table", owner: `model "${model.name}"`, model: model.name }))
1701
+ const physicalNamespace = qualifiedSqlName({ name: table, namespace: model.namespace }).namespace;
1702
+ if (!claim({ namespace: "relation", physicalNamespace, name: table, what: model.isView ? "view" : "table", owner: `model "${model.name}"`, model: model.name }))
1400
1703
  continue;
1401
1704
  for (const fieldIR of model.fields) {
1402
- claim({ namespace: `column:${table}`, name: fieldIR.dbName ?? fieldIR.name, what: "column", owner: `field "${model.name}.${fieldIR.name}"`, model: model.name, field: fieldIR.name });
1705
+ claim({ namespace: `column:${table}`, physicalNamespace, name: fieldIR.dbName ?? fieldIR.name, what: "column", owner: `field "${model.name}.${fieldIR.name}"`, model: model.name, field: fieldIR.name });
1403
1706
  }
1404
1707
  if (model.isView)
1405
1708
  continue;
1406
- const columnOf = (name) => model.fields.find((candidate) => candidate.name === name)?.dbName ?? name;
1709
+ const fields = fieldsByModel.get(model);
1710
+ const columnOf = (name) => fields.get(name)?.dbName ?? name;
1407
1711
  const seenIndexKeys = new Set;
1408
1712
  const declarations = [
1409
1713
  ...model.indexes.map((index) => ({ index, site: index.kind === "unique" ? "@@unique" : "@@index" })),
@@ -1411,45 +1715,45 @@ function validatePhysicalNameUniqueness(params) {
1411
1715
  ];
1412
1716
  for (const { index, site } of declarations) {
1413
1717
  const unique = index.kind === "unique";
1414
- const columns = index.fields.map((entry) => `${columnOf(entry.name)}${entry.sort === "desc" ? " desc" : ""}`);
1415
- const key = `${unique ? "u" : "i"}${index.nullsNotDistinct === true ? "n" : ""}:${unique ? "btree" : index.kind}:${columns.join(",")}:${index.where?.trim() ?? ""}`;
1718
+ const columns = index.fields.map((entry) => [columnOf(entry.name), entry.sort === "desc", entry.operatorClass === undefined ? null : [entry.operatorClass.namespace, entry.operatorClass.name]]);
1719
+ const key = JSON.stringify([unique, index.nullsNotDistinct === true, unique ? "btree" : index.kind, columns, index.where?.trim() ?? ""]);
1416
1720
  if (seenIndexKeys.has(key))
1417
1721
  continue;
1418
1722
  seenIndexKeys.add(key);
1419
1723
  const name = index.name ?? defaultIndexName({ table, columns: index.fields.map((entry) => columnOf(entry.name)), unique, dialect });
1420
- claim({ namespace: indexNamespace(table), name, what: unique ? "unique index" : "index", owner: `${site} on "${model.name}"`, model: model.name });
1724
+ claim({ namespace: indexNamespace(table), physicalNamespace, name, what: unique ? "unique index" : "index", owner: `${site} on "${model.name}"`, model: model.name });
1421
1725
  if (index.constraintName !== undefined) {
1422
- claim({ namespace: constraintNamespace(table), name: index.constraintName, what: "unique constraint", owner: `${site} on "${model.name}"`, model: model.name });
1726
+ claim({ namespace: constraintNamespace(table), physicalNamespace, name: index.constraintName, what: "unique constraint", owner: `${site} on "${model.name}"`, model: model.name });
1423
1727
  }
1424
1728
  }
1425
1729
  if (dialect === "postgres" && (model.primaryKey !== undefined || model.fields.some((fieldIR) => fieldIR.isId))) {
1426
1730
  const name = model.primaryKeyConstraintName ?? defaultPrimaryKeyName({ table, dialect });
1427
- claim({ namespace: "relation", name, what: "primary key", owner: `model "${model.name}"`, model: model.name });
1428
- claim({ namespace: constraintNamespace(table), name, what: "primary key", owner: `model "${model.name}"`, model: model.name });
1731
+ claim({ namespace: "relation", physicalNamespace, name, what: "primary key", owner: `model "${model.name}"`, model: model.name });
1732
+ claim({ namespace: constraintNamespace(table), physicalNamespace, name, what: "primary key", owner: `model "${model.name}"`, model: model.name });
1429
1733
  }
1430
1734
  for (const relation of model.relations) {
1431
1735
  if (relation.fields === undefined || relation.fields.length === 0)
1432
1736
  continue;
1433
- const target = schema.models.find((candidate) => candidate.name === relation.targetModel);
1737
+ const target = modelByName.get(relation.targetModel);
1434
1738
  if (target !== undefined && target.isView)
1435
1739
  continue;
1436
1740
  const name = relation.constraintName ?? defaultForeignKeyName({ table, columns: relation.fields.map((entry) => columnOf(entry)), dialect });
1437
- claim({ namespace: constraintNamespace(table), name, what: "foreign key", owner: `relation "${model.name}.${relation.name}"`, model: model.name });
1741
+ claim({ namespace: constraintNamespace(table), physicalNamespace, name, what: "foreign key", owner: `relation "${model.name}.${relation.name}"`, model: model.name });
1438
1742
  }
1439
1743
  for (const checkIR of model.checks ?? []) {
1440
- claim({ namespace: constraintNamespace(table), name: checkIR.name, what: "CHECK constraint", owner: `model "${model.name}"`, model: model.name });
1744
+ claim({ namespace: constraintNamespace(table), physicalNamespace, name: checkIR.name, what: "CHECK constraint", owner: `model "${model.name}"`, model: model.name });
1441
1745
  }
1442
1746
  for (const exclusion of model.exclusions ?? []) {
1443
- claim({ namespace: constraintNamespace(table), name: exclusion.name, what: "exclusion constraint", owner: `model "${model.name}"`, model: model.name });
1747
+ claim({ namespace: constraintNamespace(table), physicalNamespace, name: exclusion.name, what: "exclusion constraint", owner: `model "${model.name}"`, model: model.name });
1444
1748
  if (dialect === "postgres")
1445
- claim({ namespace: "relation", name: exclusion.name, what: "exclusion constraint", owner: `model "${model.name}"`, model: model.name });
1749
+ claim({ namespace: "relation", physicalNamespace, name: exclusion.name, what: "exclusion constraint", owner: `model "${model.name}"`, model: model.name });
1446
1750
  }
1447
1751
  }
1448
1752
  if (dialect === "postgres") {
1449
1753
  for (const enumDef of schema.enums) {
1450
1754
  if (enumStorageOf({ enumDef }) !== "database")
1451
1755
  continue;
1452
- claim({ namespace: "relation", name: enumDef.dbName ?? enumDef.name, what: "enum type", owner: `enum "${enumDef.name}"` });
1756
+ claim({ namespace: "relation", physicalNamespace: qualifiedSqlName({ name: enumDef.dbName ?? enumDef.name, namespace: enumDef.namespace }).namespace, name: enumDef.dbName ?? enumDef.name, what: "enum type", owner: `enum "${enumDef.name}"` });
1453
1757
  }
1454
1758
  }
1455
1759
  const seenJoinTables = new Set;
@@ -1459,17 +1763,24 @@ function validatePhysicalNameUniqueness(params) {
1459
1763
  continue;
1460
1764
  if (!isImplicitManyToManyRelation({ model, relation, models: schema.models }))
1461
1765
  continue;
1766
+ const target = modelByName.get(relation.targetModel);
1767
+ const physicalNamespace = model.namespace ?? "public";
1768
+ if ((target?.namespace ?? "public") !== physicalNamespace) {
1769
+ issues.push({ rule: "implicit-join-namespace", message: "Implicit many-to-many endpoints must share one fixed namespace", model: model.name, field: relation.name });
1770
+ continue;
1771
+ }
1462
1772
  const join = implicitJoinTable({ modelA: model.name, modelB: relation.targetModel, ...relation.relationName === undefined ? {} : { relationName: relation.relationName } });
1463
- if (seenJoinTables.has(join.tableName))
1773
+ const joinKey = JSON.stringify([physicalNamespace, join.tableName]);
1774
+ if (seenJoinTables.has(joinKey))
1464
1775
  continue;
1465
- seenJoinTables.add(join.tableName);
1776
+ seenJoinTables.add(joinKey);
1466
1777
  const owner = `implicit relation "${model.name}.${relation.name}"`;
1467
- claim({ namespace: "relation", name: join.tableName, what: "join table", owner, model: model.name });
1778
+ claim({ namespace: "relation", physicalNamespace, name: join.tableName, what: "join table", owner, model: model.name });
1468
1779
  const derived = implicitJoinTableObjectNames({ table: join.tableName });
1469
- claim({ namespace: indexNamespace(join.tableName), name: derived.uniqueIndex, what: "unique index", owner, model: model.name });
1470
- claim({ namespace: indexNamespace(join.tableName), name: derived.indexB, what: "index", owner, model: model.name });
1471
- claim({ namespace: constraintNamespace(join.tableName), name: derived.foreignKeyA, what: "foreign key", owner, model: model.name });
1472
- claim({ namespace: constraintNamespace(join.tableName), name: derived.foreignKeyB, what: "foreign key", owner, model: model.name });
1780
+ claim({ namespace: indexNamespace(join.tableName), physicalNamespace, name: derived.uniqueIndex, what: "unique index", owner, model: model.name });
1781
+ claim({ namespace: indexNamespace(join.tableName), physicalNamespace, name: derived.indexB, what: "index", owner, model: model.name });
1782
+ claim({ namespace: constraintNamespace(join.tableName), physicalNamespace, name: derived.foreignKeyA, what: "foreign key", owner, model: model.name });
1783
+ claim({ namespace: constraintNamespace(join.tableName), physicalNamespace, name: derived.foreignKeyB, what: "foreign key", owner, model: model.name });
1473
1784
  }
1474
1785
  }
1475
1786
  }
@@ -1551,7 +1862,7 @@ function validateIdentifierLengths(params) {
1551
1862
  }
1552
1863
  }
1553
1864
  function validateModel(params) {
1554
- const { model, modelByName, enumByName, issues } = params;
1865
+ const { model, modelByName, enumByName, issues, lookups, postgres } = params;
1555
1866
  const memberNames = new Set;
1556
1867
  for (const field of model.fields) {
1557
1868
  if (memberNames.has(field.name)) {
@@ -1653,8 +1964,23 @@ function validateModel(params) {
1653
1964
  }
1654
1965
  memberNames.add(relation.name);
1655
1966
  }
1656
- const fieldNames = new Set(model.fields.map((f) => f.name));
1657
- const hasIdField = model.fields.some((f) => f.isId);
1967
+ const fieldNames = new Set(lookups.fieldsByModel.get(model).keys());
1968
+ const idFields = model.fields.filter((f) => f.isId);
1969
+ const hasIdField = idFields.length > 0;
1970
+ if (idFields.length > 1) {
1971
+ issues.push({
1972
+ rule: "multiple-field-ids",
1973
+ message: `Model "${model.name}" has multiple @id fields (${idFields.map((field) => field.name).join(", ")}); declare one compound primary key instead`,
1974
+ model: model.name
1975
+ });
1976
+ }
1977
+ if (idFields.length === 1 && model.primaryKey !== undefined && (model.primaryKey.fields.length !== 1 || model.primaryKey.fields[0] !== idFields[0]?.name)) {
1978
+ issues.push({
1979
+ rule: "conflicting-primary-key",
1980
+ message: `Model "${model.name}" declares @id on "${idFields[0]?.name}" and a different block primary key; both declarations must name the same single logical field`,
1981
+ model: model.name
1982
+ });
1983
+ }
1658
1984
  if (!model.isView && !hasIdField && model.primaryKey === undefined) {
1659
1985
  issues.push({
1660
1986
  rule: "missing-id",
@@ -1713,8 +2039,14 @@ function validateModel(params) {
1713
2039
  });
1714
2040
  }
1715
2041
  }
2042
+ if (postgres && new Set(relation.references).size !== relation.references.length) {
2043
+ issues.push({ rule: "relation-duplicate-column", message: `Relation "${model.name}.${relation.name}" repeats a referenced column`, model: model.name, field: relation.name });
2044
+ }
1716
2045
  if (target !== undefined) {
1717
- const targetFieldNames = new Set(target.fields.map((f) => f.name));
2046
+ if (postgres && referencesPartialOnly({ target, references: relation.references, schema: params.schema, lookups })) {
2047
+ 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 });
2048
+ }
2049
+ const targetFieldNames = lookups.fieldsByModel.get(target);
1718
2050
  for (const refField of relation.references) {
1719
2051
  if (!targetFieldNames.has(refField)) {
1720
2052
  issues.push({
@@ -1835,6 +2167,20 @@ function validatePredicateFields(params) {
1835
2167
  }
1836
2168
  function assertValidSchemaIR(params) {
1837
2169
  const dialect = providerToDialect({ provider: params.schema.datasource.provider });
2170
+ assertIndexOperatorClasses({ schema: params.schema, dialect });
2171
+ for (const entry of [...params.schema.models, ...params.schema.enums])
2172
+ if (entry.namespace !== undefined)
2173
+ assertSqlNamespace({ namespace: entry.namespace, dialect });
2174
+ for (const model of params.schema.models)
2175
+ for (const field of model.fields) {
2176
+ if (!isEnumType(field.type) || field.type.namespace === undefined)
2177
+ continue;
2178
+ assertSqlNamespace({ namespace: field.type.namespace, dialect });
2179
+ const enumType = field.type;
2180
+ const declared = params.schema.enums.find((entry) => entry.name === enumType.enum || entry.dbName === enumType.enum);
2181
+ if (declared !== undefined && (declared.namespace ?? "public") !== enumType.namespace)
2182
+ 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 } });
2183
+ }
1838
2184
  for (const model of params.schema.models) {
1839
2185
  if (model.exclusions !== undefined && !Array.isArray(model.exclusions))
1840
2186
  throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Exclusions must be an array", meta: { rule: "exclusion-definition" } });
@@ -1868,6 +2214,16 @@ function assertValidSchemaIR(params) {
1868
2214
  if (dialect === "mysql" && counters.length > 1)
1869
2215
  throw unsupportedCapability({ dialect, feature: "multiple auto-increment columns" });
1870
2216
  for (const field of model.fields) {
2217
+ const timeOfDay = field.isUnsupported === true || field.type !== "DateTime" ? undefined : timeOfDayType({ nativeType: field.nativeType });
2218
+ if (timeOfDay !== undefined && !DIALECT_CAPABILITIES[dialect].timeOfDayTypes.includes(timeOfDay)) {
2219
+ 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";
2220
+ const error = unsupportedCapability({ dialect, feature: "timeOfDayTypes", hint: `${model.name}.${field.name} declares @db.${field.nativeType ?? ""} (${timeOfDay}) \u2014 ${instead}` });
2221
+ throw new VibeError({ code: error.code, message: error.message, meta: { ...error.meta, model: model.name, field: field.name, nativeType: field.nativeType } });
2222
+ }
2223
+ if (timeOfDay !== undefined && dialect === "mysql" && field.default?.kind === "now") {
2224
+ 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)` });
2225
+ throw new VibeError({ code: error.code, message: error.message, meta: { ...error.meta, model: model.name, field: field.name, nativeType: field.nativeType } });
2226
+ }
1871
2227
  if (field.default?.kind === "list") {
1872
2228
  if (dialect !== "postgres")
1873
2229
  throw unsupportedCapability({ dialect, feature: "scalar-list defaults" });
@@ -2094,11 +2450,14 @@ export {
2094
2450
  RlsContextError,
2095
2451
  SCOPED_REFUSAL_REASONS,
2096
2452
  SQLITE_CAPABILITIES,
2453
+ SQL_NAMESPACES,
2097
2454
  ScopedQueryError,
2098
2455
  VIBE_ERROR_CODES,
2099
2456
  VibeError,
2100
2457
  assertIdentifierFits,
2458
+ assertIndexOperatorClasses,
2101
2459
  assertMigrationObjects,
2460
+ assertSqlNamespace,
2102
2461
  assertValidSchemaIR,
2103
2462
  buildComputedManifest,
2104
2463
  buildExtensionRegistry,
@@ -2132,14 +2491,19 @@ export {
2132
2491
  postgresGeneratedObjectName,
2133
2492
  predicateFieldNames,
2134
2493
  predicateKey,
2494
+ prepareDatabaseModuleSchema,
2135
2495
  providerToDialect,
2496
+ qualifiedSqlName,
2136
2497
  scalarListDefault,
2137
2498
  serializeJsonDefault,
2499
+ sqlNamespaceIssue,
2138
2500
  sqlObjectKey,
2501
+ sqlObjectNamespace,
2502
+ timeOfDayType,
2139
2503
  unsupportedCapability,
2140
2504
  validateComputedConfig,
2141
2505
  validateRlsContextIR,
2142
2506
  validateSchemaIR
2143
2507
  };
2144
2508
 
2145
- //# debugId=47DE6CAFB1D4DC2864756E2164756E21
2509
+ //# debugId=99BBBD6C3E58D9DD64756E2164756E21