@prisma-next/sql-orm-client 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,37 +1,37 @@
1
1
  {
2
2
  "name": "@prisma-next/sql-orm-client",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "description": "ORM client for Prisma Next — fluent, type-safe model collections",
8
8
  "dependencies": {
9
- "@prisma-next/contract": "0.7.0",
10
- "@prisma-next/framework-components": "0.7.0",
11
- "@prisma-next/operations": "0.7.0",
12
- "@prisma-next/sql-contract": "0.7.0",
13
- "@prisma-next/sql-operations": "0.7.0",
14
- "@prisma-next/utils": "0.7.0",
15
- "@prisma-next/sql-runtime": "0.7.0",
16
- "@prisma-next/sql-relational-core": "0.7.0"
9
+ "@prisma-next/contract": "0.8.0",
10
+ "@prisma-next/framework-components": "0.8.0",
11
+ "@prisma-next/operations": "0.8.0",
12
+ "@prisma-next/sql-contract": "0.8.0",
13
+ "@prisma-next/sql-operations": "0.8.0",
14
+ "@prisma-next/sql-relational-core": "0.8.0",
15
+ "@prisma-next/sql-runtime": "0.8.0",
16
+ "@prisma-next/utils": "0.8.0"
17
17
  },
18
18
  "devDependencies": {
19
+ "@prisma-next/adapter-postgres": "0.8.0",
20
+ "@prisma-next/driver-postgres": "0.8.0",
21
+ "@prisma-next/cli": "0.8.0",
22
+ "@prisma-next/extension-pgvector": "0.8.0",
23
+ "@prisma-next/family-sql": "0.8.0",
24
+ "@prisma-next/ids": "0.8.0",
25
+ "@prisma-next/sql-contract-ts": "0.8.0",
26
+ "@prisma-next/target-postgres": "0.8.0",
27
+ "@prisma-next/test-utils": "0.8.0",
28
+ "@prisma-next/tsconfig": "0.8.0",
29
+ "@prisma-next/tsdown": "0.8.0",
19
30
  "@types/pg": "8.20.0",
20
31
  "pg": "8.20.0",
21
32
  "tsdown": "0.22.0",
22
33
  "typescript": "5.9.3",
23
- "vitest": "4.1.5",
24
- "@prisma-next/adapter-postgres": "0.7.0",
25
- "@prisma-next/cli": "0.7.0",
26
- "@prisma-next/driver-postgres": "0.7.0",
27
- "@prisma-next/extension-pgvector": "0.7.0",
28
- "@prisma-next/family-sql": "0.7.0",
29
- "@prisma-next/ids": "0.7.0",
30
- "@prisma-next/sql-contract-ts": "0.7.0",
31
- "@prisma-next/target-postgres": "0.7.0",
32
- "@prisma-next/test-utils": "0.7.0",
33
- "@prisma-next/tsconfig": "0.7.0",
34
- "@prisma-next/tsdown": "0.7.0"
34
+ "vitest": "4.1.5"
35
35
  },
36
36
  "files": [
37
37
  "dist",
package/src/collection.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import type { Contract } from '@prisma-next/contract/types';
2
- import { AsyncIterableResult } from '@prisma-next/framework-components/runtime';
2
+ import type {
3
+ AnnotationValue,
4
+ MetaBuilder,
5
+ OperationKind,
6
+ } from '@prisma-next/framework-components/runtime';
7
+ import { AsyncIterableResult, createMetaBuilder } from '@prisma-next/framework-components/runtime';
3
8
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
4
9
  import {
5
10
  type AnyExpression,
@@ -85,6 +90,7 @@ import {
85
90
  compileUpdateCount,
86
91
  compileUpdateReturning,
87
92
  compileUpsertReturning,
93
+ mergeAnnotations,
88
94
  } from './query-plan';
89
95
  import {
90
96
  type AggregateBuilder,
@@ -642,19 +648,51 @@ export class Collection<
642
648
  return this.#clone({ offset: n });
643
649
  }
644
650
 
645
- all(): AsyncIterableResult<Row> {
646
- return this.#dispatch();
651
+ /**
652
+ * Read terminal: stream all rows matching the current state.
653
+ *
654
+ * Accepts an optional `configure` callback that receives a
655
+ * `MetaBuilder<'read'>` so the caller can attach typed user
656
+ * annotations to the executed plan. `meta.annotate(...)` enforces
657
+ * applicability at the type level and at runtime; annotations are
658
+ * merged into `plan.meta.annotations` at compile time.
659
+ */
660
+ all(configure?: (meta: MetaBuilder<'read'>) => void): AsyncIterableResult<Row> {
661
+ return this.#withAnnotationsFromMeta(configure, 'all').#dispatch();
647
662
  }
648
663
 
649
664
  async first(): Promise<Row | null>;
665
+ async first(
666
+ filter: undefined,
667
+ configure: (meta: MetaBuilder<'read'>) => void,
668
+ ): Promise<Row | null>;
650
669
  async first(
651
670
  filter: (model: ModelAccessor<TContract, ModelName>) => WhereArg,
671
+ configure?: (meta: MetaBuilder<'read'>) => void,
652
672
  ): Promise<Row | null>;
653
- async first(filter: ShorthandWhereFilter<TContract, ModelName>): Promise<Row | null>;
673
+ async first(
674
+ filter: ShorthandWhereFilter<TContract, ModelName>,
675
+ configure?: (meta: MetaBuilder<'read'>) => void,
676
+ ): Promise<Row | null>;
677
+ /**
678
+ * Read terminal: return the first matching row, or `null`.
679
+ *
680
+ * Accepts an optional `filter` (function or shorthand) followed by an
681
+ * optional `configure` callback that receives a `MetaBuilder<'read'>`
682
+ * for attaching typed annotations. To attach annotations without
683
+ * narrowing further, pass `undefined` as the filter (or chain
684
+ * `.where(...)` first):
685
+ *
686
+ * ```typescript
687
+ * await db.User.first({ id }, (meta) => meta.annotate(cacheAnnotation({ ttl: 60 })));
688
+ * await db.User.first(undefined, (meta) => meta.annotate(cacheAnnotation({ ttl: 60 })));
689
+ * ```
690
+ */
654
691
  async first(
655
692
  filter?:
656
693
  | ((model: ModelAccessor<TContract, ModelName>) => WhereArg)
657
694
  | ShorthandWhereFilter<TContract, ModelName>,
695
+ configure?: (meta: MetaBuilder<'read'>) => void,
658
696
  ): Promise<Row | null> {
659
697
  const scoped =
660
698
  filter === undefined
@@ -662,13 +700,22 @@ export class Collection<
662
700
  : typeof filter === 'function'
663
701
  ? this.where(filter)
664
702
  : this.where(filter);
665
- const limited = scoped.take(1);
703
+ const limited = scoped.take(1).#withAnnotationsFromMeta(configure, 'first');
666
704
  const rows = await limited.#dispatch().toArray();
667
705
  return rows[0] ?? null;
668
706
  }
669
707
 
708
+ /**
709
+ * Read terminal: run an aggregate query (count, sum, avg, min, max)
710
+ * built via the `AggregateBuilder` callback.
711
+ *
712
+ * Accepts an optional `configure` callback that receives a
713
+ * `MetaBuilder<'read'>` for attaching typed annotations.
714
+ * Annotations are merged into the compiled plan's `meta.annotations`.
715
+ */
670
716
  async aggregate<Spec extends AggregateSpec>(
671
717
  fn: (aggregate: AggregateBuilder<TContract, ModelName>) => Spec,
718
+ configure?: (meta: MetaBuilder<'read'>) => void,
672
719
  ): Promise<AggregateResult<Spec>> {
673
720
  const aggregateSpec = fn(createAggregateBuilder(this.contract, this.modelName));
674
721
  const entries = Object.entries(aggregateSpec);
@@ -682,11 +729,11 @@ export class Collection<
682
729
  }
683
730
  }
684
731
 
685
- const compiled = compileAggregate(
686
- this.contract,
687
- this.tableName,
688
- this.state.filters,
689
- aggregateSpec,
732
+ const annotationsMap = this.#collectAnnotationsFromMeta(configure, 'read', 'aggregate');
733
+
734
+ const compiled = mergeAnnotations(
735
+ compileAggregate(this.contract, this.tableName, this.state.filters, aggregateSpec),
736
+ annotationsMap,
690
737
  );
691
738
  const rows = await executeQueryPlan<Record<string, unknown>>(
692
739
  this.ctx.runtime,
@@ -695,14 +742,36 @@ export class Collection<
695
742
  return normalizeAggregateResult(aggregateSpec, rows[0] ?? {});
696
743
  }
697
744
 
698
- async create(data: ResolvedCreateInput<TContract, ModelName, State['variantName']>): Promise<Row>;
699
- async create(data: MutationCreateInputWithRelations<TContract, ModelName>): Promise<Row>;
745
+ async create(
746
+ data: ResolvedCreateInput<TContract, ModelName, State['variantName']>,
747
+ configure?: (meta: MetaBuilder<'write'>) => void,
748
+ ): Promise<Row>;
749
+ async create(
750
+ data: MutationCreateInputWithRelations<TContract, ModelName>,
751
+ configure?: (meta: MetaBuilder<'write'>) => void,
752
+ ): Promise<Row>;
753
+ /**
754
+ * Write terminal: insert one row and return it.
755
+ *
756
+ * Accepts an optional `configure` callback that receives a
757
+ * `MetaBuilder<'write'>` for attaching typed annotations.
758
+ * Annotations are merged into the compiled mutation plan's
759
+ * `meta.annotations`.
760
+ *
761
+ * Note: when the input contains nested-mutation callbacks, the
762
+ * operation is executed as a graph of internal queries via
763
+ * `withMutationScope`. In that path, annotations apply to the
764
+ * logical `create()` call but do not currently flow into each
765
+ * constituent SQL statement issued for the related rows.
766
+ */
700
767
  async create(
701
768
  data:
702
769
  | ResolvedCreateInput<TContract, ModelName, State['variantName']>
703
770
  | MutationCreateInputWithRelations<TContract, ModelName>,
771
+ configure?: (meta: MetaBuilder<'write'>) => void,
704
772
  ): Promise<Row> {
705
773
  assertReturningCapability(this.contract, 'create()');
774
+ const annotationsMap = this.#collectAnnotationsFromMeta(configure, 'write', 'create');
706
775
 
707
776
  if (
708
777
  hasNestedMutationCallbacks(this.contract, this.modelName, data as Record<string, unknown>)
@@ -722,9 +791,10 @@ export class Collection<
722
791
  return reloaded;
723
792
  }
724
793
 
725
- const rows = await this.createAll([
726
- data as ResolvedCreateInput<TContract, ModelName, State['variantName']>,
727
- ]);
794
+ const rows = await this.#createAllWithAnnotations(
795
+ [data as ResolvedCreateInput<TContract, ModelName, State['variantName']>],
796
+ annotationsMap,
797
+ );
728
798
  const created = rows[0];
729
799
  if (created) {
730
800
  return created;
@@ -735,6 +805,17 @@ export class Collection<
735
805
 
736
806
  createAll(
737
807
  data: readonly ResolvedCreateInput<TContract, ModelName, State['variantName']>[],
808
+ configure?: (meta: MetaBuilder<'write'>) => void,
809
+ ): AsyncIterableResult<Row> {
810
+ return this.#createAllWithAnnotations(
811
+ data,
812
+ this.#collectAnnotationsFromMeta(configure, 'write', 'createAll'),
813
+ );
814
+ }
815
+
816
+ #createAllWithAnnotations(
817
+ data: readonly ResolvedCreateInput<TContract, ModelName, State['variantName']>[],
818
+ annotationsMap: ReadonlyMap<string, AnnotationValue<unknown, OperationKind>> | undefined,
738
819
  ): AsyncIterableResult<Row> {
739
820
  if (data.length === 0) {
740
821
  const generator = async function* (): AsyncGenerator<Row, void, unknown> {};
@@ -762,7 +843,7 @@ export class Collection<
762
843
  this.tableName,
763
844
  mappedRows,
764
845
  selectedForInsert,
765
- );
846
+ ).map((plan) => mergeAnnotations(plan, annotationsMap));
766
847
  return dispatchSplitMutationRows<Row>({
767
848
  contract: this.contract,
768
849
  runtime: this.ctx.runtime,
@@ -774,11 +855,9 @@ export class Collection<
774
855
  });
775
856
  }
776
857
 
777
- const compiled = compileInsertReturning(
778
- this.contract,
779
- this.tableName,
780
- mappedRows,
781
- selectedForInsert,
858
+ const compiled = mergeAnnotations(
859
+ compileInsertReturning(this.contract, this.tableName, mappedRows, selectedForInsert),
860
+ annotationsMap,
782
861
  );
783
862
  return dispatchMutationRows<Row>({
784
863
  contract: this.contract,
@@ -947,26 +1026,33 @@ export class Collection<
947
1026
 
948
1027
  async createCount(
949
1028
  data: readonly ResolvedCreateInput<TContract, ModelName, State['variantName']>[],
1029
+ configure?: (meta: MetaBuilder<'write'>) => void,
950
1030
  ): Promise<number> {
951
1031
  if (data.length === 0) {
952
1032
  return 0;
953
1033
  }
954
1034
 
955
1035
  this.#assertNotMtiVariant('createCount()');
1036
+ const annotationsMap = this.#collectAnnotationsFromMeta(configure, 'write', 'createCount');
956
1037
 
957
1038
  const rows = data as readonly Record<string, unknown>[];
958
1039
  const mappedRows = this.#mapCreateRows(rows);
959
1040
  applyCreateDefaults(this.ctx, this.tableName, mappedRows);
960
1041
 
961
1042
  if (this.contract.capabilities?.['sql']?.['defaultInInsert'] !== true) {
962
- const plans = compileInsertCountSplit(this.contract, this.tableName, mappedRows);
1043
+ const plans = compileInsertCountSplit(this.contract, this.tableName, mappedRows).map((plan) =>
1044
+ mergeAnnotations(plan, annotationsMap),
1045
+ );
963
1046
  for (const plan of plans) {
964
1047
  await executeQueryPlan<Record<string, unknown>>(this.ctx.runtime, plan).toArray();
965
1048
  }
966
1049
  return data.length;
967
1050
  }
968
1051
 
969
- const compiled = compileInsertCount(this.contract, this.tableName, mappedRows);
1052
+ const compiled = mergeAnnotations(
1053
+ compileInsertCount(this.contract, this.tableName, mappedRows),
1054
+ annotationsMap,
1055
+ );
970
1056
  await executeQueryPlan<Record<string, unknown>>(this.ctx.runtime, compiled).toArray();
971
1057
  return data.length;
972
1058
  }
@@ -976,13 +1062,17 @@ export class Collection<
976
1062
  * On conflict, `ON CONFLICT DO NOTHING RETURNING ...` may return zero rows,
977
1063
  * so this method may issue a follow-up reload query to return the existing row.
978
1064
  */
979
- async upsert(input: {
980
- create: ResolvedCreateInput<TContract, ModelName, State['variantName']>;
981
- update: Partial<DefaultModelRow<TContract, ModelName>>;
982
- conflictOn?: UniqueConstraintCriterion<TContract, ModelName>;
983
- }): Promise<Row> {
1065
+ async upsert(
1066
+ input: {
1067
+ create: ResolvedCreateInput<TContract, ModelName, State['variantName']>;
1068
+ update: Partial<DefaultModelRow<TContract, ModelName>>;
1069
+ conflictOn?: UniqueConstraintCriterion<TContract, ModelName>;
1070
+ },
1071
+ configure?: (meta: MetaBuilder<'write'>) => void,
1072
+ ): Promise<Row> {
984
1073
  assertReturningCapability(this.contract, 'upsert()');
985
1074
  this.#assertNotMtiVariant('upsert()');
1075
+ const annotationsMap = this.#collectAnnotationsFromMeta(configure, 'write', 'upsert');
986
1076
 
987
1077
  const mappedCreateRows = this.#mapCreateRows([input.create as Record<string, unknown>]);
988
1078
  const createValues = mappedCreateRows[0] ?? {};
@@ -1006,13 +1096,16 @@ export class Collection<
1006
1096
  this.state.selectedFields,
1007
1097
  parentJoinColumns,
1008
1098
  );
1009
- const compiled = compileUpsertReturning(
1010
- this.contract,
1011
- this.tableName,
1012
- createValues,
1013
- updateValues,
1014
- conflictColumns,
1015
- selectedForUpsert,
1099
+ const compiled = mergeAnnotations(
1100
+ compileUpsertReturning(
1101
+ this.contract,
1102
+ this.tableName,
1103
+ createValues,
1104
+ updateValues,
1105
+ conflictColumns,
1106
+ selectedForUpsert,
1107
+ ),
1108
+ annotationsMap,
1016
1109
  );
1017
1110
  const row = await executeMutationReturningSingleRow<Row>({
1018
1111
  contract: this.contract,
@@ -1042,10 +1135,25 @@ export class Collection<
1042
1135
  throw new Error(`upsert() for model "${this.modelName}" did not return a row`);
1043
1136
  }
1044
1137
 
1138
+ /**
1139
+ * Write terminal: update matching rows and return the first one (or
1140
+ * null when no row matched).
1141
+ *
1142
+ * Accepts an optional `configure` callback that receives a
1143
+ * `MetaBuilder<'write'>` for attaching typed annotations.
1144
+ *
1145
+ * Note: when the input contains nested-mutation callbacks, the
1146
+ * operation is executed as a graph of internal queries via
1147
+ * `withMutationScope`. In that path, annotations apply to the logical
1148
+ * `update()` call but do not currently flow into each constituent SQL
1149
+ * statement issued for the related rows.
1150
+ */
1045
1151
  async update(
1046
1152
  data: State['hasWhere'] extends true ? MutationUpdateInput<TContract, ModelName> : never,
1153
+ configure?: (meta: MetaBuilder<'write'>) => void,
1047
1154
  ): Promise<Row | null> {
1048
1155
  assertReturningCapability(this.contract, 'update()');
1156
+ const annotationsMap = this.#collectAnnotationsFromMeta(configure, 'write', 'update');
1049
1157
 
1050
1158
  if (
1051
1159
  hasNestedMutationCallbacks(this.contract, this.modelName, data as Record<string, unknown>)
@@ -1072,10 +1180,11 @@ export class Collection<
1072
1180
  return null;
1073
1181
  }
1074
1182
  const narrowed = scoped.#clone({ filters: [identityWhere] });
1075
- const rows = await narrowed.updateAll(
1183
+ const rows = await narrowed.#updateAllWithAnnotations(
1076
1184
  data as State['hasWhere'] extends true
1077
1185
  ? Partial<DefaultModelRow<TContract, ModelName>>
1078
1186
  : never,
1187
+ annotationsMap,
1079
1188
  );
1080
1189
  return rows[0] ?? null;
1081
1190
  });
@@ -1083,6 +1192,17 @@ export class Collection<
1083
1192
 
1084
1193
  updateAll(
1085
1194
  data: State['hasWhere'] extends true ? Partial<DefaultModelRow<TContract, ModelName>> : never,
1195
+ configure?: (meta: MetaBuilder<'write'>) => void,
1196
+ ): AsyncIterableResult<Row> {
1197
+ return this.#updateAllWithAnnotations(
1198
+ data,
1199
+ this.#collectAnnotationsFromMeta(configure, 'write', 'updateAll'),
1200
+ );
1201
+ }
1202
+
1203
+ #updateAllWithAnnotations(
1204
+ data: State['hasWhere'] extends true ? Partial<DefaultModelRow<TContract, ModelName>> : never,
1205
+ annotationsMap: ReadonlyMap<string, AnnotationValue<unknown, OperationKind>> | undefined,
1086
1206
  ): AsyncIterableResult<Row> {
1087
1207
  assertReturningCapability(this.contract, 'updateAll()');
1088
1208
 
@@ -1099,12 +1219,15 @@ export class Collection<
1099
1219
  this.state.selectedFields,
1100
1220
  parentJoinColumns,
1101
1221
  );
1102
- const compiled = compileUpdateReturning(
1103
- this.contract,
1104
- this.tableName,
1105
- mappedData,
1106
- this.state.filters,
1107
- selectedForUpdate,
1222
+ const compiled = mergeAnnotations(
1223
+ compileUpdateReturning(
1224
+ this.contract,
1225
+ this.tableName,
1226
+ mappedData,
1227
+ this.state.filters,
1228
+ selectedForUpdate,
1229
+ ),
1230
+ annotationsMap,
1108
1231
  );
1109
1232
  return dispatchMutationRows<Row>({
1110
1233
  contract: this.contract,
@@ -1119,6 +1242,7 @@ export class Collection<
1119
1242
 
1120
1243
  async updateCount(
1121
1244
  data: State['hasWhere'] extends true ? Partial<DefaultModelRow<TContract, ModelName>> : never,
1245
+ configure?: (meta: MetaBuilder<'write'>) => void,
1122
1246
  ): Promise<number> {
1123
1247
  const mappedData = mapModelDataToStorageRow(this.contract, this.modelName, data);
1124
1248
  if (Object.keys(mappedData).length === 0) {
@@ -1127,6 +1251,9 @@ export class Collection<
1127
1251
 
1128
1252
  applyUpdateDefaults(this.ctx, this.tableName, mappedData);
1129
1253
 
1254
+ // Annotations attach to the write, not the matching read.
1255
+ const annotationsMap = this.#collectAnnotationsFromMeta(configure, 'write', 'updateCount');
1256
+
1130
1257
  const primaryKeyColumn = resolvePrimaryKeyColumn(this.contract, this.tableName);
1131
1258
  const countState: CollectionState = {
1132
1259
  ...emptyState(),
@@ -1139,21 +1266,28 @@ export class Collection<
1139
1266
  countCompiled,
1140
1267
  ).toArray();
1141
1268
 
1142
- const compiled = compileUpdateCount(
1143
- this.contract,
1144
- this.tableName,
1145
- mappedData,
1146
- this.state.filters,
1269
+ const compiled = mergeAnnotations(
1270
+ compileUpdateCount(this.contract, this.tableName, mappedData, this.state.filters),
1271
+ annotationsMap,
1147
1272
  );
1148
1273
  await executeQueryPlan<Record<string, unknown>>(this.ctx.runtime, compiled).toArray();
1149
1274
 
1150
1275
  return matchingRows.length;
1151
1276
  }
1152
1277
 
1278
+ /**
1279
+ * Write terminal: delete matching rows and return the first one (or
1280
+ * null when no row matched).
1281
+ *
1282
+ * Accepts an optional `configure` callback that receives a
1283
+ * `MetaBuilder<'write'>` for attaching typed annotations.
1284
+ */
1153
1285
  async delete(
1154
1286
  this: State['hasWhere'] extends true ? Collection<TContract, ModelName, Row, State> : never,
1287
+ configure?: (meta: MetaBuilder<'write'>) => void,
1155
1288
  ): Promise<Row | null> {
1156
1289
  assertReturningCapability(this.contract, 'delete()');
1290
+ const annotationsMap = this.#collectAnnotationsFromMeta(configure, 'write', 'delete');
1157
1291
  return withMutationScope(this.ctx.runtime, async (scope) => {
1158
1292
  const scoped = this.#withRuntime(scope);
1159
1293
  const identityWhere = await scoped.#findFirstMatchingRowIdentityWhere();
@@ -1161,29 +1295,38 @@ export class Collection<
1161
1295
  return null;
1162
1296
  }
1163
1297
  const narrowed = scoped.#clone({ filters: [identityWhere] });
1164
- const rows = await narrowed.#executeDeleteReturning().toArray();
1298
+ const rows = await narrowed.#executeDeleteReturning(annotationsMap).toArray();
1165
1299
  return rows[0] ?? null;
1166
1300
  });
1167
1301
  }
1168
1302
 
1169
1303
  deleteAll(
1170
1304
  this: State['hasWhere'] extends true ? Collection<TContract, ModelName, Row, State> : never,
1305
+ configure?: (meta: MetaBuilder<'write'>) => void,
1306
+ ): AsyncIterableResult<Row> {
1307
+ return (this as Collection<TContract, ModelName, Row, State>).#deleteAllWithAnnotations(
1308
+ this.#collectAnnotationsFromMeta(configure, 'write', 'deleteAll'),
1309
+ );
1310
+ }
1311
+
1312
+ #deleteAllWithAnnotations(
1313
+ annotationsMap: ReadonlyMap<string, AnnotationValue<unknown, OperationKind>> | undefined,
1171
1314
  ): AsyncIterableResult<Row> {
1172
1315
  assertReturningCapability(this.contract, 'deleteAll()');
1173
- return this.#executeDeleteReturning();
1316
+ return this.#executeDeleteReturning(annotationsMap);
1174
1317
  }
1175
1318
 
1176
- #executeDeleteReturning(): AsyncIterableResult<Row> {
1319
+ #executeDeleteReturning(
1320
+ annotationsMap: ReadonlyMap<string, AnnotationValue<unknown, OperationKind>> | undefined,
1321
+ ): AsyncIterableResult<Row> {
1177
1322
  const parentJoinColumns = this.state.includes.map((include) => include.localColumn);
1178
1323
  const { selectedForQuery: selectedForDelete, hiddenColumns } = augmentSelectionForJoinColumns(
1179
1324
  this.state.selectedFields,
1180
1325
  parentJoinColumns,
1181
1326
  );
1182
- const compiled = compileDeleteReturning(
1183
- this.contract,
1184
- this.tableName,
1185
- this.state.filters,
1186
- selectedForDelete,
1327
+ const compiled = mergeAnnotations(
1328
+ compileDeleteReturning(this.contract, this.tableName, this.state.filters, selectedForDelete),
1329
+ annotationsMap,
1187
1330
  );
1188
1331
  return dispatchMutationRows<Row>({
1189
1332
  contract: this.contract,
@@ -1198,7 +1341,11 @@ export class Collection<
1198
1341
 
1199
1342
  async deleteCount(
1200
1343
  this: State['hasWhere'] extends true ? Collection<TContract, ModelName, Row, State> : never,
1344
+ configure?: (meta: MetaBuilder<'write'>) => void,
1201
1345
  ): Promise<number> {
1346
+ // Annotations attach to the write, not the matching read.
1347
+ const annotationsMap = this.#collectAnnotationsFromMeta(configure, 'write', 'deleteCount');
1348
+
1202
1349
  const primaryKeyColumn = resolvePrimaryKeyColumn(this.contract, this.tableName);
1203
1350
  const countState: CollectionState = {
1204
1351
  ...emptyState(),
@@ -1211,7 +1358,10 @@ export class Collection<
1211
1358
  countCompiled,
1212
1359
  ).toArray();
1213
1360
 
1214
- const compiled = compileDeleteCount(this.contract, this.tableName, this.state.filters);
1361
+ const compiled = mergeAnnotations(
1362
+ compileDeleteCount(this.contract, this.tableName, this.state.filters),
1363
+ annotationsMap,
1364
+ );
1215
1365
  await executeQueryPlan<Record<string, unknown>>(this.ctx.runtime, compiled).toArray();
1216
1366
 
1217
1367
  return matchingRows.length;
@@ -1388,4 +1538,60 @@ export class Collection<
1388
1538
  modelName: this.modelName,
1389
1539
  });
1390
1540
  }
1541
+
1542
+ /**
1543
+ * Invokes the user-supplied configurator (if any) against a freshly
1544
+ * constructed read meta builder, and returns a clone whose
1545
+ * `state.annotations` carries the recorded map. Used by read
1546
+ * terminals that flow annotations through state (`all`, `first`).
1547
+ *
1548
+ * Returns the receiver unchanged when no configurator was supplied
1549
+ * or when the configurator did not call `meta.annotate(...)`. The
1550
+ * meta builder's `annotate` method enforces applicability at the
1551
+ * type level and at runtime, so terminal code does not need to
1552
+ * re-validate.
1553
+ */
1554
+ #withAnnotationsFromMeta(
1555
+ configure: ((meta: MetaBuilder<'read'>) => void) | undefined,
1556
+ terminalName: string,
1557
+ ): this {
1558
+ if (configure === undefined) {
1559
+ return this;
1560
+ }
1561
+ const meta = createMetaBuilder('read', terminalName);
1562
+ configure(meta);
1563
+ if (meta.annotations.size === 0) {
1564
+ return this;
1565
+ }
1566
+ const next = new Map(this.state.annotations);
1567
+ for (const [namespace, value] of meta.annotations) {
1568
+ next.set(namespace, value);
1569
+ }
1570
+ return this.#clone({ annotations: next }) as this;
1571
+ }
1572
+
1573
+ /**
1574
+ * Invokes the user-supplied configurator (if any) against a freshly
1575
+ * constructed meta builder of the given operation kind, and returns
1576
+ * the recorded annotation map (or `undefined` when empty). Used by
1577
+ * terminals where annotations don't flow through `state` — the
1578
+ * compiled plan is post-wrapped via `mergeAnnotations` instead.
1579
+ * Read terminals `all` and `first` populate `state.annotations`
1580
+ * via `#withAnnotationsFromMeta` instead; `aggregate` uses this
1581
+ * post-wrap path because its compile function doesn't take `state`.
1582
+ * The meta builder's `annotate` method enforces applicability at the
1583
+ * type level and at runtime.
1584
+ */
1585
+ #collectAnnotationsFromMeta<K extends OperationKind>(
1586
+ configure: ((meta: MetaBuilder<K>) => void) | undefined,
1587
+ kind: K,
1588
+ terminalName: string,
1589
+ ): ReadonlyMap<string, AnnotationValue<unknown, OperationKind>> | undefined {
1590
+ if (configure === undefined) {
1591
+ return undefined;
1592
+ }
1593
+ const meta = createMetaBuilder(kind, terminalName);
1594
+ configure(meta);
1595
+ return meta.annotations.size === 0 ? undefined : meta.annotations;
1596
+ }
1391
1597
  }
@@ -1,4 +1,10 @@
1
1
  import type { Contract } from '@prisma-next/contract/types';
2
+ import type {
3
+ AnnotationValue,
4
+ MetaBuilder,
5
+ OperationKind,
6
+ } from '@prisma-next/framework-components/runtime';
7
+ import { createMetaBuilder } from '@prisma-next/framework-components/runtime';
2
8
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
3
9
  import {
4
10
  AggregateExpr,
@@ -13,7 +19,7 @@ import { createAggregateBuilder, isAggregateSelector } from './aggregate-builder
13
19
  import { getFieldToColumnMap } from './collection-contract';
14
20
  import { mapStorageRowToModelFields } from './collection-runtime';
15
21
  import { executeQueryPlan } from './execute-query-plan';
16
- import { compileGroupedAggregate } from './query-plan';
22
+ import { compileGroupedAggregate, mergeAnnotations } from './query-plan';
17
23
  import type {
18
24
  AggregateBuilder,
19
25
  AggregateResult,
@@ -82,8 +88,16 @@ export class GroupedCollection<
82
88
  }) as GroupedCollection<TContract, ModelName, GroupFields>;
83
89
  }
84
90
 
91
+ /**
92
+ * Read terminal: run a grouped aggregate query.
93
+ *
94
+ * Accepts an optional `configure` callback that receives a
95
+ * `MetaBuilder<'read'>` for attaching typed annotations.
96
+ * Annotations are merged into the compiled plan's `meta.annotations`.
97
+ */
85
98
  async aggregate<Spec extends AggregateSpec>(
86
99
  fn: (aggregate: AggregateBuilder<TContract, ModelName>) => Spec,
100
+ configure?: (meta: MetaBuilder<'read'>) => void,
87
101
  ): Promise<
88
102
  Array<
89
103
  SimplifyDeep<
@@ -103,13 +117,25 @@ export class GroupedCollection<
103
117
  }
104
118
  }
105
119
 
106
- const compiled = compileGroupedAggregate(
107
- this.contract,
108
- this.tableName,
109
- this.baseFilters,
110
- this.groupByColumns,
111
- aggregateSpec,
112
- combineWhereExprs(this.havingFilters),
120
+ let annotationsMap: ReadonlyMap<string, AnnotationValue<unknown, OperationKind>> | undefined;
121
+ if (configure !== undefined) {
122
+ const meta = createMetaBuilder('read', 'groupBy.aggregate');
123
+ configure(meta);
124
+ if (meta.annotations.size > 0) {
125
+ annotationsMap = meta.annotations;
126
+ }
127
+ }
128
+
129
+ const compiled = mergeAnnotations(
130
+ compileGroupedAggregate(
131
+ this.contract,
132
+ this.tableName,
133
+ this.baseFilters,
134
+ this.groupByColumns,
135
+ aggregateSpec,
136
+ combineWhereExprs(this.havingFilters),
137
+ ),
138
+ annotationsMap,
113
139
  );
114
140
  const rows = await executeQueryPlan<Record<string, unknown>>(
115
141
  this.ctx.runtime,