reddb_server/storage/schema/cast_catalog.rs
1//! Cast catalog — Fase 3 foundation for explicit / implicit type
2//! coercion.
3//!
4//! Mirrors PostgreSQL's `pg_cast` catalog: a static table of allowed
5//! source→target type conversions, each with a **context** that
6//! controls when the coercion is legal:
7//!
8//! - `Implicit` — the resolver may insert this cast without asking
9//! the user (e.g. `int → float` when adding an int to a float).
10//! - `Assignment` — allowed only when assigning to a column of the
11//! target type in an INSERT / UPDATE (e.g. `float → int` with
12//! truncation). Not available during expression evaluation.
13//! - `Explicit` — only via a user-written `CAST(expr AS type)` or
14//! `expr::type`.
15//!
16//! The table is deliberately small for Week 3: just the numeric and
17//! string families plus the handful of built-in widening / narrowing
18//! paths the existing runtime evaluator already implements. Later
19//! weeks will flesh it out to cover dates, network, domain, and
20//! user-defined casts (see `CREATE CAST` in the PG docs).
21//!
22//! The catalog is queried by `find_cast(src, target, context)` which
23//! returns `true` when the coercion is legal for the requested
24//! context. The expression resolver in Fase 3 analyze will call this
25//! to decide whether operator overload candidates are viable.
26
27use super::types::DataType;
28
29/// Context in which a cast is being attempted. Matches the PG
30/// `CoercionContext` enum modulo feature gaps.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub enum CastContext {
33 /// The cast is being used inside an expression and must be
34 /// inserted implicitly by the resolver — `a + b` where `a` is
35 /// `int` and `b` is `float` requires an `int → float` implicit
36 /// cast on the left operand.
37 Implicit,
38 /// The cast is being applied because the resolver is about to
39 /// assign the value to a target column of a specific type
40 /// (INSERT / UPDATE RHS → column). Stricter than Implicit
41 /// because assignment-time coercions may lose information
42 /// (truncation, saturation).
43 Assignment,
44 /// The cast is being applied because the user wrote it
45 /// explicitly — `CAST(expr AS type)` or `expr::type`. This is
46 /// the widest context; most allowed casts include Explicit.
47 Explicit,
48}
49
50impl CastContext {
51 /// Returns `true` when a cast legal in `self` is also legal in
52 /// `other`. Used when the catalog entry lists its "minimum"
53 /// required context and the resolver asks whether a specific
54 /// usage satisfies that minimum.
55 ///
56 /// Ordering (widest → narrowest): `Explicit ⊇ Assignment ⊇ Implicit`.
57 /// An implicit cast can be used anywhere; an explicit-only cast
58 /// needs an explicit call site.
59 pub fn allows(self, other: CastContext) -> bool {
60 use CastContext::*;
61 matches!(
62 (self, other),
63 (Explicit, _)
64 | (Assignment, Assignment)
65 | (Assignment, Implicit)
66 | (Implicit, Implicit)
67 )
68 }
69}
70
71/// One row in the static cast catalog. Equivalent to a PG `pg_cast`
72/// tuple modulo the `castfunc` / `castmethod` fields — all reddb
73/// built-in casts today go through the `schema::coerce` module, so
74/// we only need to record the (src, target, context) triple plus a
75/// `lossy` flag that informs diagnostics.
76#[derive(Debug, Clone, Copy)]
77pub struct CastEntry {
78 pub src: DataType,
79 pub target: DataType,
80 /// Minimum context in which this cast may be applied. Implicit
81 /// means "always allowed", Assignment means "INSERT/UPDATE RHS",
82 /// Explicit means "CAST(…) only".
83 pub context: CastContext,
84 /// Whether the cast may lose information (truncation, overflow).
85 /// Diagnostics use this to warn users writing lossy implicit
86 /// conversions (rare — the table avoids listing lossy casts at
87 /// Implicit context on purpose).
88 pub lossy: bool,
89}
90
91/// Static catalog of built-in casts. Each row is a compile-time
92/// constant so lookups are cache-friendly and the whole table lives
93/// in the read-only segment. Categories covered today:
94///
95/// - Numeric widening (int ↔ float ↔ bigint ↔ unsigned)
96/// - Numeric narrowing (explicit / assignment only)
97/// - Anything → text (display_string path)
98/// - text → domain types (email, url, phone, semver, cidr, …)
99/// when the string parses cleanly
100/// - Boolean ↔ integer
101///
102/// Adding a row is cheap — just append. Removing a row is a
103/// breaking change because existing queries may depend on the cast.
104pub const CAST_CATALOG: &[CastEntry] = &[
105 // ── Numeric widening (implicit, lossless) ──
106 entry(
107 DataType::Integer,
108 DataType::BigInt,
109 CastContext::Implicit,
110 false,
111 ),
112 entry(
113 DataType::Integer,
114 DataType::Float,
115 CastContext::Implicit,
116 false,
117 ),
118 entry(
119 DataType::Integer,
120 DataType::Decimal,
121 CastContext::Implicit,
122 false,
123 ),
124 entry(
125 DataType::UnsignedInteger,
126 DataType::Integer,
127 CastContext::Implicit,
128 false,
129 ),
130 entry(
131 DataType::UnsignedInteger,
132 DataType::Float,
133 CastContext::Implicit,
134 false,
135 ),
136 entry(
137 DataType::BigInt,
138 DataType::Float,
139 CastContext::Implicit,
140 false,
141 ),
142 // ── Numeric narrowing (assignment — may truncate) ──
143 entry(
144 DataType::Float,
145 DataType::Integer,
146 CastContext::Assignment,
147 true,
148 ),
149 entry(
150 DataType::Float,
151 DataType::BigInt,
152 CastContext::Assignment,
153 true,
154 ),
155 entry(
156 DataType::Float,
157 DataType::UnsignedInteger,
158 CastContext::Assignment,
159 true,
160 ),
161 entry(
162 DataType::Integer,
163 DataType::UnsignedInteger,
164 CastContext::Assignment,
165 true,
166 ),
167 // ── Any → Text (explicit — every type can be stringified but we
168 // don't want silent string casts in arithmetic expressions) ──
169 entry(
170 DataType::Integer,
171 DataType::Text,
172 CastContext::Explicit,
173 false,
174 ),
175 entry(
176 DataType::UnsignedInteger,
177 DataType::Text,
178 CastContext::Explicit,
179 false,
180 ),
181 entry(
182 DataType::Float,
183 DataType::Text,
184 CastContext::Explicit,
185 false,
186 ),
187 entry(
188 DataType::Boolean,
189 DataType::Text,
190 CastContext::Explicit,
191 false,
192 ),
193 entry(
194 DataType::Timestamp,
195 DataType::Text,
196 CastContext::Explicit,
197 false,
198 ),
199 entry(DataType::Date, DataType::Text, CastContext::Explicit, false),
200 entry(DataType::Time, DataType::Text, CastContext::Explicit, false),
201 entry(DataType::Uuid, DataType::Text, CastContext::Explicit, false),
202 entry(
203 DataType::IpAddr,
204 DataType::Text,
205 CastContext::Explicit,
206 false,
207 ),
208 // ── Text → domain validators (explicit — parsing may fail) ──
209 entry(
210 DataType::Text,
211 DataType::Integer,
212 CastContext::Explicit,
213 true,
214 ),
215 entry(DataType::Text, DataType::Float, CastContext::Explicit, true),
216 entry(
217 DataType::Text,
218 DataType::Boolean,
219 CastContext::Explicit,
220 true,
221 ),
222 entry(DataType::Text, DataType::Email, CastContext::Explicit, true),
223 entry(DataType::Text, DataType::Url, CastContext::Explicit, true),
224 entry(DataType::Text, DataType::Phone, CastContext::Explicit, true),
225 entry(
226 DataType::Text,
227 DataType::Semver,
228 CastContext::Explicit,
229 true,
230 ),
231 entry(DataType::Text, DataType::Cidr, CastContext::Explicit, true),
232 entry(DataType::Text, DataType::Date, CastContext::Explicit, true),
233 entry(DataType::Text, DataType::Time, CastContext::Explicit, true),
234 entry(DataType::Text, DataType::Uuid, CastContext::Explicit, true),
235 entry(DataType::Text, DataType::Color, CastContext::Explicit, true),
236 entry(
237 DataType::Text,
238 DataType::AssetCode,
239 CastContext::Explicit,
240 true,
241 ),
242 entry(DataType::Text, DataType::Money, CastContext::Explicit, true),
243 entry(
244 DataType::Text,
245 DataType::IpAddr,
246 CastContext::Explicit,
247 true,
248 ),
249 // ── Boolean ↔ Integer (explicit to avoid surprise truth-y casts) ──
250 entry(
251 DataType::Boolean,
252 DataType::Integer,
253 CastContext::Explicit,
254 false,
255 ),
256 entry(
257 DataType::Integer,
258 DataType::Boolean,
259 CastContext::Explicit,
260 false,
261 ),
262 // ── Identity casts (implicit, free) ──
263 // Listed so the resolver can always find a trivially-valid entry
264 // when both sides are the same type — otherwise it would fall
265 // through to the "no cast found" error path.
266 entry(
267 DataType::Integer,
268 DataType::Integer,
269 CastContext::Implicit,
270 false,
271 ),
272 entry(
273 DataType::Float,
274 DataType::Float,
275 CastContext::Implicit,
276 false,
277 ),
278 entry(DataType::Text, DataType::Text, CastContext::Implicit, false),
279 entry(
280 DataType::Boolean,
281 DataType::Boolean,
282 CastContext::Implicit,
283 false,
284 ),
285];
286
287/// Helper for building const catalog entries without `..Default::default()`
288/// noise. Const-fn so the whole table stays compile-time.
289const fn entry(src: DataType, target: DataType, context: CastContext, lossy: bool) -> CastEntry {
290 CastEntry {
291 src,
292 target,
293 context,
294 lossy,
295 }
296}
297
298/// Look up whether a cast from `src` to `target` is legal in the
299/// given `ctx`. Returns the matching catalog entry so callers can
300/// inspect the `lossy` flag for diagnostics. Identity casts
301/// (`src == target`) always succeed with an implicit, lossless
302/// synthetic entry if the catalog doesn't list the specific type.
303///
304/// Lookup is linear across the static table — fine for a 40-entry
305/// catalog, and the whole array sits in L1 cache. Future weeks can
306/// switch to a hash-backed index if the table grows past ~500 rows.
307pub fn find_cast(src: DataType, target: DataType, ctx: CastContext) -> Option<CastEntry> {
308 if src == target {
309 return Some(CastEntry {
310 src,
311 target,
312 context: CastContext::Implicit,
313 lossy: false,
314 });
315 }
316 CAST_CATALOG
317 .iter()
318 .find(|e| e.src == src && e.target == target && e.context.allows(ctx))
319 .copied()
320}
321
322/// Returns `true` when `src` can be implicitly coerced to `target`
323/// in expression context — the hot path for operator overload
324/// resolution. Equivalent to `find_cast(src, target, Implicit).is_some()`
325/// but inlines better at call sites.
326pub fn can_implicit_cast(src: DataType, target: DataType) -> bool {
327 find_cast(src, target, CastContext::Implicit).is_some()
328}
329
330/// Returns `true` when `src` can be coerced to `target` for
331/// assignment to a column of type `target` — INSERT / UPDATE RHS.
332pub fn can_assignment_cast(src: DataType, target: DataType) -> bool {
333 find_cast(src, target, CastContext::Assignment).is_some()
334}
335
336/// Returns `true` when the user-written `CAST(src AS target)` is
337/// allowed. The Explicit context is the widest — anything allowed
338/// for Implicit or Assignment is also allowed here.
339pub fn can_explicit_cast(src: DataType, target: DataType) -> bool {
340 find_cast(src, target, CastContext::Explicit).is_some()
341}