1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
//! Typed grouped aggregate queries (`GROUP BY` roll-ups), issue #1364.
//!
//! `count`, `sum`, `avg`, `min` and `max` over a table, bucketed by a group
//! column, expressed declaratively on a `#[autumn_web::repository]` trait:
//!
//! ```rust,ignore
//! #[autumn_web::repository(Vote, table = "votes")]
//! pub trait VoteRepository {
//! /// SUM(value) GROUP BY post_id → `Vec<(post_id, Option<sum>)>`.
//! fn sum_value_grouped_by_post_id() -> Vec<(i64, Option<i64>)>;
//! /// COUNT(*) GROUP BY variant → `Vec<(variant, count)>`.
//! fn count_grouped_by_variant() -> Vec<(String, i64)>;
//! }
//! ```
//!
//! Each declared method becomes an **inherent method** on the generated `Pg*`
//! repository struct that returns a lazy [`GroupedAggregate`] builder (mirroring
//! `find_in_batches`). Nothing runs until a terminal [`load`](GroupedAggregate::load):
//!
//! ```rust,ignore
//! // Top-5 posts by score, highest first.
//! let top: Vec<(i64, Option<i64>)> = repo
//! .sum_value_grouped_by_post_id()
//! .order_by_aggregate_desc()
//! .limit(5)
//! .load()
//! .await?;
//!
//! // A day-bucketed time series over a bounded window.
//! let per_day: Vec<(chrono::NaiveDateTime, i64)> = repo
//! .count_grouped_by_created_at()
//! .bucket(DateBucket::Day)
//! .filter_range(window_start, window_end)
//! .load()
//! .await?;
//! ```
//!
//! ## Value (`V`) and key (`K`) type rules
//!
//! The trait method declares the pair type `Vec<(K, V)>`; the macro reads `K`
//! and `V` from it and bakes the matching Postgres bind/result SQL types.
//!
//! | method | `V` |
//! |---------------------|--------------------------------|
//! | `count_grouped_by_` | `i64` |
//! | `sum_*_grouped_by_` | `Option<T>` (`T` = column type)|
//! | `min_*_grouped_by_` | `Option<T>` |
//! | `max_*_grouped_by_` | `Option<T>` |
//! | `avg_*_grouped_by_` | `Option<f64>` |
//!
//! `K` is the group column's Rust type (or, under [`bucket`](GroupedAggregate::bucket),
//! the bucket-start timestamp's type). `sum`/`min`/`max`/`avg` are null-safe:
//! a group whose values are all `NULL` yields `None`, and an empty result set
//! is an empty `Vec`.
//!
//! A nullable group-key **type** (`K = Option<T>`) is unsupported and rejected
//! at compile time. A nullable group-key **column** is safe, however: rows with
//! a `NULL` group key are silently **excluded** from the results (the generated
//! SQL guards the group column with `IS NOT NULL`), so a `NULL` group is simply
//! omitted rather than deserialized into the non-nullable `K`. Nullable
//! **value** columns are fine — an all-`NULL` group yields `(key, None)`.
//!
//! ## Encrypted columns
//!
//! Grouped aggregates are **not** available on an `#[encrypted(...)]` column
//! (neither as the group key nor as an aggregated value): the column stores
//! ciphertext, so grouping would return ciphertext keys and `.filter_eq(..)`
//! would compare plaintext against ciphertext and match nothing. A method that
//! groups on (or aggregates over) an encrypted column returns an error at call
//! time. Use a raw query instead, or group on a non-encrypted column.
//!
//! ## Scoping
//!
//! The generated query composes the repository's soft-delete filter, tenant
//! scoping and read routing exactly like `count`, and acquires its connection
//! through the same read-route helper — so replica routing and multi-tenancy
//! come for free. `sum`/`avg`/`min`/`max` cannot be merged across shards, so a
//! sharded, tenant-scoped repository used via `across_tenants()` rejects
//! grouped aggregates rather than returning a per-shard-partial answer.
use Future;
use Pin;
use crateAutumnResult;
/// Time-bucket granularity for a `date_trunc`-grouped aggregate (AC5).
///
/// Passing a bucket to [`GroupedAggregate::bucket`] groups by
/// `date_trunc('<unit>', <group_col>)` instead of the raw column, so the key
/// `K` becomes the bucket-start timestamp. The group column must be a
/// timestamp type.
/// Ordering applied to the aggregated value for top-N queries (AC3).
/// The mutable builder state a [`GroupedAggregate`] threads to its executor.
///
/// Public so the macro-generated executor can read it; construct one only via
/// the generated repository methods and the chainable builder setters.
/// A boxed, owned future produced by a grouped-aggregate executor.
type AggFuture<'a, K, V> = ;
/// The macro-generated executor: given the finalized options, runs the
/// parameterized `GROUP BY` query against the repository it captured.
type AggExec<'a, K, V> = ;
/// A lazy, chainable builder for one grouped aggregate query.
///
/// Created by a generated `count_grouped_by_*` / `sum_*_grouped_by_*` /
/// `avg_*` / `min_*` / `max_*` repository method. Chain the setters, then call
/// [`load`](Self::load) to execute. Dropping the builder without loading runs
/// no query.
///
/// `K` is the group-column key type and `V` the aggregated value type (see the
/// [module docs](crate::aggregate) for the `V` rules).
/// `.bucket()` is a compile-time-gated setter: it exists **only** when the group
/// key `K` is a timestamp type, because it swaps the raw group column for
/// `date_trunc('<unit>', <group_col>)`, whose result is always a `timestamp`.
///
/// Defining it on these key-specific impls (rather than the blanket
/// `impl<'a, K, V>`) means a non-temporal key — e.g. an `i64` `post_id` from
/// `count_grouped_by_post_id()` — has no `.bucket()` method at all, so an invalid
/// `date_trunc(unit, bigint)` query is rejected by the type system instead of
/// failing at runtime.
///
/// Only `NaiveDateTime` (`timestamp`) and `DateTime<Utc>` (`timestamptz`) are
/// bucketable. `NaiveDate` (`date`) is intentionally excluded: Postgres
/// `date_trunc(unit, date)` returns a `timestamp`, which would not match a
/// `NaiveDate` result row.