Skip to main content

reddb_server/storage/schema/
operator_catalog.rs

1//! Operator catalog — static table of built-in operator
2//! signatures, Fase 3 type-system extension.
3//!
4//! Parallel to `function_catalog` but scoped to infix /
5//! prefix operators. Where `function_catalog` holds one row
6//! per function overload (`UPPER(text) -> text`, `ABS(int) ->
7//! int`, …), this module holds one row per operator overload
8//! (`+(int, int) -> int`, `+(float, float) -> float`, `||(text,
9//! text) -> text`, …).
10//!
11//! Mirrors PG's `pg_operator` with the same simplifications as
12//! `function_catalog`:
13//!
14//! - Name is a `&'static str` symbol ('+', '-', '||', '=', …)
15//! - `lhs_type` / `rhs_type` / `return_type` are concrete
16//!   `DataType`s; polymorphism (`anyelement`) deferred.
17//! - `kind` distinguishes Infix, Prefix, Postfix.
18//! - Pointer to the backing scalar function is NOT stored —
19//!   the runtime evaluator in `expr_eval.rs` still dispatches
20//!   on `BinOp` tags rather than through a function indirection.
21//!   That indirection lands when we have a real function-
22//!   invocation layer for scalar dispatch.
23//!
24//! The catalog is used by the Fase 3 typer (`expr_typing.rs`)
25//! to resolve `BinaryOp` nodes: instead of the hand-rolled
26//! `binop_result_type` match, the typer can walk the catalog
27//! with the call-site LHS/RHS types and pick the best overload
28//! via `func_select_candidate`-style heuristics.
29//!
30//! This module is **not yet wired** into the typer. Wiring
31//! flips `expr_typing::binop_result_type` to call
32//! `operator_catalog::resolve` and falls back to the hand-
33//! rolled path only when no catalog entry matches.
34
35use super::types::DataType;
36
37/// Operator position — infix for binary ops, prefix for unary
38/// `-` and `NOT`, postfix for legacy SQL shapes we don't have
39/// today but reserve for completeness.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum OperatorKind {
42    Infix,
43    Prefix,
44    Postfix,
45}
46
47/// One row in the static operator catalog.
48#[derive(Debug, Clone, Copy)]
49pub struct OperatorEntry {
50    /// Operator symbol: "+", "-", "*", "/", "%", "||", "=",
51    /// "<>", "<", "<=", ">", ">=", "AND", "OR", "NOT".
52    pub name: &'static str,
53    /// Left-hand operand type. For `Prefix` operators this is
54    /// unused and should be `DataType::Nullable` (the "don't
55    /// care" marker).
56    pub lhs_type: DataType,
57    /// Right-hand operand type. Always populated.
58    pub rhs_type: DataType,
59    /// Result type of the operator.
60    pub return_type: DataType,
61    /// Infix / Prefix / Postfix.
62    pub kind: OperatorKind,
63}
64
65const fn infix(name: &'static str, lhs: DataType, rhs: DataType, ret: DataType) -> OperatorEntry {
66    OperatorEntry {
67        name,
68        lhs_type: lhs,
69        rhs_type: rhs,
70        return_type: ret,
71        kind: OperatorKind::Infix,
72    }
73}
74
75const fn prefix(name: &'static str, operand: DataType, ret: DataType) -> OperatorEntry {
76    OperatorEntry {
77        name,
78        lhs_type: DataType::Nullable,
79        rhs_type: operand,
80        return_type: ret,
81        kind: OperatorKind::Prefix,
82    }
83}
84
85/// Static catalog of built-in operators. Grouped by symbol
86/// family for readability.
87pub const OPERATOR_CATALOG: &[OperatorEntry] = &[
88    // ── Arithmetic: + ──
89    infix("+", DataType::Integer, DataType::Integer, DataType::Integer),
90    infix("+", DataType::Integer, DataType::Float, DataType::Float),
91    infix("+", DataType::Float, DataType::Integer, DataType::Float),
92    infix("+", DataType::Float, DataType::Float, DataType::Float),
93    infix("+", DataType::BigInt, DataType::BigInt, DataType::BigInt),
94    infix("+", DataType::Decimal, DataType::Decimal, DataType::Decimal),
95    // ── Arithmetic: - ──
96    infix("-", DataType::Integer, DataType::Integer, DataType::Integer),
97    infix("-", DataType::Float, DataType::Float, DataType::Float),
98    infix("-", DataType::BigInt, DataType::BigInt, DataType::BigInt),
99    infix("-", DataType::Decimal, DataType::Decimal, DataType::Decimal),
100    // Unary negation — prefix operator.
101    prefix("-", DataType::Integer, DataType::Integer),
102    prefix("-", DataType::Float, DataType::Float),
103    prefix("-", DataType::BigInt, DataType::BigInt),
104    prefix("-", DataType::Decimal, DataType::Decimal),
105    // ── Arithmetic: * ──
106    infix("*", DataType::Integer, DataType::Integer, DataType::Integer),
107    infix("*", DataType::Float, DataType::Float, DataType::Float),
108    infix("*", DataType::BigInt, DataType::BigInt, DataType::BigInt),
109    // ── Arithmetic: / (always produces Float) ──
110    infix("/", DataType::Integer, DataType::Integer, DataType::Float),
111    infix("/", DataType::Float, DataType::Float, DataType::Float),
112    infix("/", DataType::BigInt, DataType::BigInt, DataType::Float),
113    // ── Arithmetic: % (modulo) ──
114    infix("%", DataType::Integer, DataType::Integer, DataType::Integer),
115    infix("%", DataType::BigInt, DataType::BigInt, DataType::BigInt),
116    // ── String concat: || ──
117    infix("||", DataType::Text, DataType::Text, DataType::Text),
118    // ── Comparison: = ──
119    infix("=", DataType::Integer, DataType::Integer, DataType::Boolean),
120    infix("=", DataType::Float, DataType::Float, DataType::Boolean),
121    infix("=", DataType::Text, DataType::Text, DataType::Boolean),
122    infix("=", DataType::Boolean, DataType::Boolean, DataType::Boolean),
123    infix("=", DataType::Uuid, DataType::Uuid, DataType::Boolean),
124    infix(
125        "=",
126        DataType::Timestamp,
127        DataType::Timestamp,
128        DataType::Boolean,
129    ),
130    // ── Comparison: <> ──
131    infix(
132        "<>",
133        DataType::Integer,
134        DataType::Integer,
135        DataType::Boolean,
136    ),
137    infix("<>", DataType::Float, DataType::Float, DataType::Boolean),
138    infix("<>", DataType::Text, DataType::Text, DataType::Boolean),
139    // ── Ordered comparisons: <, <=, >, >= ──
140    infix("<", DataType::Integer, DataType::Integer, DataType::Boolean),
141    infix("<", DataType::Float, DataType::Float, DataType::Boolean),
142    infix("<", DataType::Text, DataType::Text, DataType::Boolean),
143    infix(
144        "<",
145        DataType::Timestamp,
146        DataType::Timestamp,
147        DataType::Boolean,
148    ),
149    infix(
150        "<=",
151        DataType::Integer,
152        DataType::Integer,
153        DataType::Boolean,
154    ),
155    infix("<=", DataType::Float, DataType::Float, DataType::Boolean),
156    infix("<=", DataType::Text, DataType::Text, DataType::Boolean),
157    infix(">", DataType::Integer, DataType::Integer, DataType::Boolean),
158    infix(">", DataType::Float, DataType::Float, DataType::Boolean),
159    infix(">", DataType::Text, DataType::Text, DataType::Boolean),
160    infix(
161        ">=",
162        DataType::Integer,
163        DataType::Integer,
164        DataType::Boolean,
165    ),
166    infix(">=", DataType::Float, DataType::Float, DataType::Boolean),
167    infix(">=", DataType::Text, DataType::Text, DataType::Boolean),
168    // ── Logical: AND / OR ──
169    infix(
170        "AND",
171        DataType::Boolean,
172        DataType::Boolean,
173        DataType::Boolean,
174    ),
175    infix(
176        "OR",
177        DataType::Boolean,
178        DataType::Boolean,
179        DataType::Boolean,
180    ),
181    prefix("NOT", DataType::Boolean, DataType::Boolean),
182];
183
184/// Look up every overload for a given operator symbol.
185/// Returns a `Vec` of static references so the typer can
186/// score each candidate without copying.
187pub fn lookup(name: &str) -> Vec<&'static OperatorEntry> {
188    OPERATOR_CATALOG.iter().filter(|e| e.name == name).collect()
189}
190
191/// Resolve an operator call to the best-matching overload.
192/// Same heuristic as `function_catalog::resolve` but for
193/// binary operators:
194///
195/// 1. Filter by exact name match.
196/// 2. Filter by kind (infix / prefix / postfix).
197/// 3. Score each overload by counting exact type matches on
198///    both operand positions.
199/// 4. Tie-break by preferred return type within category.
200///
201/// For prefix operators the `lhs` parameter is ignored — pass
202/// `DataType::Nullable` as a placeholder.
203pub fn resolve(
204    name: &str,
205    kind: OperatorKind,
206    lhs: DataType,
207    rhs: DataType,
208) -> Option<&'static OperatorEntry> {
209    let candidates: Vec<&'static OperatorEntry> = OPERATOR_CATALOG
210        .iter()
211        .filter(|e| e.name == name && e.kind == kind)
212        .collect();
213
214    if candidates.is_empty() {
215        return None;
216    }
217
218    let mut best: Option<(usize, &'static OperatorEntry)> = None;
219    for entry in candidates {
220        let lhs_match = match kind {
221            OperatorKind::Infix => (entry.lhs_type == lhs) as usize,
222            OperatorKind::Prefix | OperatorKind::Postfix => 0,
223        };
224        let rhs_match = (entry.rhs_type == rhs) as usize;
225        let score = lhs_match + rhs_match;
226
227        // Reject zero-exact-match candidates unless the operator
228        // has only one overload (single entry → always-pick).
229        if score == 0 && OPERATOR_CATALOG.iter().filter(|e| e.name == name).count() > 1 {
230            continue;
231        }
232
233        match best {
234            None => best = Some((score, entry)),
235            Some((prev_score, prev_entry)) => {
236                if score > prev_score
237                    || (score == prev_score
238                        && entry.return_type.is_preferred()
239                        && !prev_entry.return_type.is_preferred())
240                {
241                    best = Some((score, entry));
242                }
243            }
244        }
245    }
246
247    best.map(|(_, entry)| entry)
248}