uqa-sql 0.5.4

PostgreSQL-compatible SQL compiler built on libpg_query
Documentation
//
// Unified Query Algebra
//
// Copyright (c) 2023-2026 Cognica, Inc.
//

//! Domain and array coercions retain the collation selected by their actual coercion nodes.

use super::*;

struct Domains;

fn domain(name: &str) -> ColumnType {
    let (oid, collation_oid) = match name {
        "d_posix" => (50_010, 951),
        "d_plain" => (50_020, 100),
        "d_array" => (50_030, 951),
        _ => panic!("unknown test domain"),
    };
    ColumnType::Domain {
        schema: "public".into(),
        name: name.into(),
        oid,
        array_oid: Some(oid + 1),
        collation_oid: Some(collation_oid),
        base: Box::new(if name == "d_array" {
            ColumnType::Array(Box::new(ColumnType::Text))
        } else {
            ColumnType::Text
        }),
    }
}

impl FunctionTypeResolver for Domains {
    fn resolve_function_type(
        &self,
        _: &str,
        _: Option<&FunctionBinding>,
        _: &[Option<String>],
        _: &[Option<ColumnType>],
        _: bool,
    ) -> Result<Option<ColumnType>, SQLError> {
        Ok(None)
    }
    fn resolve_type_name(&self, name: &str) -> Result<Option<ColumnType>, SQLError> {
        let (name, array) = name
            .strip_suffix("[]")
            .map_or((name, false), |name| (name, true));
        let name = name.strip_prefix("public.").unwrap_or(name);
        Ok(matches!(name, "d_posix" | "d_plain" | "d_array").then(|| {
            let ty = domain(name);
            if array {
                ColumnType::Array(Box::new(ty))
            } else {
                ty
            }
        }))
    }
}
impl RoutineResolution for Domains {}

#[derive(serde::Deserialize)]
struct Reference {
    sql: String,
    #[serde(default)]
    collations: Vec<u32>,
    error: Option<serde_json::Value>,
}

#[test]
fn domain_cast_collations_match_postgresql_for_literals_columns_and_identity_casts() {
    let crate::Statement::CreateTable(mut table) = crate::compile(
        "CREATE TABLE t(c text,p text,plain text,n integer,a text[],d text[],e text)",
    )
    .unwrap()
    .remove(0) else {
        panic!("table")
    };
    table.columns[0].collation_oid = Some(950);
    table.columns[1].collation_oid = Some(951);
    table.columns[4].collation_oid = Some(950);
    table.columns[5].ty = ColumnType::Array(Box::new(domain("d_posix")));
    table.columns[5].collation_oid = Some(950);
    table.columns[6].ty = domain("d_posix");
    table.columns[6].collation_oid = Some(950);
    let binding = BindingContext {
        catalog: crate::binding::fixture::catalog(BTreeMap::from([(
            RelationIdentity::new("public", "t"),
            crate::binding::fixture::table_definition(table.columns),
        )])),
        ..crate::binding::fixture::empty_binding_context()
    };
    let cases: Vec<Reference> =
        serde_json::from_str(include_str!("domain_cast_collations.json")).unwrap();
    assert_eq!(cases.len(), 30);
    for case in cases {
        let UnifiedPlan::Query(query) =
            UnifiedPlan::lower(crate::compile(&case.sql).unwrap().remove(0))
        else {
            panic!("query")
        };
        let mut preparation = Preparation {
            routines: &Domains,
            scope: &mut SchemaScope::for_analysis(&binding).unwrap(),
            parameters: ParameterTypes::new(&[]),
            schema_expression: None,
        };
        let prepared = preparation.query(&query, None);
        let ordinary =
            crate::binding::analyze_query_plan_schema(&Domains, &query, &[], &binding, None);
        let mut results = vec![prepared, ordinary];
        if case.error.is_none() {
            results.push(crate::binding::bind_query_plan_schema(
                &Domains,
                &query,
                &[],
                &binding,
                None,
            ));
        }
        for result in results {
            if let Some(expected) = &case.error {
                let error = result
                    .err()
                    .unwrap_or_else(|| panic!("expected PostgreSQL error: {}", case.sql));
                assert_eq!(
                    error.sqlstate(),
                    expected["sqlstate"].as_str(),
                    "{}",
                    case.sql
                );
                assert_eq!(
                    error.to_string(),
                    expected["message"].as_str().unwrap(),
                    "{}",
                    case.sql
                );
                assert_eq!(error.detail(), expected["detail"].as_str(), "{}", case.sql);
                assert_eq!(error.hint(), expected["hint"].as_str(), "{}", case.sql);
            } else {
                let output = result.unwrap_or_else(|error| panic!("{}: {error}", case.sql));
                assert_eq!(output.len(), case.collations.len(), "{}", case.sql);
                for (position, oid) in case.collations.iter().enumerate() {
                    assert_eq!(
                        output.column_collation(position),
                        Some(*oid),
                        "{}",
                        case.sql
                    );
                }
            }
        }
    }
}

#[test]
fn domain_and_unknown_common_types_match_postgresql() {
    #[derive(serde::Deserialize)]
    struct Case {
        sql: String,
        #[serde(rename = "type")]
        type_name: String,
        collation: u32,
    }
    let cases: Vec<Case> = serde_json::from_str(include_str!("domain_common_types.json")).unwrap();
    assert_eq!(cases.len(), 30);
    let binding = crate::binding::fixture::empty_binding_context();
    for case in cases {
        let expected = match case.type_name.as_str() {
            "pg_catalog.text" => ColumnType::Text,
            "pg_catalog._text" => ColumnType::Array(Box::new(ColumnType::Text)),
            "public.d_posix" => domain("d_posix"),
            "public._d_posix" => ColumnType::Array(Box::new(domain("d_posix"))),
            "public.d_array" => domain("d_array"),
            name => panic!("unexpected PostgreSQL output {name}"),
        };
        let statement = crate::compile(&case.sql).unwrap().remove(0);
        if let crate::Statement::Select(select) = &statement {
            if select.set_op.is_none() && !select.projections.is_empty() {
                let expression =
                    crate::plan::ExpressionPlan::lower(select.projections[0].expr.clone());
                assert_eq!(
                    crate::type_resolution::scalar_type_with_resolver(
                        &expression.scalar,
                        &RowSchema::default(),
                        &[],
                        &Domains,
                    )
                    .unwrap_or_else(|error| panic!("scalar: {}: {error}", case.sql)),
                    Some(expected.clone()),
                    "scalar: {}",
                    case.sql,
                );
            }
        }
        let UnifiedPlan::Query(query) = UnifiedPlan::lower(statement) else {
            panic!("query")
        };
        let mut preparation = Preparation {
            routines: &Domains,
            scope: &mut SchemaScope::for_analysis(&binding).unwrap(),
            parameters: ParameterTypes::new(&[]),
            schema_expression: None,
        };
        for (entry, output) in [
            ("prepared", preparation.query(&query, None)),
            (
                "analyzed",
                crate::binding::analyze_query_plan_schema(&Domains, &query, &[], &binding, None),
            ),
            (
                "bound",
                crate::binding::bind_query_plan_schema(&Domains, &query, &[], &binding, None),
            ),
        ] {
            let output = output.unwrap_or_else(|error| panic!("{entry}: {}: {error}", case.sql));
            assert_eq!(
                output.column_type(0),
                Some(&expected),
                "{entry}: {}",
                case.sql
            );
            assert_eq!(
                output.column_collation(0),
                Some(case.collation),
                "{entry}: {}",
                case.sql
            );
        }
    }
}

#[test]
fn common_type_selection_keeps_native_deferred_outputs_distinct_from_unknown() {
    let binding = crate::binding::fixture::empty_binding_context();
    let mut preparation = Preparation {
        routines: &Domains,
        scope: &mut SchemaScope::for_analysis(&binding).unwrap(),
        parameters: ParameterTypes::new(&[]),
        schema_expression: None,
    };
    let known = || ExpressionType::resolved(Some(domain("d_posix")));
    for (mut inputs, expected) in [
        (
            vec![known(), ExpressionType::resolved(None)],
            Some(domain("d_posix")),
        ),
        (
            vec![ExpressionType::resolved(None), known()],
            Some(domain("d_posix")),
        ),
        (
            vec![ExpressionType::unknown(), ExpressionType::resolved(None)],
            None,
        ),
        (
            vec![
                known(),
                ExpressionType::unknown(),
                ExpressionType::resolved(None),
            ],
            Some(ColumnType::Text),
        ),
    ] {
        assert_eq!(
            preparation
                .common(
                    crate::type_resolution::CommonTypeContext::Coalesce,
                    &mut inputs
                )
                .unwrap(),
            expected
        );
        assert!(inputs.iter().any(ExpressionType::is_deferred));
    }
}