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::{IntervalFields, RangeSubtype};
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub enum ColumnType {
13    /// A declaration awaiting catalog type resolution. This variant is never a stored column type.
14    Named(String),
15    SmallInteger,
16    Integer,
17    BigInteger,
18    /// `PostgreSQL` object identifier (`pg_catalog.oid`).
19    Oid,
20    /// `PostgreSQL` transaction identifier (`pg_catalog.xid`).
21    Xid,
22    Boolean,
23    /// `PostgreSQL`'s non-null zero-width `void` pseudo-type.
24    Void,
25    Text,
26    /// `PostgreSQL` cursor portal name (`pg_catalog.refcursor`).
27    RefCursor,
28    Name,
29    Uuid,
30    Varchar(Option<u32>),
31    /// Internal unconstrained `bpchar` type used after common-type selection.
32    Bpchar,
33    /// `PostgreSQL` blank-padded `CHARACTER(n)` / `CHAR(n)` (`bpchar`).
34    /// The length counts Unicode scalar values and defaults to one when the
35    /// declaration omits an explicit modifier.
36    Character(u32),
37    Real,
38    DoublePrecision,
39    /// `NUMERIC(precision, scale)` -- exact decimal storage. When
40    /// `scale` is `Some(s)` the engine rounds `INSERT` values to `s`
41    /// fractional digits. `precision` is captured for round-tripping
42    /// the catalog text but is not currently enforced.
43    Numeric {
44        precision: Option<u32>,
45        scale: Option<i32>,
46    },
47    /// `JSON` / `JSONB` columns store typed JSON values.
48    Json,
49    /// `JSONB` columns store typed JSON values with `PostgreSQL` JSONB operators.
50    JsonB,
51    /// `BYTEA` columns store opaque bytes.
52    Bytea,
53    /// `PostgreSQL`'s internal single-byte `"char"` catalog type.
54    InternalChar,
55    Regproc,
56    /// `PostgreSQL` routine-signature object identifier (`pg_catalog.regprocedure`).
57    Regprocedure,
58    /// `PostgreSQL` relation object identifier (`pg_catalog.regclass`).
59    Regclass,
60    /// `PostgreSQL` namespace object identifier (`pg_catalog.regnamespace`).
61    Regnamespace,
62    /// `PostgreSQL` role object identifier (`pg_catalog.regrole`).
63    Regrole,
64    Regtype,
65    PgNodeTree,
66    AclItem,
67    Int2Vector,
68    OidVector,
69    AnyArray,
70    /// `PostgreSQL`'s anonymous composite pseudo-type (OID 2249).
71    Record,
72    /// A `PostgreSQL` array whose elements retain their declared SQL type.
73    /// Nested array bounds are represented recursively.
74    Array(Box<ColumnType>),
75    /// `DATE` columns store days since 1970-01-01.
76    Date,
77    /// `TIME` columns store microseconds since midnight.
78    Time,
79    /// `TIME(p)` with an explicit fractional-second precision.
80    TimePrecision(u32),
81    /// `TIME WITH TIME ZONE` columns store local time plus offset.
82    TimeTz,
83    /// `TIME(p) WITH TIME ZONE` with an explicit fractional-second precision.
84    TimeTzPrecision(u32),
85    /// `TIMESTAMP WITHOUT TIME ZONE` columns store naive microseconds
86    /// since 1970-01-01 00:00:00.
87    Timestamp,
88    /// `TIMESTAMP(p)` with an explicit fractional-second precision.
89    TimestampPrecision(u32),
90    /// `TIMESTAMP WITH TIME ZONE` columns store UTC microseconds since
91    /// 1970-01-01 00:00:00Z.
92    TimestampTz,
93    /// `TIMESTAMP(p) WITH TIME ZONE` with an explicit fractional-second precision.
94    TimestampTzPrecision(u32),
95    Interval,
96    /// An interval retaining its stored-field restriction and fractional-second precision.
97    IntervalWithFields {
98        fields: IntervalFields,
99        precision: Option<u32>,
100    },
101    /// One of `PostgreSQL`'s six built-in range identities. Values use a
102    /// canonical textual carrier so bounds remain durable across every
103    /// storage backend while the declared subtype stays in row metadata.
104    Range(RangeSubtype),
105    /// The `PostgreSQL` multirange paired with one built-in range subtype.
106    Multirange(RangeSubtype),
107    /// `VECTOR(N)` columns store an `N`-dimensional `f32` embedding.
108    Vector(u32),
109    /// `TENSOR(N)` columns store an array of `N`-dimensional `f32`
110    /// embeddings. The row remains the retrieval identity; vector
111    /// indexes score against the best element in the tensor.
112    Tensor(u32),
113    /// A named `PostgreSQL` domain retaining both its own type identity and the
114    /// base type used for value conversion and operator selection.
115    Domain {
116        schema: String,
117        name: String,
118        oid: u32,
119        base: Box<ColumnType>,
120    },
121}
122
123pub(crate) fn builtin_array_element_name(type_name: &str) -> Option<&'static str> {
124    Some(match type_name {
125        "_bool" => "bool",
126        "_bytea" => "bytea",
127        "_char" => "\"char\"",
128        "_name" => "name",
129        "_int8" => "int8",
130        "_int2" => "int2",
131        "_int2vector" => "int2vector",
132        "_int4" => "int4",
133        "_regproc" => "regproc",
134        "_regprocedure" => "regprocedure",
135        "_regclass" => "regclass",
136        "_regrole" => "regrole",
137        "_text" => "text",
138        "_refcursor" => "refcursor",
139        "_oid" => "oid",
140        "_oidvector" => "oidvector",
141        "_bpchar" => "bpchar",
142        "_varchar" => "varchar",
143        "_float4" => "float4",
144        "_float8" => "float8",
145        "_aclitem" => "aclitem",
146        "_date" => "date",
147        "_time" => "time",
148        "_timestamp" => "timestamp",
149        "_timestamptz" => "timestamptz",
150        "_interval" => "interval",
151        "_numeric" => "numeric",
152        "_timetz" => "timetz",
153        "_record" => "record",
154        "_uuid" => "uuid",
155        "_json" => "json",
156        "_jsonb" => "jsonb",
157        "_regtype" => "regtype",
158        "_xid" => "xid",
159        "_pg_node_tree" => "pg_node_tree",
160        "_int4range" => "int4range",
161        "_int8range" => "int8range",
162        "_numrange" => "numrange",
163        "_daterange" => "daterange",
164        "_tsrange" => "tsrange",
165        "_tstzrange" => "tstzrange",
166        "_int4multirange" => "int4multirange",
167        "_int8multirange" => "int8multirange",
168        "_nummultirange" => "nummultirange",
169        "_datemultirange" => "datemultirange",
170        "_tsmultirange" => "tsmultirange",
171        "_tstzmultirange" => "tstzmultirange",
172        _ => return None,
173    })
174}
175
176impl ColumnType {
177    /// Retain the SQL type identity without a declaration's length, scale, or temporal precision.
178    #[must_use]
179    pub fn without_type_modifiers(&self) -> Self {
180        match self {
181            Self::Varchar(_) => Self::Varchar(None),
182            Self::Character(_) => Self::Bpchar,
183            Self::Numeric { .. } => Self::Numeric {
184                precision: None,
185                scale: None,
186            },
187            Self::Array(element) => Self::Array(Box::new(element.without_type_modifiers())),
188            other => other.without_temporal_modifiers().clone(),
189        }
190    }
191
192    #[must_use]
193    pub const fn temporal_precision(&self) -> Option<u32> {
194        match self {
195            Self::IntervalWithFields { precision, .. } => *precision,
196            Self::TimePrecision(p)
197            | Self::TimeTzPrecision(p)
198            | Self::TimestampPrecision(p)
199            | Self::TimestampTzPrecision(p) => Some(*p),
200            _ => None,
201        }
202    }
203
204    #[must_use]
205    pub const fn without_temporal_modifiers(&self) -> &Self {
206        match self {
207            Self::IntervalWithFields { .. } => &Self::Interval,
208            Self::TimePrecision(_) => &Self::Time,
209            Self::TimeTzPrecision(_) => &Self::TimeTz,
210            Self::TimestampPrecision(_) => &Self::Timestamp,
211            Self::TimestampTzPrecision(_) => &Self::TimestampTz,
212            other => other,
213        }
214    }
215
216    pub(crate) fn with_temporal_precision(
217        self,
218        precision: Option<i64>,
219    ) -> Result<Self, crate::SQLError> {
220        let Some(precision) = precision else {
221            return Ok(self);
222        };
223        if precision < 0 {
224            return Err(crate::SQLError::Routine {
225                sqlstate: "22023".into(),
226                message: format!(
227                    "{} precision must not be negative",
228                    self.regtype_name().to_uppercase()
229                ),
230            });
231        }
232        let precision = u32::try_from(precision.min(6)).expect("bounded temporal precision");
233        Ok(match self {
234            Self::Time => Self::TimePrecision(precision),
235            Self::TimeTz => Self::TimeTzPrecision(precision),
236            Self::Timestamp => Self::TimestampPrecision(precision),
237            Self::TimestampTz => Self::TimestampTzPrecision(precision),
238            other => other,
239        })
240    }
241
242    pub(crate) fn with_interval_modifiers(
243        fields: IntervalFields,
244        precision: Option<i64>,
245    ) -> Result<Self, crate::SQLError> {
246        let precision = precision
247            .map(|precision| {
248                if precision < 0 {
249                    return Err(crate::SQLError::Routine {
250                        sqlstate: "22023".into(),
251                        message: "INTERVAL precision must not be negative".into(),
252                    });
253                }
254                Ok(u32::try_from(precision.min(6)).expect("bounded interval precision"))
255            })
256            .transpose()?;
257        if fields == IntervalFields::All && precision.is_none() {
258            return Ok(Self::Interval);
259        }
260        Ok(Self::IntervalWithFields { fields, precision })
261    }
262
263    #[must_use]
264    pub fn is_integer(&self) -> bool {
265        match self {
266            Self::SmallInteger | Self::Integer | Self::BigInteger | Self::Oid | Self::Xid => true,
267            Self::Domain { base, .. } => base.is_integer(),
268            _ => false,
269        }
270    }
271
272    #[must_use]
273    pub fn is_character_string(&self) -> bool {
274        match self {
275            Self::Text
276            | Self::Name
277            | Self::Varchar(_)
278            | Self::Bpchar
279            | Self::Character(_)
280            | Self::InternalChar
281            | Self::PgNodeTree
282            | Self::AclItem => true,
283            Self::Domain { base, .. } => base.is_character_string(),
284            _ => false,
285        }
286    }
287
288    /// Parse the canonical or accepted spelling of one implemented SQL type.
289    /// This is shared by expression binding and row-schema propagation so a
290    /// cast's declared type is not reconstructed from its runtime value.
291    #[expect(
292        clippy::too_many_lines,
293        reason = "exhaustive AST migration preserves every serialized variant"
294    )]
295    pub fn from_sql_name(name: &str) -> Result<Self, crate::SQLError> {
296        let normalized = name.trim().to_ascii_lowercase();
297        if let Some(element) = builtin_array_element_name(&normalized) {
298            return Self::from_sql_name(element).map(|ty| Self::Array(Box::new(ty)));
299        }
300        if let Some(element) = normalized.strip_suffix("[]") {
301            let element_type = Self::from_sql_name(element)?;
302            if matches!(element_type, Self::Void) {
303                return Err(crate::SQLError::Routine {
304                    sqlstate: "42704".into(),
305                    message: format!("type \"{normalized}\" does not exist"),
306                });
307            }
308            return Ok(Self::Array(Box::new(element_type)));
309        }
310        let (base, modifier) = split_type_modifier(&normalized);
311        let base = base.strip_prefix("pg_catalog.").unwrap_or(&base);
312        let temporal_precision = || {
313            modifier
314                .map(|value| {
315                    value.trim().parse::<i64>().map_err(|_| {
316                        crate::SQLError::TypeMismatch(format!(
317                            "invalid temporal precision: {value}"
318                        ))
319                    })
320                })
321                .transpose()
322        };
323        let character_length = || -> Result<Option<u32>, crate::SQLError> {
324            modifier
325                .map(|value| {
326                    value
327                        .parse::<u32>()
328                        .ok()
329                        .filter(|length| *length > 0)
330                        .ok_or_else(|| {
331                            crate::SQLError::TypeMismatch(format!(
332                                "character length must be greater than zero, got {value}"
333                            ))
334                        })
335                })
336                .transpose()
337        };
338        match base {
339            "smallint" | "int2" => Ok(Self::SmallInteger),
340            "integer" | "int" | "int4" => Ok(Self::Integer),
341            "bigint" | "int8" => Ok(Self::BigInteger),
342            "oid" => Ok(Self::Oid),
343            "xid" => Ok(Self::Xid),
344            "boolean" | "bool" => Ok(Self::Boolean),
345            "void" => Ok(Self::Void),
346            "text" => Ok(Self::Text),
347            "refcursor" => Ok(Self::RefCursor),
348            "name" => Ok(Self::Name),
349            "uuid" => Ok(Self::Uuid),
350            "varchar" | "character varying" => Ok(Self::Varchar(character_length()?)),
351            "character" | "char" => Ok(Self::Character(character_length()?.unwrap_or(1))),
352            "bpchar" => Ok(character_length()?.map_or(Self::Bpchar, Self::Character)),
353            "real" | "float4" => Ok(Self::Real),
354            "double" | "double precision" | "float8" => Ok(Self::DoublePrecision),
355            "numeric" | "decimal" => {
356                let (precision, scale) = match modifier {
357                    None => (None, None),
358                    Some(modifier) => {
359                        let mut parts = modifier.split(',').map(str::trim);
360                        let precision = parts
361                            .next()
362                            .and_then(|value| value.parse::<u32>().ok())
363                            .ok_or_else(|| {
364                                crate::SQLError::TypeMismatch(format!(
365                                    "invalid numeric modifier `{modifier}`"
366                                ))
367                            })?;
368                        let scale = parts
369                            .next()
370                            .map(|value| value.parse::<i32>())
371                            .transpose()
372                            .map_err(|_| {
373                                crate::SQLError::TypeMismatch(format!(
374                                    "invalid numeric modifier `{modifier}`"
375                                ))
376                            })?
377                            .unwrap_or(0);
378                        if parts.next().is_some() {
379                            return Err(crate::SQLError::TypeMismatch(format!(
380                                "invalid numeric modifier `{modifier}`"
381                            )));
382                        }
383                        (Some(precision), Some(scale))
384                    }
385                };
386                Ok(Self::Numeric { precision, scale })
387            }
388            "json" => Ok(Self::Json),
389            "jsonb" => Ok(Self::JsonB),
390            "bytea" => Ok(Self::Bytea),
391            "\"char\"" => Ok(Self::InternalChar),
392            "regproc" => Ok(Self::Regproc),
393            "regprocedure" => Ok(Self::Regprocedure),
394            "regclass" => Ok(Self::Regclass),
395            "regnamespace" => Ok(Self::Regnamespace),
396            "regrole" => Ok(Self::Regrole),
397            "regtype" => Ok(Self::Regtype),
398            "pg_node_tree" => Ok(Self::PgNodeTree),
399            "aclitem" => Ok(Self::AclItem),
400            "int2vector" => Ok(Self::Int2Vector),
401            "oidvector" => Ok(Self::OidVector),
402            "anyarray" => Ok(Self::AnyArray),
403            "record" => Ok(Self::Record),
404            "date" => Ok(Self::Date),
405            "time" | "time without time zone" => {
406                Self::Time.with_temporal_precision(temporal_precision()?)
407            }
408            "timetz" | "time with time zone" => {
409                Self::TimeTz.with_temporal_precision(temporal_precision()?)
410            }
411            "timestamp" | "datetime" | "timestamp without time zone" => {
412                Self::Timestamp.with_temporal_precision(temporal_precision()?)
413            }
414            "timestamptz" | "timestamp with time zone" => {
415                Self::TimestampTz.with_temporal_precision(temporal_precision()?)
416            }
417            "interval" => Self::with_interval_modifiers(IntervalFields::All, temporal_precision()?),
418            other if other.starts_with("interval ") => {
419                let fields = IntervalFields::from_sql_suffix(&other[9..]).ok_or_else(|| {
420                    crate::SQLError::TypeMismatch(format!("invalid interval fields: {other}"))
421                })?;
422                Self::with_interval_modifiers(fields, temporal_precision()?)
423            }
424            "int4range" => Ok(Self::Range(RangeSubtype::Integer)),
425            "int8range" => Ok(Self::Range(RangeSubtype::BigInteger)),
426            "numrange" => Ok(Self::Range(RangeSubtype::Numeric)),
427            "daterange" => Ok(Self::Range(RangeSubtype::Date)),
428            "tsrange" => Ok(Self::Range(RangeSubtype::Timestamp)),
429            "tstzrange" => Ok(Self::Range(RangeSubtype::TimestampTz)),
430            "int4multirange" => Ok(Self::Multirange(RangeSubtype::Integer)),
431            "int8multirange" => Ok(Self::Multirange(RangeSubtype::BigInteger)),
432            "nummultirange" => Ok(Self::Multirange(RangeSubtype::Numeric)),
433            "datemultirange" => Ok(Self::Multirange(RangeSubtype::Date)),
434            "tsmultirange" => Ok(Self::Multirange(RangeSubtype::Timestamp)),
435            "tstzmultirange" => Ok(Self::Multirange(RangeSubtype::TimestampTz)),
436            "vector" => modifier
437                .and_then(|value| value.parse::<u32>().ok())
438                .filter(|dimension| *dimension > 0)
439                .map(Self::Vector)
440                .ok_or_else(|| crate::SQLError::TypeMismatch("VECTOR requires a dimension".into())),
441            "tensor" => modifier
442                .and_then(|value| value.parse::<u32>().ok())
443                .filter(|dimension| *dimension > 0)
444                .map(Self::Tensor)
445                .ok_or_else(|| crate::SQLError::TypeMismatch("TENSOR requires a dimension".into())),
446            other => Err(crate::SQLError::Unsupported(format!(
447                "SQL type `{other}` is not supported"
448            ))),
449        }
450    }
451
452    #[must_use]
453    pub fn sql_name(&self) -> String {
454        match self {
455            Self::Named(name) => name.clone(),
456            Self::SmallInteger => "smallint".into(),
457            Self::Integer => "integer".into(),
458            Self::BigInteger => "bigint".into(),
459            Self::Oid => "oid".into(),
460            Self::Xid => "xid".into(),
461            Self::Boolean => "boolean".into(),
462            Self::Void => "void".into(),
463            Self::Text => "text".into(),
464            Self::RefCursor => "refcursor".into(),
465            Self::Name => "name".into(),
466            Self::Uuid => "uuid".into(),
467            Self::Varchar(Some(length)) => format!("character varying({length})"),
468            Self::Varchar(None) => "character varying".into(),
469            Self::Bpchar => "bpchar".into(),
470            Self::Character(length) => format!("character({length})"),
471            Self::Real => "real".into(),
472            Self::DoublePrecision => "double precision".into(),
473            Self::Numeric {
474                precision: Some(precision),
475                scale: Some(scale),
476            } => format!("numeric({precision},{scale})"),
477            Self::Numeric { .. } => "numeric".into(),
478            Self::Json => "json".into(),
479            Self::JsonB => "jsonb".into(),
480            Self::Bytea => "bytea".into(),
481            Self::InternalChar => "\"char\"".into(),
482            Self::Regproc => "regproc".into(),
483            Self::Regprocedure => "regprocedure".into(),
484            Self::Regclass => "regclass".into(),
485            Self::Regnamespace => "regnamespace".into(),
486            Self::Regrole => "regrole".into(),
487            Self::Regtype => "regtype".into(),
488            Self::PgNodeTree => "pg_node_tree".into(),
489            Self::AclItem => "aclitem".into(),
490            Self::Int2Vector => "int2vector".into(),
491            Self::OidVector => "oidvector".into(),
492            Self::AnyArray => "anyarray".into(),
493            Self::Record => "record".into(),
494            Self::Array(element) => format!("{}[]", element.sql_name()),
495            Self::Date => "date".into(),
496            Self::Time => "time without time zone".into(),
497            Self::TimePrecision(p) => format!("time({p}) without time zone"),
498            Self::TimeTz => "time with time zone".into(),
499            Self::TimeTzPrecision(p) => format!("time({p}) with time zone"),
500            Self::Timestamp => "timestamp without time zone".into(),
501            Self::TimestampPrecision(p) => format!("timestamp({p}) without time zone"),
502            Self::TimestampTz => "timestamp with time zone".into(),
503            Self::TimestampTzPrecision(p) => format!("timestamp({p}) with time zone"),
504            Self::Interval => "interval".into(),
505            Self::IntervalWithFields { fields, precision } => {
506                let precision =
507                    precision.map_or_else(String::new, |precision| format!("({precision})"));
508                format!("interval{}{precision}", fields.sql_suffix())
509            }
510            Self::Range(subtype) => subtype.range_name().into(),
511            Self::Multirange(subtype) => subtype.multirange_name().into(),
512            Self::Vector(dimension) => format!("vector({dimension})"),
513            Self::Tensor(dimension) => format!("tensor({dimension})"),
514            Self::Domain { schema, name, .. } => format!(
515                "{}.{}",
516                crate::compiler::render_relation_component(schema),
517                crate::compiler::render_relation_component(name)
518            ),
519        }
520    }
521
522    /// Name emitted by `PostgreSQL`'s `regtype` output, including
523    /// `pg_typeof(...)`.
524    #[must_use]
525    pub fn regtype_name(&self) -> String {
526        if matches!(self, Self::IntervalWithFields { .. }) {
527            return "interval".into();
528        }
529        if self.temporal_precision().is_some() {
530            return self.without_temporal_modifiers().regtype_name();
531        }
532        match self {
533            Self::Varchar(_) => "character varying".into(),
534            Self::Bpchar | Self::Character(_) => "character".into(),
535            Self::Numeric { .. } => "numeric".into(),
536            Self::Vector(_) => "vector".into(),
537            Self::Tensor(_) => "tensor".into(),
538            Self::Domain { .. } => self.sql_name(),
539            Self::Array(element) => format!("{}[]", element.regtype_name()),
540            other => other.sql_name(),
541        }
542    }
543}
544
545/// Split a SQL type modifier while retaining qualifiers after its parentheses.
546pub(crate) fn split_type_modifier(ty: &str) -> (std::borrow::Cow<'_, str>, Option<&str>) {
547    use std::borrow::Cow;
548    match (ty.find('('), ty.rfind(')')) {
549        (Some(open), Some(close)) if close > open => {
550            let prefix = ty[..open].trim_end();
551            let suffix = ty[close + 1..].trim();
552            let base = if suffix.is_empty() {
553                Cow::Borrowed(prefix)
554            } else {
555                Cow::Owned(format!("{prefix} {suffix}"))
556            };
557            (base, Some(ty[open + 1..close].trim()))
558        }
559        _ => (Cow::Borrowed(ty), None),
560    }
561}