Skip to main content

uqa_sql/
params.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Bind parameters for `Engine::sql(query, params)`.
8
9use crate::SQLError;
10use uqa_core::{
11    memory::{Produced, ProductionControl, ProductionVec},
12    Value,
13};
14
15use crate::ast::ColumnType;
16
17/// Value bound to a `$N` placeholder.
18#[derive(Debug, Clone)]
19pub enum SQLParam {
20    Scalar(Value),
21    /// A scalar value whose declared SQL type must survive runtime [`Value`] carrier normalization.
22    TypedScalar {
23        value: Value,
24        ty: ColumnType,
25    },
26    Vector(Vec<f32>),
27    Tensor(Vec<Vec<f32>>),
28}
29
30impl SQLParam {
31    pub fn scalar(value: Value) -> Self {
32        Self::Scalar(value)
33    }
34
35    #[must_use]
36    pub fn typed_scalar(value: Value, ty: ColumnType) -> Self {
37        Self::TypedScalar { value, ty }
38    }
39
40    /// Return the scalar carrier without changing the semantics of untyped [`SQLParam::Scalar`] values.
41    #[must_use]
42    pub fn scalar_value(&self) -> Option<&Value> {
43        match self {
44            Self::Scalar(value) | Self::TypedScalar { value, .. } => Some(value),
45            Self::Vector(_) | Self::Tensor(_) => None,
46        }
47    }
48
49    /// Return the explicit SQL type carried only by [`SQLParam::TypedScalar`].
50    #[must_use]
51    pub fn declared_scalar_type(&self) -> Option<&ColumnType> {
52        match self {
53            Self::TypedScalar { ty, .. } => Some(ty),
54            Self::Scalar(_) | Self::Vector(_) | Self::Tensor(_) => None,
55        }
56    }
57
58    /// Materialize the parameter carrier with ordinary SQL value semantics.
59    pub fn to_value(&self) -> Result<Value, SQLError> {
60        self.to_value_with_control(&ProductionControl::uncontrolled())
61            .map(|value| value.into_uncontrolled().expect("ordinary parameter value"))
62    }
63
64    /// Copy a borrowed parameter while retaining every container and child payload under its producer's allowance.
65    pub fn to_value_with_control(
66        &self,
67        control: &ProductionControl<'_>,
68    ) -> Result<Produced<Value>, SQLError> {
69        control.check()?;
70        match self {
71            Self::Scalar(value) | Self::TypedScalar { value, .. } => Ok(control.copy_value(value)?),
72            Self::Vector(values) => vector_value(values, control),
73            Self::Tensor(vectors) => {
74                let mut output = ProductionVec::new(*control);
75                output.reserve(vectors.len())?;
76                for values in vectors {
77                    output.push_produced(vector_value(values, control)?)?;
78                }
79                let (values, memory) = output.finish()?.into_parts();
80                Ok(control.finish(Value::List(values), memory)?)
81            }
82        }
83    }
84
85    pub fn vector(v: Vec<f32>) -> Self {
86        Self::Vector(v)
87    }
88
89    pub fn tensor(v: Vec<Vec<f32>>) -> Self {
90        Self::Tensor(v)
91    }
92}
93
94fn vector_value(
95    values: &[f32],
96    control: &ProductionControl<'_>,
97) -> Result<Produced<Value>, SQLError> {
98    let mut output = ProductionVec::new(*control);
99    output.reserve(values.len())?;
100    for value in values {
101        output.push_produced(
102            control.finish(Value::Float(f64::from(*value)), control.empty_reservation())?,
103        )?;
104    }
105    let (values, memory) = output.finish()?.into_parts();
106    Ok(control.finish(Value::List(values), memory)?)
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn typed_scalar_preserves_declared_type_without_changing_scalar_access() {
115        let value = Value::Int(7);
116        let typed = SQLParam::typed_scalar(value.clone(), ColumnType::SmallInteger);
117        assert_eq!(typed.scalar_value(), Some(&value));
118        assert_eq!(
119            typed.declared_scalar_type(),
120            Some(&ColumnType::SmallInteger)
121        );
122
123        let scalar = SQLParam::scalar(value.clone());
124        assert_eq!(scalar.scalar_value(), Some(&value));
125        assert_eq!(scalar.declared_scalar_type(), None);
126    }
127
128    #[test]
129    fn parameter_value_owners_keep_tensor_and_scalar_payloads_until_drop() {
130        use uqa_core::{memory::MemoryBudget, CancellationToken};
131        let budget = MemoryBudget::new(1 << 16);
132        let token = CancellationToken::new();
133        let control = ProductionControl::new(&budget, &token, &token);
134        for (parameter, expected) in [
135            (
136                SQLParam::typed_scalar(Value::Str("payload".repeat(32)), ColumnType::Text),
137                Value::Str("payload".repeat(32)),
138            ),
139            (
140                SQLParam::Vector(vec![1.0, 2.5]),
141                Value::List(vec![Value::Float(1.0), Value::Float(2.5)]),
142            ),
143            (
144                SQLParam::Tensor(vec![vec![1.0, 2.5], vec![]]),
145                Value::List(vec![
146                    Value::List(vec![Value::Float(1.0), Value::Float(2.5)]),
147                    Value::List(vec![]),
148                ]),
149            ),
150        ] {
151            let value = parameter.to_value_with_control(&control).unwrap();
152            assert_eq!(*value, expected);
153            assert!(value.reserved_bytes() > 0);
154            assert_eq!(budget.used(), value.reserved_bytes());
155            drop(value);
156            assert_eq!(budget.used(), 0);
157            assert_eq!(parameter.to_value().unwrap(), expected);
158        }
159    }
160
161    #[test]
162    fn parameter_production_releases_partial_output_on_quota_and_both_tokens() {
163        use uqa_core::{memory::MemoryBudget, CancellationToken};
164        let budget = MemoryBudget::new(256);
165        let original = CancellationToken::new();
166        let invoking = CancellationToken::new();
167        let control = ProductionControl::new(&budget, &original, &invoking);
168        let held = control.copy_text("held").unwrap();
169        let parameter = SQLParam::Tensor(vec![vec![1.0; 64], vec![2.0; 64]]);
170        assert_eq!(
171            parameter
172                .to_value_with_control(&control)
173                .unwrap_err()
174                .sqlstate(),
175            Some("53200")
176        );
177        assert_eq!(budget.used(), held.reserved_bytes());
178        for token in [&original, &invoking] {
179            token.cancel();
180            assert_eq!(
181                parameter
182                    .to_value_with_control(&control)
183                    .unwrap_err()
184                    .sqlstate(),
185                Some("57014")
186            );
187            assert_eq!(budget.used(), held.reserved_bytes());
188            token.reset();
189        }
190        assert_eq!(
191            parameter.to_value().unwrap(),
192            Value::List(vec![
193                Value::List(vec![Value::Float(1.0); 64]),
194                Value::List(vec![Value::Float(2.0); 64])
195            ])
196        );
197    }
198}