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
//! Arithmetic operator overloads on [`Expr<T>`] — gated on a sealed
//! [`Numeric`] trait so only framework-blessed numeric types compose.
//!
//! # What
//!
//! `impl<T: Numeric> std::ops::Add<Expr<T>> for Expr<T>` (and likewise
//! `Sub`, `Mul`, `Div`) — each consumes both operands and returns an
//! `Expr<T>` whose node is the matching [`super::node::ExprNode`]
//! arithmetic variant. [`Numeric`] is sealed via the standard private-
//! supertrait pattern so downstream crates cannot extend the trait;
//! Djogi is the sole arbiter of which types admit SQL arithmetic.
//!
//! # Why sealed?
//!
//! 1. **Avoid accidental impls.** A user adding a newtype `struct
//! Percent(f64);` should not be able to `impl Numeric for Percent`
//! and silently enter an arithmetic composition path the emitter
//! has no rule for. The sealed pattern blocks the trait at the
//! crate boundary.
//! 2. **Framework controls the bind surface.** Adding `Decimal` or
//! `Interval` to `Numeric` must happen alongside the matching
//! [`super::node::ExprNode`] bind wiring and the [`FilterValue`
//! ][crate::query::condition::FilterValue] variant. Sealing keeps
//! those two sides in lockstep.
//! 3. **Forward-compat for Phase 5.** The plan explicitly reserves
//! `Decimal` for Phase 5 and `Interval` for the interval-arithmetic
//! milestone. Phase 4 ships the integer + float subset only; sealing
//! means adding the missing types later is additive (one new impl)
//! rather than breaking (an existing blanket impl would already
//! admit them).
//!
//! # Why the operator overloads and not named `.add(..)` methods?
//!
//! The ergonomic target is Django-style:
//!
//! ```ignore
//! .update_expr(|f| f.view_count.set_expr(f.view_count.as_expr() + 1))
//! ```
//!
//! — `+` reads as SQL `+`. Named methods would force every arithmetic
//! site to be an explicit function call, which buries the
//! composition. `std::ops::Add` is the idiomatic choice; the sealed
//! `Numeric` bound keeps it safe.
use crateExpr;
use crateExprNode;
use ;
/// Sealed marker trait — only Djogi-blessed numeric types implement
/// this. Phase 4 ships integer + float; Phase 5 extends with `Decimal`;
/// the interval-arithmetic milestone extends again with `Interval`.
///
/// Crate-private supertrait `sealed::Sealed` is the seal — downstream
/// code cannot name `sealed::Sealed`, so `impl Numeric for MyType {}`
/// fails at "the trait `Sealed` is not implemented for `MyType`".
///
/// # Sum / avg cast targets
///
/// Each `Numeric` impl carries a pair of associated constants —
/// [`Numeric::SUM_CAST`] and [`Numeric::AVG_CAST`] — that name the
/// Postgres type the aggregate result must be cast to so it decodes
/// back into the Rust type the typed
/// [`super::aggregate::AggregateExpr<Out>`] surface promised.
///
/// Why they exist: Postgres widens integer aggregates. `SUM(BIGINT)`
/// returns `NUMERIC`; `AVG(BIGINT)` returns `NUMERIC`; `SUM(SMALLINT)`
/// returns `BIGINT`. The typed wrapper's `Out = V` / `Out = f64`
/// promise only holds if we emit an explicit `::<pg_type>` narrowing
/// cast. Stamping the target as an associated `&'static str` per
/// `Numeric` impl keeps the aggregate builder trivial — one
/// `V::SUM_CAST` / `V::AVG_CAST` lookup, no per-type switch on the
/// method side.
// Per-type cast targets. Integer sums widen in Postgres
// (SMALLINT -> BIGINT, BIGINT -> NUMERIC); we narrow back to the input
// type so the typed `Out = V` decode path works. Floats don't widen on
// SUM, but we still emit the cast for uniformity — Postgres treats
// `REAL::REAL` and `DOUBLE PRECISION::DOUBLE PRECISION` as no-ops, so
// the round-trip SQL is semantically identical and the emitter stays
// one code path across all Numeric types.
//
// AVG always returns NUMERIC for integer inputs and DOUBLE PRECISION
// for floating-point inputs; casting to DOUBLE PRECISION lets us
// decode into `f64` uniformly, which matches the typed surface's
// `Out = f64` promise on `avg()`.
// Per-type cast target matches the Rust type `Out = V` promises:
// `i16` maps to Postgres `SMALLINT`, `i32` to `INTEGER`, `i64`
// to `BIGINT`, `f32` to `REAL`, `f64` to `DOUBLE PRECISION`. Narrowing
// casts from a wider Postgres type (e.g. `NUMERIC -> BIGINT`) raise a
// `numeric_value_out_of_range` error if the sum genuinely overflows
// the target, which is the correct behaviour — users aggregating values
// that wouldn't fit back into `V` should declare a larger `V` on the
// column or use `ctx.raw_scalar`. This is documented on `sum()` below.
// `time::Duration` is the Rust-side representation of a Postgres `INTERVAL`.
// Adding it to `Numeric` lets `Expr<Duration> + Expr<Duration>` (interval +
// interval), `Expr<Duration> * Expr<i64>` (when mixed-type Mul lands), and
// most importantly `Expr<Duration>` to act as the RHS in the heterogeneous
// `Expr<OffsetDateTime> + Expr<Duration>` impl below.
//
// SUM_CAST / AVG_CAST: Postgres `SUM(INTERVAL)` returns `INTERVAL` (no
// widening), and `AVG(INTERVAL)` returns `INTERVAL`. The cast targets match
// the Postgres names for INTERVAL to satisfy the trait contract, but they are
// only used by the aggregate terminal's `emit_aggregate_with_cast` path, which
// is rarely exercised for INTERVAL columns. Any caller who needs precision
// INTERVAL aggregation should use `ctx.raw_scalar` until a typed aggregate
// path lands.
// The operator impls are one-per-op because `std::ops::Add` /
// `::Sub` / `::Mul` / `::Div` are separate traits. A macro would hide
// the boilerplate but also the place where a future reviewer would
// look to audit "which arithmetic forms does the emitter support?".
// Explicit impls make that audit a grep for `impl Add for Expr`.
// ── Interval +/- interval arithmetic ────────────────────────────────────
//
// `crate::Interval` is `djogi::Interval` (a three-component
// month/day/microsecond representation). SQL supports interval
// arithmetic natively, and callers now need `Expr<crate::Interval>`
// to compose `col + $interval` / `col - $interval` update expressions
// through narrow impls.
// ── Heterogeneous datetime + interval arithmetic ───────────────────────────
//
// `Expr<OffsetDateTime> + Expr<Duration>` → `Expr<OffsetDateTime>`.
//
// Postgres supports `TIMESTAMPTZ + INTERVAL` natively, producing a
// `TIMESTAMPTZ` result. The typed `Add` impl above only covers
// `Expr<T> + Expr<T>` where `T: Numeric` (same-type addition). The
// DateTime + Interval combination is fundamentally heterogeneous: the two
// operands have different types. A dedicated impl here keeps the
// crate-private `Add` seal intact — downstream crates cannot add new
// numeric types — while still covering the idiomatic date-arithmetic shape.
//
// `Expr<OffsetDateTime> - Expr<Duration>` is the corresponding subtraction.