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