Skip to main content

uqa_sql/ast/
types.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7use serde::{Deserialize, Serialize};
8
9use super::RangeSubtype;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub enum ColumnType {
13    SmallInteger,
14    Integer,
15    BigInteger,
16    /// `PostgreSQL` object identifier (`pg_catalog.oid`).
17    Oid,
18    /// `PostgreSQL` transaction identifier (`pg_catalog.xid`).
19    Xid,
20    Boolean,
21    /// `PostgreSQL`'s non-null zero-width `void` pseudo-type.
22    Void,
23    Text,
24    /// `PostgreSQL` cursor portal name (`pg_catalog.refcursor`).
25    RefCursor,
26    Name,
27    Uuid,
28    Varchar(Option<u32>),
29    /// Internal unconstrained `bpchar` type used after common-type selection.
30    Bpchar,
31    /// `PostgreSQL` blank-padded `CHARACTER(n)` / `CHAR(n)` (`bpchar`).
32    /// The length counts Unicode scalar values and defaults to one when the
33    /// declaration omits an explicit modifier.
34    Character(u32),
35    Real,
36    DoublePrecision,
37    /// `NUMERIC(precision, scale)` -- exact decimal storage. When
38    /// `scale` is `Some(s)` the engine rounds `INSERT` values to `s`
39    /// fractional digits. `precision` is captured for round-tripping
40    /// the catalog text but is not currently enforced.
41    Numeric {
42        precision: Option<u32>,
43        scale: Option<i32>,
44    },
45    /// `JSON` / `JSONB` columns store typed JSON values.
46    Json,
47    /// `JSONB` columns store typed JSON values with `PostgreSQL` JSONB operators.
48    JsonB,
49    /// `BYTEA` columns store opaque bytes.
50    Bytea,
51    /// `PostgreSQL`'s internal single-byte `"char"` catalog type.
52    InternalChar,
53    Regproc,
54    /// `PostgreSQL` routine-signature object identifier (`pg_catalog.regprocedure`).
55    Regprocedure,
56    /// `PostgreSQL` relation object identifier (`pg_catalog.regclass`).
57    Regclass,
58    /// `PostgreSQL` namespace object identifier (`pg_catalog.regnamespace`).
59    Regnamespace,
60    /// `PostgreSQL` role object identifier (`pg_catalog.regrole`).
61    Regrole,
62    Regtype,
63    PgNodeTree,
64    AclItem,
65    Int2Vector,
66    OidVector,
67    AnyArray,
68    /// `PostgreSQL`'s anonymous composite pseudo-type (OID 2249).
69    Record,
70    /// A `PostgreSQL` array whose elements retain their declared SQL type.
71    /// Nested array bounds are represented recursively.
72    Array(Box<ColumnType>),
73    /// `DATE` columns store days since 1970-01-01.
74    Date,
75    /// `TIME` columns store microseconds since midnight.
76    Time,
77    /// `TIME WITH TIME ZONE` columns store local time plus offset.
78    TimeTz,
79    /// `TIMESTAMP WITHOUT TIME ZONE` columns store naive microseconds
80    /// since 1970-01-01 00:00:00.
81    Timestamp,
82    /// `TIMESTAMP WITH TIME ZONE` columns store UTC microseconds since
83    /// 1970-01-01 00:00:00Z.
84    TimestampTz,
85    Interval,
86    /// One of `PostgreSQL`'s six built-in range identities. Values use a
87    /// canonical textual carrier so bounds remain durable across every
88    /// storage backend while the declared subtype stays in row metadata.
89    Range(RangeSubtype),
90    /// The `PostgreSQL` multirange paired with one built-in range subtype.
91    Multirange(RangeSubtype),
92    /// `VECTOR(N)` columns store an `N`-dimensional `f32` embedding.
93    Vector(u32),
94    /// `TENSOR(N)` columns store an array of `N`-dimensional `f32`
95    /// embeddings. The row remains the retrieval identity; vector
96    /// indexes score against the best element in the tensor.
97    Tensor(u32),
98    /// A named `PostgreSQL` domain retaining both its own type identity and the
99    /// base type used for value conversion and operator selection.
100    Domain {
101        schema: String,
102        name: String,
103        oid: u32,
104        base: Box<ColumnType>,
105    },
106}
107
108pub(crate) fn builtin_array_element_name(type_name: &str) -> Option<&'static str> {
109    Some(match type_name {
110        "_bool" => "bool",
111        "_bytea" => "bytea",
112        "_char" => "\"char\"",
113        "_name" => "name",
114        "_int8" => "int8",
115        "_int2" => "int2",
116        "_int2vector" => "int2vector",
117        "_int4" => "int4",
118        "_regproc" => "regproc",
119        "_regprocedure" => "regprocedure",
120        "_regclass" => "regclass",
121        "_regrole" => "regrole",
122        "_text" => "text",
123        "_refcursor" => "refcursor",
124        "_oid" => "oid",
125        "_oidvector" => "oidvector",
126        "_bpchar" => "bpchar",
127        "_varchar" => "varchar",
128        "_float4" => "float4",
129        "_float8" => "float8",
130        "_aclitem" => "aclitem",
131        "_date" => "date",
132        "_time" => "time",
133        "_timestamp" => "timestamp",
134        "_timestamptz" => "timestamptz",
135        "_interval" => "interval",
136        "_numeric" => "numeric",
137        "_timetz" => "timetz",
138        "_record" => "record",
139        "_uuid" => "uuid",
140        "_json" => "json",
141        "_jsonb" => "jsonb",
142        "_regtype" => "regtype",
143        "_xid" => "xid",
144        "_pg_node_tree" => "pg_node_tree",
145        "_int4range" => "int4range",
146        "_int8range" => "int8range",
147        "_numrange" => "numrange",
148        "_daterange" => "daterange",
149        "_tsrange" => "tsrange",
150        "_tstzrange" => "tstzrange",
151        "_int4multirange" => "int4multirange",
152        "_int8multirange" => "int8multirange",
153        "_nummultirange" => "nummultirange",
154        "_datemultirange" => "datemultirange",
155        "_tsmultirange" => "tsmultirange",
156        "_tstzmultirange" => "tstzmultirange",
157        _ => return None,
158    })
159}
160
161impl ColumnType {
162    #[must_use]
163    pub fn is_integer(&self) -> bool {
164        match self {
165            Self::SmallInteger | Self::Integer | Self::BigInteger | Self::Oid | Self::Xid => true,
166            Self::Domain { base, .. } => base.is_integer(),
167            _ => false,
168        }
169    }
170
171    #[must_use]
172    pub fn is_character_string(&self) -> bool {
173        match self {
174            Self::Text
175            | Self::Name
176            | Self::Varchar(_)
177            | Self::Bpchar
178            | Self::Character(_)
179            | Self::InternalChar
180            | Self::PgNodeTree
181            | Self::AclItem => true,
182            Self::Domain { base, .. } => base.is_character_string(),
183            _ => false,
184        }
185    }
186
187    /// Parse the canonical or accepted spelling of one implemented SQL type.
188    /// This is shared by expression binding and row-schema propagation so a
189    /// cast's declared type is not reconstructed from its runtime value.
190    #[expect(
191        clippy::too_many_lines,
192        reason = "exhaustive AST migration preserves every serialized variant"
193    )]
194    pub fn from_sql_name(name: &str) -> Result<Self, crate::SQLError> {
195        let normalized = name.trim().to_ascii_lowercase();
196        if let Some(element) = builtin_array_element_name(&normalized) {
197            return Self::from_sql_name(element).map(|ty| Self::Array(Box::new(ty)));
198        }
199        if let Some(element) = normalized.strip_suffix("[]") {
200            let element_type = Self::from_sql_name(element)?;
201            if matches!(element_type, Self::Void) {
202                return Err(crate::SQLError::Routine {
203                    sqlstate: "42704".into(),
204                    message: format!("type \"{normalized}\" does not exist"),
205                });
206            }
207            return Ok(Self::Array(Box::new(element_type)));
208        }
209        let (base, modifier) = normalized
210            .strip_suffix(')')
211            .and_then(|prefix| prefix.rsplit_once('('))
212            .map_or((normalized.as_str(), None), |(base, modifier)| {
213                (base.trim(), Some(modifier.trim()))
214            });
215        let base = base.strip_prefix("pg_catalog.").unwrap_or(base);
216        let character_length = || -> Result<Option<u32>, crate::SQLError> {
217            modifier
218                .map(|value| {
219                    value
220                        .parse::<u32>()
221                        .ok()
222                        .filter(|length| *length > 0)
223                        .ok_or_else(|| {
224                            crate::SQLError::TypeMismatch(format!(
225                                "character length must be greater than zero, got {value}"
226                            ))
227                        })
228                })
229                .transpose()
230        };
231        match base {
232            "smallint" | "int2" | "smallserial" | "serial2" => Ok(Self::SmallInteger),
233            "integer" | "int" | "int4" | "serial" | "serial4" => Ok(Self::Integer),
234            "bigint" | "int8" | "bigserial" | "serial8" => Ok(Self::BigInteger),
235            "oid" => Ok(Self::Oid),
236            "xid" => Ok(Self::Xid),
237            "boolean" | "bool" => Ok(Self::Boolean),
238            "void" => Ok(Self::Void),
239            "text" => Ok(Self::Text),
240            "refcursor" => Ok(Self::RefCursor),
241            "name" => Ok(Self::Name),
242            "uuid" => Ok(Self::Uuid),
243            "varchar" | "character varying" => Ok(Self::Varchar(character_length()?)),
244            "character" | "char" => Ok(Self::Character(character_length()?.unwrap_or(1))),
245            "bpchar" => Ok(character_length()?.map_or(Self::Bpchar, Self::Character)),
246            "real" | "float4" => Ok(Self::Real),
247            "double" | "double precision" | "float8" => Ok(Self::DoublePrecision),
248            "numeric" | "decimal" => {
249                let (precision, scale) = match modifier {
250                    None => (None, None),
251                    Some(modifier) => {
252                        let mut parts = modifier.split(',').map(str::trim);
253                        let precision = parts
254                            .next()
255                            .and_then(|value| value.parse::<u32>().ok())
256                            .ok_or_else(|| {
257                                crate::SQLError::TypeMismatch(format!(
258                                    "invalid numeric modifier `{modifier}`"
259                                ))
260                            })?;
261                        let scale = parts
262                            .next()
263                            .map(|value| value.parse::<i32>())
264                            .transpose()
265                            .map_err(|_| {
266                                crate::SQLError::TypeMismatch(format!(
267                                    "invalid numeric modifier `{modifier}`"
268                                ))
269                            })?
270                            .unwrap_or(0);
271                        if parts.next().is_some() {
272                            return Err(crate::SQLError::TypeMismatch(format!(
273                                "invalid numeric modifier `{modifier}`"
274                            )));
275                        }
276                        (Some(precision), Some(scale))
277                    }
278                };
279                Ok(Self::Numeric { precision, scale })
280            }
281            "json" => Ok(Self::Json),
282            "jsonb" => Ok(Self::JsonB),
283            "bytea" => Ok(Self::Bytea),
284            "\"char\"" => Ok(Self::InternalChar),
285            "regproc" => Ok(Self::Regproc),
286            "regprocedure" => Ok(Self::Regprocedure),
287            "regclass" => Ok(Self::Regclass),
288            "regnamespace" => Ok(Self::Regnamespace),
289            "regrole" => Ok(Self::Regrole),
290            "regtype" => Ok(Self::Regtype),
291            "pg_node_tree" => Ok(Self::PgNodeTree),
292            "aclitem" => Ok(Self::AclItem),
293            "int2vector" => Ok(Self::Int2Vector),
294            "oidvector" => Ok(Self::OidVector),
295            "anyarray" => Ok(Self::AnyArray),
296            "record" => Ok(Self::Record),
297            "date" => Ok(Self::Date),
298            "time" | "time without time zone" => Ok(Self::Time),
299            "timetz" | "time with time zone" => Ok(Self::TimeTz),
300            "timestamp" | "datetime" | "timestamp without time zone" => Ok(Self::Timestamp),
301            "timestamptz" | "timestamp with time zone" => Ok(Self::TimestampTz),
302            "interval" => Ok(Self::Interval),
303            "int4range" => Ok(Self::Range(RangeSubtype::Integer)),
304            "int8range" => Ok(Self::Range(RangeSubtype::BigInteger)),
305            "numrange" => Ok(Self::Range(RangeSubtype::Numeric)),
306            "daterange" => Ok(Self::Range(RangeSubtype::Date)),
307            "tsrange" => Ok(Self::Range(RangeSubtype::Timestamp)),
308            "tstzrange" => Ok(Self::Range(RangeSubtype::TimestampTz)),
309            "int4multirange" => Ok(Self::Multirange(RangeSubtype::Integer)),
310            "int8multirange" => Ok(Self::Multirange(RangeSubtype::BigInteger)),
311            "nummultirange" => Ok(Self::Multirange(RangeSubtype::Numeric)),
312            "datemultirange" => Ok(Self::Multirange(RangeSubtype::Date)),
313            "tsmultirange" => Ok(Self::Multirange(RangeSubtype::Timestamp)),
314            "tstzmultirange" => Ok(Self::Multirange(RangeSubtype::TimestampTz)),
315            "vector" => modifier
316                .and_then(|value| value.parse::<u32>().ok())
317                .filter(|dimension| *dimension > 0)
318                .map(Self::Vector)
319                .ok_or_else(|| crate::SQLError::TypeMismatch("VECTOR requires a dimension".into())),
320            "tensor" => modifier
321                .and_then(|value| value.parse::<u32>().ok())
322                .filter(|dimension| *dimension > 0)
323                .map(Self::Tensor)
324                .ok_or_else(|| crate::SQLError::TypeMismatch("TENSOR requires a dimension".into())),
325            other => Err(crate::SQLError::Unsupported(format!(
326                "SQL type `{other}` is not supported"
327            ))),
328        }
329    }
330
331    #[must_use]
332    pub fn sql_name(&self) -> String {
333        match self {
334            Self::SmallInteger => "smallint".into(),
335            Self::Integer => "integer".into(),
336            Self::BigInteger => "bigint".into(),
337            Self::Oid => "oid".into(),
338            Self::Xid => "xid".into(),
339            Self::Boolean => "boolean".into(),
340            Self::Void => "void".into(),
341            Self::Text => "text".into(),
342            Self::RefCursor => "refcursor".into(),
343            Self::Name => "name".into(),
344            Self::Uuid => "uuid".into(),
345            Self::Varchar(Some(length)) => format!("character varying({length})"),
346            Self::Varchar(None) => "character varying".into(),
347            Self::Bpchar => "bpchar".into(),
348            Self::Character(length) => format!("character({length})"),
349            Self::Real => "real".into(),
350            Self::DoublePrecision => "double precision".into(),
351            Self::Numeric {
352                precision: Some(precision),
353                scale: Some(scale),
354            } => format!("numeric({precision},{scale})"),
355            Self::Numeric { .. } => "numeric".into(),
356            Self::Json => "json".into(),
357            Self::JsonB => "jsonb".into(),
358            Self::Bytea => "bytea".into(),
359            Self::InternalChar => "\"char\"".into(),
360            Self::Regproc => "regproc".into(),
361            Self::Regprocedure => "regprocedure".into(),
362            Self::Regclass => "regclass".into(),
363            Self::Regnamespace => "regnamespace".into(),
364            Self::Regrole => "regrole".into(),
365            Self::Regtype => "regtype".into(),
366            Self::PgNodeTree => "pg_node_tree".into(),
367            Self::AclItem => "aclitem".into(),
368            Self::Int2Vector => "int2vector".into(),
369            Self::OidVector => "oidvector".into(),
370            Self::AnyArray => "anyarray".into(),
371            Self::Record => "record".into(),
372            Self::Array(element) => format!("{}[]", element.sql_name()),
373            Self::Date => "date".into(),
374            Self::Time => "time without time zone".into(),
375            Self::TimeTz => "time with time zone".into(),
376            Self::Timestamp => "timestamp without time zone".into(),
377            Self::TimestampTz => "timestamp with time zone".into(),
378            Self::Interval => "interval".into(),
379            Self::Range(subtype) => subtype.range_name().into(),
380            Self::Multirange(subtype) => subtype.multirange_name().into(),
381            Self::Vector(dimension) => format!("vector({dimension})"),
382            Self::Tensor(dimension) => format!("tensor({dimension})"),
383            Self::Domain { schema, name, .. } => format!("{schema}.{name}"),
384        }
385    }
386
387    /// Name emitted by `PostgreSQL`'s `regtype` output, including
388    /// `pg_typeof(...)`.
389    #[must_use]
390    pub fn regtype_name(&self) -> String {
391        match self {
392            Self::Varchar(_) => "character varying".into(),
393            Self::Bpchar | Self::Character(_) => "character".into(),
394            Self::Numeric { .. } => "numeric".into(),
395            Self::Vector(_) => "vector".into(),
396            Self::Tensor(_) => "tensor".into(),
397            Self::Domain { schema, name, .. } => format!("{schema}.{name}"),
398            Self::Array(element) => format!("{}[]", element.regtype_name()),
399            other => other.sql_name(),
400        }
401    }
402}