use super::{
validate_sql_function_record, ColumnType, RoutineTypeCatalog, SQLError,
SQLFunctionResultColumn, SQLFunctionResultKind,
};
#[derive(Clone, Copy, PartialEq, Eq)]
enum RecordResultContext {
SQLTuple,
Procedural,
Trigger,
}
pub fn validate_anonymous_record_result(
types: &dyn RoutineTypeCatalog,
source: &[Option<ColumnType>],
target: &[SQLFunctionResultColumn],
sql_kind: Option<SQLFunctionResultKind>,
) -> Result<(), SQLError> {
if sql_kind == Some(SQLFunctionResultKind::Value) {
return validate_sql_function_record(
types,
source,
&target
.iter()
.map(|column| column.ty.clone())
.collect::<Vec<_>>(),
);
}
validate_record_fields(
types,
source,
target,
if sql_kind == Some(SQLFunctionResultKind::Tuple) {
RecordResultContext::SQLTuple
} else {
RecordResultContext::Procedural
},
)
}
pub fn validate_trigger_record_result(
types: &dyn RoutineTypeCatalog,
source: &[Option<ColumnType>],
target: &[SQLFunctionResultColumn],
) -> Result<(), SQLError> {
validate_record_fields(types, source, target, RecordResultContext::Trigger)
}
fn validate_record_fields(
types: &dyn RoutineTypeCatalog,
source: &[Option<ColumnType>],
target: &[SQLFunctionResultColumn],
context: RecordResultContext,
) -> Result<(), SQLError> {
let tuple = context == RecordResultContext::SQLTuple;
if source.len() != target.len() {
let detail = if tuple {
format!(
"Final statement returns too {} columns.",
if source.len() < target.len() {
"few"
} else {
"many"
}
)
} else {
format!(
"Number of returned columns ({}) does not match expected column count ({}).",
source.len(),
target.len()
)
};
return Err(mismatch(context, detail));
}
for (index, (source, target)) in source.iter().zip(target).enumerate() {
if tuple
&& source
.as_ref()
.is_none_or(|source| crate::assignment_type_compatible(source, &target.ty))
{
continue;
}
if !tuple
&& source
.as_ref()
.is_some_and(|source| same_identity(source, &target.ty))
{
continue;
}
let source = source
.as_ref()
.map_or_else(|| Ok("unknown".into()), |source| types.format_type(source))?;
let expected = types.format_type(&target.ty)?;
let detail = if tuple {
format!(
"Final statement returns {source} instead of {expected} at column {}.",
index + 1
)
} else {
format!("Returned type {source} does not match expected type {expected} in column \"{}\" (position {}).", target.name, index + 1)
};
return Err(mismatch(context, detail));
}
Ok(())
}
fn same_identity(source: &ColumnType, target: &ColumnType) -> bool {
use crate::catalog::type_metadata::{pg_type_modifier, pg_type_oid};
pg_type_oid(source) == pg_type_oid(target)
&& (pg_type_modifier(target) < 0 || pg_type_modifier(source) == pg_type_modifier(target))
}
fn mismatch(context: RecordResultContext, detail: String) -> SQLError {
SQLError::Diagnostic {
sqlstate: if context == RecordResultContext::SQLTuple {
"42P13"
} else {
"42804"
}
.into(),
message: match context {
RecordResultContext::SQLTuple => {
"return type mismatch in function declared to return record"
}
RecordResultContext::Procedural => {
"returned record type does not match expected record type"
}
RecordResultContext::Trigger => {
"returned row structure does not match the structure of the triggering table"
}
}
.into(),
detail: Some(detail),
hint: None,
}
}