Skip to main content

razeri_plugin_contract/
formula_renderer.rs

1// Copyright (C) 2026 Gabriel Shea
2// Licensed under either of the Apache License, Version 2.0 or the MIT
3// license, at your option.
4
5//! Formula-level rendering extensions with ANSI defaults.
6
7use crate::DataTypeRef;
8use crate::SqlRenderer;
9
10/// Formula-level function rendering with ANSI-safe defaults.
11///
12/// These methods replace platform `match` blocks in formula rendering code.
13pub trait FormulaRenderer: SqlRenderer {
14    /// Render `STRING_AGG` / LISTAGG style aggregation.
15    fn render_string_agg(
16        &self,
17        expr: &str,
18        sep: &str,
19        distinct: bool,
20        order: Option<&str>,
21    ) -> String {
22        let d = if distinct { "DISTINCT " } else { "" };
23        match order {
24            Some(o) => format!("STRING_AGG({d}{expr}, '{sep}' ORDER BY {o})"),
25            None => format!("STRING_AGG({d}{expr}, '{sep}')"),
26        }
27    }
28
29    /// Render `CONCAT_WS`.
30    fn render_concat_ws(&self, separator: &str, parts: &[&str]) -> String {
31        format!("CONCAT_WS('{separator}', {})", parts.join(", "))
32    }
33
34    /// Render `FIRST_VALUE` with optional IGNORE NULLS.
35    fn render_first_value(&self, expr: &str, ignore_nulls: bool) -> String {
36        if ignore_nulls {
37            format!("FIRST_VALUE({expr}) IGNORE NULLS")
38        } else {
39            format!("FIRST_VALUE({expr})")
40        }
41    }
42
43    /// Render `STDDEV_POP` / `STDDEV_SAMP`.
44    fn render_stddev(&self, expr: &str, population: bool) -> String {
45        let func = if population { "STDDEV_POP" } else { "STDDEV_SAMP" };
46        format!("{func}({expr})")
47    }
48
49    /// Render `VAR_POP` / `VAR_SAMP`.
50    fn render_variance(&self, expr: &str, population: bool) -> String {
51        let func = if population { "VAR_POP" } else { "VAR_SAMP" };
52        format!("{func}({expr})")
53    }
54
55    /// Render `PERCENTILE_CONT` / `PERCENTILE_DISC`.
56    fn render_percentile(&self, expr: &str, percentile: f64, continuous: bool) -> String {
57        let func = if continuous { "PERCENTILE_CONT" } else { "PERCENTILE_DISC" };
58        format!("{func}({percentile}) WITHIN GROUP (ORDER BY {expr})")
59    }
60
61    /// Render MEDIAN.
62    fn render_median(&self, expr: &str) -> String {
63        format!("PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY {expr})")
64    }
65
66    /// Render COUNT IF semantics.
67    fn render_count_if(&self, condition: &str) -> String {
68        format!("COUNT(CASE WHEN {condition} THEN 1 END)")
69    }
70
71    /// Render SUM IF semantics.
72    fn render_sum_if(&self, expr: &str, condition: &str) -> String {
73        format!("SUM(CASE WHEN {condition} THEN {expr} END)")
74    }
75
76    /// Render AVG IF semantics.
77    fn render_avg_if(&self, expr: &str, condition: &str) -> String {
78        format!("AVG(CASE WHEN {condition} THEN {expr} END)")
79    }
80
81    /// Render SPLIT / `STRING_TO_ARRAY` style expression.
82    fn render_split(&self, expr: &str, delimiter: &str) -> String {
83        format!("STRING_TO_ARRAY({expr}, '{delimiter}')")
84    }
85
86    /// Render `REGEXP_LIKE` / RLIKE.
87    fn render_regexp_like(&self, expr: &str, pattern: &str) -> String {
88        format!("REGEXP_LIKE({expr}, '{pattern}')")
89    }
90
91    /// Render `REGEXP_REPLACE`.
92    fn render_regexp_replace(&self, expr: &str, pattern: &str, replacement: &str) -> String {
93        format!("REGEXP_REPLACE({expr}, '{pattern}', '{replacement}')")
94    }
95
96    /// Render `REGEXP_EXTRACT` / `REGEXP_SUBSTR`.
97    fn render_regexp_extract(&self, expr: &str, pattern: &str, group: Option<usize>) -> String {
98        match group {
99            Some(g) => format!("REGEXP_SUBSTR({expr}, '{pattern}', 1, 1, 'e', {g})"),
100            None => format!("REGEXP_SUBSTR({expr}, '{pattern}')"),
101        }
102    }
103
104    // There is deliberately no `render_generate_sid` here, and there never will
105    // be one. A `GENERATE_SID` formula in a view and a dimension load over the
106    // same column must key a member the same way, so Razeri composes both from
107    // one derivation and asks the dialect only for its
108    // [`SidSpelling`](crate::SidSpelling). Eight dialect crates used to spell
109    // their own; a second definition that does not exist cannot drift back.
110
111    /// Render NVL2 semantics.
112    fn render_nvl2(&self, expr: &str, not_null: &str, is_null: &str) -> String {
113        format!("CASE WHEN {expr} IS NOT NULL THEN {not_null} ELSE {is_null} END")
114    }
115
116    /// Render multi-branch IF semantics.
117    fn render_multi_if(&self, conditions: &[(&str, &str)], else_: &str) -> String {
118        use std::fmt::Write;
119        let mut sql = "CASE".to_string();
120        for (cond, then_expr) in conditions {
121            let _ = write!(sql, " WHEN {cond} THEN {then_expr}");
122        }
123        let _ = write!(sql, " ELSE {else_} END");
124        sql
125    }
126
127    // ================================================================
128    // Operator rendering
129    // ================================================================
130    //
131    // An operator your engine does not have is answered `None`, and Razeri
132    // then refuses a formula that needs it, naming the operator and your
133    // engine. It never writes the nearest function in the operator's place:
134    // a function call is not an operator, and a formula that composes one
135    // would be written wrong.
136
137    /// **This engine's exponentiation operator**, or `None` where it has none.
138    ///
139    /// The default is `^`, which is exponentiation on `PostgreSQL` and
140    /// `DuckDB` and exclusive-or on several other engines. Answer `None` where
141    /// your engine raises to a power only through a function such as
142    /// `POWER(a, b)`.
143    fn render_power_operator(&self) -> Option<String> {
144        Some("^".to_string())
145    }
146
147    /// **This engine's case-insensitive `LIKE`**, or `None` where it has none.
148    fn render_ilike_operator(&self) -> Option<String> {
149        Some("ILIKE".to_string())
150    }
151
152    /// **This engine's negated case-insensitive `LIKE`**, or `None` where it
153    /// has none.
154    fn render_not_ilike_operator(&self) -> Option<String> {
155        Some("NOT ILIKE".to_string())
156    }
157
158    /// **This engine's string concatenation operator**, or `None` where it
159    /// joins strings only through a function.
160    fn render_concat_operator(&self) -> Option<String> {
161        Some("||".to_string())
162    }
163
164    /// Render the bitwise XOR operator.
165    #[deprecated(
166        since = "0.14.0",
167        note = "answer `render_bitwise_operator(BitwiseOperator::Xor)` instead, which can say \
168                the engine has none"
169    )]
170    fn render_bitwise_xor_operator(&self) -> String {
171        "^".to_string()
172    }
173
174    /// **This engine's spelling of one bitwise operator**, or `None` where it
175    /// has no such operator.
176    ///
177    /// The default is the operator most engines write — `&`, `|`, `~`, `<<`,
178    /// `>>` — and, for an exclusive or, what
179    /// [`render_bitwise_xor_operator`](Self::render_bitwise_xor_operator)
180    /// answers (`^` unless a dialect overrode it). Answer `None` where your
181    /// engine offers the operation only as a function (`BITAND(a, b)`,
182    /// `bitAnd(a, b)`): a formula that needs it is then refused by name, never
183    /// written with a function in the operator's place. Two engines read `^`
184    /// as something else entirely — a power on one, nothing at all on another
185    /// — so an exclusive or is worth checking on yours.
186    fn render_bitwise_operator(&self, operator: BitwiseOperator) -> Option<String> {
187        Some(match operator {
188            BitwiseOperator::And => "&".to_owned(),
189            BitwiseOperator::Or => "|".to_owned(),
190            #[allow(deprecated)]
191            BitwiseOperator::Xor => self.render_bitwise_xor_operator(),
192            BitwiseOperator::Not => "~".to_owned(),
193            BitwiseOperator::ShiftLeft => "<<".to_owned(),
194            BitwiseOperator::ShiftRight => ">>".to_owned(),
195        })
196    }
197
198    // ================================================================
199    // Conditional / cast rendering
200    // ================================================================
201
202    /// Render `TRY_CAST` expression (takes pre-rendered type string).
203    fn render_try_cast_str(&self, expr: &str, type_str: &str) -> String {
204        format!("CAST({expr} AS {type_str})")
205    }
206
207    /// Render CONVERT expression with optional style.
208    fn render_convert(&self, expr: &str, type_str: &str, style: Option<i32>) -> String {
209        let _ = style;
210        format!("CAST({expr} AS {type_str})")
211    }
212
213    /// Render `SAFE_CAST` expression.
214    fn render_safe_cast(&self, expr: &str, type_str: &str) -> String {
215        format!("CAST({expr} AS {type_str})")
216    }
217
218    // ================================================================
219    // Null handling
220    // ================================================================
221
222    /// Render IFNULL / ISNULL / COALESCE expression.
223    fn render_if_null(&self, expr: &str, replacement: &str) -> String {
224        format!("COALESCE({expr}, {replacement})")
225    }
226
227    // ================================================================
228    // Parameter references
229    // ================================================================
230
231    /// Render a named parameter reference.
232    fn render_named_param(&self, name: &str) -> String {
233        format!(":{name}")
234    }
235
236    /// Render a positional parameter reference.
237    fn render_positional_param(&self, pos: usize) -> String {
238        let _ = pos;
239        "?".to_string()
240    }
241
242    // ================================================================
243    // Data type reference rendering (CAST expressions)
244    // ================================================================
245
246    /// Render a [`DataTypeRef`] as a SQL type name string.
247    ///
248    /// The default implementation produces ANSI-standard type names.
249    /// Dialects override this for platform-specific names
250    /// (e.g., `ClickHouse` `Int32` instead of `INT`).
251    fn render_data_type_ref(&self, dt: &DataTypeRef) -> String {
252        match dt {
253            DataTypeRef::Int => "INT".to_string(),
254            DataTypeRef::BigInt => "BIGINT".to_string(),
255            DataTypeRef::SmallInt => "SMALLINT".to_string(),
256            DataTypeRef::TinyInt => "TINYINT".to_string(),
257            DataTypeRef::Float => "FLOAT".to_string(),
258            DataTypeRef::Double => "DOUBLE PRECISION".to_string(),
259            DataTypeRef::Real => "REAL".to_string(),
260            DataTypeRef::Decimal { precision, scale } => format!("DECIMAL({precision}, {scale})"),
261            DataTypeRef::Numeric { precision, scale } => format!("NUMERIC({precision}, {scale})"),
262            DataTypeRef::Varchar { length } => match length {
263                Some(len) => format!("VARCHAR({len})"),
264                None => "VARCHAR".to_string(),
265            },
266            DataTypeRef::Char { length } => format!("CHAR({length})"),
267            DataTypeRef::Text | DataTypeRef::Xml => "TEXT".to_string(),
268            DataTypeRef::NVarchar { length } => match length {
269                Some(len) => format!("NVARCHAR({len})"),
270                None => "NVARCHAR(MAX)".to_string(),
271            },
272            DataTypeRef::NChar { length } => format!("NCHAR({length})"),
273            DataTypeRef::Binary { length } => match length {
274                Some(len) => format!("BINARY({len})"),
275                None => "BINARY".to_string(),
276            },
277            DataTypeRef::VarBinary { length } => match length {
278                Some(len) => format!("VARBINARY({len})"),
279                None => "VARBINARY".to_string(),
280            },
281            DataTypeRef::Blob => "BLOB".to_string(),
282            DataTypeRef::Date => "DATE".to_string(),
283            DataTypeRef::Time { precision } => match precision {
284                Some(p) => format!("TIME({p})"),
285                None => "TIME".to_string(),
286            },
287            DataTypeRef::DateTime => "DATETIME".to_string(),
288            DataTypeRef::DateTime2 { precision } => match precision {
289                Some(p) => format!("DATETIME2({p})"),
290                None => "DATETIME2".to_string(),
291            },
292            DataTypeRef::Timestamp { precision } => match precision {
293                Some(p) => format!("TIMESTAMP({p})"),
294                None => "TIMESTAMP".to_string(),
295            },
296            DataTypeRef::TimestampTz { precision } => match precision {
297                Some(p) => format!("TIMESTAMP({p}) WITH TIME ZONE"),
298                None => "TIMESTAMP WITH TIME ZONE".to_string(),
299            },
300            DataTypeRef::Interval => "INTERVAL".to_string(),
301            DataTypeRef::Boolean => "BOOLEAN".to_string(),
302            DataTypeRef::Bit => "BIT".to_string(),
303            DataTypeRef::Json => "VARIANT".to_string(),
304            DataTypeRef::Jsonb | DataTypeRef::Variant => "JSON".to_string(),
305            DataTypeRef::Uuid => "UUID".to_string(),
306            DataTypeRef::Custom(name) => name.clone(),
307        }
308    }
309}
310
311/// ANSI-standard rendering of [`DataTypeRef`].
312///
313/// Used as a fallback for dialects that don't override type rendering.
314#[must_use]
315pub fn render_data_type_ref_ansi(dt: &DataTypeRef) -> String {
316    match dt {
317        DataTypeRef::Int => "INT".to_string(),
318        DataTypeRef::BigInt => "BIGINT".to_string(),
319        DataTypeRef::SmallInt => "SMALLINT".to_string(),
320        DataTypeRef::TinyInt => "TINYINT".to_string(),
321        DataTypeRef::Float => "FLOAT".to_string(),
322        DataTypeRef::Double => "DOUBLE PRECISION".to_string(),
323        DataTypeRef::Real => "REAL".to_string(),
324        DataTypeRef::Decimal { precision, scale } => format!("DECIMAL({precision}, {scale})"),
325        DataTypeRef::Numeric { precision, scale } => format!("NUMERIC({precision}, {scale})"),
326        DataTypeRef::Varchar { length } => match length {
327            Some(len) => format!("VARCHAR({len})"),
328            None => "VARCHAR".to_string(),
329        },
330        DataTypeRef::Char { length } => format!("CHAR({length})"),
331        DataTypeRef::Text | DataTypeRef::Xml => "TEXT".to_string(),
332        DataTypeRef::NVarchar { length } => match length {
333            Some(len) => format!("NVARCHAR({len})"),
334            None => "NVARCHAR(MAX)".to_string(),
335        },
336        DataTypeRef::NChar { length } => format!("NCHAR({length})"),
337        DataTypeRef::Binary { length } => match length {
338            Some(len) => format!("BINARY({len})"),
339            None => "BINARY".to_string(),
340        },
341        DataTypeRef::VarBinary { length } => match length {
342            Some(len) => format!("VARBINARY({len})"),
343            None => "VARBINARY".to_string(),
344        },
345        DataTypeRef::Blob => "BLOB".to_string(),
346        DataTypeRef::Date => "DATE".to_string(),
347        DataTypeRef::Time { precision } => match precision {
348            Some(p) => format!("TIME({p})"),
349            None => "TIME".to_string(),
350        },
351        DataTypeRef::DateTime => "DATETIME".to_string(),
352        DataTypeRef::DateTime2 { precision } => match precision {
353            Some(p) => format!("DATETIME2({p})"),
354            None => "DATETIME2".to_string(),
355        },
356        DataTypeRef::Timestamp { precision } => match precision {
357            Some(p) => format!("TIMESTAMP({p})"),
358            None => "TIMESTAMP".to_string(),
359        },
360        DataTypeRef::TimestampTz { precision } => match precision {
361            Some(p) => format!("TIMESTAMP({p}) WITH TIME ZONE"),
362            None => "TIMESTAMP WITH TIME ZONE".to_string(),
363        },
364        DataTypeRef::Interval => "INTERVAL".to_string(),
365        DataTypeRef::Boolean => "BOOLEAN".to_string(),
366        DataTypeRef::Bit => "BIT".to_string(),
367        DataTypeRef::Json => "VARIANT".to_string(),
368        DataTypeRef::Jsonb | DataTypeRef::Variant => "JSON".to_string(),
369        DataTypeRef::Uuid => "UUID".to_string(),
370        DataTypeRef::Custom(name) => name.clone(),
371    }
372}
373
374/// **One bitwise operator**, handed to
375/// [`FormulaRenderer::render_bitwise_operator`] to spell.
376#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
377#[non_exhaustive]
378pub enum BitwiseOperator {
379    /// Both bits set: `a & b` on most engines.
380    And,
381    /// Either bit set: `a | b`.
382    Or,
383    /// Exactly one bit set: `a ^ b` on some engines, `a # b` on others.
384    Xor,
385    /// Every bit flipped, of one value: `~a`.
386    Not,
387    /// The bits moved toward the high end: `a << n`.
388    ShiftLeft,
389    /// The bits moved toward the low end: `a >> n`.
390    ShiftRight,
391}
392
393impl BitwiseOperator {
394    /// Every operator, in the order the formula vocabulary lists them.
395    pub const ALL: [Self; 6] =
396        [Self::And, Self::Or, Self::Xor, Self::Not, Self::ShiftLeft, Self::ShiftRight];
397
398    /// The operator as most engines write it, which is also how a refusal
399    /// names it.
400    #[must_use]
401    pub const fn standard_spelling(self) -> &'static str {
402        match self {
403            Self::And => "&",
404            Self::Or => "|",
405            Self::Xor => "^",
406            Self::Not => "~",
407            Self::ShiftLeft => "<<",
408            Self::ShiftRight => ">>",
409        }
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416    use crate::DataType;
417
418    struct TestDialect;
419
420    impl SqlRenderer for TestDialect {
421        fn render_data_type(&self, _dt: &DataType) -> String {
422            "TEXT".to_string()
423        }
424
425        fn quote_identifier(&self, name: &str) -> String {
426            format!("\"{name}\"")
427        }
428
429        fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
430            match schema {
431                Some(s) => format!("\"{s}\".\"{name}\""),
432                None => format!("\"{name}\""),
433            }
434        }
435
436        fn render_current_timestamp(&self) -> String {
437            "CURRENT_TIMESTAMP".to_string()
438        }
439    }
440
441    impl FormulaRenderer for TestDialect {}
442
443    #[test]
444    fn formula_renderer_defaults_render_string_agg_with_order() {
445        let d = TestDialect;
446        assert_eq!(
447            d.render_string_agg("name", ",", false, Some("name ASC")),
448            "STRING_AGG(name, ',' ORDER BY name ASC)"
449        );
450    }
451
452    #[test]
453    fn formula_renderer_defaults_render_string_agg_with_distinct() {
454        let d = TestDialect;
455        assert_eq!(d.render_string_agg("name", ",", true, None), "STRING_AGG(DISTINCT name, ',')");
456    }
457
458    #[test]
459    fn formula_renderer_defaults_render_count_if() {
460        let d = TestDialect;
461        assert_eq!(d.render_count_if("x > 0"), "COUNT(CASE WHEN x > 0 THEN 1 END)");
462    }
463
464    /// A dialect that has stated no spelling renders the guess, and the guess
465    /// says so in its name. The value matters — it is what a plugin author sees
466    /// before they have decided anything — and the name is what lets Razeri's
467    /// conformance checks tell an undecided dialect from the agnostic renderer,
468    /// whose identical vocabulary is ANSI on purpose.
469    #[test]
470    fn a_dialect_that_states_no_spelling_renders_the_guess_and_is_named_as_one() {
471        use crate::sid_spelling::UNKNOWN_PLATFORM;
472        let d = TestDialect;
473        let spelling = crate::SqlRenderer::sid_spelling(&d);
474        assert_eq!(spelling.platform, UNKNOWN_PLATFORM);
475        assert_eq!(spelling.hash, ("xxhash64(", ")"));
476    }
477}