Skip to main content

razeri_plugin_contract/
agnostic.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//! Agnostic dialect and ANSI data-type rendering.
6//!
7//! [`AnsiDialect`] is a zero-dependency [`SqlRenderer`] for the
8//! `Agnostic` pseudo-platform.  It uses ANSI-standard SQL defaults and
9//! double-quote identifier quoting, matching the `DialectFamily::Razeri`
10//! conventions.
11//!
12//! [`render_data_type_ansi`] is a reusable free function that maps
13//! [`DataType`] to ANSI SQL type names.
14
15use crate::{
16    DataType,
17    data_type::{FloatPrecision, IntegerSize, JsonStorageFormat},
18};
19
20use crate::{DdlRenderer, FormulaRenderer, SqlRenderer};
21
22/// Render a [`DataType`] as an ANSI SQL type name.
23///
24/// This is the shared implementation used by [`AnsiDialect`] and dialect
25/// plugins for platforms that follow standard SQL naming.
26///
27/// Standard SQL has an array type and no map or record type, so an array
28/// renders as `T[]` with its element type spelled by this same function, and a
29/// map or a struct renders as `JSON`.
30#[must_use]
31pub fn render_data_type_ansi(dt: &DataType) -> String {
32    match dt {
33        DataType::Integer(i) => match i.size {
34            IntegerSize::Tiny => "TINYINT",
35            IntegerSize::Small => "SMALLINT",
36            IntegerSize::Standard => "INTEGER",
37            IntegerSize::Big => "BIGINT",
38        }
39        .to_string(),
40        DataType::Decimal(d) => format!("DECIMAL({}, {})", d.precision, d.scale),
41        DataType::Float(f) => match f.precision {
42            FloatPrecision::Single => "FLOAT",
43            FloatPrecision::Double => "DOUBLE PRECISION",
44        }
45        .to_string(),
46        DataType::Boolean(_) => "BOOLEAN".to_string(),
47        DataType::Char(c) => format!("CHAR({})", c.length),
48        DataType::VarChar(vc) => match vc.max_length {
49            Some(len) => format!("VARCHAR({len})"),
50            None => "TEXT".to_string(),
51        },
52        DataType::Text(_) => "TEXT".to_string(),
53        DataType::Binary(b) => format!("BINARY({})", b.length),
54        DataType::VarBinary(vb) => match vb.max_length {
55            Some(len) => format!("VARBINARY({len})"),
56            None => "BLOB".to_string(),
57        },
58        DataType::Date(_) => "DATE".to_string(),
59        DataType::Time(t) => match t.precision {
60            Some(digits) => format!("TIME({digits})"),
61            None => "TIME".to_string(),
62        },
63        // 0 is whole seconds, written as such.
64        DataType::Timestamp(ts) => format!("TIMESTAMP({})", ts.precision),
65        DataType::TimestampTz(ts) => {
66            let base = "TIMESTAMP WITH TIME ZONE";
67            if ts.precision > 0 { format!("{base}({})", ts.precision) } else { base.to_string() }
68        }
69        DataType::DateTime(dt_inner) => {
70            if dt_inner.precision > 0 {
71                format!("DATETIME({})", dt_inner.precision)
72            } else {
73                "DATETIME".to_string()
74            }
75        }
76        DataType::Interval(_) => "INTERVAL".to_string(),
77        DataType::Uuid(_) => "UUID".to_string(),
78        DataType::Json(j) => match j.storage_format {
79            JsonStorageFormat::Binary => "JSONB".to_string(),
80            JsonStorageFormat::Text => "JSON".to_string(),
81        },
82        DataType::Array(arr) => {
83            let elem = render_data_type_ansi(&arr.element_type);
84            format!("{elem}[]")
85        }
86        // Standard SQL has neither a map nor a record type, so both say JSON.
87        DataType::Map(_) | DataType::Struct(_) => "JSON".to_string(),
88    }
89}
90
91/// **The ANSI `CAST` target spelling for a [`DataType`]** — a *different*
92/// mapping from [`render_data_type_ansi`], on purpose.
93///
94/// A column declaration and a `CAST` target are two grammars on most engines.
95/// The clearest divergence is the smallest integer: a column may be declared
96/// `TINYINT`, but ANSI's narrowest portable `CAST` target is `SMALLINT`, and an
97/// engine that has no one-byte integer answers a binder error rather than
98/// widening for you.
99///
100/// This is what [`SqlRenderer::render_cast_type`]
101/// returns until a dialect overrides it.
102#[must_use]
103pub fn render_cast_type_ansi(dt: &DataType) -> String {
104    match dt {
105        DataType::Boolean(_) => "BOOLEAN".to_owned(),
106        DataType::Integer(i) => match i.size {
107            IntegerSize::Tiny | IntegerSize::Small => "SMALLINT".to_owned(),
108            IntegerSize::Standard => "INTEGER".to_owned(),
109            IntegerSize::Big => "BIGINT".to_owned(),
110        },
111        DataType::Decimal(d) => format!("DECIMAL({}, {})", d.precision, d.scale),
112        DataType::Float(f) => match f.precision {
113            FloatPrecision::Double => "DOUBLE PRECISION".to_owned(),
114            FloatPrecision::Single => "REAL".to_owned(),
115        },
116        DataType::Char(c) => format!("CHAR({})", c.length),
117        DataType::VarChar(v) => match v.max_length {
118            Some(n) => format!("VARCHAR({n})"),
119            None => "VARCHAR".to_owned(),
120        },
121        DataType::Date(_) => "DATE".to_owned(),
122        DataType::Time(_) => "TIME".to_owned(),
123        DataType::Timestamp(_) | DataType::DateTime(_) => "TIMESTAMP".to_owned(),
124        DataType::TimestampTz(_) => "TIMESTAMP WITH TIME ZONE".to_owned(),
125        DataType::Binary(_) => "BINARY".to_owned(),
126        DataType::VarBinary(_) => "VARBINARY".to_owned(),
127        DataType::Json(_) => "JSON".to_owned(),
128        DataType::Uuid(_) => "UUID".to_owned(),
129        _ => "TEXT".to_owned(),
130    }
131}
132
133/// Zero-config [`SqlRenderer`] for the Agnostic pseudo-platform.
134///
135/// Uses ANSI SQL type names and double-quote identifier quoting.
136/// Intended for parameterless `to_sql()` / `to_ddl()` convenience
137/// methods that need a default renderer.
138pub struct AnsiDialect;
139
140impl SqlRenderer for AnsiDialect {
141    fn render_data_type(&self, dt: &DataType) -> String {
142        render_data_type_ansi(dt)
143    }
144
145    fn quote_identifier(&self, name: &str) -> String {
146        format!("\"{name}\"")
147    }
148
149    fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
150        match schema {
151            Some(s) => format!("{}.{}", self.quote_identifier(s), self.quote_identifier(name)),
152            None => self.quote_identifier(name),
153        }
154    }
155
156    fn render_current_timestamp(&self) -> String {
157        "CURRENT_TIMESTAMP".to_string()
158    }
159
160    fn is_agnostic_dialect(&self) -> bool {
161        true
162    }
163
164    fn dialect_name(&self) -> &'static str {
165        "ansi"
166    }
167
168    fn to_formula_renderer(&self) -> Box<dyn FormulaRenderer> {
169        Box::new(Self)
170    }
171
172    /// ANSI's vocabulary **on purpose**, and named so — not the guess a dialect
173    /// that has decided nothing inherits.
174    fn sid_spelling(&self) -> crate::SidSpelling {
175        crate::ANSI_SID_SPELLING
176    }
177}
178
179impl DdlRenderer for AnsiDialect {}
180
181impl FormulaRenderer for AnsiDialect {}
182
183impl crate::ReplicationRenderer for AnsiDialect {}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn agnostic_is_agnostic() {
191        let d = AnsiDialect;
192        assert!(d.is_agnostic_dialect());
193        assert_eq!(d.dialect_name(), "ansi");
194    }
195
196    #[test]
197    fn agnostic_quotes_with_double_quotes() {
198        let d = AnsiDialect;
199        assert_eq!(d.quote_identifier("my_table"), "\"my_table\"");
200    }
201
202    #[test]
203    fn agnostic_qualifies_name() {
204        let d = AnsiDialect;
205        assert_eq!(d.qualify_name(Some("public"), "users"), "\"public\".\"users\"");
206        assert_eq!(d.qualify_name(None, "users"), "\"users\"");
207    }
208
209    #[test]
210    fn ansi_data_type_integer() {
211        assert_eq!(render_data_type_ansi(&DataType::int()), "INTEGER");
212        assert_eq!(render_data_type_ansi(&DataType::bigint()), "BIGINT");
213        assert_eq!(render_data_type_ansi(&DataType::smallint()), "SMALLINT");
214    }
215
216    #[test]
217    fn ansi_data_type_varchar() {
218        assert_eq!(render_data_type_ansi(&DataType::varchar(100)), "VARCHAR(100)");
219        assert_eq!(render_data_type_ansi(&DataType::text()), "TEXT");
220    }
221
222    #[test]
223    fn ansi_data_type_timestamp() {
224        assert_eq!(render_data_type_ansi(&DataType::timestamp()), "TIMESTAMP(6)");
225        assert_eq!(render_data_type_ansi(&DataType::date()), "DATE");
226    }
227
228    /// Standard SQL has an array and no map or record, so the default answers
229    /// `T[]` for one and `JSON` for the other two — a spelling, never a blank.
230    #[test]
231    fn ansi_answers_for_every_nested_type() {
232        use crate::data_type::{ArrayDataType, MapDataType, StructDataType, StructField};
233
234        let array = DataType::Array(ArrayDataType::new(DataType::varchar(100)));
235        let map = DataType::Map(MapDataType::new(DataType::varchar(50), DataType::bigint()));
236        let record = DataType::Struct(StructDataType::new(vec![
237            StructField::new("street", DataType::varchar(200)),
238            StructField::new("zip", DataType::varchar(10)),
239        ]));
240        let array_of_records =
241            DataType::Array(ArrayDataType::new(DataType::Struct(StructDataType::new(vec![
242                StructField::new("key", DataType::varchar(100)),
243                StructField::new("value", DataType::varchar(1000)),
244            ]))));
245
246        assert_eq!(render_data_type_ansi(&array), "VARCHAR(100)[]");
247        assert_eq!(render_data_type_ansi(&map), "JSON");
248        assert_eq!(render_data_type_ansi(&record), "JSON");
249        assert_eq!(render_data_type_ansi(&array_of_records), "JSON[]");
250
251        for dt in [&array, &map, &record, &array_of_records] {
252            assert!(!render_cast_type_ansi(dt).trim().is_empty());
253        }
254    }
255}