Skip to main content

uqa_sql/binding/
catalog_sources.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Engine-independent catalog source binding.
8
9use crate::ast::OperatorJoinRelations;
10use crate::RowSchema;
11use crate::SQLError;
12
13use crate::catalog::analysis::CatalogReadView;
14use crate::catalog::resolution::RelationNameResolution;
15
16use super::analysis;
17
18/// Bind both operator-join relations independently so each retrieval operand has its own namespace.
19pub(super) fn operator_join_relation_schemas(
20    catalog: &CatalogReadView,
21    resolution: &RelationNameResolution,
22    relations: Option<&OperatorJoinRelations>,
23) -> Result<(RowSchema, RowSchema), SQLError> {
24    let relations = relations.ok_or_else(|| {
25        SQLError::TypeMismatch("operator join requires left and right table identifiers".into())
26    })?;
27    Ok((
28        relation_schema(catalog, resolution, &relations.left, "left")?,
29        relation_schema(catalog, resolution, &relations.right, "right")?,
30    ))
31}
32
33fn relation_schema(
34    catalog: &CatalogReadView,
35    resolution: &RelationNameResolution,
36    relation: &str,
37    side: &str,
38) -> Result<RowSchema, SQLError> {
39    let resolved = catalog
40        .table_name_resolved(resolution, relation)?
41        .ok_or_else(|| SQLError::UnknownTable(relation.to_string()))?;
42    let identity = crate::RelationIdentity::from_legacy_name(&resolved).map_err(|error| {
43        SQLError::Internal(format!(
44            "decode operator join {side} relation `{resolved}` schema: {error}"
45        ))
46    })?;
47    let table = catalog
48        .table_resolved(resolution, &resolved)?
49        .ok_or_else(|| SQLError::UnknownTable(relation.to_string()))?;
50    let columns = table
51        .columns
52        .iter()
53        .map(|column| column.name.clone())
54        .collect();
55    let types = table
56        .columns
57        .iter()
58        .map(|column| Some(column.ty.clone()))
59        .collect();
60    let schema = RowSchema::with_qualified_types(&identity.name, columns, types);
61    Ok(analysis::with_table_pseudo_columns(&schema, &identity.name))
62}
63
64/// Output column names of a user-defined routine used as a FROM source: OUT / INOUT / `RETURNS TABLE` parameter names. `None` when the name is not a user routine or its result is a single unnamed column, which keeps the function-name default.
65pub fn user_function_output_columns(
66    catalog: &dyn crate::catalog::analysis::AnalysisCatalog,
67    resolution: &RelationNameResolution,
68    name: &str,
69) -> Result<Option<Vec<String>>, SQLError> {
70    let Some(overloads) = catalog.sql_functions(resolution, name)? else {
71        return Ok(None);
72    };
73    for function in &overloads {
74        if let Some(columns) = crate::semantics::user_function_output_columns_for(function) {
75            return Ok(Some(columns));
76        }
77    }
78    Ok(None)
79}
80
81#[cfg(test)]
82mod tests {
83    use std::collections::BTreeMap;
84
85    use crate::ast::{ColumnDef, ColumnType};
86
87    use super::operator_join_relation_schemas;
88    use crate::RelationIdentity;
89
90    #[test]
91    fn relation_schema_binds_against_catalog_fixture_without_engine() {
92        let column = ColumnDef {
93            name: "id".into(),
94            ty: ColumnType::BigInteger,
95            object_id: None,
96            missing_value: None,
97            primary_key: true,
98            not_null: true,
99            not_null_explicit: false,
100            not_null_name: None,
101            not_null_validated: true,
102            not_null_no_inherit: false,
103            not_null_is_local: true,
104            auto_increment: None,
105            unique: false,
106            default: None,
107            generated: None,
108            check: None,
109            check_name: None,
110            check_enforced: true,
111            check_validated: true,
112            check_no_inherit: false,
113            check_is_local: true,
114            check_object_id: None,
115            references: None,
116        };
117        let catalog = crate::binding::fixture::catalog(BTreeMap::from([
118            (
119                RelationIdentity::new("app", "documents"),
120                crate::binding::fixture::table_definition(vec![column.clone()]),
121            ),
122            (
123                RelationIdentity::new("app", "archive"),
124                crate::binding::fixture::table_definition(vec![column]),
125            ),
126        ]));
127        let resolution =
128            crate::binding::fixture::resolution(vec!["app".into()], "pg_temp_fixture".into());
129        let relations = crate::ast::OperatorJoinRelations {
130            left: "documents".into(),
131            right: "archive".into(),
132        };
133        let (left, right) =
134            operator_join_relation_schemas(&catalog, &resolution, Some(&relations)).unwrap();
135        assert!(left.has_qualified_column("documents", "id"));
136        assert!(right.has_qualified_column("archive", "id"));
137        assert_eq!(left.column_type(0), Some(&ColumnType::BigInteger));
138        assert_eq!(right.column_type(0), Some(&ColumnType::BigInteger));
139    }
140}