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