Skip to main content

uqa_sql/assignment/
domain.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Domain base conversion and inherited constraint evaluation.
8
9use super::AssignmentContext;
10use crate::{ColumnType, ResultRow, RowSchema, SQLError};
11use uqa_core::Value;
12
13pub fn cast_domain_value(
14    context: &dyn AssignmentContext,
15    value: &Value,
16    source: Option<&str>,
17    ty: &ColumnType,
18) -> Result<Option<Value>, SQLError> {
19    convert_domain_value(context, value, source, ty, false)
20}
21
22pub fn assign_domain_value(
23    context: &dyn AssignmentContext,
24    value: &Value,
25    ty: &ColumnType,
26) -> Result<Option<Value>, SQLError> {
27    assign_domain_value_from(context, value, None, ty)
28}
29
30pub(super) fn assign_domain_value_from(
31    context: &dyn AssignmentContext,
32    value: &Value,
33    source: Option<&str>,
34    ty: &ColumnType,
35) -> Result<Option<Value>, SQLError> {
36    convert_domain_value(context, value, source, ty, true)
37}
38
39fn convert_domain_value(
40    context: &dyn AssignmentContext,
41    value: &Value,
42    source: Option<&str>,
43    ty: &ColumnType,
44    assignment: bool,
45) -> Result<Option<Value>, SQLError> {
46    let ColumnType::Domain { oid, .. } = ty else {
47        return Ok(None);
48    };
49    let Some(domain) = context.domain_by_oid(*oid) else {
50        return Ok(None);
51    };
52    if source
53        .and_then(|name| context.resolve_type_name(name).ok().flatten())
54        .as_ref()
55        == Some(ty)
56    {
57        return super::conversion::normalize_existing(value.clone(), ty).map(Some);
58    }
59    let mut chain = vec![domain.clone()];
60    let mut base = domain.definition.base.clone();
61    while let ColumnType::Domain {
62        oid,
63        base: underlying,
64        ..
65    } = &base
66    {
67        if let Some(parent) = context.domain_by_oid(*oid) {
68            base = parent.definition.base.clone();
69            chain.push(parent);
70        } else {
71            base = *underlying.clone();
72        }
73    }
74    let value = if assignment {
75        let source = source
76            .map(|source| {
77                context
78                    .resolve_type_name(source)
79                    .map_err(SQLError::Internal)
80            })
81            .transpose()?
82            .flatten();
83        super::conversion::coerce_assignment_value(context, value.clone(), &base, source.as_ref())?
84    } else {
85        crate::expr::cast_value_with_type_resolution(
86            value,
87            source,
88            &base.catalog_name(),
89            Some(context),
90        )?
91    };
92    if matches!(value, Value::Null)
93        && chain
94            .iter()
95            .any(|domain| domain.definition.not_null.is_some())
96    {
97        return Err(domain_error(
98            "23502",
99            format!(
100                "domain {} does not allow null values",
101                domain_display_name(context, domain.oid)?
102            ),
103        ));
104    }
105    let row = ResultRow::from([("value".into(), value.clone())]);
106    let schema = RowSchema::with_types(
107        vec!["value".into()],
108        vec![Some(domain.definition.base.clone())],
109    );
110    for check in chain
111        .iter()
112        .rev()
113        .flat_map(|domain| &domain.definition.checks)
114    {
115        let result = context.evaluate_domain_check(&check.expression, &row, &schema)?;
116        if result == Value::Bool(false) {
117            return Err(domain_error(
118                "23514",
119                format!(
120                    "value for domain {} violates check constraint \"{}\"",
121                    domain_display_name(context, domain.oid)?,
122                    check.name.as_deref().expect("bound domain constraint")
123                ),
124            ));
125        }
126    }
127    Ok(Some(value))
128}
129
130fn domain_display_name(context: &dyn AssignmentContext, oid: u32) -> Result<String, SQLError> {
131    context
132        .resolve_regtype_output(&ColumnType::Regtype, i64::from(oid))
133        .map_err(SQLError::Internal)?
134        .ok_or_else(|| SQLError::Internal("domain type has no catalog display name".into()))
135}
136
137pub fn domain_error(sqlstate: &str, message: impl Into<String>) -> SQLError {
138    SQLError::Routine {
139        sqlstate: sqlstate.into(),
140        message: message.into(),
141    }
142}