Skip to main content

uqa_sql/semantics/partition/
datum_text.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Text of stored partition bound datums, as `PostgreSQL`'s `get_const_expr` renders a constant without a type label: the value's output text quoted as a literal, except non-negative `integer`, float-looking `numeric`, and `boolean` values.
8
9use crate::ast::{ColumnType, Expr, PartitionRangeDatum};
10use crate::expr::EngineHook;
11use crate::result::format_postgres_text;
12use crate::SQLError;
13use uqa_core::Value;
14
15/// Render one bound datum of partition key type `ty`.
16pub fn partition_datum_text(
17    value: &Value,
18    ty: &ColumnType,
19    engine: Option<&dyn EngineHook>,
20) -> Result<String, SQLError> {
21    if matches!(value, Value::Null) {
22        return Ok("NULL".into());
23    }
24    let text = format_postgres_text(value, ty, engine)?;
25    Ok(match ty {
26        ColumnType::Integer if !text.starts_with('-') => text,
27        ColumnType::Numeric { .. }
28            if text.starts_with(|character: char| character.is_ascii_digit())
29                && text.contains(['e', 'E', '.']) =>
30        {
31            text
32        }
33        ColumnType::Boolean => if text == "t" { "true" } else { "false" }.into(),
34        _ => quote_literal(&text),
35    })
36}
37
38/// `get_range_partbound_string`: one range bound as `(datum, ...)`, with `MINVALUE` and `MAXVALUE` spelled out.
39pub fn range_bound_text(
40    datums: &[PartitionRangeDatum],
41    types: &[ColumnType],
42    engine: Option<&dyn EngineHook>,
43) -> Result<String, SQLError> {
44    let mut rendered = Vec::with_capacity(datums.len());
45    for (position, datum) in datums.iter().enumerate() {
46        rendered.push(match datum {
47            PartitionRangeDatum::MinValue => "MINVALUE".into(),
48            PartitionRangeDatum::MaxValue => "MAXVALUE".into(),
49            PartitionRangeDatum::Value(expression) => {
50                let ty = types.get(position).ok_or_else(|| {
51                    SQLError::Internal("partition range bound is wider than its key".into())
52                })?;
53                partition_datum_text(stored_datum(expression)?, ty, engine)?
54            }
55        });
56    }
57    Ok(format!("({})", rendered.join(", ")))
58}
59
60/// The constant a transformed bound stores for one datum.
61pub fn stored_datum(expression: &Expr) -> Result<&Value, SQLError> {
62    match expression {
63        Expr::Literal(value) | Expr::TypedLiteral { value, .. } => Ok(value),
64        other => Err(SQLError::Internal(format!(
65            "partition bound datum was not evaluated to a constant: {other:?}"
66        ))),
67    }
68}
69
70/// `simple_quote_literal` with standard-conforming strings: only single quotes are doubled.
71fn quote_literal(text: &str) -> String {
72    let mut quoted = String::with_capacity(text.len() + 2);
73    quoted.push('\'');
74    for character in text.chars() {
75        if character == '\'' {
76            quoted.push('\'');
77        }
78        quoted.push(character);
79    }
80    quoted.push('\'');
81    quoted
82}
83
84#[cfg(test)]
85mod tests {
86    use super::partition_datum_text;
87    use crate::ast::ColumnType;
88    use uqa_core::{DecimalValue, Value};
89
90    fn text(value: Value, ty: &ColumnType) -> String {
91        partition_datum_text(&value, ty, None).unwrap()
92    }
93
94    #[test]
95    fn constants_follow_get_const_expr_spelling() {
96        assert_eq!(text(Value::Null, &ColumnType::Integer), "NULL");
97        assert_eq!(text(Value::Int(2), &ColumnType::Integer), "2");
98        assert_eq!(text(Value::Int(-1), &ColumnType::Integer), "'-1'");
99        assert_eq!(text(Value::Int(2), &ColumnType::SmallInteger), "'2'");
100        assert_eq!(text(Value::Int(2), &ColumnType::BigInteger), "'2'");
101        let numeric = ColumnType::Numeric {
102            precision: None,
103            scale: None,
104        };
105        let decimal = |text: &str| Value::Decimal(DecimalValue::parse(text).unwrap());
106        assert_eq!(text(decimal("1.50"), &numeric), "1.50");
107        assert_eq!(text(decimal("2"), &numeric), "'2'");
108        assert_eq!(text(decimal("-3.5"), &numeric), "'-3.5'");
109        assert_eq!(text(Value::Bool(true), &ColumnType::Boolean), "true");
110        assert_eq!(
111            text(Value::Str("it's".into()), &ColumnType::Text),
112            "'it''s'"
113        );
114        assert_eq!(text(Value::Str("x\\y".into()), &ColumnType::Text), "'x\\y'");
115    }
116}