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    /// **Whether this engine splits a text into a list**, the way
105    /// [`render_split`](Self::render_split) writes it.
106    ///
107    /// The default is `true`. Answer `false` for an engine with no such
108    /// function, and a formula that splits is refused by name for it, rather
109    /// than written with something that returns the text unsplit.
110    fn supports_split(&self) -> bool {
111        true
112    }
113
114    /// **Whether this engine tests a text against a regular expression**,
115    /// the way [`render_regexp_like`](Self::render_regexp_like) writes it.
116    ///
117    /// The default is `true`. Answer `false` for an engine with no such test,
118    /// and a formula that asks it is refused by name for it.
119    fn supports_regexp_like(&self) -> bool {
120        true
121    }
122
123    /// **Whether this engine takes the part of a text a regular expression
124    /// matches**, the way [`render_regexp_extract`](Self::render_regexp_extract)
125    /// writes it.
126    ///
127    /// The default is `true`. Answer `false` for an engine with no such
128    /// function, and a formula that asks it is refused by name for it.
129    fn supports_regexp_extract(&self) -> bool {
130        true
131    }
132
133    /// **Whether this engine orders the values a string aggregation joins**,
134    /// the `ORDER BY` inside [`render_string_agg`](Self::render_string_agg).
135    ///
136    /// The default is `true`. Answer `false` for an engine whose string
137    /// aggregation takes no order, and a formula that asks for one is
138    /// refused by name for it, rather than joined in whatever order the
139    /// engine reads the rows.
140    fn supports_ordered_string_agg(&self) -> bool {
141        true
142    }
143
144    // There is deliberately no `render_generate_sid` here, and there never will
145    // be one. A `GENERATE_SID` formula in a view and a dimension load over the
146    // same column must key a member the same way, so Razeri composes both from
147    // one derivation and asks the dialect only for its
148    // [`SidSpelling`](crate::SidSpelling). Eight dialect crates used to spell
149    // their own; a second definition that does not exist cannot drift back.
150
151    /// Render NVL2 semantics.
152    fn render_nvl2(&self, expr: &str, not_null: &str, is_null: &str) -> String {
153        format!("CASE WHEN {expr} IS NOT NULL THEN {not_null} ELSE {is_null} END")
154    }
155
156    /// Render multi-branch IF semantics.
157    fn render_multi_if(&self, conditions: &[(&str, &str)], else_: &str) -> String {
158        use std::fmt::Write;
159        let mut sql = "CASE".to_string();
160        for (cond, then_expr) in conditions {
161            let _ = write!(sql, " WHEN {cond} THEN {then_expr}");
162        }
163        let _ = write!(sql, " ELSE {else_} END");
164        sql
165    }
166
167    // ================================================================
168    // Operator rendering
169    // ================================================================
170    //
171    // An operator your engine does not have is answered `None`, and Razeri
172    // then refuses a formula that needs it, naming the operator and your
173    // engine. It never writes the nearest function in the operator's place:
174    // a function call is not an operator, and a formula that composes one
175    // would be written wrong.
176
177    /// **This engine's exponentiation operator**, or `None` where it has none.
178    ///
179    /// The default is `^`, which is exponentiation on `PostgreSQL` and
180    /// `DuckDB` and exclusive-or on several other engines. Answer `None` where
181    /// your engine raises to a power only through a function such as
182    /// `POWER(a, b)`.
183    fn render_power_operator(&self) -> Option<String> {
184        Some("^".to_string())
185    }
186
187    /// **This engine's case-insensitive `LIKE`**, or `None` where it has none.
188    fn render_ilike_operator(&self) -> Option<String> {
189        Some("ILIKE".to_string())
190    }
191
192    /// **This engine's negated case-insensitive `LIKE`**, or `None` where it
193    /// has none.
194    fn render_not_ilike_operator(&self) -> Option<String> {
195        Some("NOT ILIKE".to_string())
196    }
197
198    /// **This engine's string concatenation operator**, or `None` where it
199    /// joins strings only through a function.
200    fn render_concat_operator(&self) -> Option<String> {
201        Some("||".to_string())
202    }
203
204    /// Render the bitwise XOR operator.
205    #[deprecated(
206        since = "0.14.0",
207        note = "answer `render_bitwise_operator(BitwiseOperator::Xor)` instead, which can say \
208                the engine has none"
209    )]
210    fn render_bitwise_xor_operator(&self) -> String {
211        "^".to_string()
212    }
213
214    /// **This engine's spelling of one bitwise operator**, or `None` where it
215    /// has no such operator.
216    ///
217    /// The default is the operator most engines write — `&`, `|`, `~`, `<<`,
218    /// `>>` — and, for an exclusive or, what
219    /// [`render_bitwise_xor_operator`](Self::render_bitwise_xor_operator)
220    /// answers (`^` unless a dialect overrode it). Answer `None` where your
221    /// engine offers the operation only as a function (`BITAND(a, b)`,
222    /// `bitAnd(a, b)`): a formula that needs it is then refused by name, never
223    /// written with a function in the operator's place. Two engines read `^`
224    /// as something else entirely — a power on one, nothing at all on another
225    /// — so an exclusive or is worth checking on yours.
226    fn render_bitwise_operator(&self, operator: BitwiseOperator) -> Option<String> {
227        Some(match operator {
228            BitwiseOperator::And => "&".to_owned(),
229            BitwiseOperator::Or => "|".to_owned(),
230            #[allow(deprecated)]
231            BitwiseOperator::Xor => self.render_bitwise_xor_operator(),
232            BitwiseOperator::Not => "~".to_owned(),
233            BitwiseOperator::ShiftLeft => "<<".to_owned(),
234            BitwiseOperator::ShiftRight => ">>".to_owned(),
235        })
236    }
237
238    // ================================================================
239    // Conditional / cast rendering
240    // ================================================================
241
242    /// Render `TRY_CAST` expression (takes pre-rendered type string).
243    fn render_try_cast_str(&self, expr: &str, type_str: &str) -> String {
244        format!("CAST({expr} AS {type_str})")
245    }
246
247    /// Render CONVERT expression with optional style.
248    fn render_convert(&self, expr: &str, type_str: &str, style: Option<i32>) -> String {
249        let _ = style;
250        format!("CAST({expr} AS {type_str})")
251    }
252
253    /// Render `SAFE_CAST` expression.
254    fn render_safe_cast(&self, expr: &str, type_str: &str) -> String {
255        format!("CAST({expr} AS {type_str})")
256    }
257
258    // ================================================================
259    // Null handling
260    // ================================================================
261
262    /// Render IFNULL / ISNULL / COALESCE expression.
263    fn render_if_null(&self, expr: &str, replacement: &str) -> String {
264        format!("COALESCE({expr}, {replacement})")
265    }
266
267    // ================================================================
268    // Parameter references
269    // ================================================================
270
271    /// Render a named parameter reference.
272    fn render_named_param(&self, name: &str) -> String {
273        format!(":{name}")
274    }
275
276    /// Render a positional parameter reference.
277    fn render_positional_param(&self, pos: usize) -> String {
278        let _ = pos;
279        "?".to_string()
280    }
281
282    // ================================================================
283    // Data type reference rendering (CAST expressions)
284    // ================================================================
285
286    /// Render a [`DataTypeRef`] as a SQL type name string.
287    ///
288    /// The default implementation produces ANSI-standard type names.
289    /// Dialects override this for platform-specific names
290    /// (e.g., `ClickHouse` `Int32` instead of `INT`).
291    fn render_data_type_ref(&self, dt: &DataTypeRef) -> String {
292        match dt {
293            DataTypeRef::Int => "INT".to_string(),
294            DataTypeRef::BigInt => "BIGINT".to_string(),
295            DataTypeRef::SmallInt => "SMALLINT".to_string(),
296            DataTypeRef::TinyInt => "TINYINT".to_string(),
297            DataTypeRef::Float => "FLOAT".to_string(),
298            DataTypeRef::Double => "DOUBLE PRECISION".to_string(),
299            DataTypeRef::Real => "REAL".to_string(),
300            DataTypeRef::Decimal { precision, scale } => format!("DECIMAL({precision}, {scale})"),
301            DataTypeRef::Numeric { precision, scale } => format!("NUMERIC({precision}, {scale})"),
302            DataTypeRef::Varchar { length } => match length {
303                Some(len) => format!("VARCHAR({len})"),
304                None => "VARCHAR".to_string(),
305            },
306            DataTypeRef::Char { length } => format!("CHAR({length})"),
307            DataTypeRef::Text | DataTypeRef::Xml => "TEXT".to_string(),
308            DataTypeRef::NVarchar { length } => match length {
309                Some(len) => format!("NVARCHAR({len})"),
310                None => "NVARCHAR(MAX)".to_string(),
311            },
312            DataTypeRef::NChar { length } => format!("NCHAR({length})"),
313            DataTypeRef::Binary { length } => match length {
314                Some(len) => format!("BINARY({len})"),
315                None => "BINARY".to_string(),
316            },
317            DataTypeRef::VarBinary { length } => match length {
318                Some(len) => format!("VARBINARY({len})"),
319                None => "VARBINARY".to_string(),
320            },
321            DataTypeRef::Blob => "BLOB".to_string(),
322            DataTypeRef::Date => "DATE".to_string(),
323            DataTypeRef::Time { precision } => match precision {
324                Some(p) => format!("TIME({p})"),
325                None => "TIME".to_string(),
326            },
327            DataTypeRef::DateTime => "DATETIME".to_string(),
328            DataTypeRef::DateTime2 { precision } => match precision {
329                Some(p) => format!("DATETIME2({p})"),
330                None => "DATETIME2".to_string(),
331            },
332            DataTypeRef::Timestamp { precision } => match precision {
333                Some(p) => format!("TIMESTAMP({p})"),
334                None => "TIMESTAMP".to_string(),
335            },
336            DataTypeRef::TimestampTz { precision } => match precision {
337                Some(p) => format!("TIMESTAMP({p}) WITH TIME ZONE"),
338                None => "TIMESTAMP WITH TIME ZONE".to_string(),
339            },
340            DataTypeRef::Interval => "INTERVAL".to_string(),
341            DataTypeRef::Boolean => "BOOLEAN".to_string(),
342            DataTypeRef::Bit => "BIT".to_string(),
343            DataTypeRef::Json => "VARIANT".to_string(),
344            DataTypeRef::Jsonb | DataTypeRef::Variant => "JSON".to_string(),
345            DataTypeRef::Uuid => "UUID".to_string(),
346            DataTypeRef::Custom(name) => name.clone(),
347        }
348    }
349}
350
351/// ANSI-standard rendering of [`DataTypeRef`].
352///
353/// Used as a fallback for dialects that don't override type rendering.
354#[must_use]
355pub fn render_data_type_ref_ansi(dt: &DataTypeRef) -> String {
356    match dt {
357        DataTypeRef::Int => "INT".to_string(),
358        DataTypeRef::BigInt => "BIGINT".to_string(),
359        DataTypeRef::SmallInt => "SMALLINT".to_string(),
360        DataTypeRef::TinyInt => "TINYINT".to_string(),
361        DataTypeRef::Float => "FLOAT".to_string(),
362        DataTypeRef::Double => "DOUBLE PRECISION".to_string(),
363        DataTypeRef::Real => "REAL".to_string(),
364        DataTypeRef::Decimal { precision, scale } => format!("DECIMAL({precision}, {scale})"),
365        DataTypeRef::Numeric { precision, scale } => format!("NUMERIC({precision}, {scale})"),
366        DataTypeRef::Varchar { length } => match length {
367            Some(len) => format!("VARCHAR({len})"),
368            None => "VARCHAR".to_string(),
369        },
370        DataTypeRef::Char { length } => format!("CHAR({length})"),
371        DataTypeRef::Text | DataTypeRef::Xml => "TEXT".to_string(),
372        DataTypeRef::NVarchar { length } => match length {
373            Some(len) => format!("NVARCHAR({len})"),
374            None => "NVARCHAR(MAX)".to_string(),
375        },
376        DataTypeRef::NChar { length } => format!("NCHAR({length})"),
377        DataTypeRef::Binary { length } => match length {
378            Some(len) => format!("BINARY({len})"),
379            None => "BINARY".to_string(),
380        },
381        DataTypeRef::VarBinary { length } => match length {
382            Some(len) => format!("VARBINARY({len})"),
383            None => "VARBINARY".to_string(),
384        },
385        DataTypeRef::Blob => "BLOB".to_string(),
386        DataTypeRef::Date => "DATE".to_string(),
387        DataTypeRef::Time { precision } => match precision {
388            Some(p) => format!("TIME({p})"),
389            None => "TIME".to_string(),
390        },
391        DataTypeRef::DateTime => "DATETIME".to_string(),
392        DataTypeRef::DateTime2 { precision } => match precision {
393            Some(p) => format!("DATETIME2({p})"),
394            None => "DATETIME2".to_string(),
395        },
396        DataTypeRef::Timestamp { precision } => match precision {
397            Some(p) => format!("TIMESTAMP({p})"),
398            None => "TIMESTAMP".to_string(),
399        },
400        DataTypeRef::TimestampTz { precision } => match precision {
401            Some(p) => format!("TIMESTAMP({p}) WITH TIME ZONE"),
402            None => "TIMESTAMP WITH TIME ZONE".to_string(),
403        },
404        DataTypeRef::Interval => "INTERVAL".to_string(),
405        DataTypeRef::Boolean => "BOOLEAN".to_string(),
406        DataTypeRef::Bit => "BIT".to_string(),
407        DataTypeRef::Json => "VARIANT".to_string(),
408        DataTypeRef::Jsonb | DataTypeRef::Variant => "JSON".to_string(),
409        DataTypeRef::Uuid => "UUID".to_string(),
410        DataTypeRef::Custom(name) => name.clone(),
411    }
412}
413
414/// **One bitwise operator**, handed to
415/// [`FormulaRenderer::render_bitwise_operator`] to spell.
416#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
417#[non_exhaustive]
418pub enum BitwiseOperator {
419    /// Both bits set: `a & b` on most engines.
420    And,
421    /// Either bit set: `a | b`.
422    Or,
423    /// Exactly one bit set: `a ^ b` on some engines, `a # b` on others.
424    Xor,
425    /// Every bit flipped, of one value: `~a`.
426    Not,
427    /// The bits moved toward the high end: `a << n`.
428    ShiftLeft,
429    /// The bits moved toward the low end: `a >> n`.
430    ShiftRight,
431}
432
433impl BitwiseOperator {
434    /// Every operator, in the order the formula vocabulary lists them.
435    pub const ALL: [Self; 6] =
436        [Self::And, Self::Or, Self::Xor, Self::Not, Self::ShiftLeft, Self::ShiftRight];
437
438    /// The operator as most engines write it, which is also how a refusal
439    /// names it.
440    #[must_use]
441    pub const fn standard_spelling(self) -> &'static str {
442        match self {
443            Self::And => "&",
444            Self::Or => "|",
445            Self::Xor => "^",
446            Self::Not => "~",
447            Self::ShiftLeft => "<<",
448            Self::ShiftRight => ">>",
449        }
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456    use crate::DataType;
457
458    struct TestDialect;
459
460    impl SqlRenderer for TestDialect {
461        fn render_data_type(&self, _dt: &DataType) -> String {
462            "TEXT".to_string()
463        }
464
465        fn quote_identifier(&self, name: &str) -> String {
466            format!("\"{name}\"")
467        }
468
469        fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
470            match schema {
471                Some(s) => format!("\"{s}\".\"{name}\""),
472                None => format!("\"{name}\""),
473            }
474        }
475
476        fn render_current_timestamp(&self) -> String {
477            "CURRENT_TIMESTAMP".to_string()
478        }
479    }
480
481    impl FormulaRenderer for TestDialect {}
482
483    #[test]
484    fn formula_renderer_defaults_render_string_agg_with_order() {
485        let d = TestDialect;
486        assert_eq!(
487            d.render_string_agg("name", ",", false, Some("name ASC")),
488            "STRING_AGG(name, ',' ORDER BY name ASC)"
489        );
490    }
491
492    #[test]
493    fn formula_renderer_defaults_render_string_agg_with_distinct() {
494        let d = TestDialect;
495        assert_eq!(d.render_string_agg("name", ",", true, None), "STRING_AGG(DISTINCT name, ',')");
496    }
497
498    #[test]
499    fn formula_renderer_defaults_render_count_if() {
500        let d = TestDialect;
501        assert_eq!(d.render_count_if("x > 0"), "COUNT(CASE WHEN x > 0 THEN 1 END)");
502    }
503
504    /// A dialect that has stated no spelling renders the guess, and the guess
505    /// says so in its name. The value matters — it is what a plugin author sees
506    /// before they have decided anything — and the name is what lets Razeri's
507    /// conformance checks tell an undecided dialect from the agnostic renderer,
508    /// whose identical vocabulary is ANSI on purpose.
509    #[test]
510    fn a_dialect_that_states_no_spelling_renders_the_guess_and_is_named_as_one() {
511        use crate::sid_spelling::UNKNOWN_PLATFORM;
512        let d = TestDialect;
513        let spelling = crate::SqlRenderer::sid_spelling(&d);
514        assert_eq!(spelling.platform, UNKNOWN_PLATFORM);
515        assert_eq!(spelling.hash, ("xxhash64(", ")"));
516    }
517}