Skip to main content

uqa_sql/routines/result_check/
anonymous.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Caller-defined record columns retain the routine's original result semantics.
8
9use super::{
10    validate_sql_function_record, ColumnType, RoutineTypeCatalog, SQLError,
11    SQLFunctionResultColumn, SQLFunctionResultKind,
12};
13
14#[derive(Clone, Copy, PartialEq, Eq)]
15enum RecordResultContext {
16    SQLTuple,
17    Procedural,
18    Trigger,
19}
20
21/// SQL target lists assign each column; a whole SQL or procedural record keeps its producer's field identities.
22pub fn validate_anonymous_record_result(
23    types: &dyn RoutineTypeCatalog,
24    source: &[Option<ColumnType>],
25    target: &[SQLFunctionResultColumn],
26    sql_kind: Option<SQLFunctionResultKind>,
27) -> Result<(), SQLError> {
28    if sql_kind == Some(SQLFunctionResultKind::Value) {
29        return validate_sql_function_record(
30            types,
31            source,
32            &target
33                .iter()
34                .map(|column| column.ty.clone())
35                .collect::<Vec<_>>(),
36        );
37    }
38    validate_record_fields(
39        types,
40        source,
41        target,
42        if sql_kind == Some(SQLFunctionResultKind::Tuple) {
43            RecordResultContext::SQLTuple
44        } else {
45            RecordResultContext::Procedural
46        },
47    )
48}
49
50/// A trigger returns its relation's exact descriptor; validating the fields does not coerce their values again.
51pub fn validate_trigger_record_result(
52    types: &dyn RoutineTypeCatalog,
53    source: &[Option<ColumnType>],
54    target: &[SQLFunctionResultColumn],
55) -> Result<(), SQLError> {
56    validate_record_fields(types, source, target, RecordResultContext::Trigger)
57}
58
59fn validate_record_fields(
60    types: &dyn RoutineTypeCatalog,
61    source: &[Option<ColumnType>],
62    target: &[SQLFunctionResultColumn],
63    context: RecordResultContext,
64) -> Result<(), SQLError> {
65    let tuple = context == RecordResultContext::SQLTuple;
66    if source.len() != target.len() {
67        let detail = if tuple {
68            format!(
69                "Final statement returns too {} columns.",
70                if source.len() < target.len() {
71                    "few"
72                } else {
73                    "many"
74                }
75            )
76        } else {
77            format!(
78                "Number of returned columns ({}) does not match expected column count ({}).",
79                source.len(),
80                target.len()
81            )
82        };
83        return Err(mismatch(context, detail));
84    }
85    for (index, (source, target)) in source.iter().zip(target).enumerate() {
86        if tuple
87            && source
88                .as_ref()
89                .is_none_or(|source| crate::assignment_type_compatible(source, &target.ty))
90        {
91            continue;
92        }
93        if !tuple
94            && source
95                .as_ref()
96                .is_some_and(|source| same_identity(source, &target.ty))
97        {
98            continue;
99        }
100        let source = source
101            .as_ref()
102            .map_or_else(|| Ok("unknown".into()), |source| types.format_type(source))?;
103        let expected = types.format_type(&target.ty)?;
104        let detail = if tuple {
105            format!(
106                "Final statement returns {source} instead of {expected} at column {}.",
107                index + 1
108            )
109        } else {
110            format!("Returned type {source} does not match expected type {expected} in column \"{}\" (position {}).", target.name, index + 1)
111        };
112        return Err(mismatch(context, detail));
113    }
114    Ok(())
115}
116
117fn same_identity(source: &ColumnType, target: &ColumnType) -> bool {
118    use crate::catalog::type_metadata::{pg_type_modifier, pg_type_oid};
119    pg_type_oid(source) == pg_type_oid(target)
120        && (pg_type_modifier(target) < 0 || pg_type_modifier(source) == pg_type_modifier(target))
121}
122
123fn mismatch(context: RecordResultContext, detail: String) -> SQLError {
124    SQLError::Diagnostic {
125        sqlstate: if context == RecordResultContext::SQLTuple {
126            "42P13"
127        } else {
128            "42804"
129        }
130        .into(),
131        message: match context {
132            RecordResultContext::SQLTuple => {
133                "return type mismatch in function declared to return record"
134            }
135            RecordResultContext::Procedural => {
136                "returned record type does not match expected record type"
137            }
138            RecordResultContext::Trigger => {
139                "returned row structure does not match the structure of the triggering table"
140            }
141        }
142        .into(),
143        detail: Some(detail),
144        hint: None,
145    }
146}