@prisma-next/sql-orm-client 0.5.0-dev.6 → 0.5.0-dev.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "@prisma-next/sql-orm-client",
3
- "version": "0.5.0-dev.6",
3
+ "version": "0.5.0-dev.61",
4
+ "license": "Apache-2.0",
4
5
  "type": "module",
5
6
  "sideEffects": false,
6
7
  "description": "ORM client for Prisma Next — fluent, type-safe model collections",
7
8
  "dependencies": {
8
- "@prisma-next/contract": "0.5.0-dev.6",
9
- "@prisma-next/operations": "0.5.0-dev.6",
10
- "@prisma-next/runtime-executor": "0.5.0-dev.6",
11
- "@prisma-next/sql-relational-core": "0.5.0-dev.6",
12
- "@prisma-next/sql-operations": "0.5.0-dev.6",
13
- "@prisma-next/sql-runtime": "0.5.0-dev.6",
14
- "@prisma-next/utils": "0.5.0-dev.6",
15
- "@prisma-next/sql-contract": "0.5.0-dev.6"
9
+ "@prisma-next/contract": "0.5.0-dev.61",
10
+ "@prisma-next/framework-components": "0.5.0-dev.61",
11
+ "@prisma-next/operations": "0.5.0-dev.61",
12
+ "@prisma-next/sql-operations": "0.5.0-dev.61",
13
+ "@prisma-next/sql-relational-core": "0.5.0-dev.61",
14
+ "@prisma-next/sql-contract": "0.5.0-dev.61",
15
+ "@prisma-next/utils": "0.5.0-dev.61",
16
+ "@prisma-next/sql-runtime": "0.5.0-dev.61"
16
17
  },
17
18
  "devDependencies": {
18
19
  "@types/pg": "8.16.0",
@@ -20,15 +21,14 @@
20
21
  "tsdown": "0.18.4",
21
22
  "typescript": "5.9.3",
22
23
  "vitest": "4.0.17",
23
- "@prisma-next/adapter-postgres": "0.5.0-dev.6",
24
- "@prisma-next/driver-postgres": "0.5.0-dev.6",
25
- "@prisma-next/framework-components": "0.5.0-dev.6",
26
- "@prisma-next/cli": "0.5.0-dev.6",
27
- "@prisma-next/extension-pgvector": "0.5.0-dev.6",
28
- "@prisma-next/family-sql": "0.5.0-dev.6",
29
- "@prisma-next/sql-contract-ts": "0.5.0-dev.6",
30
- "@prisma-next/target-postgres": "0.5.0-dev.6",
31
- "@prisma-next/ids": "0.5.0-dev.6",
24
+ "@prisma-next/adapter-postgres": "0.5.0-dev.61",
25
+ "@prisma-next/cli": "0.5.0-dev.61",
26
+ "@prisma-next/driver-postgres": "0.5.0-dev.61",
27
+ "@prisma-next/extension-pgvector": "0.5.0-dev.61",
28
+ "@prisma-next/family-sql": "0.5.0-dev.61",
29
+ "@prisma-next/sql-contract-ts": "0.5.0-dev.61",
30
+ "@prisma-next/ids": "0.5.0-dev.61",
31
+ "@prisma-next/target-postgres": "0.5.0-dev.61",
32
32
  "@prisma-next/tsconfig": "0.0.0",
33
33
  "@prisma-next/tsdown": "0.0.0",
34
34
  "@prisma-next/test-utils": "0.0.1"
@@ -1,6 +1,23 @@
1
+ /**
2
+ * Collection row dispatch.
3
+ *
4
+ * Per-row decoding is performed upstream in `sql-runtime`'s row-yielding async
5
+ * generator (it `await`s `decodeRow` once per row before yielding). This file
6
+ * never calls codec query-time methods directly; it consumes plain decoded
7
+ * cells through `executeQueryPlan` → `scope.execute(plan)` →
8
+ * `AsyncIterableResult<Row>`. Every `for await` / `.toArray()` consumer below
9
+ * therefore sees plain `T` values, not `Promise<T>`.
10
+ *
11
+ * See `packages/2-sql/5-runtime/src/codecs/decoding.ts` for the decode-once-
12
+ * per-row contract; this file is the consumer side of that contract. See also
13
+ * ADR 030 (codecs registry & decode boundary) and the m3 coverage in
14
+ * `test/integration/codec-async.test.ts` and `test/codec-async.types.test-d.ts`.
15
+ */
16
+
1
17
  import type { Contract } from '@prisma-next/contract/types';
2
- import { AsyncIterableResult } from '@prisma-next/runtime-executor';
18
+ import { AsyncIterableResult } from '@prisma-next/framework-components/runtime';
3
19
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
20
+ import type { RuntimeScope } from '@prisma-next/sql-relational-core/types';
4
21
  import { isToOneCardinality, resolvePolymorphismInfo } from './collection-contract';
5
22
  import {
6
23
  acquireRuntimeScope,
@@ -25,7 +42,6 @@ import type {
25
42
  IncludeExpr,
26
43
  IncludeScalar,
27
44
  RelationCardinalityTag,
28
- RuntimeScope,
29
45
  } from './types';
30
46
 
31
47
  export function dispatchCollectionRows<Row>(options: {
@@ -1,5 +1,5 @@
1
1
  import type { Contract } from '@prisma-next/contract/types';
2
- import { AsyncIterableResult } from '@prisma-next/runtime-executor';
2
+ import { AsyncIterableResult } from '@prisma-next/framework-components/runtime';
3
3
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
4
4
  import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
5
5
  import { stitchIncludes } from './collection-dispatch';
@@ -1,13 +1,14 @@
1
1
  import type { Contract } from '@prisma-next/contract/types';
2
- import { AsyncIterableResult } from '@prisma-next/runtime-executor';
2
+ import { AsyncIterableResult } from '@prisma-next/framework-components/runtime';
3
3
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
4
+ import type { RuntimeScope } from '@prisma-next/sql-relational-core/types';
4
5
  import {
5
6
  getColumnToFieldMap,
6
7
  getCompleteColumnToFieldMap,
7
8
  getFieldToColumnMap,
8
9
  type PolymorphismInfo,
9
10
  } from './collection-contract';
10
- import type { CollectionContext, RuntimeConnection, RuntimeScope } from './types';
11
+ import type { CollectionContext, RuntimeConnection } from './types';
11
12
 
12
13
  export interface RowEnvelope {
13
14
  readonly raw: Record<string, unknown>;
package/src/collection.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Contract } from '@prisma-next/contract/types';
2
- import { AsyncIterableResult } from '@prisma-next/runtime-executor';
2
+ import { AsyncIterableResult } from '@prisma-next/framework-components/runtime';
3
3
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
4
4
  import {
5
5
  BinaryExpr,
@@ -119,11 +119,17 @@ function applyCreateDefaults(
119
119
  tableName: string,
120
120
  rows: Record<string, unknown>[],
121
121
  ): void {
122
+ // Per-operation cache for generators with `stability: 'query'` (e.g.
123
+ // `timestampNow` for `temporal.updatedAt()`): one generated value
124
+ // shared across every row in this insert. Per-field generators
125
+ // (e.g. `cuid`) ignore the cache and vary per row.
126
+ const defaultValueCache = rows.length > 1 ? new Map<string, unknown>() : undefined;
122
127
  for (const row of rows) {
123
128
  const applied = ctx.context.applyMutationDefaults({
124
129
  op: 'create',
125
130
  table: tableName,
126
131
  values: row,
132
+ ...(defaultValueCache ? { defaultValueCache } : {}),
127
133
  });
128
134
  for (const def of applied) {
129
135
  row[def.column] = def.value;
@@ -131,6 +137,21 @@ function applyCreateDefaults(
131
137
  }
132
138
  }
133
139
 
140
+ function applyUpdateDefaults(
141
+ ctx: CollectionContext<Contract<SqlStorage>>,
142
+ tableName: string,
143
+ values: Record<string, unknown>,
144
+ ): void {
145
+ const applied = ctx.context.applyMutationDefaults({
146
+ op: 'update',
147
+ table: tableName,
148
+ values,
149
+ });
150
+ for (const def of applied) {
151
+ values[def.column] = def.value;
152
+ }
153
+ }
154
+
134
155
  type WhereDirectInput = WhereArg;
135
156
 
136
157
  function isToWhereExprInput(value: unknown): value is ToWhereExpr {
@@ -965,6 +986,9 @@ export class Collection<
965
986
  applyCreateDefaults(this.ctx, this.tableName, [createValues]);
966
987
  const updateValues = mapModelDataToStorageRow(this.contract, this.modelName, input.update);
967
988
  const hasUpdateValues = Object.keys(updateValues).length > 0;
989
+ if (hasUpdateValues) {
990
+ applyUpdateDefaults(this.ctx, this.tableName, updateValues);
991
+ }
968
992
  const conflictColumns = resolveUpsertConflictColumns(
969
993
  this.contract,
970
994
  this.modelName,
@@ -1057,6 +1081,8 @@ export class Collection<
1057
1081
  return new AsyncIterableResult(generator());
1058
1082
  }
1059
1083
 
1084
+ applyUpdateDefaults(this.ctx, this.tableName, mappedData);
1085
+
1060
1086
  const parentJoinColumns = this.state.includes.map((include) => include.localColumn);
1061
1087
  const { selectedForQuery: selectedForUpdate, hiddenColumns } = augmentSelectionForJoinColumns(
1062
1088
  this.state.selectedFields,
@@ -1088,6 +1114,8 @@ export class Collection<
1088
1114
  return 0;
1089
1115
  }
1090
1116
 
1117
+ applyUpdateDefaults(this.ctx, this.tableName, mappedData);
1118
+
1091
1119
  const primaryKeyColumn = resolvePrimaryKeyColumn(this.contract, this.tableName);
1092
1120
  const countState: CollectionState = {
1093
1121
  ...emptyState(),
@@ -1,11 +1,10 @@
1
- import type { ExecutionPlan } from '@prisma-next/contract/types';
2
- import type { AsyncIterableResult } from '@prisma-next/runtime-executor';
3
- import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
4
- import type { RuntimeScope } from './types';
1
+ import type { AsyncIterableResult } from '@prisma-next/framework-components/runtime';
2
+ import type { SqlExecutionPlan, SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
3
+ import type { RuntimeScope } from '@prisma-next/sql-relational-core/types';
5
4
 
6
5
  export function executeQueryPlan<Row>(
7
6
  scope: RuntimeScope,
8
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
7
+ plan: SqlExecutionPlan<Row> | SqlQueryPlan<Row>,
9
8
  ): AsyncIterableResult<Row> {
10
9
  return scope.execute(plan);
11
10
  }
package/src/filters.ts CHANGED
@@ -73,7 +73,7 @@ function assertFieldHasEqualityTrait(
73
73
  ): void {
74
74
  const fieldType = modelOf(context.contract, modelName)?.fields?.[fieldName]?.type;
75
75
  const codecId = fieldType?.kind === 'scalar' ? fieldType.codecId : undefined;
76
- const traits = codecId ? context.codecs.traitsOf(codecId) : [];
76
+ const traits = codecId ? (context.codecDescriptors.descriptorFor(codecId)?.traits ?? []) : [];
77
77
  if (!traits.includes('equality')) {
78
78
  throw new Error(
79
79
  `Shorthand filter on "${modelName}.${fieldName}": field does not support equality comparisons`,
@@ -3,10 +3,29 @@ import type { SqlStorage } from '@prisma-next/sql-contract/types';
3
3
 
4
4
  export type IncludeStrategy = 'lateral' | 'correlated' | 'multiQuery';
5
5
 
6
+ /**
7
+ * Choose the SQL emission strategy for nested includes based on the
8
+ * contract's declared capabilities.
9
+ *
10
+ * - `'lateral'`: outer SELECT with one LATERAL JOIN per relation,
11
+ * aggregating to JSON. Requires both `lateral` and `jsonAgg`.
12
+ * Postgres has both.
13
+ * - `'correlated'`: outer SELECT with one correlated subquery per
14
+ * relation, aggregating to JSON. Requires `jsonAgg` only.
15
+ * SQLite has `jsonAgg` (via `json_group_array`) but no LATERAL.
16
+ * - `'multiQuery'`: fallback. One SELECT per relation, stitched
17
+ * together in JS via `WHERE pk IN (parent-pk-values)`. Always
18
+ * correct; just N+1 round-trips.
19
+ *
20
+ * The capability flags are looked up under the contract's
21
+ * `targetFamily` and `target` namespaces — the two layers the contract
22
+ * emitter actually populates. Cross-namespace ("`postgres.lateral`
23
+ * found while running SQLite") false positives are impossible because
24
+ * we only inspect the running target's namespaces.
25
+ */
6
26
  export function selectIncludeStrategy(contract: Contract<SqlStorage>): IncludeStrategy {
7
- const capabilities = contract.capabilities as Record<string, unknown> | undefined;
8
- const hasLateral = hasCapability(capabilities?.['lateral']);
9
- const hasJsonAgg = hasCapability(capabilities?.['jsonAgg']);
27
+ const hasLateral = capabilityFlag(contract, 'lateral');
28
+ const hasJsonAgg = capabilityFlag(contract, 'jsonAgg');
10
29
 
11
30
  if (hasLateral && hasJsonAgg) {
12
31
  return 'lateral';
@@ -19,15 +38,18 @@ export function selectIncludeStrategy(contract: Contract<SqlStorage>): IncludeSt
19
38
  return 'multiQuery';
20
39
  }
21
40
 
22
- function hasCapability(value: unknown): boolean {
23
- if (value === true) {
24
- return true;
25
- }
26
-
27
- if (typeof value !== 'object' || value === null) {
28
- return false;
29
- }
30
-
31
- const flags = value as Record<string, unknown>;
32
- return Object.values(flags).some((flag) => flag === true);
41
+ /**
42
+ * Read a capability flag from the contract's target/family namespaces.
43
+ *
44
+ * The contract emitter populates `capabilities[targetFamily]` (universal
45
+ * SQL flags like `jsonAgg`, `returning`) and `capabilities[target]`
46
+ * (target-specific flags like `lateral` on Postgres). Either may
47
+ * declare a given flag; the family namespace declares the floor and the
48
+ * target namespace can extend on top.
49
+ */
50
+ function capabilityFlag(contract: Contract<SqlStorage>, flag: string): boolean {
51
+ return (
52
+ contract.capabilities[contract.targetFamily]?.[flag] === true ||
53
+ contract.capabilities[contract.target]?.[flag] === true
54
+ );
33
55
  }
@@ -7,12 +7,11 @@ import {
7
7
  BinaryExpr,
8
8
  ColumnRef,
9
9
  ExistsExpr,
10
- OperationExpr,
11
- ParamRef,
12
10
  ProjectionItem,
13
11
  SelectAst,
14
12
  TableSource,
15
13
  } from '@prisma-next/sql-relational-core/ast';
14
+ import type { Expression, ScopeField } from '@prisma-next/sql-relational-core/expression';
16
15
  import type { ExecutionContext } from '@prisma-next/sql-relational-core/query-lane-context';
17
16
  import {
18
17
  getFieldToColumnMap,
@@ -67,15 +66,16 @@ export function createModelAccessor<
67
66
  }
68
67
 
69
68
  for (const [name, entry] of Object.entries(context.queryOperations.entries())) {
70
- const self = entry.args[0];
71
69
  const op: NamedOp = [name, entry];
72
- if (self?.codecId) {
70
+ const self = entry.self;
71
+ if (!self) continue;
72
+ if (self.codecId !== undefined) {
73
73
  registerOp(self.codecId, op);
74
- } else if (self?.traits) {
75
- for (const codec of context.codecs.values()) {
76
- const codecTraits: readonly string[] = codec.traits ?? [];
77
- if (self.traits.every((t) => codecTraits.includes(t))) {
78
- registerOp(codec.id, op);
74
+ } else if (self.traits !== undefined) {
75
+ for (const descriptor of context.codecDescriptors.values()) {
76
+ const descriptorTraits: readonly string[] = descriptor.traits;
77
+ if (self.traits.every((t) => descriptorTraits.includes(t))) {
78
+ registerOp(descriptor.codecId, op);
79
79
  }
80
80
  }
81
81
  }
@@ -93,96 +93,96 @@ export function createModelAccessor<
93
93
  }
94
94
 
95
95
  const columnName = fieldToColumn[prop] ?? prop;
96
- const traits = resolveFieldTraits(contract, modelName, prop, context);
97
- const codecId = resolveFieldCodecId(contract, tableName, columnName);
98
- const operations = codecId ? (opsByCodecId.get(codecId) ?? []) : [];
99
- return createScalarFieldAccessor(tableName, columnName, codecId, traits, operations, context);
96
+ const column = resolveColumn(contract, tableName, columnName);
97
+ // Unknown fields return `undefined`, matching plain JS object semantics.
98
+ // The `ModelAccessor<TContract, ModelName>` type already rejects typos
99
+ // at compile time for TS consumers, and contexts that iterate accessor
100
+ // keys (e.g. relation-shorthand predicates) can detect missing fields
101
+ // with an `undefined` check and raise their own, domain-specific error.
102
+ if (!column) {
103
+ return undefined;
104
+ }
105
+ const traits = context.codecDescriptors.descriptorFor(column.codecId)?.traits ?? [];
106
+ const operations = opsByCodecId.get(column.codecId) ?? [];
107
+ return createScalarFieldAccessor(
108
+ tableName,
109
+ columnName,
110
+ column.codecId,
111
+ column.nullable,
112
+ traits,
113
+ operations,
114
+ context,
115
+ );
100
116
  },
101
117
  });
102
118
  }
103
119
 
104
- function resolveFieldTraits(
105
- contract: Contract<SqlStorage>,
106
- modelName: string,
107
- fieldName: string,
108
- context: ExecutionContext,
109
- ): readonly string[] {
110
- const fieldType = modelOf(contract, modelName)?.fields?.[fieldName]?.type;
111
- const codecId = fieldType?.kind === 'scalar' ? fieldType.codecId : undefined;
112
- if (!codecId) return [];
113
- return context.codecs.traitsOf(codecId);
114
- }
115
-
116
- function resolveFieldCodecId(
120
+ function resolveColumn(
117
121
  contract: Contract<SqlStorage>,
118
122
  tableName: string,
119
123
  columnName: string,
120
- ): string | undefined {
121
- const table = contract.storage.tables?.[tableName];
122
- return table?.columns?.[columnName]?.codecId;
124
+ ): { readonly codecId: string; readonly nullable: boolean } | undefined {
125
+ const column = contract.storage.tables?.[tableName]?.columns?.[columnName];
126
+ if (!column) return undefined;
127
+ return { codecId: column.codecId, nullable: column.nullable };
123
128
  }
124
129
 
125
130
  function createScalarFieldAccessor(
126
131
  tableName: string,
127
132
  columnName: string,
128
- codecId: string | undefined,
133
+ codecId: string,
134
+ nullable: boolean,
129
135
  traits: readonly string[],
130
136
  operations: readonly NamedOp[],
131
137
  context: ExecutionContext,
132
138
  ): Partial<ComparisonMethodFns<unknown>> {
133
139
  const column = ColumnRef.of(tableName, columnName);
134
- const methods: Record<string, unknown> = {};
135
-
140
+ const comparisonEntries: Array<[string, unknown]> = [];
136
141
  for (const [name, meta] of Object.entries(COMPARISON_METHODS_META)) {
137
- if (meta.traits.some((t) => !traits.includes(t))) {
138
- continue;
139
- }
140
- methods[name] = meta.create(column, codecId);
142
+ if (meta.traits.some((t) => !traits.includes(t))) continue;
143
+ comparisonEntries.push([name, meta.create(column, codecId)]);
141
144
  }
142
145
 
146
+ const accessor = {
147
+ returnType: { codecId, nullable },
148
+ buildAst: () => column,
149
+ ...Object.fromEntries(comparisonEntries),
150
+ } as Expression<ScopeField> & Record<string, unknown>;
151
+
143
152
  for (const [name, entry] of operations) {
144
- methods[name] = createExtensionMethodFactory(column, name, entry, context);
153
+ accessor[name] = createExtensionMethodFactory(accessor, entry, context);
145
154
  }
146
155
 
147
- return methods as Partial<ComparisonMethodFns<unknown>>;
156
+ return accessor as Partial<ComparisonMethodFns<unknown>>;
148
157
  }
149
158
 
150
159
  function createExtensionMethodFactory(
151
- column: ColumnRef,
152
- methodName: string,
160
+ selfExpr: Expression<ScopeField>,
153
161
  entry: SqlOperationEntry,
154
162
  context: ExecutionContext,
155
163
  ): (...args: unknown[]) => unknown {
156
- const returnTraits = context.codecs.traitsOf(entry.returns.codecId);
157
- const isPredicate = returnTraits.includes('boolean');
158
-
159
164
  return (...args: unknown[]) => {
160
- const userArgSpecs = entry.args.slice(1);
161
- const astArgs = userArgSpecs.map((argSpec, i) => {
162
- return ParamRef.of(args[i], argSpec.codecId ? { codecId: argSpec.codecId } : undefined);
163
- });
164
-
165
- const opExpr = new OperationExpr({
166
- method: methodName,
167
- self: column,
168
- args: astArgs,
169
- returns: entry.returns,
170
- lowering: entry.lowering,
171
- });
165
+ // `entry.impl` is typed `(...args: never[]) => QueryOperationReturn` —
166
+ // `never[]` args block direct invocation with unknown values, and the
167
+ // declared return omits `buildAst` (sql-contract intentionally doesn't
168
+ // depend on relational-core). Cast here to the practical shape: authors
169
+ // always return Expression<ScopeField> via `buildOperation`.
170
+ const impl = entry.impl as (self: unknown, ...args: unknown[]) => Expression<ScopeField>;
171
+ const result = impl(selfExpr, ...args);
172
+ const returnCodecId = result.returnType.codecId;
173
+ const returnTraits = context.codecDescriptors.descriptorFor(returnCodecId)?.traits ?? [];
174
+ const isPredicate = returnTraits.includes('boolean');
172
175
 
173
176
  if (isPredicate) {
174
- return opExpr;
177
+ return result.buildAst();
175
178
  }
176
179
 
180
+ const resultAst = result.buildAst();
177
181
  const methods: Record<string, unknown> = {};
178
-
179
182
  for (const [resultMethodName, meta] of Object.entries(COMPARISON_METHODS_META)) {
180
- if (meta.traits.some((t) => !returnTraits.includes(t))) {
181
- continue;
182
- }
183
- methods[resultMethodName] = meta.create(opExpr, entry.returns.codecId);
183
+ if (meta.traits.some((t) => !returnTraits.includes(t))) continue;
184
+ methods[resultMethodName] = meta.create(resultAst, returnCodecId);
184
185
  }
185
-
186
186
  return methods;
187
187
  };
188
188
  }
@@ -293,8 +293,14 @@ function toRelationWhereExpr<TContract extends Contract<SqlStorage>>(
293
293
  const fieldAccessor = (accessor as Record<string, Partial<ComparisonMethodFns<unknown>>>)[
294
294
  fieldName
295
295
  ];
296
+ // Unknown field in the shorthand predicate — the Proxy returns undefined
297
+ // for fields the contract doesn't declare. Surface it explicitly: silent
298
+ // skip would drop user intent (e.g. a typo'd `nmae: 'Alice'` filter would
299
+ // match every row).
296
300
  if (!fieldAccessor) {
297
- continue;
301
+ throw new Error(
302
+ `Shorthand filter on "${relatedModelName}.${fieldName}": field is not defined on the model`,
303
+ );
298
304
  }
299
305
 
300
306
  if (value === null) {
@@ -7,6 +7,7 @@ import {
7
7
  LiteralExpr,
8
8
  } from '@prisma-next/sql-relational-core/ast';
9
9
  import type { ExecutionContext } from '@prisma-next/sql-relational-core/query-lane-context';
10
+ import type { RuntimeScope } from '@prisma-next/sql-relational-core/types';
10
11
  import {
11
12
  getColumnToFieldMap,
12
13
  resolveFieldToColumn,
@@ -40,7 +41,6 @@ import type {
40
41
  RelationMutation,
41
42
  RelationMutator,
42
43
  RuntimeQueryable,
43
- RuntimeScope,
44
44
  } from './types';
45
45
  import { emptyState } from './types';
46
46
 
@@ -236,6 +236,15 @@ async function updateFirstGraph(
236
236
 
237
237
  const mappedUpdateData = mapModelDataToStorageRow(contract, modelName, scalarData);
238
238
  if (Object.keys(mappedUpdateData).length > 0) {
239
+ const tableName = resolveModelTableName(contract, modelName);
240
+ const appliedUpdateDefaults = context.applyMutationDefaults({
241
+ op: 'update',
242
+ table: tableName,
243
+ values: mappedUpdateData,
244
+ });
245
+ for (const def of appliedUpdateDefaults) {
246
+ mappedUpdateData[def.column] = def.value;
247
+ }
239
248
  const pkFilter = buildPrimaryKeyFilterFromRow(contract, modelName, existingRow);
240
249
  const pkWhere = shorthandToWhereExpr(
241
250
  context,
@@ -246,7 +255,6 @@ async function updateFirstGraph(
246
255
  throw new Error(`Failed to build primary key filter for model "${modelName}"`);
247
256
  }
248
257
 
249
- const tableName = resolveModelTableName(contract, modelName);
250
258
  const compiled = compileUpdateReturning(
251
259
  contract,
252
260
  tableName,
@@ -18,16 +18,30 @@ import { buildOrmQueryPlan, deriveParamsFromAst } from './query-plan-meta';
18
18
  import type { AggregateSelector } from './types';
19
19
  import { combineWhereExprs } from './where-utils';
20
20
 
21
- function toAggregateExpr(tableName: string, selector: AggregateSelector<unknown>): AggregateExpr {
21
+ function toAggregateProjection(
22
+ contract: Contract<SqlStorage>,
23
+ tableName: string,
24
+ selector: AggregateSelector<unknown>,
25
+ ): { expr: AggregateExpr; codecId: string | undefined } {
22
26
  if (selector.fn === 'count') {
23
- return AggregateExpr.count();
27
+ // count() returns a target-specific bigint; mapping isn't derivable here
28
+ // without target coupling, so we leave codecId unstamped.
29
+ return { expr: AggregateExpr.count(), codecId: undefined };
24
30
  }
25
31
 
26
32
  if (!selector.column) {
27
33
  throw new Error(`Aggregate selector "${selector.fn}" requires a field`);
28
34
  }
29
35
 
30
- return new AggregateExpr(selector.fn, ColumnRef.of(tableName, selector.column));
36
+ const expr = new AggregateExpr(selector.fn, ColumnRef.of(tableName, selector.column));
37
+ // min/max preserve the input column's type, so propagate the column codec.
38
+ // sum widens (int4 → int8 in Postgres) and avg → numeric; both need
39
+ // target+input-aware mapping that doesn't exist yet, so leave unstamped.
40
+ if (selector.fn === 'min' || selector.fn === 'max') {
41
+ const codecId = contract.storage.tables[tableName]?.columns[selector.column]?.codecId;
42
+ return { expr, codecId };
43
+ }
44
+ return { expr, codecId: undefined };
31
45
  }
32
46
 
33
47
  // ORM HAVING filters use literal binding (values inlined at plan-build time),
@@ -115,17 +129,18 @@ export function compileAggregate(
115
129
  throw new Error('aggregate() requires at least one aggregation selector');
116
130
  }
117
131
 
118
- const projection: ProjectionItem[] = entries.map(([alias, selector]) =>
119
- ProjectionItem.of(alias, toAggregateExpr(tableName, selector)),
120
- );
132
+ const projection: ProjectionItem[] = entries.map(([alias, selector]) => {
133
+ const { expr, codecId } = toAggregateProjection(contract, tableName, selector);
134
+ return ProjectionItem.of(alias, expr, codecId);
135
+ });
121
136
  let ast = SelectAst.from(TableSource.named(tableName)).withProjection(projection);
122
137
  const where = combineWhereExprs(filters);
123
138
  if (where) {
124
139
  ast = ast.withWhere(where);
125
140
  }
126
141
 
127
- const { params, paramDescriptors } = deriveParamsFromAst(ast);
128
- return buildOrmQueryPlan(contract, ast, params, paramDescriptors);
142
+ const { params } = deriveParamsFromAst(ast);
143
+ return buildOrmQueryPlan(contract, ast, params);
129
144
  }
130
145
 
131
146
  export function compileGroupedAggregate(
@@ -145,11 +160,15 @@ export function compileGroupedAggregate(
145
160
  throw new Error('groupBy().aggregate() requires at least one aggregation selector');
146
161
  }
147
162
 
163
+ const table = contract.storage.tables[tableName];
148
164
  const projection: ProjectionItem[] = [
149
- ...groupByColumns.map((column) => ProjectionItem.of(column, ColumnRef.of(tableName, column))),
150
- ...entries.map(([alias, selector]) =>
151
- ProjectionItem.of(alias, toAggregateExpr(tableName, selector)),
165
+ ...groupByColumns.map((column) =>
166
+ ProjectionItem.of(column, ColumnRef.of(tableName, column), table?.columns[column]?.codecId),
152
167
  ),
168
+ ...entries.map(([alias, selector]) => {
169
+ const { expr, codecId } = toAggregateProjection(contract, tableName, selector);
170
+ return ProjectionItem.of(alias, expr, codecId);
171
+ }),
153
172
  ];
154
173
 
155
174
  let ast = SelectAst.from(TableSource.named(tableName))
@@ -164,6 +183,6 @@ export function compileGroupedAggregate(
164
183
  ast = ast.withHaving(validateGroupedHavingExpr(havingExpr));
165
184
  }
166
185
 
167
- const { params, paramDescriptors } = deriveParamsFromAst(ast);
168
- return buildOrmQueryPlan(contract, ast, params, paramDescriptors);
186
+ const { params } = deriveParamsFromAst(ast);
187
+ return buildOrmQueryPlan(contract, ast, params);
169
188
  }