Skip to main content

uqa_sql/
prepared.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Prepared definitions, result descriptors, and SQL EXECUTE argument contracts.
8
9use crate::{ColumnType, RowSchema, SQLError, SQLParam};
10use uqa_core::Value;
11pub mod arguments;
12pub mod definition;
13pub mod dependencies;
14pub mod entry;
15pub mod planning;
16
17pub fn declared_parameter_types(
18    resolver: &dyn crate::FunctionTypeResolver,
19    logical_plan: &mut crate::plan::UnifiedPlan,
20    declared: &[ColumnType],
21) -> Result<Vec<Option<ColumnType>>, SQLError> {
22    let mut parameter_types = declared
23        .iter()
24        .map(|ty| match ty {
25            crate::ast::ColumnType::Named(name) if is_unknown_type(name)? => Ok(None),
26            _ => crate::type_resolution::resolve_declared_column_type(resolver, ty).map(Some),
27        })
28        .collect::<Result<Vec<_>, _>>()?;
29    logical_plan.rewrite_scalar_expressions(&mut |expression| {
30        if let crate::ScalarExpr::Param(index) = expression {
31            parameter_types.resize(parameter_types.len().max(*index), None);
32        }
33    });
34    Ok(parameter_types)
35}
36
37fn is_unknown_type(name: &str) -> Result<bool, crate::SQLError> {
38    Ok(crate::parse_regtype_name(name)?.is_some_and(|parsed| {
39        parsed.array_dimensions == 0
40            && !parsed.has_type_modifiers
41            && match parsed.names.as_slice() {
42                [local] => local == "unknown",
43                [schema, local] => schema == "pg_catalog" && local == "unknown",
44                _ => false,
45            }
46    }))
47}
48
49pub fn analyze_prepared_plan(
50    routines: &dyn crate::routines::RoutineResolution,
51    plan: &crate::plan::UnifiedPlan,
52    parameter_types: &[Option<ColumnType>],
53    scope: &crate::binding::context::BindingContext<'_>,
54) -> Result<Option<RowSchema>, SQLError> {
55    let params = parameter_types
56        .iter()
57        .map(|ty| match ty {
58            Some(ty) => SQLParam::typed_scalar(Value::Null, ty.clone()),
59            None => SQLParam::Scalar(Value::Null),
60        })
61        .collect::<Vec<_>>();
62    match plan {
63        crate::plan::UnifiedPlan::Query(query) => {
64            crate::binding::analyze_query_plan_schema(routines, query, &params, scope, None)
65                .map(Some)
66        }
67        crate::plan::UnifiedPlan::Command(command) => {
68            crate::binding::analyze_prepared_command_schema(routines, command, &params, scope)
69        }
70    }
71}
72
73pub fn prepared_result_schema_matches(left: Option<&RowSchema>, right: Option<&RowSchema>) -> bool {
74    match (left, right) {
75        (None, None) => true,
76        (Some(left), Some(right)) => {
77            left.columns() == right.columns()
78                && left.column_types().len() == right.column_types().len()
79                && left
80                    .column_types()
81                    .iter()
82                    .zip(right.column_types())
83                    .all(|(left, right)| {
84                        prepared_type_identity(left.as_ref())
85                            == prepared_type_identity(right.as_ref())
86                    })
87        }
88        _ => false,
89    }
90}
91
92fn prepared_type_identity(ty: Option<&ColumnType>) -> Option<(u32, i32)> {
93    ty.map(|ty| {
94        if let ColumnType::Domain { oid, .. } = ty {
95            (*oid, -1)
96        } else {
97            let metadata = crate::catalog::result_type::postgres_result_type(ty);
98            (metadata.type_oid, metadata.type_modifier)
99        }
100    })
101}
102
103pub fn statement_error(sqlstate: &str, name: &str, reason: &str) -> SQLError {
104    error(sqlstate, format!("prepared statement \"{name}\" {reason}"))
105}
106
107pub fn execute_parameter_types(
108    name: &str,
109    types: Option<Vec<Option<ColumnType>>>,
110    argument_count: usize,
111) -> Result<Vec<Option<ColumnType>>, SQLError> {
112    let types = types.ok_or_else(|| statement_error("26000", name, "does not exist"))?;
113    // SQL EXECUTE ignores argument lists for parameterless definitions; protocol Bind checks its own arity.
114    if types.is_empty() {
115        return Ok(Vec::new());
116    }
117    if argument_count != types.len() {
118        return Err(error(
119            "42601",
120            format!("wrong number of parameters for prepared statement \"{name}\""),
121        ));
122    }
123    Ok(types)
124}
125
126pub(super) fn error(sqlstate: &str, message: impl Into<String>) -> SQLError {
127    SQLError::Routine {
128        sqlstate: sqlstate.into(),
129        message: message.into(),
130    }
131}
132
133#[cfg(test)]
134mod tests;