Skip to main content

uqa_sql/catalog/node_tree/
values.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Typed scalar Datums in `PostgreSQL` catalog expression nodes.
8
9pub(super) mod numeric;
10pub(super) mod temporal;
11
12use super::{invalid, Field, Node};
13use crate::catalog::type_metadata::{
14    pg_type_by_value, pg_type_collation_oid, pg_type_len, pg_type_modifier, pg_type_oid,
15};
16use crate::expr::composites::{
17    retained::{capture_type, Descriptors},
18    CompositeConstantSource, CompositeTypeCatalog,
19};
20use crate::{ColumnType, SQLError};
21use uqa_core::Value;
22
23/// Unresolved ROW fields retain `PostgreSQL`'s unknown/cstring Datum until a consumer selects a type.
24pub(super) fn unknown_constant(value: &Value) -> Result<Node, SQLError> {
25    let bytes = match value {
26        Value::Null => Field::Null,
27        Value::Str(text) => {
28            let mut bytes = text.as_bytes().to_vec();
29            bytes.push(0);
30            Field::Datum {
31                length: bytes.len(),
32                bytes,
33            }
34        }
35        _ => return Err(invalid("unknown constant is not a string or NULL")),
36    };
37    Ok(Node::new(
38        "CONST",
39        [
40            ("consttype", 705.into()),
41            ("consttypmod", (-1).into()),
42            ("constcollid", 0.into()),
43            ("constlen", (-2).into()),
44            ("constbyval", false.into()),
45            ("constisnull", matches!(value, Value::Null).into()),
46            ("location", (-1).into()),
47            ("constvalue", bytes),
48        ],
49    ))
50}
51
52pub(super) fn unknown_text(node: &Node) -> Result<&str, SQLError> {
53    let Field::Datum { length, bytes } = node.field("constvalue")? else {
54        return Err(invalid("unknown constant has no Datum"));
55    };
56    if node.integer("constlen")? != -2
57        || node.boolean("constbyval")?
58        || *length != bytes.len()
59        || bytes.last() != Some(&0)
60        || bytes[..bytes.len() - 1].contains(&0)
61    {
62        return Err(invalid("invalid unknown constant Datum"));
63    }
64    std::str::from_utf8(&bytes[..bytes.len() - 1])
65        .map_err(|_| invalid("invalid UTF-8 in unknown Datum"))
66}
67
68pub(super) fn constant(value: &Value, ty: &ColumnType) -> Result<Node, SQLError> {
69    constant_with_source(value, ty, None, None)
70}
71
72/// Encode an already admitted typed constant, preserving any retained physical source and its catalog descriptors.
73pub fn constant_with_source(
74    value: &Value,
75    ty: &ColumnType,
76    source: Option<&CompositeConstantSource>,
77    catalog: Option<&dyn CompositeTypeCatalog>,
78) -> Result<Node, SQLError> {
79    let value = source.map_or(value, |source| &source.value);
80    let bytes = if matches!(value, Value::Null) {
81        Field::Null
82    } else {
83        let descriptors = if let Some(source) = source {
84            source
85                .descriptors
86                .iter()
87                .map(|descriptor| (descriptor.type_oid, std::sync::Arc::new(descriptor.clone())))
88                .collect()
89        } else if matches!(value, Value::Datum(_)) {
90            Descriptors::new()
91        } else {
92            let mut descriptors = Descriptors::new();
93            capture_type(ty, catalog, &mut descriptors)?;
94            descriptors
95        };
96        datum(value, ty, &descriptors)?
97    };
98    Ok(Node::new(
99        "CONST",
100        [
101            ("consttype", pg_type_oid(ty).into()),
102            ("consttypmod", pg_type_modifier(ty).into()),
103            ("constcollid", pg_type_collation_oid(ty).into()),
104            ("constlen", pg_type_len(ty).into()),
105            ("constbyval", pg_type_by_value(ty).into()),
106            ("constisnull", matches!(value, Value::Null).into()),
107            ("location", (-1).into()),
108            ("constvalue", bytes),
109        ],
110    ))
111}
112
113fn datum(value: &Value, ty: &ColumnType, descriptors: &Descriptors) -> Result<Field, SQLError> {
114    let mut representation = ty;
115    while let ColumnType::Domain { base, .. } = representation {
116        representation = base;
117    }
118    let mut bytes = match (value, representation) {
119        (Value::Bool(value), ColumnType::Boolean) => vec![u8::from(*value)],
120        (
121            Value::Int(value),
122            ColumnType::SmallInteger
123            | ColumnType::Integer
124            | ColumnType::BigInteger
125            | ColumnType::Oid
126            | ColumnType::Xid
127            | ColumnType::Regclass
128            | ColumnType::Regtype
129            | ColumnType::Regcollation
130            | ColumnType::Regnamespace
131            | ColumnType::Regrole
132            | ColumnType::Regproc
133            | ColumnType::Regprocedure,
134        ) => value.to_le_bytes().to_vec(),
135        (Value::Float(value), ColumnType::Real) => (*value as f32).to_le_bytes().to_vec(),
136        (Value::Float(value), ColumnType::DoublePrecision) => value.to_le_bytes().to_vec(),
137        (Value::Enum(value), ColumnType::Enum(_)) => value
138            .label_oid()
139            .ok_or_else(|| invalid("enum constant has no retained label identity"))?
140            .to_le_bytes()
141            .to_vec(),
142        (Value::Str(_), ColumnType::InternalChar) => {
143            vec![crate::expr::internal_char::value_byte(value)
144                .ok_or_else(|| SQLError::Internal("invalid internal char carrier".into()))?]
145        }
146        (Value::Str(value) | Value::FixedChar(value), ColumnType::Name) => {
147            let mut bytes = value.as_bytes().to_vec();
148            if bytes.len() >= 64 {
149                return Err(SQLError::Internal("unclipped name Datum".into()));
150            }
151            bytes.resize(64, 0);
152            bytes
153        }
154        (
155            Value::Str(value) | Value::FixedChar(value),
156            ColumnType::Text
157            | ColumnType::Varchar(_)
158            | ColumnType::Bpchar
159            | ColumnType::Character(_)
160            | ColumnType::RefCursor,
161        ) => varlena(value.as_bytes())?,
162        (Value::Bytes(value), ColumnType::Bytea) => varlena(value)?,
163        (Value::Str(value), ColumnType::Uuid) => crate::expr::parse_uuid_bytes(value)?.to_vec(),
164        (Value::Json(value), ColumnType::Json) => varlena(value.as_bytes())?,
165        (Value::JsonB(value), ColumnType::JsonB) => {
166            varlena(&crate::expr::encode_jsonb_datum(value)?)?
167        }
168        (Value::Array(array), ColumnType::Array(element)) => {
169            let payload =
170                crate::expr::composites::retained::array_payload(array, element, descriptors)
171                    .ok_or_else(|| {
172                        SQLError::Unsupported(format!(
173                            "catalog array Datum encoding for {}",
174                            ty.sql_name()
175                        ))
176                    })?;
177            varlena(&payload)?
178        }
179        (Value::Record(fields), ColumnType::Composite(reference)) => {
180            crate::expr::composites::retained::record_bytes(fields, reference.oid, descriptors)
181                .ok_or_else(|| invalid("composite constant has no retained tuple layout"))?
182        }
183        (Value::Datum(_), ColumnType::Array(_) | ColumnType::Composite(_)) => {
184            let Value::Datum(owned) = crate::expr::datums::copy_constant_field(value, ty)? else {
185                return Err(SQLError::Internal(
186                    "physical catalog constant changed carrier".into(),
187                ));
188            };
189            owned.bytes().to_vec()
190        }
191        (Value::Decimal(value), ColumnType::Numeric { .. }) => varlena(&numeric::encode(value)?)?,
192        (Value::Temporal(value), _) => temporal::encode(value, pg_type_oid(representation))?,
193        _ => {
194            return Err(SQLError::Unsupported(format!(
195                "catalog Datum encoding for {}",
196                ty.sql_name()
197            )))
198        }
199    };
200    let length = if pg_type_by_value(ty) {
201        bytes.resize(8, 0);
202        usize::try_from(pg_type_len(ty))
203            .map_err(|_| SQLError::Internal("invalid by-value Datum length".into()))?
204    } else {
205        bytes.len()
206    };
207    Ok(Field::Datum { length, bytes })
208}
209
210pub(super) fn scalar_text(node: &Node) -> Result<String, SQLError> {
211    let oid = u32::try_from(node.integer("consttype")?)
212        .map_err(|_| invalid("invalid scalar constant type"))?;
213    let ty = crate::catalog::type_metadata::builtin_scalar_type(oid)
214        .ok_or_else(|| invalid("unknown scalar constant type"))?;
215    if node.boolean("constbyval")? || node.integer("constlen")? != pg_type_len(ty) {
216        return Err(invalid("invalid scalar constant layout"));
217    }
218    let Field::Datum { length, bytes } = node.field("constvalue")? else {
219        return Err(invalid("constant has no Datum"));
220    };
221    if *ty == ColumnType::Uuid {
222        if *length != 16 {
223            return Err(invalid("invalid UUID Datum length"));
224        }
225        let bytes = bytes
226            .as_slice()
227            .try_into()
228            .map_err(|_| invalid("invalid UUID Datum length"))?;
229        return Ok(crate::expr::format_uuid(bytes));
230    }
231    let payload = varlena_payload(*length, bytes)?;
232    let control = uqa_core::memory::ProductionControl::uncontrolled();
233    match ty {
234        ColumnType::Bytea => crate::expr::bytea_to_string_with_control(payload, &control)
235            .map(|text| text.into_uncontrolled().expect("ordinary bytea text")),
236        ColumnType::Json => std::str::from_utf8(payload)
237            .map(str::to_owned)
238            .map_err(|_| invalid("invalid JSON Datum encoding")),
239        ColumnType::JsonB => crate::expr::decode_jsonb_datum_with_control(payload, &control)
240            .map(|text| text.into_uncontrolled().expect("ordinary JSONB text")),
241        _ => Err(invalid("unsupported scalar constant type")),
242    }
243}
244
245pub(super) fn varlena_payload(length: usize, bytes: &[u8]) -> Result<&[u8], SQLError> {
246    let header = bytes
247        .get(..4)
248        .and_then(|bytes| bytes.try_into().ok())
249        .map(u32::from_le_bytes)
250        .ok_or_else(|| invalid("truncated varlena Datum"))?;
251    if bytes.len() != length || header & 3 != 0 || usize::try_from(header >> 2).ok() != Some(length)
252    {
253        return Err(invalid("invalid varlena Datum length"));
254    }
255    Ok(&bytes[4..])
256}
257
258pub(super) fn catalog_text(
259    node: &Node,
260    catalog: Option<&dyn crate::expr::SQLValueCatalog>,
261) -> Result<Option<String>, SQLError> {
262    let oid = u32::try_from(node.integer("consttype")?)
263        .map_err(|_| invalid("invalid catalog constant type"))?;
264    let ty = if let Some(element) = crate::catalog::type_metadata::builtin_array_element(oid) {
265        ColumnType::Array(Box::new(element.clone()))
266    } else if let Some(ty) = catalog
267        .map(|catalog| catalog.value_type_by_oid(oid))
268        .transpose()?
269        .flatten()
270    {
271        ty
272    } else {
273        return Ok(None);
274    };
275    if node.boolean("constbyval")? != pg_type_by_value(&ty)
276        || node.integer("constlen")? != pg_type_len(&ty)
277    {
278        return Err(invalid("invalid catalog constant layout"));
279    }
280    let Field::Datum { length, bytes } = node.field("constvalue")? else {
281        return Err(invalid("constant has no Datum"));
282    };
283    if pg_type_len(&ty) == -1 {
284        varlena_payload(*length, bytes)?;
285    } else if i64::try_from(*length).ok() != Some(pg_type_len(&ty))
286        || (!pg_type_by_value(&ty) && bytes.len() != *length)
287    {
288        return Err(invalid("invalid catalog constant length"));
289    }
290    let value = Value::Datum(uqa_core::DatumValue::new(oid, 0, bytes.clone()));
291    crate::result::format_postgres_text_with_catalog(&value, &ty, catalog).map(Some)
292}
293
294fn varlena(payload: &[u8]) -> Result<Vec<u8>, SQLError> {
295    let length = payload
296        .len()
297        .checked_add(4)
298        .and_then(|length| u32::try_from(length).ok())
299        .filter(|length| *length <= u32::MAX >> 2)
300        .ok_or_else(|| SQLError::Routine {
301            sqlstate: "54000".into(),
302            message: "catalog Datum is too large".into(),
303        })?;
304    Ok((length << 2)
305        .to_le_bytes()
306        .into_iter()
307        .chain(payload.iter().copied())
308        .collect())
309}