@prisma/orm-family-sql 8.0.0-rc.16-dev.7 → 8.0.0-rc.16-dev.9

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.
@@ -2784,1389 +2784,1389 @@ function assertFieldHasEqualityTrait(context, namespaceId, modelName, fieldName)
2784
2784
  trait: "equality"
2785
2785
  } });
2786
2786
  }
2787
- function hasThrough$1(relation) {
2788
- return relation.through !== void 0;
2787
+ function nullability(nullable) {
2788
+ return nullable ? "may be null" : "is never null";
2789
2789
  }
2790
- var SqlTableBinding = class SqlTableBinding {
2791
- #storage;
2792
- #reference;
2793
- constructor(storage, reference) {
2794
- this.#storage = Object.freeze({ ...storage });
2795
- this.#reference = reference;
2796
- Object.freeze(this);
2797
- }
2798
- static unaliased(storage) {
2799
- return new SqlTableBinding(storage, storage.tableName);
2800
- }
2801
- static aliased(storage, alias) {
2802
- return new SqlTableBinding(storage, alias);
2803
- }
2804
- column(columnName) {
2805
- return ColumnRef.of(this.#reference, columnName);
2806
- }
2807
- tableSource(contract) {
2808
- return tableSourceForContract(contract, this.#storage.namespaceId, this.#storage.tableName, this.#reference);
2809
- }
2810
- isReferencedAs(candidate) {
2811
- return this.#reference === candidate;
2812
- }
2813
- isStoredAt(namespaceId, tableName) {
2814
- return this.#storage.namespaceId === namespaceId && this.#storage.tableName === tableName;
2815
- }
2816
- };
2817
- var ModelAccessorScope = class ModelAccessorScope {
2818
- current;
2819
- #visibleBindings;
2820
- #aliasCounter;
2821
- constructor(current, visibleBindings, aliasCounter) {
2822
- this.current = current;
2823
- this.#visibleBindings = Object.freeze([...visibleBindings]);
2824
- this.#aliasCounter = aliasCounter;
2825
- Object.freeze(this);
2826
- }
2827
- static root(namespaceId, tableName) {
2828
- const binding = SqlTableBinding.unaliased({
2829
- namespaceId,
2830
- tableName
2790
+ function isFieldBuilder(value) {
2791
+ return "build" in value && typeof value.build === "function";
2792
+ }
2793
+ function isListMultiplicity(value) {
2794
+ return typeof value === "object" && value !== null && "elementNullable" in value && typeof value.elementNullable === "boolean";
2795
+ }
2796
+ function isMultiplicity(value) {
2797
+ return value === void 0 || value === false || isListMultiplicity(value);
2798
+ }
2799
+ function isFieldSpec(value) {
2800
+ return "codecId" in value && typeof value.codecId === "string" && "nullable" in value && typeof value.nullable === "boolean" && (!("many" in value) || isMultiplicity(value.many));
2801
+ }
2802
+ function multiplicity(many) {
2803
+ return isListMultiplicity(many) ? { elementNullable: many.elementNullable } : false;
2804
+ }
2805
+ const FIELD_DECLARATION_FIX = "Declare each field with a field builder, such as field.temporal.timestamptz().optional(), or with { codecId, nullable }. Declare a list with .many() or many: { elementNullable: false }, or, when its elements may be null, with .many({ elementsNullable: true }) or many: { elementNullable: true }.";
2806
+ function declaredFieldSpec(name, declaration) {
2807
+ if (typeof declaration === "object" && declaration !== null && isFieldBuilder(declaration)) {
2808
+ const built = declaration.build();
2809
+ const descriptor = typeof built === "object" && built !== null && "descriptor" in built ? built.descriptor : void 0;
2810
+ const nullable = typeof built === "object" && built !== null && "nullable" in built ? built.nullable : void 0;
2811
+ if (typeof descriptor !== "object" || descriptor === null || !("codecId" in descriptor) || typeof descriptor.codecId !== "string" || typeof nullable !== "boolean") throw ormError("ORM.ARGUMENT_INVALID", `Cannot define the fragment: the field builder for ${name} names no column type`, {
2812
+ why: "A fragment for any model matches each declared field by its codec and nullability, and this builder refers to a named type instead of a column type.",
2813
+ fix: "Declare the field with a builder that has a column type, such as field.text() or field.column(textColumn), or with { codecId, nullable }.",
2814
+ meta: { field: name }
2831
2815
  });
2832
- return new ModelAccessorScope(binding, [binding], { nextId: 1 });
2833
- }
2834
- forRelation(namespaceId, tableName) {
2835
- const binding = this.#allocateBinding(namespaceId, tableName, "rel");
2836
- return new ModelAccessorScope(binding, [...this.#visibleBindings, binding], this.#aliasCounter);
2837
- }
2838
- forManyToManyRelation(childNamespaceId, childTableName, junctionNamespaceId, junctionTableName) {
2839
- const initialChildScope = this.forRelation(childNamespaceId, childTableName);
2840
- const junctionBinding = initialChildScope.#allocateBinding(junctionNamespaceId, junctionTableName, "junction");
2841
- return {
2842
- childScope: new ModelAccessorScope(initialChildScope.current, [...initialChildScope.#visibleBindings, junctionBinding], this.#aliasCounter),
2843
- junctionBinding
2844
- };
2845
- }
2846
- forJoinedSource(namespaceId, tableName) {
2847
- if (this.current.isStoredAt(namespaceId, tableName)) return this;
2848
- const binding = SqlTableBinding.unaliased({
2849
- namespaceId,
2850
- tableName
2816
+ const many = typeof built === "object" && built !== null && "many" in built && built.many;
2817
+ if (!isMultiplicity(many)) throw ormError("ORM.ARGUMENT_INVALID", `Cannot define the fragment: the field builder for ${name} builds a many that is not false or { elementNullable }`, {
2818
+ why: `A field builder's build() returns many as false or undefined for one value, or as { elementNullable } with a boolean elementNullable for a list; received ${describeReceived(many)} for ${name}.`,
2819
+ fix: FIELD_DECLARATION_FIX,
2820
+ meta: { field: name }
2851
2821
  });
2852
- return new ModelAccessorScope(binding, [...this.#visibleBindings, binding], this.#aliasCounter);
2853
- }
2854
- #allocateBinding(namespaceId, tableName, aliasKind) {
2855
- const storage = {
2856
- namespaceId,
2857
- tableName
2822
+ return {
2823
+ codecId: descriptor.codecId,
2824
+ nullable,
2825
+ many: multiplicity(many)
2858
2826
  };
2859
- if (!this.#visibleBindings.some((binding) => binding.isReferencedAs(tableName))) return SqlTableBinding.unaliased(storage);
2860
- return SqlTableBinding.aliased(storage, this.#allocateAlias(aliasKind));
2861
- }
2862
- #allocateAlias(kind) {
2863
- while (true) {
2864
- const alias = `__orm_${kind}_${this.#aliasCounter.nextId}`;
2865
- this.#aliasCounter.nextId += 1;
2866
- if (!this.#visibleBindings.some((binding) => binding.isReferencedAs(alias))) return alias;
2867
- }
2868
- }
2869
- };
2870
- function createModelAccessor(context, namespaceId, modelName, variantName) {
2871
- const tableName = resolveModelTableName(context.contract, namespaceId, modelName);
2872
- return createModelAccessorInScope(context, namespaceId, modelName, variantName, ModelAccessorScope.root(namespaceId, tableName));
2873
- }
2874
- function createModelAccessorInScope(context, namespaceId, modelName, variantName, scope) {
2875
- const contract = context.contract;
2876
- const fieldToColumn = getFieldToColumnMap(contract, namespaceId, modelName);
2877
- const tableName = resolveModelTableName(contract, namespaceId, modelName);
2878
- const modelRelations = resolveModelRelations(contract, namespaceId, modelName);
2879
- const variantFieldColumns = variantName ? resolveVariantFieldColumns(contract, namespaceId, modelName, variantName) : {};
2880
- const variantCoordinates = variantName ? {
2881
- name: variantName,
2882
- relations: resolveModelRelations(contract, namespaceId, variantName),
2883
- tableName: resolvePolymorphismInfo(contract, namespaceId, modelName)?.variants.get(variantName)?.table ?? tableName
2884
- } : void 0;
2885
- const opsByCodecId = /* @__PURE__ */ new Map();
2886
- function registerOp(codecId, op) {
2887
- let existing = opsByCodecId.get(codecId);
2888
- if (!existing) {
2889
- existing = [];
2890
- opsByCodecId.set(codecId, existing);
2891
- }
2892
- existing.push(op);
2893
- }
2894
- for (const [name, entry] of Object.entries(context.queryOperations.entries())) {
2895
- const op = [name, entry];
2896
- const self = entry.self;
2897
- if (!self) continue;
2898
- if (self.codecId !== void 0) registerOp(self.codecId, op);
2899
- else if (self.traits !== void 0) for (const descriptor of context.codecDescriptors.values()) {
2900
- const descriptorTraits = descriptor.traits;
2901
- if (self.traits.every((t) => descriptorTraits.includes(t))) registerOp(descriptor.codecId, op);
2902
- }
2903
2827
  }
2904
- return blindCast(new Proxy({}, { get(_target, prop) {
2905
- if (typeof prop !== "string") return;
2906
- if (variantCoordinates) {
2907
- const variantRelation = variantCoordinates.relations[prop];
2908
- if (variantRelation) return createRelationFilterAccessor(context, namespaceId, variantCoordinates.name, scope.forJoinedSource(namespaceId, variantCoordinates.tableName), variantRelation);
2909
- }
2910
- const relation = modelRelations[prop];
2911
- if (relation) return createRelationFilterAccessor(context, namespaceId, modelName, scope, relation);
2912
- const variantField = variantFieldColumns[prop];
2913
- const resolvedTable = variantField?.table ?? tableName;
2914
- const fieldBinding = scope.forJoinedSource(namespaceId, resolvedTable).current;
2915
- const columnName = variantField?.column ?? fieldToColumn[prop] ?? prop;
2916
- const column = resolveColumn(contract, namespaceId, resolvedTable, columnName);
2917
- if (!column) return;
2918
- const traits = codecTraits(context, column.codecId);
2919
- const operations = opsByCodecId.get(column.codecId) ?? [];
2920
- const codec = codecRefForStorageColumn(contract.storage, namespaceId, resolvedTable, columnName);
2921
- return createScalarFieldAccessor(fieldBinding, columnName, column.codecId, column.nullable, codec, traits, operations, context);
2922
- } }));
2828
+ if (typeof declaration === "object" && declaration !== null && isFieldSpec(declaration)) return {
2829
+ codecId: declaration.codecId,
2830
+ nullable: declaration.nullable,
2831
+ many: multiplicity(declaration.many)
2832
+ };
2833
+ throw ormError("ORM.ARGUMENT_INVALID", `Cannot define the fragment: the declaration of field ${name} is not a field builder or { codecId, nullable }`, {
2834
+ why: `Each field of a fragment is declared with a field builder or with an object that has a string codecId, a boolean nullable and, for a list, many: { elementNullable } with a boolean elementNullable; received ${describeReceived(declaration)} for ${name}.`,
2835
+ fix: FIELD_DECLARATION_FIX,
2836
+ meta: { field: name }
2837
+ });
2923
2838
  }
2924
- function createScalarFieldAccessor(tableBinding, columnName, codecId, nullable, codec, traits, operations, context) {
2925
- const column = tableBinding.column(columnName);
2926
- const comparisonEntries = [];
2927
- for (const [name, meta] of Object.entries(COMPARISON_METHODS_META)) {
2928
- if (meta.traits.some((t) => !traits.includes(t))) continue;
2929
- comparisonEntries.push([name, meta.create(column, codec)]);
2930
- }
2931
- const accessor = blindCast({
2932
- returnType: {
2933
- codecId,
2934
- nullable,
2935
- codec
2936
- },
2937
- buildAst: () => column,
2938
- ...Object.fromEntries(comparisonEntries)
2839
+ function declaredFieldSpecs(declarations) {
2840
+ if (typeof declarations !== "object" || declarations === null || Array.isArray(declarations)) throw ormError("ORM.ARGUMENT_INVALID", "Cannot define the fragment: the fields are not an object", {
2841
+ why: `The first argument of fragment maps the name of each field the fragment needs to its declaration; received ${describeReceived(declarations)}.`,
2842
+ fix: FIELD_DECLARATION_FIX,
2843
+ meta: { argument: "fields" }
2939
2844
  });
2940
- for (const [name, entry] of operations) accessor[name] = createExtensionMethodFactory(accessor, entry, context);
2941
- return blindCast(accessor);
2845
+ return Object.entries(declarations).map(([name, declaration]) => [name, declaredFieldSpec(name, declaration)]);
2942
2846
  }
2943
- function createExtensionMethodFactory(selfExpr, entry, context) {
2944
- return (...args) => {
2945
- const result = blindCast(entry.impl)(selfExpr, ...args);
2946
- const returnCodecId = result.returnType.codecId;
2947
- const returnTraits = codecTraits(context, returnCodecId);
2948
- if (returnTraits.includes("boolean")) return result.buildAst();
2949
- const resultAst = result.buildAst();
2950
- const returnCodec = { codecId: returnCodecId };
2951
- const methods = {};
2952
- for (const [resultMethodName, meta] of Object.entries(COMPARISON_METHODS_META)) {
2953
- if (meta.traits.some((t) => !returnTraits.includes(t))) continue;
2954
- methods[resultMethodName] = meta.create(resultAst, returnCodec);
2955
- }
2956
- return methods;
2957
- };
2847
+ /** Throws `ORM.ARGUMENT_INVALID` unless `body`, the body of a fragment, is a function. */
2848
+ function assertFragmentBody(body) {
2849
+ if (typeof body !== "function") throw ormError("ORM.ARGUMENT_INVALID", "Cannot define the fragment: the body is not a function", {
2850
+ why: `The body of a fragment is a function that receives a collection and returns one; received ${describeReceived(body)}.`,
2851
+ fix: "Pass a function, such as (rows) => rows.where(...).",
2852
+ meta: { argument: "body" }
2853
+ });
2958
2854
  }
2959
- const RELATION_ACCESSOR_METHOD_NAMES = /* @__PURE__ */ new Set([
2960
- "some",
2961
- "every",
2962
- "none",
2963
- "count"
2964
- ]);
2965
- function createRelationFilterAccessor(context, parentNamespaceId, parentModelName, parentScope, relation) {
2966
- const relatedTableName = resolveModelTableName(context.contract, relation.toNamespace, relation.to);
2967
- const correlate = () => correlateRelatedRows(context, parentNamespaceId, parentModelName, parentScope, relatedTableName, relation);
2968
- const filters = {
2969
- some: (predicate) => buildExistsExpr(context, relation, correlate(), "some", predicate),
2970
- every: (predicate) => buildExistsExpr(context, relation, correlate(), "every", predicate),
2971
- none: (predicate) => buildExistsExpr(context, relation, correlate(), "none", predicate)
2972
- };
2973
- if (isToOneCardinality(relation.cardinality)) return new Proxy(filters, { get(target, prop) {
2974
- if (typeof prop !== "string") return void 0;
2975
- if (Object.hasOwn(target, prop)) return Reflect.get(target, prop);
2976
- if (RELATION_ACCESSOR_METHOD_NAMES.has(prop)) return void 0;
2977
- return relatedOrderableField(context, relation, relatedTableName, correlate, prop);
2978
- } });
2979
- return {
2980
- ...filters,
2981
- count: (predicate) => createOrderable(() => buildRelationCountExpr(context, relation, correlate(), predicate))
2982
- };
2855
+ function hasContract(ctx) {
2856
+ return "context" in ctx && typeof ctx.context === "object" && ctx.context !== null && "contract" in ctx.context && typeof ctx.context.contract === "object" && ctx.context.contract !== null;
2983
2857
  }
2984
- function createOrderable(buildExpr) {
2985
- return {
2986
- asc: (options) => checkedOrderByItem("asc", buildExpr(), options),
2987
- desc: (options) => checkedOrderByItem("desc", buildExpr(), options)
2988
- };
2858
+ function isModelCollection(value) {
2859
+ return typeof value === "object" && value !== null && "ctx" in value && typeof value.ctx === "object" && value.ctx !== null && hasContract(value.ctx) && "modelName" in value && typeof value.modelName === "string" && "namespaceId" in value && typeof value.namespaceId === "string";
2989
2860
  }
2990
- function relatedOrderableField(context, relation, relatedTableName, correlate, fieldName) {
2991
- const fieldToColumn = getFieldToColumnMap(context.contract, relation.toNamespace, relation.to);
2992
- if (!Object.hasOwn(fieldToColumn, fieldName)) return void 0;
2993
- const columnName = fieldToColumn[fieldName];
2994
- if (columnName === void 0) return void 0;
2995
- const column = resolveColumn(context.contract, relation.toNamespace, relatedTableName, columnName);
2996
- if (!column || !hasTrait(context, column.codecId, "order")) return void 0;
2997
- return createOrderable(() => {
2998
- const rows = correlate();
2999
- return SubqueryExpr.of(rows.source.withProjection([ProjectionItem.of(columnName, rows.childScope.current.column(columnName))]).withWhere(rows.correlation));
2861
+ /** Throws `ORM.ARGUMENT_INVALID` unless `value`, what a fragment is applied to, is a collection. */
2862
+ function assertFragmentReceiver(value) {
2863
+ if (!isModelCollection(value)) throw ormError("ORM.ARGUMENT_INVALID", "Cannot apply the fragment: it was not given a collection", {
2864
+ why: `A fragment is applied to a collection, such as db.orm.public.Post; received ${describeReceived(value)}.`,
2865
+ fix: "Pass the fragment to with on a collection: collection.with(fragment).",
2866
+ meta: { argument: "collection" }
3000
2867
  });
3001
2868
  }
3002
- function buildRelationCountExpr(context, relation, rows, predicate) {
3003
- const childWhere = toRelationWhereExpr(context, relation.toNamespace, relation.to, predicate, rows.childScope);
3004
- return SubqueryExpr.of(rows.source.withProjection([ProjectionItem.of("count", plainAggregateExpr("count", void 0))]).withWhere(childWhere ? and(rows.correlation, childWhere) : rows.correlation));
2869
+ /** Throws `ORM.ARGUMENT_INVALID` unless `value` is a collection of the model and namespace a fragment for one model was made from. */
2870
+ function assertModelFragmentReceiver(source, value) {
2871
+ assertFragmentReceiver(value);
2872
+ if (value.modelName !== source.modelName || value.namespaceId !== source.namespaceId) {
2873
+ const made = `${source.namespaceId}.${source.modelName}`;
2874
+ const given = `${value.namespaceId}.${value.modelName}`;
2875
+ throw ormError("ORM.ARGUMENT_INVALID", `Cannot apply a fragment for ${made} to a collection of ${given}`, {
2876
+ why: `The fragment was made with ${made}.fragment(...), and its body was written for that model.`,
2877
+ fix: `Apply the fragment to a collection of ${made}, or make a fragment from ${given}.`,
2878
+ meta: {
2879
+ model: source.modelName,
2880
+ namespace: source.namespaceId,
2881
+ receivedModel: value.modelName,
2882
+ receivedNamespace: value.namespaceId
2883
+ }
2884
+ });
2885
+ }
3005
2886
  }
3006
- function correlateRelatedRows(context, parentNamespaceId, parentModelName, parentScope, relatedTableName, relation) {
3007
- if (hasThrough$1(relation)) {
3008
- const { through } = relation;
3009
- const { childScope, junctionBinding } = parentScope.forManyToManyRelation(relation.toNamespace, relatedTableName, through.namespaceId, through.table);
3010
- const junctionJoinOn = buildPairedColumnExprs(junctionBinding, through.childColumns, childScope.current, through.targetColumns);
3011
- const parentLocalColumns = relation.on.localFields.map((field) => resolveFieldToColumn(context.contract, parentNamespaceId, parentModelName, field));
3012
- return {
3013
- childScope,
3014
- source: SelectAst.from(childScope.current.tableSource(context.contract)).withJoins([JoinAst.inner(junctionBinding.tableSource(context.contract), junctionJoinOn)]),
3015
- correlation: buildPairedColumnExprs(junctionBinding, through.parentColumns, parentScope.current, parentLocalColumns),
3016
- keyColumn: firstJoinColumn(through.targetColumns, "targetColumns")
3017
- };
2887
+ /** The model as error messages name it: with its namespace when the contract has more than one. */
2888
+ function modelLabel(collection) {
2889
+ return Object.keys(collection.ctx.context.contract.domain.namespaces).length > 1 ? `${collection.namespaceId}.${collection.modelName}` : collection.modelName;
2890
+ }
2891
+ /** Throws `ORM.ARGUMENT_INVALID` unless `result`, what a fragment's body returned, is a collection of the receiver's model, namespace and class. */
2892
+ function assertFragmentResult(receiver, result) {
2893
+ if (!(isModelCollection(result) && result.modelName === receiver.modelName && result.namespaceId === receiver.namespaceId && result instanceof receiver.constructor)) {
2894
+ const label = modelLabel(receiver);
2895
+ throw ormError("ORM.ARGUMENT_INVALID", `Cannot apply the fragment to ${label}: its body did not return a collection of ${label}`, {
2896
+ why: `The body of a fragment for any model returns the collection it received, after where, orderBy, limit or offset; it returned ${isModelCollection(result) ? `a collection of ${modelLabel(result)}` : describeReceived(result)}.`,
2897
+ fix: "Return the collection the body receives, or one made from it by calling its methods.",
2898
+ meta: {
2899
+ model: receiver.modelName,
2900
+ namespace: receiver.namespaceId,
2901
+ returned: isModelCollection(result) ? result.modelName : describeReceived(result)
2902
+ }
2903
+ });
3018
2904
  }
3019
- const childScope = parentScope.forRelation(relation.toNamespace, relatedTableName);
3020
- return {
3021
- childScope,
3022
- source: SelectAst.from(childScope.current.tableSource(context.contract)),
3023
- correlation: buildJoinWhere(context.contract, parentNamespaceId, parentModelName, parentScope.current, childScope.current, relation),
3024
- keyColumn: firstTargetColumn(context.contract, relation) ?? "id"
3025
- };
3026
2905
  }
3027
- function buildExistsExpr(context, relation, rows, mode, predicate) {
3028
- const childWhere = toRelationWhereExpr(context, relation.toNamespace, relation.to, predicate, rows.childScope);
3029
- const filterPlan = planRelationFilterMode(rows.correlation, childWhere, mode);
3030
- if (filterPlan.kind === "constantTrue") return AndExpr.true();
3031
- const subquery = rows.source.withProjection([ProjectionItem.of("_exists", rows.childScope.current.column(rows.keyColumn))]).withWhere(filterPlan.where);
3032
- return filterPlan.notExists ? ExistsExpr.notExists(subquery) : ExistsExpr.exists(subquery);
2906
+ function elementNullability(elementNullable) {
2907
+ return elementNullable ? "may be null" : "are never null";
3033
2908
  }
3034
- function planRelationFilterMode(joinWhere, childWhere, mode) {
3035
- if (mode === "every") {
3036
- if (!childWhere) return { kind: "constantTrue" };
3037
- return {
3038
- kind: "exists",
3039
- notExists: true,
3040
- where: and(joinWhere, not(childWhere))
2909
+ function sameMultiplicity(a, b) {
2910
+ return a === false || b === false ? a === b : a.elementNullable === b.elementNullable;
2911
+ }
2912
+ function fieldShape(many) {
2913
+ return many === false ? "holds one value" : `is a list whose elements ${elementNullability(many.elementNullable)}`;
2914
+ }
2915
+ function declarationFor(many) {
2916
+ if (many === false) return "as one value, without .many() or many";
2917
+ return many.elementNullable ? "as a list whose elements may be null, with .many({ elementsNullable: true }) or many: { elementNullable: true }" : "as a list whose elements are never null, with .many() or many: { elementNullable: false }";
2918
+ }
2919
+ function describeField(field) {
2920
+ const value = `with codec ${field.codecId} that ${nullability(field.nullable)}`;
2921
+ return field.many === false ? value : `${value} and whose elements ${elementNullability(field.many.elementNullable)}`;
2922
+ }
2923
+ function assertFragmentFields(collection, fields) {
2924
+ const { contract } = collection.ctx.context;
2925
+ const { modelName, namespaceId } = collection;
2926
+ const label = modelLabel(collection);
2927
+ const model = modelOf(contract, namespaceId, modelName);
2928
+ for (const [name, spec] of fields) {
2929
+ const declared = `The fragment was declared for models that have a ${spec.many === false ? "field" : "list field"} ${name} ${describeField(spec)}.`;
2930
+ const meta = {
2931
+ model: modelName,
2932
+ namespace: namespaceId,
2933
+ field: name,
2934
+ codecId: spec.codecId,
2935
+ nullable: spec.nullable,
2936
+ many: spec.many
3041
2937
  };
2938
+ const modelFields = model?.fields ?? {};
2939
+ if (!Object.hasOwn(modelFields, name)) throw ormError("ORM.FIELD_UNKNOWN", `Cannot apply a fragment to ${label}: it has no field ${name}`, {
2940
+ why: declared,
2941
+ fix: `Apply the fragment to a model that has a field ${name}, or remove ${name} from the declaration in the fragment.`,
2942
+ meta
2943
+ });
2944
+ const fieldMany = multiplicity(modelFields[name]?.many);
2945
+ const column = resolveColumn(contract, namespaceId, collection.tableName, resolveFieldToColumn(contract, namespaceId, modelName, name));
2946
+ const actual = column === void 0 ? `${label}.${name} has no column.` : fieldMany === false ? `${label}.${name} has codec ${column.codecId} and ${nullability(column.nullable)}.` : `${label}.${name} is a list ${describeField({
2947
+ codecId: column.codecId,
2948
+ nullable: column.nullable,
2949
+ many: fieldMany
2950
+ })}.`;
2951
+ const sameCodecAndNullability = column?.codecId === spec.codecId && column.nullable === spec.nullable;
2952
+ if (!sameCodecAndNullability || !sameMultiplicity(fieldMany, spec.many)) throw ormError("ORM.FIELD_UNKNOWN", `Cannot apply a fragment to ${label}: its field ${name} does not match the declaration`, {
2953
+ why: `${declared} ${actual}`,
2954
+ fix: sameCodecAndNullability ? `Apply the fragment to a model whose ${name} field ${fieldShape(spec.many)}, or declare ${name} ${declarationFor(fieldMany)}.` : `Apply the fragment to a model whose ${name} field has that codec and nullability, or change the declaration in the fragment.`,
2955
+ meta
2956
+ });
3042
2957
  }
3043
- if (mode === "none") return {
3044
- kind: "exists",
3045
- notExists: true,
3046
- where: childWhere ? and(joinWhere, childWhere) : joinWhere
3047
- };
2958
+ }
2959
+ /**
2960
+ * Define a fragment for any model that has the declared fields. Reached as the `fragment` method of the client `orm()` returns.
2961
+ */
2962
+ function defineFieldFragment(declarations, body) {
2963
+ const fields = declaredFieldSpecs(declarations);
2964
+ assertFragmentBody(body);
2965
+ return blindCast((collection) => {
2966
+ const receiver = collection;
2967
+ assertFragmentReceiver(receiver);
2968
+ assertFragmentFields(receiver, fields);
2969
+ const result = body(blindCast(collection));
2970
+ assertFragmentResult(receiver, result);
2971
+ return result;
2972
+ });
2973
+ }
2974
+ const SHOWN_LENGTH = 64;
2975
+ function shown(value) {
2976
+ const characters = Array.from(value);
2977
+ const cut = characters.length > SHOWN_LENGTH;
3048
2978
  return {
3049
- kind: "exists",
3050
- notExists: false,
3051
- where: childWhere ? and(joinWhere, childWhere) : joinWhere
2979
+ text: JSON.stringify(cut ? `${characters.slice(0, SHOWN_LENGTH).join("")}…` : value),
2980
+ cut
3052
2981
  };
3053
2982
  }
3054
- function firstJoinColumn(columns, label) {
3055
- const first = columns[0];
3056
- if (!first) throw new InternalError(`Relation metadata is missing ${label}`);
3057
- return first;
2983
+ function describeReceived(value) {
2984
+ if (value === void 0 || value === null) return String(value);
2985
+ if (Array.isArray(value)) return "an array";
2986
+ const kind = typeof value;
2987
+ return kind === "object" ? "an object" : `a ${kind}`;
3058
2988
  }
3059
- function buildPairedColumnExprs(leftTable, leftColumns, rightTable, rightColumns) {
3060
- if (leftColumns.length !== rightColumns.length) throw new InternalError(`Relation metadata has mismatched join column counts: ${leftColumns.length} left column(s), ${rightColumns.length} right column(s)`);
3061
- if (leftColumns.length === 0) throw new InternalError("Relation metadata is missing join columns");
3062
- const exprs = [];
3063
- for (let i = 0; i < leftColumns.length; i++) {
3064
- const left = leftColumns[i];
3065
- const right = rightColumns[i];
3066
- if (!left || !right) throw new InternalError(`Relation metadata is missing a join column pair at index ${i}`);
3067
- exprs.push(BinaryExpr.eq(leftTable.column(left), rightTable.column(right)));
3068
- }
3069
- if (exprs.length === 1 && exprs[0]) return exprs[0];
3070
- return and(...exprs);
2989
+ function cutNote(subject, value) {
2990
+ return value.cut ? ` The ${subject} above is cut to its first ${SHOWN_LENGTH} characters.` : "";
3071
2991
  }
3072
- function toRelationWhereExpr(context, relatedNamespaceId, relatedModelName, predicate, scope) {
3073
- if (!predicate) return;
3074
- const accessor = createModelAccessorInScope(context, relatedNamespaceId, relatedModelName, void 0, scope);
3075
- if (typeof predicate === "function") return predicate(accessor);
3076
- const exprs = [];
3077
- for (const [fieldName, value] of Object.entries(predicate)) {
3078
- if (value === void 0) continue;
3079
- const fieldAccessor = blindCast(accessor)[fieldName];
3080
- if (!fieldAccessor) throw ormError("ORM.FIELD_UNKNOWN", `Shorthand filter on "${relatedModelName}.${fieldName}": field is not defined on the model`, { meta: {
3081
- model: relatedModelName,
3082
- field: fieldName
3083
- } });
3084
- if (value === null) {
3085
- if (!fieldAccessor.isNull) throw new InternalError(`Shorthand filter on "${relatedModelName}.${fieldName}": isNull is unexpectedly missing — this is a bug in trait gating`);
3086
- exprs.push(fieldAccessor.isNull());
3087
- continue;
3088
- }
3089
- if (!fieldAccessor.eq) throw ormError("ORM.FILTER_UNSUPPORTED", `Shorthand filter on "${relatedModelName}.${fieldName}": field does not support equality comparisons`, { meta: {
3090
- model: relatedModelName,
3091
- field: fieldName,
3092
- trait: "equality"
3093
- } });
3094
- exprs.push(fieldAccessor.eq(value));
3095
- }
3096
- if (exprs.length === 0) return;
3097
- return exprs.length === 1 ? exprs[0] : and(...exprs);
2992
+ function fieldCodecId(collection, field) {
2993
+ const { contract } = collection.ctx.context;
2994
+ const column = resolveFieldToColumn(contract, collection.namespaceId, collection.modelName, field);
2995
+ return resolveColumn(contract, collection.namespaceId, collection.tableName, column)?.codecId;
3098
2996
  }
3099
- function buildJoinWhere(contract, parentNamespaceId, parentModelName, parentTable, relatedTable, relation) {
3100
- const localFields = relation.on?.localFields ?? [];
3101
- const targetFields = relation.on?.targetFields ?? [];
3102
- const joinExprs = [];
3103
- const count = Math.min(localFields.length, targetFields.length);
3104
- for (let i = 0; i < count; i++) {
3105
- const localField = localFields[i];
3106
- const targetField = targetFields[i];
3107
- if (!localField || !targetField) continue;
3108
- const localColumn = resolveFieldToColumn(contract, parentNamespaceId, parentModelName, localField);
3109
- const targetColumn = resolveFieldToColumn(contract, relation.toNamespace, relation.to, targetField);
3110
- joinExprs.push(BinaryExpr.eq(relatedTable.column(targetColumn), parentTable.column(localColumn)));
3111
- }
3112
- if (joinExprs.length === 0) throw new InternalError("Relation metadata is missing join columns");
3113
- const firstExpr = joinExprs[0];
3114
- if (joinExprs.length === 1 && firstExpr !== void 0) return firstExpr;
3115
- return and(...joinExprs);
2997
+ function canOrder(collection, field) {
2998
+ const codecId = fieldCodecId(collection, field);
2999
+ return codecId !== void 0 && hasTrait(collection.ctx.context, codecId, "order");
3116
3000
  }
3117
- function firstTargetColumn(contract, relation) {
3118
- const firstField = (relation.on?.targetFields)?.[0];
3119
- if (!firstField) return;
3120
- return resolveFieldToColumn(contract, relation.toNamespace, relation.to, firstField);
3001
+ function orderableFieldNames(collection) {
3002
+ const model = modelOf(collection.ctx.context.contract, collection.namespaceId, collection.modelName);
3003
+ return Object.keys(model?.fields ?? {}).filter((field) => canOrder(collection, field));
3121
3004
  }
3122
- var GroupedCollection = class GroupedCollection {
3123
- ctx;
3124
- contract;
3125
- modelName;
3126
- tableName;
3127
- namespaceId;
3128
- preGroupState;
3129
- groupByFields;
3130
- groupByColumns;
3131
- havingFilters;
3132
- postGroup;
3133
- constructor(ctx, modelName, options) {
3134
- this.ctx = ctx;
3135
- this.contract = ctx.context.contract;
3136
- this.modelName = modelName;
3137
- this.tableName = options.tableName;
3138
- this.namespaceId = blindCast(options.namespaceId);
3139
- this.preGroupState = options.preGroupState;
3140
- this.groupByFields = options.groupByFields;
3141
- this.groupByColumns = options.groupByColumns;
3142
- this.havingFilters = options.havingFilters;
3143
- this.postGroup = options.postGroup;
3144
- }
3145
- #clone(overrides) {
3146
- return blindCast(new GroupedCollection(this.ctx, this.modelName, {
3147
- tableName: this.tableName,
3148
- namespaceId: this.namespaceId,
3149
- preGroupState: this.preGroupState,
3150
- groupByFields: this.groupByFields,
3151
- groupByColumns: this.groupByColumns,
3152
- havingFilters: this.havingFilters,
3153
- postGroup: this.postGroup,
3154
- ...overrides
3155
- }));
3156
- }
3157
- having(predicate) {
3158
- const havingExpr = predicate(createHavingBuilder(this.contract, this.ctx.context.aggregateDescriptors, this.namespaceId, this.modelName, this.tableName));
3159
- return this.#clone({ havingFilters: [...this.havingFilters, havingExpr] });
3160
- }
3161
- /**
3162
- * Append an `ORDER BY` clause on the grouped rows themselves — orders by
3163
- * group key. Ordering by an aggregate alias needs a builder surface over
3164
- * the aliases and isn't supported here. Unlocks post-group `limit(...)` /
3165
- * `offset(...)`, which page a group order that would otherwise be undefined.
3166
- */
3167
- orderBy(selection) {
3168
- const selectors = Array.isArray(selection) ? selection : [selection];
3169
- if (selectors.length === 0) throw ormError("ORM.ARGUMENT_INVALID", `orderBy() for model "${this.modelName}" requires at least one selector`, { meta: {
3170
- method: "orderBy",
3171
- model: this.modelName
3172
- } });
3173
- const accessor = createModelAccessor(this.ctx.context, this.namespaceId, this.modelName);
3174
- const nextOrders = selectors.map((selector) => selector(accessor));
3175
- return this.#clone({ postGroup: {
3176
- ...this.postGroup,
3177
- orderBy: [...this.postGroup.orderBy, ...nextOrders]
3178
- } });
3005
+ function whyNotOrderable(collection, name) {
3006
+ const { modelName } = collection;
3007
+ const model = modelOf(collection.ctx.context.contract, collection.namespaceId, modelName);
3008
+ const quoted = shown(name).text;
3009
+ if (Object.hasOwn(model?.relations ?? {}, name)) return `${quoted} is a relation of ${modelName}, not a field.`;
3010
+ if (!Object.hasOwn(model?.fields ?? {}, name)) return `${modelName} has no field ${quoted}.`;
3011
+ return canOrder(collection, name) ? `${quoted} is not one of the fields allowed for ordering.` : `The codec ${fieldCodecId(collection, name)} of ${modelName}.${name} cannot be ordered.`;
3012
+ }
3013
+ /**
3014
+ * An `orderBy` selector for a field named by a string, such as an order parameter of a request. `allowed` lists the fields a request may order by. A name that is not in `allowed` or not an orderable field of the collection's model, and a direction other than `asc` or `desc`, throw `ORM.ARGUMENT_INVALID`. An undefined direction is `asc`.
3015
+ *
3016
+ * ```ts
3017
+ * db.Post.orderBy(orderByField(db.Post, input.orderBy, input.direction, ['title', 'createdAt']));
3018
+ * ```
3019
+ */
3020
+ function orderByField(collection, name, direction, allowed) {
3021
+ const receiver = collection;
3022
+ if (!isModelCollection(receiver)) throw ormError("ORM.ARGUMENT_INVALID", "Cannot order: orderByField was not given a collection", {
3023
+ why: `orderByField takes the collection it orders, such as db.orm.public.Post; received ${describeReceived(receiver)}.`,
3024
+ fix: "Pass the collection the selector is for as the first argument.",
3025
+ meta: { argument: "collection" }
3026
+ });
3027
+ const { modelName } = collection;
3028
+ const requestedName = name;
3029
+ const requestedDirection = direction === void 0 ? "asc" : direction;
3030
+ const allowedList = allowed;
3031
+ if (!Array.isArray(allowedList) || allowedList.some((field) => typeof field !== "string")) throw ormError("ORM.ARGUMENT_INVALID", `Cannot order ${modelName}: the allowed fields are not a list of names`, {
3032
+ why: `orderByField takes the list of fields a request may order by; received ${describeReceived(allowedList)}.`,
3033
+ fix: `Pass an array of field names of ${modelName}, such as ['title', 'createdAt'].`,
3034
+ meta: {
3035
+ model: modelName,
3036
+ argument: "allowed"
3037
+ }
3038
+ });
3039
+ if (typeof requestedDirection !== "string") throw ormError("ORM.ARGUMENT_INVALID", `Cannot order ${modelName}: the direction is not a string`, {
3040
+ why: `An order direction is "asc" or "desc"; received ${describeReceived(requestedDirection)}.`,
3041
+ fix: "Pass \"asc\" or \"desc\".",
3042
+ meta: {
3043
+ model: modelName,
3044
+ direction: requestedDirection
3045
+ }
3046
+ });
3047
+ if (!isOrderByDirection(requestedDirection)) {
3048
+ const shownDirection = shown(requestedDirection);
3049
+ throw ormError("ORM.ARGUMENT_INVALID", `Cannot order ${modelName} in direction ${shownDirection.text}`, {
3050
+ why: "An order direction is \"asc\" or \"desc\".",
3051
+ fix: `Pass "asc" or "desc".${cutNote("direction", shownDirection)}`,
3052
+ meta: {
3053
+ model: modelName,
3054
+ direction: requestedDirection
3055
+ }
3056
+ });
3179
3057
  }
3180
- /**
3181
- * Apply `LIMIT n` to the grouped rows. Replaces any previous post-group
3182
- * limit. Requires a prior `orderBy(...)` — a database may return groups in
3183
- * any order, so "the first n groups" is undefined without one.
3184
- */
3185
- limit(n) {
3186
- return this.#clone({ postGroup: {
3187
- ...this.postGroup,
3188
- limit: typeof n === "number" ? n : toExpr(n)
3189
- } });
3058
+ const orderable = orderableFieldNames(collection);
3059
+ const names = allowedList.filter((field) => orderable.includes(field));
3060
+ const orderByOneOf = names.length === 0 ? `Pass an allowed list that names at least one field of ${modelName} that can be ordered.` : `Order by one of: ${names.join(", ")}.`;
3061
+ if (typeof requestedName !== "string") throw ormError("ORM.ARGUMENT_INVALID", `Cannot order ${modelName}: the field name is not a string`, {
3062
+ why: `An order field name is a string; received ${describeReceived(requestedName)}.`,
3063
+ fix: orderByOneOf,
3064
+ meta: {
3065
+ model: modelName,
3066
+ field: requestedName
3067
+ }
3068
+ });
3069
+ if (!names.includes(requestedName)) {
3070
+ const shownName = shown(requestedName);
3071
+ throw ormError("ORM.ARGUMENT_INVALID", `Cannot order ${modelName} by ${shownName.text}`, {
3072
+ why: whyNotOrderable(collection, requestedName),
3073
+ fix: `${orderByOneOf}${cutNote("name", shownName)}`,
3074
+ meta: {
3075
+ model: modelName,
3076
+ field: requestedName
3077
+ }
3078
+ });
3190
3079
  }
3191
- /**
3192
- * Apply `OFFSET n` to the grouped rows. Replaces any previous post-group
3193
- * offset. Requires a prior `orderBy(...)`, same as `limit(...)` — both
3194
- * pagination methods need a deterministic group order.
3195
- */
3196
- offset(n) {
3197
- return this.#clone({ postGroup: {
3198
- ...this.postGroup,
3199
- offset: typeof n === "number" ? n : toExpr(n)
3200
- } });
3080
+ const field = blindCast(requestedName);
3081
+ const order = requestedDirection;
3082
+ return (row) => row[field][order]();
3083
+ }
3084
+ function hasThrough$1(relation) {
3085
+ return relation.through !== void 0;
3086
+ }
3087
+ var SqlTableBinding = class SqlTableBinding {
3088
+ #storage;
3089
+ #reference;
3090
+ constructor(storage, reference) {
3091
+ this.#storage = Object.freeze({ ...storage });
3092
+ this.#reference = reference;
3093
+ Object.freeze(this);
3201
3094
  }
3202
- /**
3203
- * Read terminal: run a grouped aggregate query.
3204
- *
3205
- * Accepts an optional `configure` callback that receives a
3206
- * `MetaBuilder<'read'>` for attaching typed annotations.
3207
- * Annotations are merged into the compiled plan's `meta.annotations`.
3208
- */
3209
- get prepared() {
3210
- return { aggregate: (fn, configure) => this.#describeAggregate(fn, configure) };
3095
+ static unaliased(storage) {
3096
+ return new SqlTableBinding(storage, storage.tableName);
3211
3097
  }
3212
- async aggregate(fn, configure) {
3213
- const description = this.#describeAggregate(fn, configure);
3214
- return description.consume(queryPlanRows(this.ctx.runtime, description.plan));
3098
+ static aliased(storage, alias) {
3099
+ return new SqlTableBinding(storage, alias);
3215
3100
  }
3216
- #describeAggregate(fn, configure) {
3217
- const aggregateSpec = fn(createAggregateBuilder(this.contract, this.ctx.context.aggregateDescriptors, this.namespaceId, this.modelName));
3218
- const aggregateEntries = Object.entries(aggregateSpec);
3219
- if (aggregateEntries.length === 0) throw ormError("ORM.AGGREGATE_SELECTOR_MISSING", "groupBy().aggregate() requires at least one aggregation selector", { meta: {
3220
- method: "groupBy.aggregate",
3221
- model: this.modelName
3222
- } });
3223
- for (const [alias, selector] of aggregateEntries) if (!isAggregateSelector(selector)) throw ormError("ORM.AGGREGATE_SELECTOR_INVALID", `groupBy().aggregate() selector "${alias}" is invalid`, { meta: {
3224
- method: "groupBy.aggregate",
3225
- model: this.modelName,
3226
- alias
3227
- } });
3228
- let annotationsMap;
3229
- if (configure !== void 0) {
3230
- const meta = createMetaBuilder("read", "groupBy.aggregate");
3231
- configure(meta);
3232
- if (meta.annotations.size > 0) annotationsMap = meta.annotations;
3233
- }
3234
- const compiled = mergeAnnotations(compileGroupedAggregate(this.contract, this.ctx.context.aggregateDescriptors, this.namespaceId, this.tableName, this.preGroupState, this.groupByColumns, aggregateSpec, combineWhereExprs(this.havingFilters), this.modelName, this.postGroup), annotationsMap);
3235
- const mapRow = createStorageRowMapper(this.contract, this.namespaceId, this.modelName);
3101
+ column(columnName) {
3102
+ return ColumnRef.of(this.#reference, columnName);
3103
+ }
3104
+ tableSource(contract) {
3105
+ return tableSourceForContract(contract, this.#storage.namespaceId, this.#storage.tableName, this.#reference);
3106
+ }
3107
+ isReferencedAs(candidate) {
3108
+ return this.#reference === candidate;
3109
+ }
3110
+ isStoredAt(namespaceId, tableName) {
3111
+ return this.#storage.namespaceId === namespaceId && this.#storage.tableName === tableName;
3112
+ }
3113
+ };
3114
+ var ModelAccessorScope = class ModelAccessorScope {
3115
+ current;
3116
+ #visibleBindings;
3117
+ #aliasCounter;
3118
+ constructor(current, visibleBindings, aliasCounter) {
3119
+ this.current = current;
3120
+ this.#visibleBindings = Object.freeze([...visibleBindings]);
3121
+ this.#aliasCounter = aliasCounter;
3122
+ Object.freeze(this);
3123
+ }
3124
+ static root(namespaceId, tableName) {
3125
+ const binding = SqlTableBinding.unaliased({
3126
+ namespaceId,
3127
+ tableName
3128
+ });
3129
+ return new ModelAccessorScope(binding, [binding], { nextId: 1 });
3130
+ }
3131
+ forRelation(namespaceId, tableName) {
3132
+ const binding = this.#allocateBinding(namespaceId, tableName, "rel");
3133
+ return new ModelAccessorScope(binding, [...this.#visibleBindings, binding], this.#aliasCounter);
3134
+ }
3135
+ forManyToManyRelation(childNamespaceId, childTableName, junctionNamespaceId, junctionTableName) {
3136
+ const initialChildScope = this.forRelation(childNamespaceId, childTableName);
3137
+ const junctionBinding = initialChildScope.#allocateBinding(junctionNamespaceId, junctionTableName, "junction");
3236
3138
  return {
3237
- plan: compiled,
3238
- async consume(source) {
3239
- return (await source.toArray()).map((row) => {
3240
- const mapped = mapRow(row);
3241
- for (const [alias] of aggregateEntries) mapped[alias] = row[alias];
3242
- return blindCast(mapped);
3243
- });
3244
- }
3139
+ childScope: new ModelAccessorScope(initialChildScope.current, [...initialChildScope.#visibleBindings, junctionBinding], this.#aliasCounter),
3140
+ junctionBinding
3245
3141
  };
3246
3142
  }
3247
- };
3248
- /**
3249
- * The having metric methods, one per operation the registry contributes —
3250
- * the runtime mirror of the contract's emitted aggregate map, which is what
3251
- * types the surface as {@link HavingBuilder}. HAVING compares the value
3252
- * inside the database, so only an operation's plain `AggregateExpr` form is
3253
- * sound here: an operation outside the SQL aggregate alphabet exists only in
3254
- * its descriptor-lowered form — a rendering for the driver boundary — and is
3255
- * refused. The typed surface already excludes it; the runtime refusal covers
3256
- * dynamic invocation.
3257
- */
3258
- function createHavingBuilder(contract, aggregates, namespaceId, modelName, tableName) {
3259
- const fieldToColumn = getFieldToColumnMap(contract, namespaceId, modelName);
3260
- const builder = {};
3261
- for (const operation of aggregateOperationNames(aggregates)) builder[operation] = (field) => {
3262
- if (!isAggregateFn(operation)) throw ormError("ORM.AGGREGATE_PROJECTION_ONLY", `Aggregate operation '${operation}' is projection-only: it has no plain SQL form for HAVING, ORDER BY, or comparison positions.`, {
3263
- why: "An operation outside the SQL aggregate alphabet reaches SQL only through its descriptor's lowering hook — a rendering for the driver boundary. HAVING and ORDER BY compare the value inside the database, where that rendering would change SQL semantics.",
3264
- fix: `Project '${operation}' in a select and filter or order on the projected value, or use an operation from the SQL aggregate alphabet.`,
3265
- meta: { operation }
3143
+ forJoinedSource(namespaceId, tableName) {
3144
+ if (this.current.isStoredAt(namespaceId, tableName)) return this;
3145
+ const binding = SqlTableBinding.unaliased({
3146
+ namespaceId,
3147
+ tableName
3266
3148
  });
3267
- return createHavingComparisonMethods(new AggregateExpr(operation, field === void 0 ? void 0 : ColumnRef.of(tableName, fieldToColumn[field] ?? field)), () => resolveAggregate({
3268
- aggregates,
3269
- contract,
3149
+ return new ModelAccessorScope(binding, [...this.#visibleBindings, binding], this.#aliasCounter);
3150
+ }
3151
+ #allocateBinding(namespaceId, tableName, aliasKind) {
3152
+ const storage = {
3270
3153
  namespaceId,
3271
- tableName,
3272
- fn: operation,
3273
- column: field === void 0 ? void 0 : fieldToColumn[field] ?? field
3274
- }).codec.codecId);
3275
- };
3276
- return blindCast(builder);
3154
+ tableName
3155
+ };
3156
+ if (!this.#visibleBindings.some((binding) => binding.isReferencedAs(tableName))) return SqlTableBinding.unaliased(storage);
3157
+ return SqlTableBinding.aliased(storage, this.#allocateAlias(aliasKind));
3158
+ }
3159
+ #allocateAlias(kind) {
3160
+ while (true) {
3161
+ const alias = `__orm_${kind}_${this.#aliasCounter.nextId}`;
3162
+ this.#aliasCounter.nextId += 1;
3163
+ if (!this.#visibleBindings.some((binding) => binding.isReferencedAs(alias))) return alias;
3164
+ }
3165
+ }
3166
+ };
3167
+ function createModelAccessor(context, namespaceId, modelName, variantName) {
3168
+ const tableName = resolveModelTableName(context.contract, namespaceId, modelName);
3169
+ return createModelAccessorInScope(context, namespaceId, modelName, variantName, ModelAccessorScope.root(namespaceId, tableName));
3277
3170
  }
3278
- function createHavingComparisonMethods(metric, outputCodecId) {
3279
- const buildBinaryExpr = (op, value) => {
3280
- const expression = predicateExpression(value);
3281
- if (expression !== void 0 && (expression.kind !== "prepared-param-ref" || expression.codec.codecId !== outputCodecId())) throw ormError("ORM.HAVING_EXPRESSION_UNSUPPORTED", "HAVING comparands require a prepared parameter with the aggregate output codec", { meta: { kind: expression.kind } });
3282
- return predicateComparison(op, metric, expression ?? LiteralExpr.of(value));
3283
- };
3284
- return {
3285
- eq(value) {
3286
- return buildBinaryExpr("eq", value);
3287
- },
3288
- neq(value) {
3289
- return buildBinaryExpr("neq", value);
3290
- },
3291
- gt(value) {
3292
- return buildBinaryExpr("gt", value);
3293
- },
3294
- lt(value) {
3295
- return buildBinaryExpr("lt", value);
3296
- },
3297
- gte(value) {
3298
- return buildBinaryExpr("gte", value);
3171
+ function createModelAccessorInScope(context, namespaceId, modelName, variantName, scope) {
3172
+ const contract = context.contract;
3173
+ const fieldToColumn = getFieldToColumnMap(contract, namespaceId, modelName);
3174
+ const tableName = resolveModelTableName(contract, namespaceId, modelName);
3175
+ const modelRelations = resolveModelRelations(contract, namespaceId, modelName);
3176
+ const variantFieldColumns = variantName ? resolveVariantFieldColumns(contract, namespaceId, modelName, variantName) : {};
3177
+ const variantCoordinates = variantName ? {
3178
+ name: variantName,
3179
+ relations: resolveModelRelations(contract, namespaceId, variantName),
3180
+ tableName: resolvePolymorphismInfo(contract, namespaceId, modelName)?.variants.get(variantName)?.table ?? tableName
3181
+ } : void 0;
3182
+ const opsByCodecId = /* @__PURE__ */ new Map();
3183
+ function registerOp(codecId, op) {
3184
+ let existing = opsByCodecId.get(codecId);
3185
+ if (!existing) {
3186
+ existing = [];
3187
+ opsByCodecId.set(codecId, existing);
3188
+ }
3189
+ existing.push(op);
3190
+ }
3191
+ for (const [name, entry] of Object.entries(context.queryOperations.entries())) {
3192
+ const op = [name, entry];
3193
+ const self = entry.self;
3194
+ if (!self) continue;
3195
+ if (self.codecId !== void 0) registerOp(self.codecId, op);
3196
+ else if (self.traits !== void 0) for (const descriptor of context.codecDescriptors.values()) {
3197
+ const descriptorTraits = descriptor.traits;
3198
+ if (self.traits.every((t) => descriptorTraits.includes(t))) registerOp(descriptor.codecId, op);
3199
+ }
3200
+ }
3201
+ return blindCast(new Proxy({}, { get(_target, prop) {
3202
+ if (typeof prop !== "string") return;
3203
+ if (variantCoordinates) {
3204
+ const variantRelation = variantCoordinates.relations[prop];
3205
+ if (variantRelation) return createRelationFilterAccessor(context, namespaceId, variantCoordinates.name, scope.forJoinedSource(namespaceId, variantCoordinates.tableName), variantRelation);
3206
+ }
3207
+ const relation = modelRelations[prop];
3208
+ if (relation) return createRelationFilterAccessor(context, namespaceId, modelName, scope, relation);
3209
+ const variantField = variantFieldColumns[prop];
3210
+ const resolvedTable = variantField?.table ?? tableName;
3211
+ const fieldBinding = scope.forJoinedSource(namespaceId, resolvedTable).current;
3212
+ const columnName = variantField?.column ?? fieldToColumn[prop] ?? prop;
3213
+ const column = resolveColumn(contract, namespaceId, resolvedTable, columnName);
3214
+ if (!column) return;
3215
+ const traits = codecTraits(context, column.codecId);
3216
+ const operations = opsByCodecId.get(column.codecId) ?? [];
3217
+ const codec = codecRefForStorageColumn(contract.storage, namespaceId, resolvedTable, columnName);
3218
+ return createScalarFieldAccessor(fieldBinding, columnName, column.codecId, column.nullable, codec, traits, operations, context);
3219
+ } }));
3220
+ }
3221
+ function createScalarFieldAccessor(tableBinding, columnName, codecId, nullable, codec, traits, operations, context) {
3222
+ const column = tableBinding.column(columnName);
3223
+ const comparisonEntries = [];
3224
+ for (const [name, meta] of Object.entries(COMPARISON_METHODS_META)) {
3225
+ if (meta.traits.some((t) => !traits.includes(t))) continue;
3226
+ comparisonEntries.push([name, meta.create(column, codec)]);
3227
+ }
3228
+ const accessor = blindCast({
3229
+ returnType: {
3230
+ codecId,
3231
+ nullable,
3232
+ codec
3299
3233
  },
3300
- lte(value) {
3301
- return buildBinaryExpr("lte", value);
3234
+ buildAst: () => column,
3235
+ ...Object.fromEntries(comparisonEntries)
3236
+ });
3237
+ for (const [name, entry] of operations) accessor[name] = createExtensionMethodFactory(accessor, entry, context);
3238
+ return blindCast(accessor);
3239
+ }
3240
+ function createExtensionMethodFactory(selfExpr, entry, context) {
3241
+ return (...args) => {
3242
+ const result = blindCast(entry.impl)(selfExpr, ...args);
3243
+ const returnCodecId = result.returnType.codecId;
3244
+ const returnTraits = codecTraits(context, returnCodecId);
3245
+ if (returnTraits.includes("boolean")) return result.buildAst();
3246
+ const resultAst = result.buildAst();
3247
+ const returnCodec = { codecId: returnCodecId };
3248
+ const methods = {};
3249
+ for (const [resultMethodName, meta] of Object.entries(COMPARISON_METHODS_META)) {
3250
+ if (meta.traits.some((t) => !returnTraits.includes(t))) continue;
3251
+ methods[resultMethodName] = meta.create(resultAst, returnCodec);
3302
3252
  }
3253
+ return methods;
3303
3254
  };
3304
3255
  }
3305
- function createIncludeScalar(fn, state, column) {
3256
+ const RELATION_ACCESSOR_METHOD_NAMES = /* @__PURE__ */ new Set([
3257
+ "some",
3258
+ "every",
3259
+ "none",
3260
+ "count"
3261
+ ]);
3262
+ function createRelationFilterAccessor(context, parentNamespaceId, parentModelName, parentScope, relation) {
3263
+ const relatedTableName = resolveModelTableName(context.contract, relation.toNamespace, relation.to);
3264
+ const correlate = () => correlateRelatedRows(context, parentNamespaceId, parentModelName, parentScope, relatedTableName, relation);
3265
+ const filters = {
3266
+ some: (predicate) => buildExistsExpr(context, relation, correlate(), "some", predicate),
3267
+ every: (predicate) => buildExistsExpr(context, relation, correlate(), "every", predicate),
3268
+ none: (predicate) => buildExistsExpr(context, relation, correlate(), "none", predicate)
3269
+ };
3270
+ if (isToOneCardinality(relation.cardinality)) return new Proxy(filters, { get(target, prop) {
3271
+ if (typeof prop !== "string") return void 0;
3272
+ if (Object.hasOwn(target, prop)) return Reflect.get(target, prop);
3273
+ if (RELATION_ACCESSOR_METHOD_NAMES.has(prop)) return void 0;
3274
+ return relatedOrderableField(context, relation, relatedTableName, correlate, prop);
3275
+ } });
3306
3276
  return {
3307
- kind: "includeScalar",
3308
- fn,
3309
- state,
3310
- ...column !== void 0 ? { column } : {}
3277
+ ...filters,
3278
+ count: (predicate) => createOrderable(() => buildRelationCountExpr(context, relation, correlate(), predicate))
3311
3279
  };
3312
3280
  }
3313
- function createIncludeCombine(branches) {
3281
+ function createOrderable(buildExpr) {
3314
3282
  return {
3315
- kind: "includeCombine",
3316
- branches
3283
+ asc: (options) => checkedOrderByItem("asc", buildExpr(), options),
3284
+ desc: (options) => checkedOrderByItem("desc", buildExpr(), options)
3317
3285
  };
3318
3286
  }
3319
- function isIncludeScalar(value) {
3320
- if (typeof value !== "object" || value === null) return false;
3321
- const candidate = value;
3322
- return candidate.kind === "includeScalar" && typeof candidate.fn === "string" && isCollectionState(candidate.state);
3323
- }
3324
- function isIncludeCombine(value) {
3325
- if (typeof value !== "object" || value === null) return false;
3326
- const candidate = value;
3327
- if (candidate.kind !== "includeCombine") return false;
3328
- if (typeof candidate.branches !== "object" || candidate.branches === null) return false;
3329
- return true;
3330
- }
3331
- function isCollectionStateCarrier(value) {
3332
- if (typeof value !== "object" || value === null) return false;
3333
- return isCollectionState(value.state);
3287
+ function relatedOrderableField(context, relation, relatedTableName, correlate, fieldName) {
3288
+ const fieldToColumn = getFieldToColumnMap(context.contract, relation.toNamespace, relation.to);
3289
+ if (!Object.hasOwn(fieldToColumn, fieldName)) return void 0;
3290
+ const columnName = fieldToColumn[fieldName];
3291
+ if (columnName === void 0) return void 0;
3292
+ const column = resolveColumn(context.contract, relation.toNamespace, relatedTableName, columnName);
3293
+ if (!column || !hasTrait(context, column.codecId, "order")) return void 0;
3294
+ return createOrderable(() => {
3295
+ const rows = correlate();
3296
+ return SubqueryExpr.of(rows.source.withProjection([ProjectionItem.of(columnName, rows.childScope.current.column(columnName))]).withWhere(rows.correlation));
3297
+ });
3334
3298
  }
3335
- function isCollectionState(value) {
3336
- if (typeof value !== "object" || value === null) return false;
3337
- const candidate = value;
3338
- return Array.isArray(candidate.filters) && Array.isArray(candidate.includes);
3299
+ function buildRelationCountExpr(context, relation, rows, predicate) {
3300
+ const childWhere = toRelationWhereExpr(context, relation.toNamespace, relation.to, predicate, rows.childScope);
3301
+ return SubqueryExpr.of(rows.source.withProjection([ProjectionItem.of("count", plainAggregateExpr("count", void 0))]).withWhere(childWhere ? and(rows.correlation, childWhere) : rows.correlation));
3339
3302
  }
3340
- function createRelationMutator() {
3303
+ function correlateRelatedRows(context, parentNamespaceId, parentModelName, parentScope, relatedTableName, relation) {
3304
+ if (hasThrough$1(relation)) {
3305
+ const { through } = relation;
3306
+ const { childScope, junctionBinding } = parentScope.forManyToManyRelation(relation.toNamespace, relatedTableName, through.namespaceId, through.table);
3307
+ const junctionJoinOn = buildPairedColumnExprs(junctionBinding, through.childColumns, childScope.current, through.targetColumns);
3308
+ const parentLocalColumns = relation.on.localFields.map((field) => resolveFieldToColumn(context.contract, parentNamespaceId, parentModelName, field));
3309
+ return {
3310
+ childScope,
3311
+ source: SelectAst.from(childScope.current.tableSource(context.contract)).withJoins([JoinAst.inner(junctionBinding.tableSource(context.contract), junctionJoinOn)]),
3312
+ correlation: buildPairedColumnExprs(junctionBinding, through.parentColumns, parentScope.current, parentLocalColumns),
3313
+ keyColumn: firstJoinColumn(through.targetColumns, "targetColumns")
3314
+ };
3315
+ }
3316
+ const childScope = parentScope.forRelation(relation.toNamespace, relatedTableName);
3341
3317
  return {
3342
- create(data) {
3343
- return {
3344
- kind: "create",
3345
- data: Array.isArray(data) ? [...data] : [data]
3346
- };
3347
- },
3348
- connect(criteria) {
3349
- return {
3350
- kind: "connect",
3351
- criteria: Array.isArray(criteria) ? [...criteria] : [criteria]
3352
- };
3353
- },
3354
- disconnect(criteria) {
3355
- if (!criteria) return { kind: "disconnect" };
3356
- return {
3357
- kind: "disconnect",
3358
- criteria: [...criteria]
3359
- };
3360
- }
3318
+ childScope,
3319
+ source: SelectAst.from(childScope.current.tableSource(context.contract)),
3320
+ correlation: buildJoinWhere(context.contract, parentNamespaceId, parentModelName, parentScope.current, childScope.current, relation),
3321
+ keyColumn: firstTargetColumn(context.contract, relation) ?? "id"
3361
3322
  };
3362
3323
  }
3363
- function isRelationMutationDescriptor(value) {
3364
- if (!value || typeof value !== "object") return false;
3365
- const candidate = value;
3366
- if (candidate.kind !== "create" && candidate.kind !== "connect" && candidate.kind !== "disconnect") return false;
3367
- return true;
3324
+ function buildExistsExpr(context, relation, rows, mode, predicate) {
3325
+ const childWhere = toRelationWhereExpr(context, relation.toNamespace, relation.to, predicate, rows.childScope);
3326
+ const filterPlan = planRelationFilterMode(rows.correlation, childWhere, mode);
3327
+ if (filterPlan.kind === "constantTrue") return AndExpr.true();
3328
+ const subquery = rows.source.withProjection([ProjectionItem.of("_exists", rows.childScope.current.column(rows.keyColumn))]).withWhere(filterPlan.where);
3329
+ return filterPlan.notExists ? ExistsExpr.notExists(subquery) : ExistsExpr.exists(subquery);
3368
3330
  }
3369
- function isRelationMutationCallback(value) {
3370
- return typeof value === "function";
3331
+ function planRelationFilterMode(joinWhere, childWhere, mode) {
3332
+ if (mode === "every") {
3333
+ if (!childWhere) return { kind: "constantTrue" };
3334
+ return {
3335
+ kind: "exists",
3336
+ notExists: true,
3337
+ where: and(joinWhere, not(childWhere))
3338
+ };
3339
+ }
3340
+ if (mode === "none") return {
3341
+ kind: "exists",
3342
+ notExists: true,
3343
+ where: childWhere ? and(joinWhere, childWhere) : joinWhere
3344
+ };
3345
+ return {
3346
+ kind: "exists",
3347
+ notExists: false,
3348
+ where: childWhere ? and(joinWhere, childWhere) : joinWhere
3349
+ };
3371
3350
  }
3372
- function hasThrough(relation) {
3373
- return relation.through !== void 0;
3351
+ function firstJoinColumn(columns, label) {
3352
+ const first = columns[0];
3353
+ if (!first) throw new InternalError(`Relation metadata is missing ${label}`);
3354
+ return first;
3374
3355
  }
3375
- function hasNestedMutationCallbacks(contract, namespaceId, modelName, data) {
3376
- const relationNames = new Set(Object.keys(resolveModelRelations(contract, namespaceId, modelName)));
3377
- for (const [fieldName, value] of Object.entries(data)) {
3378
- if (!relationNames.has(fieldName)) continue;
3379
- if (isRelationMutationCallback(value)) return true;
3356
+ function buildPairedColumnExprs(leftTable, leftColumns, rightTable, rightColumns) {
3357
+ if (leftColumns.length !== rightColumns.length) throw new InternalError(`Relation metadata has mismatched join column counts: ${leftColumns.length} left column(s), ${rightColumns.length} right column(s)`);
3358
+ if (leftColumns.length === 0) throw new InternalError("Relation metadata is missing join columns");
3359
+ const exprs = [];
3360
+ for (let i = 0; i < leftColumns.length; i++) {
3361
+ const left = leftColumns[i];
3362
+ const right = rightColumns[i];
3363
+ if (!left || !right) throw new InternalError(`Relation metadata is missing a join column pair at index ${i}`);
3364
+ exprs.push(BinaryExpr.eq(leftTable.column(left), rightTable.column(right)));
3380
3365
  }
3381
- return false;
3366
+ if (exprs.length === 1 && exprs[0]) return exprs[0];
3367
+ return and(...exprs);
3382
3368
  }
3383
- async function executeNestedCreateMutation(options) {
3384
- return withMutationScope(options.runtime, async (scope) => createGraph(scope, options.context, options.namespaceId, options.modelName, options.data));
3385
- }
3386
- async function executeNestedUpdateMutation(options) {
3387
- return withMutationScope(options.runtime, async (scope) => updateFirstGraph(scope, options.context, options.namespaceId, options.modelName, options.filters, options.data));
3388
- }
3389
- function buildRowIdentityFilterFromRow(contract, namespaceId, modelName, row) {
3390
- const tableName = resolveModelTableName(contract, namespaceId, modelName);
3391
- const identityColumns = resolveRowIdentityColumns(contract, namespaceId, tableName);
3392
- if (identityColumns.length === 0) throw ormError("ORM.ROW_IDENTITY_MISSING", `Nested mutations on model "${modelName}" require table "${tableName}" to have a primary key or unique constraint`, { meta: {
3393
- model: modelName,
3394
- table: tableName
3395
- } });
3396
- const filter = {};
3397
- for (const column of identityColumns) {
3398
- const fieldName = toFieldName(contract, namespaceId, modelName, column);
3399
- const value = row[fieldName];
3400
- if (value === void 0) throw new InternalError(`Missing identity field "${fieldName}" while reloading model "${modelName}"`);
3401
- filter[fieldName] = value;
3402
- }
3403
- return filter;
3404
- }
3405
- async function withMutationScope(runtime, run) {
3406
- if (typeof runtime.transaction === "function") return runInTransaction(await runtime.transaction(), run);
3407
- if (typeof runtime.connection === "function") {
3408
- const connection = await runtime.connection();
3409
- try {
3410
- if (typeof connection.transaction === "function") return await runInTransaction(await connection.transaction(), run);
3411
- return await run(connection);
3412
- } finally {
3413
- await connection.release?.();
3369
+ function toRelationWhereExpr(context, relatedNamespaceId, relatedModelName, predicate, scope) {
3370
+ if (!predicate) return;
3371
+ const accessor = createModelAccessorInScope(context, relatedNamespaceId, relatedModelName, void 0, scope);
3372
+ if (typeof predicate === "function") return predicate(accessor);
3373
+ const exprs = [];
3374
+ for (const [fieldName, value] of Object.entries(predicate)) {
3375
+ if (value === void 0) continue;
3376
+ const fieldAccessor = blindCast(accessor)[fieldName];
3377
+ if (!fieldAccessor) throw ormError("ORM.FIELD_UNKNOWN", `Shorthand filter on "${relatedModelName}.${fieldName}": field is not defined on the model`, { meta: {
3378
+ model: relatedModelName,
3379
+ field: fieldName
3380
+ } });
3381
+ if (value === null) {
3382
+ if (!fieldAccessor.isNull) throw new InternalError(`Shorthand filter on "${relatedModelName}.${fieldName}": isNull is unexpectedly missing — this is a bug in trait gating`);
3383
+ exprs.push(fieldAccessor.isNull());
3384
+ continue;
3414
3385
  }
3386
+ if (!fieldAccessor.eq) throw ormError("ORM.FILTER_UNSUPPORTED", `Shorthand filter on "${relatedModelName}.${fieldName}": field does not support equality comparisons`, { meta: {
3387
+ model: relatedModelName,
3388
+ field: fieldName,
3389
+ trait: "equality"
3390
+ } });
3391
+ exprs.push(fieldAccessor.eq(value));
3415
3392
  }
3416
- return run(runtime);
3393
+ if (exprs.length === 0) return;
3394
+ return exprs.length === 1 ? exprs[0] : and(...exprs);
3417
3395
  }
3418
- async function runInTransaction(transaction, run) {
3419
- try {
3420
- const result = await run(transaction);
3421
- if (typeof transaction.commit === "function") await transaction.commit();
3422
- return result;
3423
- } catch (error) {
3424
- if (typeof transaction.rollback === "function") await transaction.rollback();
3425
- throw error;
3396
+ function buildJoinWhere(contract, parentNamespaceId, parentModelName, parentTable, relatedTable, relation) {
3397
+ const localFields = relation.on?.localFields ?? [];
3398
+ const targetFields = relation.on?.targetFields ?? [];
3399
+ const joinExprs = [];
3400
+ const count = Math.min(localFields.length, targetFields.length);
3401
+ for (let i = 0; i < count; i++) {
3402
+ const localField = localFields[i];
3403
+ const targetField = targetFields[i];
3404
+ if (!localField || !targetField) continue;
3405
+ const localColumn = resolveFieldToColumn(contract, parentNamespaceId, parentModelName, localField);
3406
+ const targetColumn = resolveFieldToColumn(contract, relation.toNamespace, relation.to, targetField);
3407
+ joinExprs.push(BinaryExpr.eq(relatedTable.column(targetColumn), parentTable.column(localColumn)));
3426
3408
  }
3409
+ if (joinExprs.length === 0) throw new InternalError("Relation metadata is missing join columns");
3410
+ const firstExpr = joinExprs[0];
3411
+ if (joinExprs.length === 1 && firstExpr !== void 0) return firstExpr;
3412
+ return and(...joinExprs);
3427
3413
  }
3428
- async function createGraph(scope, context, namespaceId, modelName, input) {
3429
- const contract = context.contract;
3430
- const parsed = parseMutationInput(contract, namespaceId, modelName, input);
3431
- const { parentOwned, childOwned, junctionOwned } = partitionByOwnership(parsed.relationMutations);
3432
- const scalarData = { ...parsed.scalarData };
3433
- for (const relationMutation of parentOwned) {
3434
- if (relationMutation.mutation.kind === "disconnect") throw ormError("ORM.RELATION_MUTATION_UNSUPPORTED", "disconnect() is only supported in update() nested mutations", { meta: {
3435
- kind: "disconnect",
3436
- relation: relationMutation.relation.relationName
3414
+ function firstTargetColumn(contract, relation) {
3415
+ const firstField = (relation.on?.targetFields)?.[0];
3416
+ if (!firstField) return;
3417
+ return resolveFieldToColumn(contract, relation.toNamespace, relation.to, firstField);
3418
+ }
3419
+ var GroupedCollection = class GroupedCollection {
3420
+ ctx;
3421
+ contract;
3422
+ modelName;
3423
+ tableName;
3424
+ namespaceId;
3425
+ preGroupState;
3426
+ groupByFields;
3427
+ groupByColumns;
3428
+ havingFilters;
3429
+ postGroup;
3430
+ constructor(ctx, modelName, options) {
3431
+ this.ctx = ctx;
3432
+ this.contract = ctx.context.contract;
3433
+ this.modelName = modelName;
3434
+ this.tableName = options.tableName;
3435
+ this.namespaceId = blindCast(options.namespaceId);
3436
+ this.preGroupState = options.preGroupState;
3437
+ this.groupByFields = options.groupByFields;
3438
+ this.groupByColumns = options.groupByColumns;
3439
+ this.havingFilters = options.havingFilters;
3440
+ this.postGroup = options.postGroup;
3441
+ }
3442
+ #clone(overrides) {
3443
+ return blindCast(new GroupedCollection(this.ctx, this.modelName, {
3444
+ tableName: this.tableName,
3445
+ namespaceId: this.namespaceId,
3446
+ preGroupState: this.preGroupState,
3447
+ groupByFields: this.groupByFields,
3448
+ groupByColumns: this.groupByColumns,
3449
+ havingFilters: this.havingFilters,
3450
+ postGroup: this.postGroup,
3451
+ ...overrides
3452
+ }));
3453
+ }
3454
+ having(predicate) {
3455
+ const havingExpr = predicate(createHavingBuilder(this.contract, this.ctx.context.aggregateDescriptors, this.namespaceId, this.modelName, this.tableName));
3456
+ return this.#clone({ havingFilters: [...this.havingFilters, havingExpr] });
3457
+ }
3458
+ /**
3459
+ * Append an `ORDER BY` clause on the grouped rows themselves — orders by
3460
+ * group key. Ordering by an aggregate alias needs a builder surface over
3461
+ * the aliases and isn't supported here. Unlocks post-group `limit(...)` /
3462
+ * `offset(...)`, which page a group order that would otherwise be undefined.
3463
+ */
3464
+ orderBy(selection) {
3465
+ const selectors = Array.isArray(selection) ? selection : [selection];
3466
+ if (selectors.length === 0) throw ormError("ORM.ARGUMENT_INVALID", `orderBy() for model "${this.modelName}" requires at least one selector`, { meta: {
3467
+ method: "orderBy",
3468
+ model: this.modelName
3469
+ } });
3470
+ const accessor = createModelAccessor(this.ctx.context, this.namespaceId, this.modelName);
3471
+ const nextOrders = selectors.map((selector) => selector(accessor));
3472
+ return this.#clone({ postGroup: {
3473
+ ...this.postGroup,
3474
+ orderBy: [...this.postGroup.orderBy, ...nextOrders]
3437
3475
  } });
3438
- await applyParentOwnedMutation(scope, context, namespaceId, modelName, scalarData, relationMutation.relation, relationMutation.mutation);
3439
3476
  }
3440
- for (const relationMutation of junctionOwned) {
3441
- if (relationMutation.mutation.kind === "disconnect") throw ormError("ORM.RELATION_MUTATION_UNSUPPORTED", "disconnect() is only supported in update() nested mutations", { meta: {
3442
- kind: "disconnect",
3443
- relation: relationMutation.relation.relationName
3477
+ /**
3478
+ * Apply `LIMIT n` to the grouped rows. Replaces any previous post-group
3479
+ * limit. Requires a prior `orderBy(...)` — a database may return groups in
3480
+ * any order, so "the first n groups" is undefined without one.
3481
+ */
3482
+ limit(n) {
3483
+ return this.#clone({ postGroup: {
3484
+ ...this.postGroup,
3485
+ limit: typeof n === "number" ? n : toExpr(n)
3444
3486
  } });
3445
- await preflightJunctionOwnedCreateMutation(scope, context, relationMutation);
3446
3487
  }
3447
- const parentRow = await insertSingleRow(scope, context, namespaceId, modelName, scalarData);
3448
- for (const relationMutation of childOwned) {
3449
- if (relationMutation.mutation.kind === "disconnect") throw ormError("ORM.RELATION_MUTATION_UNSUPPORTED", "disconnect() is only supported in update() nested mutations", { meta: {
3450
- kind: "disconnect",
3451
- relation: relationMutation.relation.relationName
3488
+ /**
3489
+ * Apply `OFFSET n` to the grouped rows. Replaces any previous post-group
3490
+ * offset. Requires a prior `orderBy(...)`, same as `limit(...)` — both
3491
+ * pagination methods need a deterministic group order.
3492
+ */
3493
+ offset(n) {
3494
+ return this.#clone({ postGroup: {
3495
+ ...this.postGroup,
3496
+ offset: typeof n === "number" ? n : toExpr(n)
3452
3497
  } });
3453
- await applyChildOwnedMutation(scope, context, namespaceId, modelName, parentRow, relationMutation.relation, relationMutation.mutation);
3454
3498
  }
3455
- for (const relationMutation of junctionOwned) await applyJunctionOwnedMutation(scope, context, namespaceId, modelName, parentRow, relationMutation.relation, relationMutation.mutation);
3456
- return parentRow;
3457
- }
3458
- async function updateFirstGraph(scope, context, namespaceId, modelName, filters, input) {
3459
- const contract = context.contract;
3460
- const existingRow = await findFirstByFilters(scope, contract, namespaceId, modelName, filters);
3461
- if (!existingRow) return null;
3462
- const parsed = parseMutationInput(contract, namespaceId, modelName, input);
3463
- const { parentOwned, childOwned, junctionOwned } = partitionByOwnership(parsed.relationMutations);
3464
- const scalarData = { ...parsed.scalarData };
3465
- for (const relationMutation of parentOwned) await applyParentOwnedMutation(scope, context, namespaceId, modelName, scalarData, relationMutation.relation, relationMutation.mutation);
3466
- for (const relationMutation of junctionOwned) await preflightJunctionOwnedCreateMutation(scope, context, relationMutation);
3467
- let parentRow = existingRow;
3468
- const mappedUpdateData = mapModelDataToStorageRow(contract, namespaceId, modelName, scalarData);
3469
- if (Object.keys(mappedUpdateData).length > 0) {
3470
- const tableName = resolveModelTableName(contract, namespaceId, modelName);
3471
- const appliedUpdateDefaults = context.applyMutationDefaults({
3472
- op: "update",
3473
- entry: tableName,
3474
- namespace: namespaceId,
3475
- values: mappedUpdateData
3476
- });
3477
- for (const def of appliedUpdateDefaults) mappedUpdateData[def.field] = def.value;
3478
- const identityWhere = shorthandToWhereExpr(context, namespaceId, modelName, castAs(buildRowIdentityFilterFromRow(contract, namespaceId, modelName, existingRow)));
3479
- if (!identityWhere) throw new InternalError(`Failed to build row identity filter for model "${modelName}"`);
3480
- const updatedRaw = (await queryPlanRows(scope, compileUpdateReturning(contract, namespaceId, tableName, mappedUpdateData, [identityWhere], void 0)).toArray())[0];
3481
- if (updatedRaw) parentRow = mapStorageRowToModelFields(contract, namespaceId, modelName, updatedRaw);
3499
+ /**
3500
+ * Read terminal: run a grouped aggregate query.
3501
+ *
3502
+ * Accepts an optional `configure` callback that receives a
3503
+ * `MetaBuilder<'read'>` for attaching typed annotations.
3504
+ * Annotations are merged into the compiled plan's `meta.annotations`.
3505
+ */
3506
+ get prepared() {
3507
+ return { aggregate: (fn, configure) => this.#describeAggregate(fn, configure) };
3482
3508
  }
3483
- for (const relationMutation of childOwned) await applyChildOwnedMutation(scope, context, namespaceId, modelName, parentRow, relationMutation.relation, relationMutation.mutation);
3484
- for (const relationMutation of junctionOwned) await applyJunctionOwnedMutation(scope, context, namespaceId, modelName, parentRow, relationMutation.relation, relationMutation.mutation);
3485
- return parentRow;
3486
- }
3487
- function parseMutationInput(contract, namespaceId, modelName, input) {
3488
- const scalarData = {};
3489
- const relationDefinitions = new Map(getRelationDefinitions(contract, namespaceId, modelName).map((relation) => [relation.relationName, relation]));
3490
- const relationMutations = [];
3491
- for (const [fieldName, value] of Object.entries(input)) {
3492
- const relation = relationDefinitions.get(fieldName);
3493
- if (!relation) {
3494
- scalarData[fieldName] = value;
3495
- continue;
3496
- }
3497
- if (!isRelationMutationCallback(value)) throw ormError("ORM.RELATION_MUTATION_INVALID", `Relation field "${fieldName}" on model "${modelName}" expects a mutator callback`, { meta: {
3498
- relation: fieldName,
3499
- model: modelName,
3500
- problem: "missing-callback"
3509
+ async aggregate(fn, configure) {
3510
+ const description = this.#describeAggregate(fn, configure);
3511
+ return description.consume(queryPlanRows(this.ctx.runtime, description.plan));
3512
+ }
3513
+ #describeAggregate(fn, configure) {
3514
+ const aggregateSpec = fn(createAggregateBuilder(this.contract, this.ctx.context.aggregateDescriptors, this.namespaceId, this.modelName));
3515
+ const aggregateEntries = Object.entries(aggregateSpec);
3516
+ if (aggregateEntries.length === 0) throw ormError("ORM.AGGREGATE_SELECTOR_MISSING", "groupBy().aggregate() requires at least one aggregation selector", { meta: {
3517
+ method: "groupBy.aggregate",
3518
+ model: this.modelName
3501
3519
  } });
3502
- const mutation = value(createRelationMutator());
3503
- if (!isRelationMutationDescriptor(mutation)) throw ormError("ORM.RELATION_MUTATION_INVALID", `Relation field "${fieldName}" on model "${modelName}" returned an invalid mutation descriptor`, { meta: {
3504
- relation: fieldName,
3505
- model: modelName,
3506
- problem: "invalid-descriptor"
3520
+ for (const [alias, selector] of aggregateEntries) if (!isAggregateSelector(selector)) throw ormError("ORM.AGGREGATE_SELECTOR_INVALID", `groupBy().aggregate() selector "${alias}" is invalid`, { meta: {
3521
+ method: "groupBy.aggregate",
3522
+ model: this.modelName,
3523
+ alias
3507
3524
  } });
3508
- relationMutations.push({
3509
- relation,
3510
- mutation
3511
- });
3512
- }
3513
- return {
3514
- scalarData,
3515
- relationMutations
3516
- };
3517
- }
3518
- function partitionByOwnership(relationMutations) {
3519
- const parentOwned = [];
3520
- const childOwned = [];
3521
- const junctionOwned = [];
3522
- for (const relationMutation of relationMutations) {
3523
- if (hasThrough(relationMutation.relation)) {
3524
- junctionOwned.push({
3525
- relation: relationMutation.relation,
3526
- mutation: relationMutation.mutation
3527
- });
3528
- continue;
3529
- }
3530
- if (relationMutation.relation.cardinality === "N:1") {
3531
- parentOwned.push(relationMutation);
3532
- continue;
3525
+ let annotationsMap;
3526
+ if (configure !== void 0) {
3527
+ const meta = createMetaBuilder("read", "groupBy.aggregate");
3528
+ configure(meta);
3529
+ if (meta.annotations.size > 0) annotationsMap = meta.annotations;
3533
3530
  }
3534
- childOwned.push(relationMutation);
3531
+ const compiled = mergeAnnotations(compileGroupedAggregate(this.contract, this.ctx.context.aggregateDescriptors, this.namespaceId, this.tableName, this.preGroupState, this.groupByColumns, aggregateSpec, combineWhereExprs(this.havingFilters), this.modelName, this.postGroup), annotationsMap);
3532
+ const mapRow = createStorageRowMapper(this.contract, this.namespaceId, this.modelName);
3533
+ return {
3534
+ plan: compiled,
3535
+ async consume(source) {
3536
+ return (await source.toArray()).map((row) => {
3537
+ const mapped = mapRow(row);
3538
+ for (const [alias] of aggregateEntries) mapped[alias] = row[alias];
3539
+ return blindCast(mapped);
3540
+ });
3541
+ }
3542
+ };
3535
3543
  }
3536
- return {
3537
- parentOwned,
3538
- childOwned,
3539
- junctionOwned
3544
+ };
3545
+ /**
3546
+ * The having metric methods, one per operation the registry contributes —
3547
+ * the runtime mirror of the contract's emitted aggregate map, which is what
3548
+ * types the surface as {@link HavingBuilder}. HAVING compares the value
3549
+ * inside the database, so only an operation's plain `AggregateExpr` form is
3550
+ * sound here: an operation outside the SQL aggregate alphabet exists only in
3551
+ * its descriptor-lowered form — a rendering for the driver boundary — and is
3552
+ * refused. The typed surface already excludes it; the runtime refusal covers
3553
+ * dynamic invocation.
3554
+ */
3555
+ function createHavingBuilder(contract, aggregates, namespaceId, modelName, tableName) {
3556
+ const fieldToColumn = getFieldToColumnMap(contract, namespaceId, modelName);
3557
+ const builder = {};
3558
+ for (const operation of aggregateOperationNames(aggregates)) builder[operation] = (field) => {
3559
+ if (!isAggregateFn(operation)) throw ormError("ORM.AGGREGATE_PROJECTION_ONLY", `Aggregate operation '${operation}' is projection-only: it has no plain SQL form for HAVING, ORDER BY, or comparison positions.`, {
3560
+ why: "An operation outside the SQL aggregate alphabet reaches SQL only through its descriptor's lowering hook — a rendering for the driver boundary. HAVING and ORDER BY compare the value inside the database, where that rendering would change SQL semantics.",
3561
+ fix: `Project '${operation}' in a select and filter or order on the projected value, or use an operation from the SQL aggregate alphabet.`,
3562
+ meta: { operation }
3563
+ });
3564
+ return createHavingComparisonMethods(new AggregateExpr(operation, field === void 0 ? void 0 : ColumnRef.of(tableName, fieldToColumn[field] ?? field)), () => resolveAggregate({
3565
+ aggregates,
3566
+ contract,
3567
+ namespaceId,
3568
+ tableName,
3569
+ fn: operation,
3570
+ column: field === void 0 ? void 0 : fieldToColumn[field] ?? field
3571
+ }).codec.codecId);
3540
3572
  };
3573
+ return blindCast(builder);
3541
3574
  }
3542
- async function applyParentOwnedMutation(scope, context, parentNamespaceId, parentModelName, scalarData, relation, mutation) {
3543
- const contract = context.contract;
3544
- if (mutation.kind === "disconnect") {
3545
- for (const localColumn of relation.localColumns) {
3546
- const parentFieldName = toFieldName(contract, parentNamespaceId, parentModelName, localColumn);
3547
- scalarData[parentFieldName] = null;
3548
- }
3549
- return;
3550
- }
3551
- if (mutation.kind === "create") {
3552
- const row = mutation.data[0];
3553
- if (!row) throw ormError("ORM.RELATION_MUTATION_INVALID", `create() nested mutation for relation "${relation.relationName}" requires data`, { meta: {
3554
- kind: "create",
3555
- relation: relation.relationName,
3556
- problem: "missing-data"
3557
- } });
3558
- copyRelatedValuesToParent(contract, parentNamespaceId, parentModelName, relation, scalarData, await createGraph(scope, context, relation.relatedNamespaceId, relation.relatedModelName, castAs(row)));
3559
- return;
3560
- }
3561
- const criterion = mutation.criteria[0];
3562
- if (!criterion) throw ormError("ORM.RELATION_MUTATION_INVALID", `connect() nested mutation for relation "${relation.relationName}" requires criterion`, { meta: {
3563
- kind: "connect",
3564
- relation: relation.relationName,
3565
- problem: "missing-criterion"
3566
- } });
3567
- const relatedRow = await findRowByCriterion(scope, context, relation.relatedNamespaceId, relation.relatedModelName, castAs(criterion));
3568
- if (!relatedRow) throw ormError("ORM.RELATION_ROW_MISSING", `connect() nested mutation for relation "${relation.relationName}" did not find a matching row`, { meta: {
3569
- kind: "connect",
3570
- relation: relation.relationName
3571
- } });
3572
- copyRelatedValuesToParent(contract, parentNamespaceId, parentModelName, relation, scalarData, relatedRow);
3573
- }
3574
- function copyRelatedValuesToParent(contract, parentNamespaceId, parentModelName, relation, scalarData, relatedRow) {
3575
- for (let i = 0; i < relation.localColumns.length; i++) {
3576
- const localColumn = relation.localColumns[i];
3577
- const targetColumn = relation.targetColumns[i];
3578
- if (!localColumn || !targetColumn) continue;
3579
- const parentFieldName = toFieldName(contract, parentNamespaceId, parentModelName, localColumn);
3580
- scalarData[parentFieldName] = relatedRow[toFieldName(contract, relation.relatedNamespaceId, relation.relatedModelName, targetColumn)];
3581
- }
3582
- }
3583
- async function applyChildOwnedMutation(scope, context, parentNamespaceId, parentModelName, parentRow, relation, mutation) {
3584
- const contract = context.contract;
3585
- const parentValues = readParentColumnValues(contract, parentNamespaceId, parentModelName, relation, parentRow);
3586
- if (mutation.kind === "create") {
3587
- for (const childInput of mutation.data) {
3588
- const payload = { ...castAs(childInput) };
3589
- for (const [childColumn, parentValue] of parentValues.entries()) {
3590
- const childFieldName = toFieldName(contract, relation.relatedNamespaceId, relation.relatedModelName, childColumn);
3591
- payload[childFieldName] = parentValue;
3592
- }
3593
- await createGraph(scope, context, relation.relatedNamespaceId, relation.relatedModelName, castAs(payload));
3594
- }
3595
- return;
3596
- }
3597
- if (mutation.kind === "connect") {
3598
- for (const criterion of mutation.criteria) {
3599
- const criterionWhere = shorthandToWhereExpr(context, relation.relatedNamespaceId, relation.relatedModelName, castAs(criterion));
3600
- if (!criterionWhere) throw ormError("ORM.RELATION_MUTATION_INVALID", `connect() nested mutation for relation "${relation.relationName}" requires non-empty criterion`, { meta: {
3601
- kind: "connect",
3602
- relation: relation.relationName,
3603
- problem: "empty-criterion"
3604
- } });
3605
- const setValues = {};
3606
- for (const [childColumn, parentValue] of parentValues.entries()) setValues[childColumn] = parentValue;
3607
- await executeUpdateCount(scope, contract, relation.relatedNamespaceId, relation.relatedTableName, setValues, [criterionWhere]);
3575
+ function createHavingComparisonMethods(metric, outputCodecId) {
3576
+ const buildBinaryExpr = (op, value) => {
3577
+ const expression = predicateExpression(value);
3578
+ if (expression !== void 0 && (expression.kind !== "prepared-param-ref" || expression.codec.codecId !== outputCodecId())) throw ormError("ORM.HAVING_EXPRESSION_UNSUPPORTED", "HAVING comparands require a prepared parameter with the aggregate output codec", { meta: { kind: expression.kind } });
3579
+ return predicateComparison(op, metric, expression ?? LiteralExpr.of(value));
3580
+ };
3581
+ return {
3582
+ eq(value) {
3583
+ return buildBinaryExpr("eq", value);
3584
+ },
3585
+ neq(value) {
3586
+ return buildBinaryExpr("neq", value);
3587
+ },
3588
+ gt(value) {
3589
+ return buildBinaryExpr("gt", value);
3590
+ },
3591
+ lt(value) {
3592
+ return buildBinaryExpr("lt", value);
3593
+ },
3594
+ gte(value) {
3595
+ return buildBinaryExpr("gte", value);
3596
+ },
3597
+ lte(value) {
3598
+ return buildBinaryExpr("lte", value);
3608
3599
  }
3609
- return;
3610
- }
3611
- const setValues = {};
3612
- for (const childColumn of parentValues.keys()) setValues[childColumn] = null;
3613
- if (!mutation.criteria || mutation.criteria.length === 0) {
3614
- const parentJoinWhere = buildChildJoinWhere(relation, parentValues);
3615
- await executeUpdateCount(scope, contract, relation.relatedNamespaceId, relation.relatedTableName, setValues, [parentJoinWhere]);
3616
- return;
3617
- }
3618
- for (const criterion of mutation.criteria) {
3619
- const criterionWhere = shorthandToWhereExpr(context, relation.relatedNamespaceId, relation.relatedModelName, castAs(criterion));
3620
- if (!criterionWhere) throw ormError("ORM.RELATION_MUTATION_INVALID", `disconnect() nested mutation for relation "${relation.relationName}" requires non-empty criterion`, { meta: {
3621
- kind: "disconnect",
3622
- relation: relation.relationName,
3623
- problem: "empty-criterion"
3624
- } });
3625
- const parentJoinWhere = buildChildJoinWhere(relation, parentValues);
3626
- await executeUpdateCount(scope, contract, relation.relatedNamespaceId, relation.relatedTableName, setValues, [and(parentJoinWhere, criterionWhere)]);
3627
- }
3628
- }
3629
- async function applyJunctionOwnedMutation(scope, context, parentNamespaceId, parentModelName, parentRow, relation, mutation) {
3630
- const contract = context.contract;
3631
- const parentPkValues = readJunctionParentValues(contract, parentNamespaceId, parentModelName, relation, parentRow);
3632
- assertJunctionPayloadWritable(relation, mutation.kind);
3633
- if (mutation.kind === "create") {
3634
- for (const childInput of mutation.data) await insertJunctionLink(scope, context, relation, parentPkValues, readJunctionTargetValues(contract, relation, await createGraph(scope, context, relation.relatedNamespaceId, relation.relatedModelName, castAs(childInput))), "create");
3635
- return;
3636
- }
3637
- if (mutation.kind === "connect") {
3638
- for (const criterion of mutation.criteria) await insertJunctionLink(scope, context, relation, parentPkValues, await resolveJunctionTargetValues(scope, context, relation, "connect", criterion), "connect");
3639
- return;
3640
- }
3641
- if (!mutation.criteria || mutation.criteria.length === 0) throw ormError("ORM.RELATION_MUTATION_INVALID", `disconnect() nested mutation for relation "${relation.relationName}" requires criterion`, { meta: {
3642
- kind: "disconnect",
3643
- relation: relation.relationName,
3644
- problem: "missing-criterion"
3645
- } });
3646
- for (const criterion of mutation.criteria) await deleteJunctionLink(scope, context, relation, parentPkValues, await resolveJunctionTargetValues(scope, context, relation, "disconnect", criterion));
3647
- }
3648
- async function preflightJunctionOwnedCreateMutation(scope, context, relationMutation) {
3649
- const { relation, mutation } = relationMutation;
3650
- assertJunctionMetadataShape(relation);
3651
- assertJunctionPayloadWritable(relation, mutation.kind);
3652
- if (mutation.kind !== "connect") return;
3653
- const seenTargetKeys = /* @__PURE__ */ new Set();
3654
- for (const criterion of mutation.criteria) {
3655
- const targetValues = await resolveJunctionTargetValues(scope, context, relation, "connect", criterion);
3656
- const targetKey = JSON.stringify([...targetValues.entries()]);
3657
- if (seenTargetKeys.has(targetKey)) throw ormError("ORM.RELATION_MUTATION_INVALID", `connect() nested mutation for relation "${relation.relationName}" resolved duplicate junction link targets; remove the duplicate criteria`, { meta: {
3658
- kind: "connect",
3659
- relation: relation.relationName,
3660
- problem: "duplicate-criteria"
3661
- } });
3662
- seenTargetKeys.add(targetKey);
3663
- }
3664
- }
3665
- function assertJunctionPayloadWritable(relation, mutationKind) {
3666
- const through = relation.through;
3667
- if (mutationKind !== "create" && mutationKind !== "connect" || through.requiredPayloadColumns.length === 0) return;
3668
- const cols = through.requiredPayloadColumns.map((c) => `\`${c}\``).join(", ");
3669
- throw ormError("ORM.RELATION_MUTATION_UNSUPPORTED", `Cannot \`${mutationKind}\` on relation \`${relation.relationName}\`: its junction \`${through.table}\` has required column(s) ${cols} the relation API can't populate. Write the \`${through.table}\` junction directly or use the SQL builder.`, { meta: {
3670
- kind: mutationKind,
3671
- relation: relation.relationName,
3672
- reason: "junction-required-columns",
3673
- junction: through.table
3674
- } });
3675
- }
3676
- async function resolveJunctionTargetValues(scope, context, relation, kind, criterion) {
3677
- const relatedRow = await findRowByCriterion(scope, context, relation.relatedNamespaceId, relation.relatedModelName, criterion);
3678
- if (!relatedRow) throw ormError("ORM.RELATION_ROW_MISSING", `${kind}() nested mutation for relation "${relation.relationName}" did not find a matching row`, { meta: {
3679
- kind,
3680
- relation: relation.relationName
3681
- } });
3682
- return readJunctionTargetValues(context.contract, relation, relatedRow);
3683
- }
3684
- function readJunctionParentValues(contract, parentNamespaceId, parentModelName, relation, parentRow) {
3685
- const values = /* @__PURE__ */ new Map();
3686
- assertJunctionParentMetadataLength(relation);
3687
- for (let i = 0; i < relation.through.parentColumns.length; i++) {
3688
- const junctionColumn = relation.through.parentColumns[i];
3689
- const parentColumn = relation.localColumns[i];
3690
- if (junctionColumn === void 0 || parentColumn === void 0) throw new InternalError(`Relation "${relation.relationName}" has incomplete junction metadata for parent columns`);
3691
- const parentFieldName = toFieldName(contract, parentNamespaceId, parentModelName, parentColumn);
3692
- const parentValue = parentRow[parentFieldName];
3693
- if (parentValue === void 0) throw new InternalError(`Nested mutation requires parent field "${parentFieldName}" to be present in returned row`);
3694
- values.set(junctionColumn, parentValue);
3695
- }
3696
- return values;
3697
- }
3698
- function readJunctionTargetValues(contract, relation, relatedRow) {
3699
- const values = /* @__PURE__ */ new Map();
3700
- assertJunctionTargetMetadataLength(relation);
3701
- for (let i = 0; i < relation.through.childColumns.length; i++) {
3702
- const junctionColumn = relation.through.childColumns[i];
3703
- const targetColumn = relation.through.targetColumns[i];
3704
- if (junctionColumn === void 0 || targetColumn === void 0) throw new InternalError(`Relation "${relation.relationName}" has incomplete junction metadata for target columns`);
3705
- const targetFieldName = toFieldName(contract, relation.relatedNamespaceId, relation.relatedModelName, targetColumn);
3706
- const targetValue = relatedRow[targetFieldName];
3707
- if (targetValue === void 0) throw new InternalError(`Nested mutation requires target field "${targetFieldName}" to be present in returned row`);
3708
- values.set(junctionColumn, targetValue);
3709
- }
3710
- return values;
3711
- }
3712
- function assertJunctionMetadataLength(relation, throughColumnName, throughColumns, pairedColumnName, pairedColumns) {
3713
- if (throughColumns.length === pairedColumns.length) return;
3714
- throw new InternalError(`Relation "${relation.relationName}" has invalid junction metadata: ${throughColumnName} has ${throughColumns.length} column(s), but ${pairedColumnName} has ${pairedColumns.length}`);
3715
- }
3716
- function assertJunctionParentMetadataLength(relation) {
3717
- assertJunctionMetadataLength(relation, "parentColumns", relation.through.parentColumns, "localColumns", relation.localColumns);
3718
- }
3719
- function assertJunctionTargetMetadataLength(relation) {
3720
- assertJunctionMetadataLength(relation, "childColumns", relation.through.childColumns, "targetColumns", relation.through.targetColumns);
3600
+ };
3721
3601
  }
3722
- function assertJunctionMetadataShape(relation) {
3723
- assertJunctionParentMetadataLength(relation);
3724
- assertJunctionTargetMetadataLength(relation);
3602
+ function createIncludeScalar(fn, state, column) {
3603
+ return {
3604
+ kind: "includeScalar",
3605
+ fn,
3606
+ state,
3607
+ ...column !== void 0 ? { column } : {}
3608
+ };
3725
3609
  }
3726
- function writeJunctionColumn(junctionRow, through, column, value, relationName) {
3727
- if (Object.hasOwn(junctionRow, column) && !Object.is(junctionRow[column], value)) throw ormError("ORM.RELATION_MUTATION_INVALID", `Cannot write junction "${through.table}": conflicting values for junction column "${column}"`, { meta: {
3728
- relation: relationName,
3729
- junction: through.table,
3730
- column
3731
- } });
3732
- junctionRow[column] = value;
3610
+ function createIncludeCombine(branches) {
3611
+ return {
3612
+ kind: "includeCombine",
3613
+ branches
3614
+ };
3733
3615
  }
3734
- async function insertJunctionLink(scope, context, relation, parentPkValues, targetPkValues, mutationKind) {
3735
- const through = relation.through;
3736
- const junctionRow = {};
3737
- for (const [column, value] of parentPkValues.entries()) writeJunctionColumn(junctionRow, through, column, value, relation.relationName);
3738
- for (const [column, value] of targetPkValues.entries()) writeJunctionColumn(junctionRow, through, column, value, relation.relationName);
3739
- const applied = context.applyMutationDefaults({
3740
- op: "create",
3741
- entry: through.table,
3742
- namespace: through.namespaceId,
3743
- values: junctionRow
3744
- });
3745
- for (const def of applied) junctionRow[def.field] = def.value;
3746
- const compiled = compileInsertCount(context.contract, through.namespaceId, through.table, [junctionRow]);
3747
- try {
3748
- await scope.execute(compiled);
3749
- } catch (error) {
3750
- if (mutationKind === "connect" && isUniqueConstraintViolation(error)) throw ormError("ORM.RELATION_LINK_DUPLICATE", `connect() nested mutation for relation "${relation.relationName}" violated a unique constraint on junction "${through.table}"; the junction link may already be present`, {
3751
- meta: {
3752
- relation: relation.relationName,
3753
- junction: through.table
3754
- },
3755
- cause: error
3756
- });
3757
- throw error;
3758
- }
3616
+ function isIncludeScalar(value) {
3617
+ if (typeof value !== "object" || value === null) return false;
3618
+ const candidate = value;
3619
+ return candidate.kind === "includeScalar" && typeof candidate.fn === "string" && isCollectionState(candidate.state);
3759
3620
  }
3760
- async function deleteJunctionLink(scope, context, relation, parentPkValues, targetPkValues) {
3761
- const through = relation.through;
3762
- const junctionRow = {};
3763
- for (const [column, value] of parentPkValues.entries()) writeJunctionColumn(junctionRow, through, column, value, relation.relationName);
3764
- for (const [column, value] of targetPkValues.entries()) writeJunctionColumn(junctionRow, through, column, value, relation.relationName);
3765
- const exprs = [];
3766
- for (const [column, value] of Object.entries(junctionRow)) exprs.push(BinaryExpr.eq(ColumnRef.of(through.table, column), LiteralExpr.of(value)));
3767
- const first = exprs[0];
3768
- const where = exprs.length === 1 && first !== void 0 ? first : and(...exprs);
3769
- const compiled = compileDeleteCount(context.contract, through.namespaceId, through.table, [where]);
3770
- await scope.execute(compiled);
3621
+ function isIncludeCombine(value) {
3622
+ if (typeof value !== "object" || value === null) return false;
3623
+ const candidate = value;
3624
+ if (candidate.kind !== "includeCombine") return false;
3625
+ if (typeof candidate.branches !== "object" || candidate.branches === null) return false;
3626
+ return true;
3771
3627
  }
3772
- function readParentColumnValues(contract, parentNamespaceId, parentModelName, relation, parentRow) {
3773
- const values = /* @__PURE__ */ new Map();
3774
- for (let i = 0; i < relation.localColumns.length; i++) {
3775
- const localColumn = relation.localColumns[i];
3776
- const targetColumn = relation.targetColumns[i];
3777
- if (!localColumn || !targetColumn) continue;
3778
- const parentFieldName = toFieldName(contract, parentNamespaceId, parentModelName, localColumn);
3779
- const parentValue = parentRow[parentFieldName];
3780
- if (parentValue === void 0) throw new InternalError(`Nested mutation requires parent field "${parentFieldName}" to be present in returned row`);
3781
- values.set(targetColumn, parentValue);
3782
- }
3783
- return values;
3628
+ function isCollectionStateCarrier(value) {
3629
+ if (typeof value !== "object" || value === null) return false;
3630
+ return isCollectionState(value.state);
3784
3631
  }
3785
- function buildChildJoinWhere(relation, childValues) {
3786
- const exprs = [];
3787
- for (const [childColumn, parentValue] of childValues.entries()) exprs.push(BinaryExpr.eq(ColumnRef.of(relation.relatedTableName, childColumn), LiteralExpr.of(parentValue)));
3788
- const first = exprs[0];
3789
- if (exprs.length === 1 && first !== void 0) return first;
3790
- return and(...exprs);
3632
+ function isCollectionState(value) {
3633
+ if (typeof value !== "object" || value === null) return false;
3634
+ const candidate = value;
3635
+ return Array.isArray(candidate.filters) && Array.isArray(candidate.includes);
3791
3636
  }
3792
- async function insertSingleRow(scope, context, namespaceId, modelName, data) {
3793
- const contract = context.contract;
3794
- const tableName = resolveModelTableName(contract, namespaceId, modelName);
3795
- const mappedData = mapModelDataToStorageRow(contract, namespaceId, modelName, data);
3796
- const applied = context.applyMutationDefaults({
3797
- op: "create",
3798
- entry: tableName,
3799
- namespace: namespaceId,
3800
- values: mappedData
3801
- });
3802
- for (const def of applied) mappedData[def.field] = def.value;
3803
- const firstRow = (await queryPlanRows(scope, compileInsertReturning(contract, namespaceId, tableName, [mappedData], void 0)).toArray())[0];
3804
- if (!firstRow) throw ormError("ORM.MUTATION_ROW_MISSING", `Nested create for model "${modelName}" did not return a row`, { meta: {
3805
- operation: "create",
3806
- model: modelName,
3807
- phase: "nested"
3808
- } });
3809
- return mapStorageRowToModelFields(contract, namespaceId, modelName, firstRow);
3637
+ function createRelationMutator() {
3638
+ return {
3639
+ create(data) {
3640
+ return {
3641
+ kind: "create",
3642
+ data: Array.isArray(data) ? [...data] : [data]
3643
+ };
3644
+ },
3645
+ connect(criteria) {
3646
+ return {
3647
+ kind: "connect",
3648
+ criteria: Array.isArray(criteria) ? [...criteria] : [criteria]
3649
+ };
3650
+ },
3651
+ disconnect(criteria) {
3652
+ if (!criteria) return { kind: "disconnect" };
3653
+ return {
3654
+ kind: "disconnect",
3655
+ criteria: [...criteria]
3656
+ };
3657
+ }
3658
+ };
3810
3659
  }
3811
- async function findRowByCriterion(scope, context, namespaceId, modelName, criterion) {
3812
- const contract = context.contract;
3813
- const whereExpr = shorthandToWhereExpr(context, namespaceId, modelName, castAs(criterion));
3814
- if (!whereExpr) throw ormError("ORM.RELATION_MUTATION_INVALID", `Nested connect for model "${modelName}" requires non-empty criterion`, { meta: {
3815
- kind: "connect",
3816
- model: modelName,
3817
- problem: "empty-criterion"
3818
- } });
3819
- const firstRow = (await queryPlanRows(scope, compileSelect(contract, namespaceId, resolveModelTableName(contract, namespaceId, modelName), {
3820
- ...emptyState(),
3821
- filters: [whereExpr],
3822
- limit: 1
3823
- })).toArray())[0];
3824
- if (!firstRow) return null;
3825
- return mapStorageRowToModelFields(contract, namespaceId, modelName, firstRow);
3660
+ function isRelationMutationDescriptor(value) {
3661
+ if (!value || typeof value !== "object") return false;
3662
+ const candidate = value;
3663
+ if (candidate.kind !== "create" && candidate.kind !== "connect" && candidate.kind !== "disconnect") return false;
3664
+ return true;
3826
3665
  }
3827
- async function findFirstByFilters(scope, contract, namespaceId, modelName, filters) {
3828
- const firstRow = (await queryPlanRows(scope, compileSelect(contract, namespaceId, resolveModelTableName(contract, namespaceId, modelName), {
3829
- ...emptyState(),
3830
- filters,
3831
- limit: 1
3832
- })).toArray())[0];
3833
- if (!firstRow) return null;
3834
- return mapStorageRowToModelFields(contract, namespaceId, modelName, firstRow);
3666
+ function isRelationMutationCallback(value) {
3667
+ return typeof value === "function";
3835
3668
  }
3836
- async function executeUpdateCount(scope, contract, namespaceId, tableName, setValues, filters) {
3837
- const compiled = compileUpdateCount(contract, namespaceId, tableName, setValues, filters);
3838
- await scope.execute(compiled);
3669
+ function hasThrough(relation) {
3670
+ return relation.through !== void 0;
3839
3671
  }
3840
- const relationDefsCache = /* @__PURE__ */ new WeakMap();
3841
- function getRelationDefinitions(contract, namespaceId, modelName) {
3842
- let perContract = relationDefsCache.get(contract);
3843
- if (!perContract) {
3844
- perContract = /* @__PURE__ */ new Map();
3845
- relationDefsCache.set(contract, perContract);
3672
+ function hasNestedMutationCallbacks(contract, namespaceId, modelName, data) {
3673
+ const relationNames = new Set(Object.keys(resolveModelRelations(contract, namespaceId, modelName)));
3674
+ for (const [fieldName, value] of Object.entries(data)) {
3675
+ if (!relationNames.has(fieldName)) continue;
3676
+ if (isRelationMutationCallback(value)) return true;
3846
3677
  }
3847
- const cacheKey = JSON.stringify([namespaceId, modelName]);
3848
- const cached = perContract.get(cacheKey);
3849
- if (cached) return cached;
3850
- const relations = resolveModelRelations(contract, namespaceId, modelName);
3851
- const definitions = Object.entries(relations).map(([relationName, relation]) => ({
3852
- relationName,
3853
- relatedModelName: relation.to,
3854
- relatedNamespaceId: relation.toNamespace,
3855
- relatedTableName: resolveModelTableName(contract, relation.toNamespace, relation.to),
3856
- cardinality: relation.cardinality,
3857
- localColumns: relation.on.localFields.map((f) => resolveFieldToColumn(contract, namespaceId, modelName, f)),
3858
- targetColumns: relation.on.targetFields.map((f) => resolveFieldToColumn(contract, relation.toNamespace, relation.to, f)),
3859
- through: relation.through ? {
3860
- table: relation.through.table,
3861
- namespaceId: relation.through.namespaceId,
3862
- parentColumns: relation.through.parentColumns,
3863
- childColumns: relation.through.childColumns,
3864
- targetColumns: relation.through.targetColumns,
3865
- requiredPayloadColumns: relation.through.requiredPayloadColumns
3866
- } : void 0
3867
- }));
3868
- perContract.set(cacheKey, definitions);
3869
- return definitions;
3870
- }
3871
- function toFieldName(contract, namespaceId, modelName, columnName) {
3872
- return getColumnToFieldMap(contract, namespaceId, modelName)[columnName] ?? columnName;
3678
+ return false;
3873
3679
  }
3874
- function nullability(nullable) {
3875
- return nullable ? "may be null" : "is never null";
3680
+ async function executeNestedCreateMutation(options) {
3681
+ return withMutationScope(options.runtime, async (scope) => createGraph(scope, options.context, options.namespaceId, options.modelName, options.data));
3876
3682
  }
3877
- function isFieldBuilder(value) {
3878
- return "build" in value && typeof value.build === "function";
3683
+ async function executeNestedUpdateMutation(options) {
3684
+ return withMutationScope(options.runtime, async (scope) => updateFirstGraph(scope, options.context, options.namespaceId, options.modelName, options.filters, options.data));
3879
3685
  }
3880
- function isListMultiplicity(value) {
3881
- return typeof value === "object" && value !== null && "elementNullable" in value && typeof value.elementNullable === "boolean";
3686
+ function buildRowIdentityFilterFromRow(contract, namespaceId, modelName, row) {
3687
+ const tableName = resolveModelTableName(contract, namespaceId, modelName);
3688
+ const identityColumns = resolveRowIdentityColumns(contract, namespaceId, tableName);
3689
+ if (identityColumns.length === 0) throw ormError("ORM.ROW_IDENTITY_MISSING", `Nested mutations on model "${modelName}" require table "${tableName}" to have a primary key or unique constraint`, { meta: {
3690
+ model: modelName,
3691
+ table: tableName
3692
+ } });
3693
+ const filter = {};
3694
+ for (const column of identityColumns) {
3695
+ const fieldName = toFieldName(contract, namespaceId, modelName, column);
3696
+ const value = row[fieldName];
3697
+ if (value === void 0) throw new InternalError(`Missing identity field "${fieldName}" while reloading model "${modelName}"`);
3698
+ filter[fieldName] = value;
3699
+ }
3700
+ return filter;
3882
3701
  }
3883
- function isMultiplicity(value) {
3884
- return value === void 0 || value === false || isListMultiplicity(value);
3702
+ async function withMutationScope(runtime, run) {
3703
+ if (typeof runtime.transaction === "function") return runInTransaction(await runtime.transaction(), run);
3704
+ if (typeof runtime.connection === "function") {
3705
+ const connection = await runtime.connection();
3706
+ try {
3707
+ if (typeof connection.transaction === "function") return await runInTransaction(await connection.transaction(), run);
3708
+ return await run(connection);
3709
+ } finally {
3710
+ await connection.release?.();
3711
+ }
3712
+ }
3713
+ return run(runtime);
3885
3714
  }
3886
- function isFieldSpec(value) {
3887
- return "codecId" in value && typeof value.codecId === "string" && "nullable" in value && typeof value.nullable === "boolean" && (!("many" in value) || isMultiplicity(value.many));
3715
+ async function runInTransaction(transaction, run) {
3716
+ try {
3717
+ const result = await run(transaction);
3718
+ if (typeof transaction.commit === "function") await transaction.commit();
3719
+ return result;
3720
+ } catch (error) {
3721
+ if (typeof transaction.rollback === "function") await transaction.rollback();
3722
+ throw error;
3723
+ }
3888
3724
  }
3889
- function multiplicity(many) {
3890
- return isListMultiplicity(many) ? { elementNullable: many.elementNullable } : false;
3725
+ async function createGraph(scope, context, namespaceId, modelName, input) {
3726
+ const contract = context.contract;
3727
+ const parsed = parseMutationInput(contract, namespaceId, modelName, input);
3728
+ const { parentOwned, childOwned, junctionOwned } = partitionByOwnership(parsed.relationMutations);
3729
+ const scalarData = { ...parsed.scalarData };
3730
+ for (const relationMutation of parentOwned) {
3731
+ if (relationMutation.mutation.kind === "disconnect") throw ormError("ORM.RELATION_MUTATION_UNSUPPORTED", "disconnect() is only supported in update() nested mutations", { meta: {
3732
+ kind: "disconnect",
3733
+ relation: relationMutation.relation.relationName
3734
+ } });
3735
+ await applyParentOwnedMutation(scope, context, namespaceId, modelName, scalarData, relationMutation.relation, relationMutation.mutation);
3736
+ }
3737
+ for (const relationMutation of junctionOwned) {
3738
+ if (relationMutation.mutation.kind === "disconnect") throw ormError("ORM.RELATION_MUTATION_UNSUPPORTED", "disconnect() is only supported in update() nested mutations", { meta: {
3739
+ kind: "disconnect",
3740
+ relation: relationMutation.relation.relationName
3741
+ } });
3742
+ await preflightJunctionOwnedCreateMutation(scope, context, relationMutation);
3743
+ }
3744
+ const parentRow = await insertSingleRow(scope, context, namespaceId, modelName, scalarData);
3745
+ for (const relationMutation of childOwned) {
3746
+ if (relationMutation.mutation.kind === "disconnect") throw ormError("ORM.RELATION_MUTATION_UNSUPPORTED", "disconnect() is only supported in update() nested mutations", { meta: {
3747
+ kind: "disconnect",
3748
+ relation: relationMutation.relation.relationName
3749
+ } });
3750
+ await applyChildOwnedMutation(scope, context, namespaceId, modelName, parentRow, relationMutation.relation, relationMutation.mutation);
3751
+ }
3752
+ for (const relationMutation of junctionOwned) await applyJunctionOwnedMutation(scope, context, namespaceId, modelName, parentRow, relationMutation.relation, relationMutation.mutation);
3753
+ return parentRow;
3891
3754
  }
3892
- const FIELD_DECLARATION_FIX = "Declare each field with a field builder, such as field.temporal.timestamptz().optional(), or with { codecId, nullable }. Declare a list with .many() or many: { elementNullable: false }, or, when its elements may be null, with .many({ elementsNullable: true }) or many: { elementNullable: true }.";
3893
- function declaredFieldSpec(name, declaration) {
3894
- if (typeof declaration === "object" && declaration !== null && isFieldBuilder(declaration)) {
3895
- const built = declaration.build();
3896
- const descriptor = typeof built === "object" && built !== null && "descriptor" in built ? built.descriptor : void 0;
3897
- const nullable = typeof built === "object" && built !== null && "nullable" in built ? built.nullable : void 0;
3898
- if (typeof descriptor !== "object" || descriptor === null || !("codecId" in descriptor) || typeof descriptor.codecId !== "string" || typeof nullable !== "boolean") throw ormError("ORM.ARGUMENT_INVALID", `Cannot define the scope: the field builder for ${name} names no column type`, {
3899
- why: "A scope for any model matches each declared field by its codec and nullability, and this builder refers to a named type instead of a column type.",
3900
- fix: "Declare the field with a builder that has a column type, such as field.text() or field.column(textColumn), or with { codecId, nullable }.",
3901
- meta: { field: name }
3755
+ async function updateFirstGraph(scope, context, namespaceId, modelName, filters, input) {
3756
+ const contract = context.contract;
3757
+ const existingRow = await findFirstByFilters(scope, contract, namespaceId, modelName, filters);
3758
+ if (!existingRow) return null;
3759
+ const parsed = parseMutationInput(contract, namespaceId, modelName, input);
3760
+ const { parentOwned, childOwned, junctionOwned } = partitionByOwnership(parsed.relationMutations);
3761
+ const scalarData = { ...parsed.scalarData };
3762
+ for (const relationMutation of parentOwned) await applyParentOwnedMutation(scope, context, namespaceId, modelName, scalarData, relationMutation.relation, relationMutation.mutation);
3763
+ for (const relationMutation of junctionOwned) await preflightJunctionOwnedCreateMutation(scope, context, relationMutation);
3764
+ let parentRow = existingRow;
3765
+ const mappedUpdateData = mapModelDataToStorageRow(contract, namespaceId, modelName, scalarData);
3766
+ if (Object.keys(mappedUpdateData).length > 0) {
3767
+ const tableName = resolveModelTableName(contract, namespaceId, modelName);
3768
+ const appliedUpdateDefaults = context.applyMutationDefaults({
3769
+ op: "update",
3770
+ entry: tableName,
3771
+ namespace: namespaceId,
3772
+ values: mappedUpdateData
3902
3773
  });
3903
- const many = typeof built === "object" && built !== null && "many" in built && built.many;
3904
- if (!isMultiplicity(many)) throw ormError("ORM.ARGUMENT_INVALID", `Cannot define the scope: the field builder for ${name} builds a many that is not false or { elementNullable }`, {
3905
- why: `A field builder's build() returns many as false or undefined for one value, or as { elementNullable } with a boolean elementNullable for a list; received ${describeReceived(many)} for ${name}.`,
3906
- fix: FIELD_DECLARATION_FIX,
3907
- meta: { field: name }
3774
+ for (const def of appliedUpdateDefaults) mappedUpdateData[def.field] = def.value;
3775
+ const identityWhere = shorthandToWhereExpr(context, namespaceId, modelName, castAs(buildRowIdentityFilterFromRow(contract, namespaceId, modelName, existingRow)));
3776
+ if (!identityWhere) throw new InternalError(`Failed to build row identity filter for model "${modelName}"`);
3777
+ const updatedRaw = (await queryPlanRows(scope, compileUpdateReturning(contract, namespaceId, tableName, mappedUpdateData, [identityWhere], void 0)).toArray())[0];
3778
+ if (updatedRaw) parentRow = mapStorageRowToModelFields(contract, namespaceId, modelName, updatedRaw);
3779
+ }
3780
+ for (const relationMutation of childOwned) await applyChildOwnedMutation(scope, context, namespaceId, modelName, parentRow, relationMutation.relation, relationMutation.mutation);
3781
+ for (const relationMutation of junctionOwned) await applyJunctionOwnedMutation(scope, context, namespaceId, modelName, parentRow, relationMutation.relation, relationMutation.mutation);
3782
+ return parentRow;
3783
+ }
3784
+ function parseMutationInput(contract, namespaceId, modelName, input) {
3785
+ const scalarData = {};
3786
+ const relationDefinitions = new Map(getRelationDefinitions(contract, namespaceId, modelName).map((relation) => [relation.relationName, relation]));
3787
+ const relationMutations = [];
3788
+ for (const [fieldName, value] of Object.entries(input)) {
3789
+ const relation = relationDefinitions.get(fieldName);
3790
+ if (!relation) {
3791
+ scalarData[fieldName] = value;
3792
+ continue;
3793
+ }
3794
+ if (!isRelationMutationCallback(value)) throw ormError("ORM.RELATION_MUTATION_INVALID", `Relation field "${fieldName}" on model "${modelName}" expects a mutator callback`, { meta: {
3795
+ relation: fieldName,
3796
+ model: modelName,
3797
+ problem: "missing-callback"
3798
+ } });
3799
+ const mutation = value(createRelationMutator());
3800
+ if (!isRelationMutationDescriptor(mutation)) throw ormError("ORM.RELATION_MUTATION_INVALID", `Relation field "${fieldName}" on model "${modelName}" returned an invalid mutation descriptor`, { meta: {
3801
+ relation: fieldName,
3802
+ model: modelName,
3803
+ problem: "invalid-descriptor"
3804
+ } });
3805
+ relationMutations.push({
3806
+ relation,
3807
+ mutation
3908
3808
  });
3909
- return {
3910
- codecId: descriptor.codecId,
3911
- nullable,
3912
- many: multiplicity(many)
3913
- };
3914
3809
  }
3915
- if (typeof declaration === "object" && declaration !== null && isFieldSpec(declaration)) return {
3916
- codecId: declaration.codecId,
3917
- nullable: declaration.nullable,
3918
- many: multiplicity(declaration.many)
3810
+ return {
3811
+ scalarData,
3812
+ relationMutations
3813
+ };
3814
+ }
3815
+ function partitionByOwnership(relationMutations) {
3816
+ const parentOwned = [];
3817
+ const childOwned = [];
3818
+ const junctionOwned = [];
3819
+ for (const relationMutation of relationMutations) {
3820
+ if (hasThrough(relationMutation.relation)) {
3821
+ junctionOwned.push({
3822
+ relation: relationMutation.relation,
3823
+ mutation: relationMutation.mutation
3824
+ });
3825
+ continue;
3826
+ }
3827
+ if (relationMutation.relation.cardinality === "N:1") {
3828
+ parentOwned.push(relationMutation);
3829
+ continue;
3830
+ }
3831
+ childOwned.push(relationMutation);
3832
+ }
3833
+ return {
3834
+ parentOwned,
3835
+ childOwned,
3836
+ junctionOwned
3919
3837
  };
3920
- throw ormError("ORM.ARGUMENT_INVALID", `Cannot define the scope: the declaration of field ${name} is not a field builder or { codecId, nullable }`, {
3921
- why: `Each field of a scope is declared with a field builder or with an object that has a string codecId, a boolean nullable and, for a list, many: { elementNullable } with a boolean elementNullable; received ${describeReceived(declaration)} for ${name}.`,
3922
- fix: FIELD_DECLARATION_FIX,
3923
- meta: { field: name }
3924
- });
3925
- }
3926
- function declaredFieldSpecs(declarations) {
3927
- if (typeof declarations !== "object" || declarations === null || Array.isArray(declarations)) throw ormError("ORM.ARGUMENT_INVALID", "Cannot define the scope: the fields are not an object", {
3928
- why: `The first argument of scope maps the name of each field the scope needs to its declaration; received ${describeReceived(declarations)}.`,
3929
- fix: FIELD_DECLARATION_FIX,
3930
- meta: { argument: "fields" }
3931
- });
3932
- return Object.entries(declarations).map(([name, declaration]) => [name, declaredFieldSpec(name, declaration)]);
3933
- }
3934
- /** Throws `ORM.ARGUMENT_INVALID` unless `body`, the body of a scope, is a function. */
3935
- function assertScopeBody(body) {
3936
- if (typeof body !== "function") throw ormError("ORM.ARGUMENT_INVALID", "Cannot define the scope: the body is not a function", {
3937
- why: `The body of a scope is a function that receives a collection and returns one; received ${describeReceived(body)}.`,
3938
- fix: "Pass a function, such as (rows) => rows.where(...).",
3939
- meta: { argument: "body" }
3940
- });
3941
- }
3942
- function hasContract(ctx) {
3943
- return "context" in ctx && typeof ctx.context === "object" && ctx.context !== null && "contract" in ctx.context && typeof ctx.context.contract === "object" && ctx.context.contract !== null;
3944
- }
3945
- function isModelCollection(value) {
3946
- return typeof value === "object" && value !== null && "ctx" in value && typeof value.ctx === "object" && value.ctx !== null && hasContract(value.ctx) && "modelName" in value && typeof value.modelName === "string" && "namespaceId" in value && typeof value.namespaceId === "string";
3947
3838
  }
3948
- /** Throws `ORM.ARGUMENT_INVALID` unless `value`, what a scope is applied to, is a collection. */
3949
- function assertScopeReceiver(value) {
3950
- if (!isModelCollection(value)) throw ormError("ORM.ARGUMENT_INVALID", "Cannot apply the scope: it was not given a collection", {
3951
- why: `A scope is applied to a collection, such as db.orm.public.Post; received ${describeReceived(value)}.`,
3952
- fix: "Pass the scope to with on a collection: collection.with(scope).",
3953
- meta: { argument: "collection" }
3954
- });
3955
- }
3956
- /** Throws `ORM.ARGUMENT_INVALID` unless `value` is a collection of the model and namespace a scope for one model was made from. */
3957
- function assertModelScopeReceiver(source, value) {
3958
- assertScopeReceiver(value);
3959
- if (value.modelName !== source.modelName || value.namespaceId !== source.namespaceId) {
3960
- const made = `${source.namespaceId}.${source.modelName}`;
3961
- const given = `${value.namespaceId}.${value.modelName}`;
3962
- throw ormError("ORM.ARGUMENT_INVALID", `Cannot apply a scope for ${made} to a collection of ${given}`, {
3963
- why: `The scope was made with ${made}.scope(...), and its body was written for that model.`,
3964
- fix: `Apply the scope to a collection of ${made}, or make a scope from ${given}.`,
3965
- meta: {
3966
- model: source.modelName,
3967
- namespace: source.namespaceId,
3968
- receivedModel: value.modelName,
3969
- receivedNamespace: value.namespaceId
3970
- }
3971
- });
3839
+ async function applyParentOwnedMutation(scope, context, parentNamespaceId, parentModelName, scalarData, relation, mutation) {
3840
+ const contract = context.contract;
3841
+ if (mutation.kind === "disconnect") {
3842
+ for (const localColumn of relation.localColumns) {
3843
+ const parentFieldName = toFieldName(contract, parentNamespaceId, parentModelName, localColumn);
3844
+ scalarData[parentFieldName] = null;
3845
+ }
3846
+ return;
3847
+ }
3848
+ if (mutation.kind === "create") {
3849
+ const row = mutation.data[0];
3850
+ if (!row) throw ormError("ORM.RELATION_MUTATION_INVALID", `create() nested mutation for relation "${relation.relationName}" requires data`, { meta: {
3851
+ kind: "create",
3852
+ relation: relation.relationName,
3853
+ problem: "missing-data"
3854
+ } });
3855
+ copyRelatedValuesToParent(contract, parentNamespaceId, parentModelName, relation, scalarData, await createGraph(scope, context, relation.relatedNamespaceId, relation.relatedModelName, castAs(row)));
3856
+ return;
3972
3857
  }
3858
+ const criterion = mutation.criteria[0];
3859
+ if (!criterion) throw ormError("ORM.RELATION_MUTATION_INVALID", `connect() nested mutation for relation "${relation.relationName}" requires criterion`, { meta: {
3860
+ kind: "connect",
3861
+ relation: relation.relationName,
3862
+ problem: "missing-criterion"
3863
+ } });
3864
+ const relatedRow = await findRowByCriterion(scope, context, relation.relatedNamespaceId, relation.relatedModelName, castAs(criterion));
3865
+ if (!relatedRow) throw ormError("ORM.RELATION_ROW_MISSING", `connect() nested mutation for relation "${relation.relationName}" did not find a matching row`, { meta: {
3866
+ kind: "connect",
3867
+ relation: relation.relationName
3868
+ } });
3869
+ copyRelatedValuesToParent(contract, parentNamespaceId, parentModelName, relation, scalarData, relatedRow);
3973
3870
  }
3974
- /** The model as error messages name it: with its namespace when the contract has more than one. */
3975
- function modelLabel(collection) {
3976
- return Object.keys(collection.ctx.context.contract.domain.namespaces).length > 1 ? `${collection.namespaceId}.${collection.modelName}` : collection.modelName;
3871
+ function copyRelatedValuesToParent(contract, parentNamespaceId, parentModelName, relation, scalarData, relatedRow) {
3872
+ for (let i = 0; i < relation.localColumns.length; i++) {
3873
+ const localColumn = relation.localColumns[i];
3874
+ const targetColumn = relation.targetColumns[i];
3875
+ if (!localColumn || !targetColumn) continue;
3876
+ const parentFieldName = toFieldName(contract, parentNamespaceId, parentModelName, localColumn);
3877
+ scalarData[parentFieldName] = relatedRow[toFieldName(contract, relation.relatedNamespaceId, relation.relatedModelName, targetColumn)];
3878
+ }
3977
3879
  }
3978
- /** Throws `ORM.ARGUMENT_INVALID` unless `result`, what a scope's body returned, is a collection of the receiver's model, namespace and class. */
3979
- function assertScopeResult(receiver, result) {
3980
- if (!(isModelCollection(result) && result.modelName === receiver.modelName && result.namespaceId === receiver.namespaceId && result instanceof receiver.constructor)) {
3981
- const label = modelLabel(receiver);
3982
- throw ormError("ORM.ARGUMENT_INVALID", `Cannot apply the scope to ${label}: its body did not return a collection of ${label}`, {
3983
- why: `The body of a scope for any model returns the collection it received, after where, orderBy, limit or offset; it returned ${isModelCollection(result) ? `a collection of ${modelLabel(result)}` : describeReceived(result)}.`,
3984
- fix: "Return the collection the body receives, or one made from it by calling its methods.",
3985
- meta: {
3986
- model: receiver.modelName,
3987
- namespace: receiver.namespaceId,
3988
- returned: isModelCollection(result) ? result.modelName : describeReceived(result)
3880
+ async function applyChildOwnedMutation(scope, context, parentNamespaceId, parentModelName, parentRow, relation, mutation) {
3881
+ const contract = context.contract;
3882
+ const parentValues = readParentColumnValues(contract, parentNamespaceId, parentModelName, relation, parentRow);
3883
+ if (mutation.kind === "create") {
3884
+ for (const childInput of mutation.data) {
3885
+ const payload = { ...castAs(childInput) };
3886
+ for (const [childColumn, parentValue] of parentValues.entries()) {
3887
+ const childFieldName = toFieldName(contract, relation.relatedNamespaceId, relation.relatedModelName, childColumn);
3888
+ payload[childFieldName] = parentValue;
3989
3889
  }
3990
- });
3890
+ await createGraph(scope, context, relation.relatedNamespaceId, relation.relatedModelName, castAs(payload));
3891
+ }
3892
+ return;
3893
+ }
3894
+ if (mutation.kind === "connect") {
3895
+ for (const criterion of mutation.criteria) {
3896
+ const criterionWhere = shorthandToWhereExpr(context, relation.relatedNamespaceId, relation.relatedModelName, castAs(criterion));
3897
+ if (!criterionWhere) throw ormError("ORM.RELATION_MUTATION_INVALID", `connect() nested mutation for relation "${relation.relationName}" requires non-empty criterion`, { meta: {
3898
+ kind: "connect",
3899
+ relation: relation.relationName,
3900
+ problem: "empty-criterion"
3901
+ } });
3902
+ const setValues = {};
3903
+ for (const [childColumn, parentValue] of parentValues.entries()) setValues[childColumn] = parentValue;
3904
+ await executeUpdateCount(scope, contract, relation.relatedNamespaceId, relation.relatedTableName, setValues, [criterionWhere]);
3905
+ }
3906
+ return;
3907
+ }
3908
+ const setValues = {};
3909
+ for (const childColumn of parentValues.keys()) setValues[childColumn] = null;
3910
+ if (!mutation.criteria || mutation.criteria.length === 0) {
3911
+ const parentJoinWhere = buildChildJoinWhere(relation, parentValues);
3912
+ await executeUpdateCount(scope, contract, relation.relatedNamespaceId, relation.relatedTableName, setValues, [parentJoinWhere]);
3913
+ return;
3914
+ }
3915
+ for (const criterion of mutation.criteria) {
3916
+ const criterionWhere = shorthandToWhereExpr(context, relation.relatedNamespaceId, relation.relatedModelName, castAs(criterion));
3917
+ if (!criterionWhere) throw ormError("ORM.RELATION_MUTATION_INVALID", `disconnect() nested mutation for relation "${relation.relationName}" requires non-empty criterion`, { meta: {
3918
+ kind: "disconnect",
3919
+ relation: relation.relationName,
3920
+ problem: "empty-criterion"
3921
+ } });
3922
+ const parentJoinWhere = buildChildJoinWhere(relation, parentValues);
3923
+ await executeUpdateCount(scope, contract, relation.relatedNamespaceId, relation.relatedTableName, setValues, [and(parentJoinWhere, criterionWhere)]);
3991
3924
  }
3992
3925
  }
3993
- function elementNullability(elementNullable) {
3994
- return elementNullable ? "may be null" : "are never null";
3995
- }
3996
- function sameMultiplicity(a, b) {
3997
- return a === false || b === false ? a === b : a.elementNullable === b.elementNullable;
3998
- }
3999
- function fieldShape(many) {
4000
- return many === false ? "holds one value" : `is a list whose elements ${elementNullability(many.elementNullable)}`;
4001
- }
4002
- function declarationFor(many) {
4003
- if (many === false) return "as one value, without .many() or many";
4004
- return many.elementNullable ? "as a list whose elements may be null, with .many({ elementsNullable: true }) or many: { elementNullable: true }" : "as a list whose elements are never null, with .many() or many: { elementNullable: false }";
4005
- }
4006
- function describeField(field) {
4007
- const value = `with codec ${field.codecId} that ${nullability(field.nullable)}`;
4008
- return field.many === false ? value : `${value} and whose elements ${elementNullability(field.many.elementNullable)}`;
3926
+ async function applyJunctionOwnedMutation(scope, context, parentNamespaceId, parentModelName, parentRow, relation, mutation) {
3927
+ const contract = context.contract;
3928
+ const parentPkValues = readJunctionParentValues(contract, parentNamespaceId, parentModelName, relation, parentRow);
3929
+ assertJunctionPayloadWritable(relation, mutation.kind);
3930
+ if (mutation.kind === "create") {
3931
+ for (const childInput of mutation.data) await insertJunctionLink(scope, context, relation, parentPkValues, readJunctionTargetValues(contract, relation, await createGraph(scope, context, relation.relatedNamespaceId, relation.relatedModelName, castAs(childInput))), "create");
3932
+ return;
3933
+ }
3934
+ if (mutation.kind === "connect") {
3935
+ for (const criterion of mutation.criteria) await insertJunctionLink(scope, context, relation, parentPkValues, await resolveJunctionTargetValues(scope, context, relation, "connect", criterion), "connect");
3936
+ return;
3937
+ }
3938
+ if (!mutation.criteria || mutation.criteria.length === 0) throw ormError("ORM.RELATION_MUTATION_INVALID", `disconnect() nested mutation for relation "${relation.relationName}" requires criterion`, { meta: {
3939
+ kind: "disconnect",
3940
+ relation: relation.relationName,
3941
+ problem: "missing-criterion"
3942
+ } });
3943
+ for (const criterion of mutation.criteria) await deleteJunctionLink(scope, context, relation, parentPkValues, await resolveJunctionTargetValues(scope, context, relation, "disconnect", criterion));
4009
3944
  }
4010
- function assertScopeFields(collection, fields) {
4011
- const { contract } = collection.ctx.context;
4012
- const { modelName, namespaceId } = collection;
4013
- const label = modelLabel(collection);
4014
- const model = modelOf(contract, namespaceId, modelName);
4015
- for (const [name, spec] of fields) {
4016
- const declared = `The scope was declared for models that have a ${spec.many === false ? "field" : "list field"} ${name} ${describeField(spec)}.`;
4017
- const meta = {
4018
- model: modelName,
4019
- namespace: namespaceId,
4020
- field: name,
4021
- codecId: spec.codecId,
4022
- nullable: spec.nullable,
4023
- many: spec.many
4024
- };
4025
- const modelFields = model?.fields ?? {};
4026
- if (!Object.hasOwn(modelFields, name)) throw ormError("ORM.FIELD_UNKNOWN", `Cannot apply a scope to ${label}: it has no field ${name}`, {
4027
- why: declared,
4028
- fix: `Apply the scope to a model that has a field ${name}, or remove ${name} from the declaration in the scope.`,
4029
- meta
4030
- });
4031
- const fieldMany = multiplicity(modelFields[name]?.many);
4032
- const column = resolveColumn(contract, namespaceId, collection.tableName, resolveFieldToColumn(contract, namespaceId, modelName, name));
4033
- const actual = column === void 0 ? `${label}.${name} has no column.` : fieldMany === false ? `${label}.${name} has codec ${column.codecId} and ${nullability(column.nullable)}.` : `${label}.${name} is a list ${describeField({
4034
- codecId: column.codecId,
4035
- nullable: column.nullable,
4036
- many: fieldMany
4037
- })}.`;
4038
- const sameCodecAndNullability = column?.codecId === spec.codecId && column.nullable === spec.nullable;
4039
- if (!sameCodecAndNullability || !sameMultiplicity(fieldMany, spec.many)) throw ormError("ORM.FIELD_UNKNOWN", `Cannot apply a scope to ${label}: its field ${name} does not match the declaration`, {
4040
- why: `${declared} ${actual}`,
4041
- fix: sameCodecAndNullability ? `Apply the scope to a model whose ${name} field ${fieldShape(spec.many)}, or declare ${name} ${declarationFor(fieldMany)}.` : `Apply the scope to a model whose ${name} field has that codec and nullability, or change the declaration in the scope.`,
4042
- meta
4043
- });
3945
+ async function preflightJunctionOwnedCreateMutation(scope, context, relationMutation) {
3946
+ const { relation, mutation } = relationMutation;
3947
+ assertJunctionMetadataShape(relation);
3948
+ assertJunctionPayloadWritable(relation, mutation.kind);
3949
+ if (mutation.kind !== "connect") return;
3950
+ const seenTargetKeys = /* @__PURE__ */ new Set();
3951
+ for (const criterion of mutation.criteria) {
3952
+ const targetValues = await resolveJunctionTargetValues(scope, context, relation, "connect", criterion);
3953
+ const targetKey = JSON.stringify([...targetValues.entries()]);
3954
+ if (seenTargetKeys.has(targetKey)) throw ormError("ORM.RELATION_MUTATION_INVALID", `connect() nested mutation for relation "${relation.relationName}" resolved duplicate junction link targets; remove the duplicate criteria`, { meta: {
3955
+ kind: "connect",
3956
+ relation: relation.relationName,
3957
+ problem: "duplicate-criteria"
3958
+ } });
3959
+ seenTargetKeys.add(targetKey);
4044
3960
  }
4045
3961
  }
4046
- /**
4047
- * Define a scope for any model that has the declared fields. Reached as the `scope` method of the client `orm()` returns.
4048
- */
4049
- function defineFieldScope(declarations, body) {
4050
- const fields = declaredFieldSpecs(declarations);
4051
- assertScopeBody(body);
4052
- return blindCast((collection) => {
4053
- const receiver = collection;
4054
- assertScopeReceiver(receiver);
4055
- assertScopeFields(receiver, fields);
4056
- const result = body(blindCast(collection));
4057
- assertScopeResult(receiver, result);
4058
- return result;
4059
- });
3962
+ function assertJunctionPayloadWritable(relation, mutationKind) {
3963
+ const through = relation.through;
3964
+ if (mutationKind !== "create" && mutationKind !== "connect" || through.requiredPayloadColumns.length === 0) return;
3965
+ const cols = through.requiredPayloadColumns.map((c) => `\`${c}\``).join(", ");
3966
+ throw ormError("ORM.RELATION_MUTATION_UNSUPPORTED", `Cannot \`${mutationKind}\` on relation \`${relation.relationName}\`: its junction \`${through.table}\` has required column(s) ${cols} the relation API can't populate. Write the \`${through.table}\` junction directly or use the SQL builder.`, { meta: {
3967
+ kind: mutationKind,
3968
+ relation: relation.relationName,
3969
+ reason: "junction-required-columns",
3970
+ junction: through.table
3971
+ } });
4060
3972
  }
4061
- const SHOWN_LENGTH = 64;
4062
- function shown(value) {
4063
- const characters = Array.from(value);
4064
- const cut = characters.length > SHOWN_LENGTH;
4065
- return {
4066
- text: JSON.stringify(cut ? `${characters.slice(0, SHOWN_LENGTH).join("")}…` : value),
4067
- cut
4068
- };
3973
+ async function resolveJunctionTargetValues(scope, context, relation, kind, criterion) {
3974
+ const relatedRow = await findRowByCriterion(scope, context, relation.relatedNamespaceId, relation.relatedModelName, criterion);
3975
+ if (!relatedRow) throw ormError("ORM.RELATION_ROW_MISSING", `${kind}() nested mutation for relation "${relation.relationName}" did not find a matching row`, { meta: {
3976
+ kind,
3977
+ relation: relation.relationName
3978
+ } });
3979
+ return readJunctionTargetValues(context.contract, relation, relatedRow);
3980
+ }
3981
+ function readJunctionParentValues(contract, parentNamespaceId, parentModelName, relation, parentRow) {
3982
+ const values = /* @__PURE__ */ new Map();
3983
+ assertJunctionParentMetadataLength(relation);
3984
+ for (let i = 0; i < relation.through.parentColumns.length; i++) {
3985
+ const junctionColumn = relation.through.parentColumns[i];
3986
+ const parentColumn = relation.localColumns[i];
3987
+ if (junctionColumn === void 0 || parentColumn === void 0) throw new InternalError(`Relation "${relation.relationName}" has incomplete junction metadata for parent columns`);
3988
+ const parentFieldName = toFieldName(contract, parentNamespaceId, parentModelName, parentColumn);
3989
+ const parentValue = parentRow[parentFieldName];
3990
+ if (parentValue === void 0) throw new InternalError(`Nested mutation requires parent field "${parentFieldName}" to be present in returned row`);
3991
+ values.set(junctionColumn, parentValue);
3992
+ }
3993
+ return values;
4069
3994
  }
4070
- function describeReceived(value) {
4071
- if (value === void 0 || value === null) return String(value);
4072
- if (Array.isArray(value)) return "an array";
4073
- const kind = typeof value;
4074
- return kind === "object" ? "an object" : `a ${kind}`;
3995
+ function readJunctionTargetValues(contract, relation, relatedRow) {
3996
+ const values = /* @__PURE__ */ new Map();
3997
+ assertJunctionTargetMetadataLength(relation);
3998
+ for (let i = 0; i < relation.through.childColumns.length; i++) {
3999
+ const junctionColumn = relation.through.childColumns[i];
4000
+ const targetColumn = relation.through.targetColumns[i];
4001
+ if (junctionColumn === void 0 || targetColumn === void 0) throw new InternalError(`Relation "${relation.relationName}" has incomplete junction metadata for target columns`);
4002
+ const targetFieldName = toFieldName(contract, relation.relatedNamespaceId, relation.relatedModelName, targetColumn);
4003
+ const targetValue = relatedRow[targetFieldName];
4004
+ if (targetValue === void 0) throw new InternalError(`Nested mutation requires target field "${targetFieldName}" to be present in returned row`);
4005
+ values.set(junctionColumn, targetValue);
4006
+ }
4007
+ return values;
4075
4008
  }
4076
- function cutNote(subject, value) {
4077
- return value.cut ? ` The ${subject} above is cut to its first ${SHOWN_LENGTH} characters.` : "";
4009
+ function assertJunctionMetadataLength(relation, throughColumnName, throughColumns, pairedColumnName, pairedColumns) {
4010
+ if (throughColumns.length === pairedColumns.length) return;
4011
+ throw new InternalError(`Relation "${relation.relationName}" has invalid junction metadata: ${throughColumnName} has ${throughColumns.length} column(s), but ${pairedColumnName} has ${pairedColumns.length}`);
4078
4012
  }
4079
- function fieldCodecId(collection, field) {
4080
- const { contract } = collection.ctx.context;
4081
- const column = resolveFieldToColumn(contract, collection.namespaceId, collection.modelName, field);
4082
- return resolveColumn(contract, collection.namespaceId, collection.tableName, column)?.codecId;
4013
+ function assertJunctionParentMetadataLength(relation) {
4014
+ assertJunctionMetadataLength(relation, "parentColumns", relation.through.parentColumns, "localColumns", relation.localColumns);
4083
4015
  }
4084
- function canOrder(collection, field) {
4085
- const codecId = fieldCodecId(collection, field);
4086
- return codecId !== void 0 && hasTrait(collection.ctx.context, codecId, "order");
4016
+ function assertJunctionTargetMetadataLength(relation) {
4017
+ assertJunctionMetadataLength(relation, "childColumns", relation.through.childColumns, "targetColumns", relation.through.targetColumns);
4087
4018
  }
4088
- function orderableFieldNames(collection) {
4089
- const model = modelOf(collection.ctx.context.contract, collection.namespaceId, collection.modelName);
4090
- return Object.keys(model?.fields ?? {}).filter((field) => canOrder(collection, field));
4019
+ function assertJunctionMetadataShape(relation) {
4020
+ assertJunctionParentMetadataLength(relation);
4021
+ assertJunctionTargetMetadataLength(relation);
4091
4022
  }
4092
- function whyNotOrderable(collection, name) {
4093
- const { modelName } = collection;
4094
- const model = modelOf(collection.ctx.context.contract, collection.namespaceId, modelName);
4095
- const quoted = shown(name).text;
4096
- if (Object.hasOwn(model?.relations ?? {}, name)) return `${quoted} is a relation of ${modelName}, not a field.`;
4097
- if (!Object.hasOwn(model?.fields ?? {}, name)) return `${modelName} has no field ${quoted}.`;
4098
- return canOrder(collection, name) ? `${quoted} is not one of the fields allowed for ordering.` : `The codec ${fieldCodecId(collection, name)} of ${modelName}.${name} cannot be ordered.`;
4023
+ function writeJunctionColumn(junctionRow, through, column, value, relationName) {
4024
+ if (Object.hasOwn(junctionRow, column) && !Object.is(junctionRow[column], value)) throw ormError("ORM.RELATION_MUTATION_INVALID", `Cannot write junction "${through.table}": conflicting values for junction column "${column}"`, { meta: {
4025
+ relation: relationName,
4026
+ junction: through.table,
4027
+ column
4028
+ } });
4029
+ junctionRow[column] = value;
4099
4030
  }
4100
- /**
4101
- * An `orderBy` selector for a field named by a string, such as an order parameter of a request. `allowed` lists the fields a request may order by. A name that is not in `allowed` or not an orderable field of the collection's model, and a direction other than `asc` or `desc`, throw `ORM.ARGUMENT_INVALID`. An undefined direction is `asc`.
4102
- *
4103
- * ```ts
4104
- * db.Post.orderBy(orderByField(db.Post, input.orderBy, input.direction, ['title', 'createdAt']));
4105
- * ```
4106
- */
4107
- function orderByField(collection, name, direction, allowed) {
4108
- const receiver = collection;
4109
- if (!isModelCollection(receiver)) throw ormError("ORM.ARGUMENT_INVALID", "Cannot order: orderByField was not given a collection", {
4110
- why: `orderByField takes the collection it orders, such as db.orm.public.Post; received ${describeReceived(receiver)}.`,
4111
- fix: "Pass the collection the selector is for as the first argument.",
4112
- meta: { argument: "collection" }
4113
- });
4114
- const { modelName } = collection;
4115
- const requestedName = name;
4116
- const requestedDirection = direction === void 0 ? "asc" : direction;
4117
- const allowedList = allowed;
4118
- if (!Array.isArray(allowedList) || allowedList.some((field) => typeof field !== "string")) throw ormError("ORM.ARGUMENT_INVALID", `Cannot order ${modelName}: the allowed fields are not a list of names`, {
4119
- why: `orderByField takes the list of fields a request may order by; received ${describeReceived(allowedList)}.`,
4120
- fix: `Pass an array of field names of ${modelName}, such as ['title', 'createdAt'].`,
4121
- meta: {
4122
- model: modelName,
4123
- argument: "allowed"
4124
- }
4125
- });
4126
- if (typeof requestedDirection !== "string") throw ormError("ORM.ARGUMENT_INVALID", `Cannot order ${modelName}: the direction is not a string`, {
4127
- why: `An order direction is "asc" or "desc"; received ${describeReceived(requestedDirection)}.`,
4128
- fix: "Pass \"asc\" or \"desc\".",
4129
- meta: {
4130
- model: modelName,
4131
- direction: requestedDirection
4132
- }
4031
+ async function insertJunctionLink(scope, context, relation, parentPkValues, targetPkValues, mutationKind) {
4032
+ const through = relation.through;
4033
+ const junctionRow = {};
4034
+ for (const [column, value] of parentPkValues.entries()) writeJunctionColumn(junctionRow, through, column, value, relation.relationName);
4035
+ for (const [column, value] of targetPkValues.entries()) writeJunctionColumn(junctionRow, through, column, value, relation.relationName);
4036
+ const applied = context.applyMutationDefaults({
4037
+ op: "create",
4038
+ entry: through.table,
4039
+ namespace: through.namespaceId,
4040
+ values: junctionRow
4133
4041
  });
4134
- if (!isOrderByDirection(requestedDirection)) {
4135
- const shownDirection = shown(requestedDirection);
4136
- throw ormError("ORM.ARGUMENT_INVALID", `Cannot order ${modelName} in direction ${shownDirection.text}`, {
4137
- why: "An order direction is \"asc\" or \"desc\".",
4138
- fix: `Pass "asc" or "desc".${cutNote("direction", shownDirection)}`,
4042
+ for (const def of applied) junctionRow[def.field] = def.value;
4043
+ const compiled = compileInsertCount(context.contract, through.namespaceId, through.table, [junctionRow]);
4044
+ try {
4045
+ await scope.execute(compiled);
4046
+ } catch (error) {
4047
+ if (mutationKind === "connect" && isUniqueConstraintViolation(error)) throw ormError("ORM.RELATION_LINK_DUPLICATE", `connect() nested mutation for relation "${relation.relationName}" violated a unique constraint on junction "${through.table}"; the junction link may already be present`, {
4139
4048
  meta: {
4140
- model: modelName,
4141
- direction: requestedDirection
4142
- }
4049
+ relation: relation.relationName,
4050
+ junction: through.table
4051
+ },
4052
+ cause: error
4143
4053
  });
4054
+ throw error;
4144
4055
  }
4145
- const orderable = orderableFieldNames(collection);
4146
- const names = allowedList.filter((field) => orderable.includes(field));
4147
- const orderByOneOf = names.length === 0 ? `Pass an allowed list that names at least one field of ${modelName} that can be ordered.` : `Order by one of: ${names.join(", ")}.`;
4148
- if (typeof requestedName !== "string") throw ormError("ORM.ARGUMENT_INVALID", `Cannot order ${modelName}: the field name is not a string`, {
4149
- why: `An order field name is a string; received ${describeReceived(requestedName)}.`,
4150
- fix: orderByOneOf,
4151
- meta: {
4152
- model: modelName,
4153
- field: requestedName
4154
- }
4056
+ }
4057
+ async function deleteJunctionLink(scope, context, relation, parentPkValues, targetPkValues) {
4058
+ const through = relation.through;
4059
+ const junctionRow = {};
4060
+ for (const [column, value] of parentPkValues.entries()) writeJunctionColumn(junctionRow, through, column, value, relation.relationName);
4061
+ for (const [column, value] of targetPkValues.entries()) writeJunctionColumn(junctionRow, through, column, value, relation.relationName);
4062
+ const exprs = [];
4063
+ for (const [column, value] of Object.entries(junctionRow)) exprs.push(BinaryExpr.eq(ColumnRef.of(through.table, column), LiteralExpr.of(value)));
4064
+ const first = exprs[0];
4065
+ const where = exprs.length === 1 && first !== void 0 ? first : and(...exprs);
4066
+ const compiled = compileDeleteCount(context.contract, through.namespaceId, through.table, [where]);
4067
+ await scope.execute(compiled);
4068
+ }
4069
+ function readParentColumnValues(contract, parentNamespaceId, parentModelName, relation, parentRow) {
4070
+ const values = /* @__PURE__ */ new Map();
4071
+ for (let i = 0; i < relation.localColumns.length; i++) {
4072
+ const localColumn = relation.localColumns[i];
4073
+ const targetColumn = relation.targetColumns[i];
4074
+ if (!localColumn || !targetColumn) continue;
4075
+ const parentFieldName = toFieldName(contract, parentNamespaceId, parentModelName, localColumn);
4076
+ const parentValue = parentRow[parentFieldName];
4077
+ if (parentValue === void 0) throw new InternalError(`Nested mutation requires parent field "${parentFieldName}" to be present in returned row`);
4078
+ values.set(targetColumn, parentValue);
4079
+ }
4080
+ return values;
4081
+ }
4082
+ function buildChildJoinWhere(relation, childValues) {
4083
+ const exprs = [];
4084
+ for (const [childColumn, parentValue] of childValues.entries()) exprs.push(BinaryExpr.eq(ColumnRef.of(relation.relatedTableName, childColumn), LiteralExpr.of(parentValue)));
4085
+ const first = exprs[0];
4086
+ if (exprs.length === 1 && first !== void 0) return first;
4087
+ return and(...exprs);
4088
+ }
4089
+ async function insertSingleRow(scope, context, namespaceId, modelName, data) {
4090
+ const contract = context.contract;
4091
+ const tableName = resolveModelTableName(contract, namespaceId, modelName);
4092
+ const mappedData = mapModelDataToStorageRow(contract, namespaceId, modelName, data);
4093
+ const applied = context.applyMutationDefaults({
4094
+ op: "create",
4095
+ entry: tableName,
4096
+ namespace: namespaceId,
4097
+ values: mappedData
4155
4098
  });
4156
- if (!names.includes(requestedName)) {
4157
- const shownName = shown(requestedName);
4158
- throw ormError("ORM.ARGUMENT_INVALID", `Cannot order ${modelName} by ${shownName.text}`, {
4159
- why: whyNotOrderable(collection, requestedName),
4160
- fix: `${orderByOneOf}${cutNote("name", shownName)}`,
4161
- meta: {
4162
- model: modelName,
4163
- field: requestedName
4164
- }
4165
- });
4099
+ for (const def of applied) mappedData[def.field] = def.value;
4100
+ const firstRow = (await queryPlanRows(scope, compileInsertReturning(contract, namespaceId, tableName, [mappedData], void 0)).toArray())[0];
4101
+ if (!firstRow) throw ormError("ORM.MUTATION_ROW_MISSING", `Nested create for model "${modelName}" did not return a row`, { meta: {
4102
+ operation: "create",
4103
+ model: modelName,
4104
+ phase: "nested"
4105
+ } });
4106
+ return mapStorageRowToModelFields(contract, namespaceId, modelName, firstRow);
4107
+ }
4108
+ async function findRowByCriterion(scope, context, namespaceId, modelName, criterion) {
4109
+ const contract = context.contract;
4110
+ const whereExpr = shorthandToWhereExpr(context, namespaceId, modelName, castAs(criterion));
4111
+ if (!whereExpr) throw ormError("ORM.RELATION_MUTATION_INVALID", `Nested connect for model "${modelName}" requires non-empty criterion`, { meta: {
4112
+ kind: "connect",
4113
+ model: modelName,
4114
+ problem: "empty-criterion"
4115
+ } });
4116
+ const firstRow = (await queryPlanRows(scope, compileSelect(contract, namespaceId, resolveModelTableName(contract, namespaceId, modelName), {
4117
+ ...emptyState(),
4118
+ filters: [whereExpr],
4119
+ limit: 1
4120
+ })).toArray())[0];
4121
+ if (!firstRow) return null;
4122
+ return mapStorageRowToModelFields(contract, namespaceId, modelName, firstRow);
4123
+ }
4124
+ async function findFirstByFilters(scope, contract, namespaceId, modelName, filters) {
4125
+ const firstRow = (await queryPlanRows(scope, compileSelect(contract, namespaceId, resolveModelTableName(contract, namespaceId, modelName), {
4126
+ ...emptyState(),
4127
+ filters,
4128
+ limit: 1
4129
+ })).toArray())[0];
4130
+ if (!firstRow) return null;
4131
+ return mapStorageRowToModelFields(contract, namespaceId, modelName, firstRow);
4132
+ }
4133
+ async function executeUpdateCount(scope, contract, namespaceId, tableName, setValues, filters) {
4134
+ const compiled = compileUpdateCount(contract, namespaceId, tableName, setValues, filters);
4135
+ await scope.execute(compiled);
4136
+ }
4137
+ const relationDefsCache = /* @__PURE__ */ new WeakMap();
4138
+ function getRelationDefinitions(contract, namespaceId, modelName) {
4139
+ let perContract = relationDefsCache.get(contract);
4140
+ if (!perContract) {
4141
+ perContract = /* @__PURE__ */ new Map();
4142
+ relationDefsCache.set(contract, perContract);
4166
4143
  }
4167
- const field = blindCast(requestedName);
4168
- const order = requestedDirection;
4169
- return (row) => row[field][order]();
4144
+ const cacheKey = JSON.stringify([namespaceId, modelName]);
4145
+ const cached = perContract.get(cacheKey);
4146
+ if (cached) return cached;
4147
+ const relations = resolveModelRelations(contract, namespaceId, modelName);
4148
+ const definitions = Object.entries(relations).map(([relationName, relation]) => ({
4149
+ relationName,
4150
+ relatedModelName: relation.to,
4151
+ relatedNamespaceId: relation.toNamespace,
4152
+ relatedTableName: resolveModelTableName(contract, relation.toNamespace, relation.to),
4153
+ cardinality: relation.cardinality,
4154
+ localColumns: relation.on.localFields.map((f) => resolveFieldToColumn(contract, namespaceId, modelName, f)),
4155
+ targetColumns: relation.on.targetFields.map((f) => resolveFieldToColumn(contract, relation.toNamespace, relation.to, f)),
4156
+ through: relation.through ? {
4157
+ table: relation.through.table,
4158
+ namespaceId: relation.through.namespaceId,
4159
+ parentColumns: relation.through.parentColumns,
4160
+ childColumns: relation.through.childColumns,
4161
+ targetColumns: relation.through.targetColumns,
4162
+ requiredPayloadColumns: relation.through.requiredPayloadColumns
4163
+ } : void 0
4164
+ }));
4165
+ perContract.set(cacheKey, definitions);
4166
+ return definitions;
4167
+ }
4168
+ function toFieldName(contract, namespaceId, modelName, columnName) {
4169
+ return getColumnToFieldMap(contract, namespaceId, modelName)[columnName] ?? columnName;
4170
4170
  }
4171
4171
  function normalizeWhereArg(arg, options) {
4172
4172
  if (arg === void 0) return;
@@ -4218,7 +4218,7 @@ function listed(items, conjunction) {
4218
4218
  const last = items.at(-1) ?? "";
4219
4219
  return items.length > 1 ? `${items.slice(0, -1).join(", ")} ${conjunction} ${last}` : last;
4220
4220
  }
4221
- const HIDDEN_BY_A_SCOPE = "A scope passed to with can add one without showing it at the call site.";
4221
+ const HIDDEN_BY_A_FRAGMENT = "A fragment passed to with can add one without showing it at the call site.";
4222
4222
  /** Throws `ORM.ARGUMENT_INVALID` when the collection has a limit, an offset, a cursor or a distinct selection, which `method`, a write of every matching row, would ignore. */
4223
4223
  function assertBulkWriteIgnoresNothing(state, modelName, method) {
4224
4224
  const { nouns, calls } = presentParts(state, [
@@ -4231,7 +4231,7 @@ function assertBulkWriteIgnoresNothing(state, modelName, method) {
4231
4231
  if (nouns.length === 0) return;
4232
4232
  const { limit, offset, cursor, distinct, distinctOn } = state;
4233
4233
  throw ormError("ORM.ARGUMENT_INVALID", `Cannot ${method} ${modelName}: the collection has ${listed(nouns, "and")}`, {
4234
- why: `${method} changes every row that matches the filter. The statement it runs cannot apply ${listed(nouns, "or")}, so it would change more rows than the chain asks for. ${HIDDEN_BY_A_SCOPE}`,
4234
+ why: `${method} changes every row that matches the filter. The statement it runs cannot apply ${listed(nouns, "or")}, so it would change more rows than the chain asks for. ${HIDDEN_BY_A_FRAGMENT}`,
4235
4235
  fix: `Remove ${listed(calls, "and")} before ${method}, or read the rows first and change them by their ids.`,
4236
4236
  meta: {
4237
4237
  model: modelName,
@@ -4257,7 +4257,7 @@ function assertRelationUpdateIgnoresNothing(state, modelName) {
4257
4257
  if (nouns.length === 0) return;
4258
4258
  const { orderBy, limit, offset, cursor, distinct, distinctOn } = state;
4259
4259
  throw ormError("ORM.ARGUMENT_INVALID", `Cannot update ${modelName} with a relation mutation: the collection has ${listed(nouns, "and")}`, {
4260
- why: `An update that changes a relation finds its row by the filter alone. It would ignore ${listed(nouns, "and")}, and could change another row than first() returns. ${HIDDEN_BY_A_SCOPE}`,
4260
+ why: `An update that changes a relation finds its row by the filter alone. It would ignore ${listed(nouns, "and")}, and could change another row than first() returns. ${HIDDEN_BY_A_FRAGMENT}`,
4261
4261
  fix: `Remove ${listed(calls, "and")} before update, or filter to the one row, such as by its id.`,
4262
4262
  meta: {
4263
4263
  model: modelName,
@@ -4393,21 +4393,21 @@ var CollectionBase = class CollectionBase {
4393
4393
  return fn(this);
4394
4394
  }
4395
4395
  /**
4396
- * Define a scope for this collection's model, such as a shared `select` and `include`. The body is typed once, against the model's plain collection. The scope accepts any collection of the model that `select` and `variant` have not narrowed.
4396
+ * Define a query fragment for this collection's model, such as a shared `select` and `include`. The body is typed once, against the model's plain collection. The fragment accepts any collection of the model that `select` and `variant` have not narrowed.
4397
4397
  *
4398
4398
  * ```ts
4399
- * const summary = db.Post.scope((posts) => posts.select('id', 'title').include('user'));
4399
+ * const summary = db.Post.fragment((posts) => posts.select('id', 'title').include('user'));
4400
4400
  * db.User.include('posts', (posts) => posts.with(summary));
4401
4401
  * ```
4402
4402
  */
4403
- scope(body) {
4404
- assertScopeBody(body);
4403
+ fragment(body) {
4404
+ assertFragmentBody(body);
4405
4405
  const source = {
4406
4406
  modelName: this.modelName,
4407
4407
  namespaceId: this.namespaceId
4408
4408
  };
4409
4409
  return (collection) => {
4410
- assertModelScopeReceiver(source, collection);
4410
+ assertModelFragmentReceiver(source, collection);
4411
4411
  return body(blindCast(collection));
4412
4412
  };
4413
4413
  }
@@ -5438,7 +5438,7 @@ function orm(options) {
5438
5438
  return new Proxy({}, { get(_target, prop) {
5439
5439
  if (typeof prop !== "string") return;
5440
5440
  if (Object.hasOwn(contract.domain.namespaces, prop)) return namespaceFacet(prop);
5441
- return prop === "scope" ? defineFieldScope : void 0;
5441
+ return prop === "fragment" ? defineFieldFragment : void 0;
5442
5442
  } });
5443
5443
  }
5444
5444
  function createCollectionRegistry(contract, collections) {