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    convert_domain_value(context, value, None, ty, true)
28}
29
30fn convert_domain_value(
31    context: &dyn AssignmentContext,
32    value: &Value,
33    source: Option<&str>,
34    ty: &ColumnType,
35    assignment: bool,
36) -> Result<Option<Value>, SQLError> {
37    let ColumnType::Domain { oid, .. } = ty else {
38        return Ok(None);
39    };
40    let Some(domain) = context.domain_by_oid(*oid) else {
41        return Ok(None);
42    };
43    if source
44        .and_then(|name| context.resolve_type_name(name).ok().flatten())
45        .as_ref()
46        == Some(ty)
47    {
48        return Ok(Some(value.clone()));
49    }
50    let mut chain = vec![domain.clone()];
51    let mut base = domain.definition.base.clone();
52    while let ColumnType::Domain {
53        oid,
54        base: underlying,
55        ..
56    } = &base
57    {
58        if let Some(parent) = context.domain_by_oid(*oid) {
59            base = parent.definition.base.clone();
60            chain.push(parent);
61        } else {
62            base = *underlying.clone();
63        }
64    }
65    let value = if assignment {
66        super::conversion::convert_value_to_column_type_with_context(context, value.clone(), &base)?
67    } else {
68        crate::expr::cast_value_with_type_resolution(
69            value,
70            source,
71            &base.sql_name(),
72            Some(context),
73        )?
74    };
75    if matches!(value, Value::Null)
76        && chain
77            .iter()
78            .any(|domain| domain.definition.not_null.is_some())
79    {
80        return Err(domain_error(
81            "23502",
82            format!(
83                "domain {} does not allow null values",
84                domain_display_name(context, domain.oid)?
85            ),
86        ));
87    }
88    let row = ResultRow::from([("value".into(), value.clone())]);
89    let schema = RowSchema::with_types(
90        vec!["value".into()],
91        vec![Some(domain.definition.base.clone())],
92    );
93    for check in chain
94        .iter()
95        .rev()
96        .flat_map(|domain| &domain.definition.checks)
97    {
98        let result = context.evaluate_domain_check(&check.expression, &row, &schema)?;
99        if result == Value::Bool(false) {
100            return Err(domain_error(
101                "23514",
102                format!(
103                    "value for domain {} violates check constraint \"{}\"",
104                    domain_display_name(context, domain.oid)?,
105                    check.name.as_deref().expect("bound domain constraint")
106                ),
107            ));
108        }
109    }
110    Ok(Some(value))
111}
112
113fn domain_display_name(context: &dyn AssignmentContext, oid: u32) -> Result<String, SQLError> {
114    context
115        .resolve_regtype_output(&ColumnType::Regtype, i64::from(oid))
116        .map_err(SQLError::Internal)?
117        .ok_or_else(|| SQLError::Internal("domain type has no catalog display name".into()))
118}
119
120pub fn domain_error(sqlstate: &str, message: impl Into<String>) -> SQLError {
121    SQLError::Routine {
122        sqlstate: sqlstate.into(),
123        message: message.into(),
124    }
125}