Skip to main content

uqa_sql/schema/
foreign_tables.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Foreign-table declaration validation over fresh schema binding scopes and loaded sequence names.
8use crate::schema::constraint_metadata::CatalogIdentityAllocator;
9use crate::schema::dependencies::regclass::SchemaReferenceCatalog;
10use crate::schema::sequences::implicit_ownership::StoredSequenceNames;
11use crate::schema::{SchemaBindingContext, SchemaExpressionCatalog};
12use crate::semantics::conflict::InferenceBindingScope;
13use crate::type_resolution::FunctionTypeResolver;
14use crate::{
15    ast::{ColumnDef, ColumnType, Expr, TableCheck},
16    SQLError,
17};
18use uqa_core::RelationIdentity;
19
20pub struct ForeignSchemaContext<'a> {
21    pub types: &'a dyn FunctionTypeResolver,
22    pub schema: &'a dyn SchemaExpressionCatalog,
23    pub bindings: &'a dyn InferenceBindingScope,
24    pub references: &'a dyn SchemaReferenceCatalog,
25    pub sequences: &'a dyn StoredSequenceNames,
26}
27
28/// SQL declarations still to be stored after the new foreign table's CHECK constraints.
29pub struct ForeignTableNotNulls {
30    pub relation_oid: u32,
31    pub declarations: Vec<crate::ast::NotNullDeclaration>,
32}
33
34pub fn validate_foreign_table_schema_envelope(columns: &[ColumnDef]) -> Result<(), SQLError> {
35    let mut names = std::collections::BTreeSet::new();
36    for column in columns {
37        if !names.insert(column.name.as_str()) {
38            return Err(SQLError::Routine {
39                sqlstate: "42701".into(),
40                message: format!("column \"{}\" specified more than once", column.name),
41            });
42        }
43        crate::schema::columns::validate_postgres_column_name(&column.name)?;
44        crate::schema::columns::validate_postgres_relation_column_type(&column.name, &column.ty)?;
45        if column.primary_key || column.unique {
46            let kind = if column.primary_key {
47                "primary key"
48            } else {
49                "unique"
50            };
51            return Err(SQLError::Unsupported(format!(
52                "{kind} constraints are not supported on foreign tables"
53            )));
54        }
55        if column.references.is_some() {
56            return Err(SQLError::Unsupported(
57                "foreign key constraints are not supported on foreign tables".into(),
58            ));
59        }
60    }
61    Ok(())
62}
63impl ForeignSchemaContext<'_> {
64    pub fn prepare_foreign_table_schema(
65        &self,
66        table_name: &str,
67        columns: &mut [ColumnDef],
68        checks: &mut Vec<TableCheck>,
69        allocate: &mut CatalogIdentityAllocator<'_>,
70        names: &crate::schema::constraint_metadata::ConstraintNameScope,
71        not_nulls: Option<ForeignTableNotNulls>,
72    ) -> Result<(), SQLError> {
73        if not_nulls.is_some() {
74            for column in columns.iter_mut() {
75                column.not_null = false;
76                column.not_null_name = None;
77                column.not_null_identity = None;
78            }
79        }
80        self.prepare_foreign_table_schema_inner(
81            table_name, columns, checks, None, allocate, names,
82        )?;
83        if let Some(not_nulls) = not_nulls {
84            let relation =
85                RelationIdentity::from_legacy_name(table_name).map_err(SQLError::Internal)?;
86            crate::schema::table_creation::not_nulls::define_foreign_not_null_constraints(
87                crate::schema::table_creation::not_nulls::ForeignNotNullContext {
88                    relation: &relation,
89                    relation_oid: not_nulls.relation_oid,
90                    checks,
91                    names,
92                },
93                columns,
94                not_nulls.declarations,
95                allocate,
96            )?;
97        }
98        Ok(())
99    }
100    pub fn prepare_stored_foreign_table_schema(
101        &self,
102        table_name: &str,
103        columns: &mut [ColumnDef],
104        checks: &mut Vec<TableCheck>,
105        dropped_attributes: &[crate::catalog::relation_attributes::DroppedAttribute],
106        allocate: &mut CatalogIdentityAllocator<'_>,
107    ) -> Result<(), SQLError> {
108        validate_foreign_table_schema_envelope(columns)?;
109        crate::binding::legacy_patterns::restore_schema(columns, checks, self.types)
110            .map_err(SQLError::Internal)?;
111        for column in columns.iter_mut() {
112            for expression in [column.default.as_mut(), column.check.as_mut()]
113                .into_iter()
114                .flatten()
115            {
116                expression.upgrade_legacy_serialized_dispatches();
117            }
118        }
119        for check in checks.iter_mut() {
120            check.expr.upgrade_legacy_serialized_dispatches();
121        }
122        self.prepare_foreign_table_schema_inner(
123            table_name,
124            columns,
125            checks,
126            Some(dropped_attributes),
127            allocate,
128            &crate::schema::constraint_metadata::ConstraintNameScope::default(),
129        )
130    }
131    fn prepare_foreign_table_schema_inner(
132        &self,
133        table_name: &str,
134        columns: &mut [ColumnDef],
135        checks: &mut Vec<TableCheck>,
136        stored_attributes: Option<&[crate::catalog::relation_attributes::DroppedAttribute]>,
137        allocate: &mut CatalogIdentityAllocator<'_>,
138        names: &crate::schema::constraint_metadata::ConstraintNameScope,
139    ) -> Result<(), SQLError> {
140        let relation = RelationIdentity::from_legacy_name(table_name).map_err(|error| {
141            SQLError::Internal(format!("decode foreign table `{table_name}`: {error}"))
142        })?;
143        let stored = stored_attributes.is_some();
144        let qualifier = relation.name.clone();
145        let check_columns = columns.to_vec();
146        for column in columns.iter_mut() {
147            if let Some(default) = &mut column.default {
148                if stored {
149                    prepare_foreign_table_sequence_references(
150                        self.references,
151                        self.sequences,
152                        default,
153                        true,
154                    )?;
155                }
156                if !validate_default_expression(self, default, &column.ty, &column.name)? {
157                    column.default = None;
158                }
159            }
160            if let Some(check) = &mut column.check {
161                if stored {
162                    prepare_foreign_table_sequence_references(
163                        self.references,
164                        self.sequences,
165                        check,
166                        true,
167                    )?;
168                }
169                validate_check_expression(self, table_name, &qualifier, &check_columns, check)?;
170                crate::catalog::regrole_dependencies::reject_stored_regrole_constants(
171                    self.schema,
172                    check,
173                    None,
174                )?;
175            }
176            if let Some(generated) = &mut column.generated {
177                prepare_foreign_table_sequence_references(
178                    self.references,
179                    self.sequences,
180                    &mut generated.expression,
181                    stored,
182                )?;
183            }
184        }
185        for check in checks.iter_mut() {
186            if stored {
187                prepare_foreign_table_sequence_references(
188                    self.references,
189                    self.sequences,
190                    &mut check.expr,
191                    true,
192                )?;
193            }
194            validate_check_expression(
195                self,
196                table_name,
197                &qualifier,
198                &check_columns,
199                &mut check.expr,
200            )?;
201            crate::catalog::regrole_dependencies::reject_stored_regrole_constants(
202                self.schema,
203                &check.expr,
204                None,
205            )?;
206        }
207        let binding = self.bindings.binding_scope()?;
208        crate::schema::generated::prepare_generated_columns(
209            &SchemaBindingContext {
210                catalog: self.schema,
211                binding: &binding.context(),
212            },
213            &qualifier,
214            columns,
215            &[],
216        )?;
217        let mut constraints = crate::ast::TableConstraintSet {
218            checks: std::mem::take(checks),
219            dropped_attributes: stored_attributes.unwrap_or_default().to_vec(),
220            ..crate::ast::TableConstraintSet::default()
221        };
222        crate::schema::constraint_metadata::materialize_constraint_metadata_with_names(
223            &relation,
224            columns,
225            &mut constraints,
226            allocate,
227            names,
228        )
229        .map_err(|error| {
230            crate::catalog::errors::storage_error("foreign table constraint identity", &error)
231        })?;
232        *checks = constraints.checks;
233        Ok(())
234    }
235}
236fn validate_default_expression(
237    context: &ForeignSchemaContext<'_>,
238    expression: &mut Expr,
239    target: &ColumnType,
240    column: &str,
241) -> Result<bool, SQLError> {
242    let binding = context.bindings.binding_scope()?;
243    crate::schema::defaults::validate_default_expression(
244        &SchemaBindingContext {
245            catalog: context.schema,
246            binding: &binding.context(),
247        },
248        expression,
249        target,
250        column,
251    )
252}
253fn validate_check_expression(
254    context: &ForeignSchemaContext<'_>,
255    table: &str,
256    qualifier: &str,
257    columns: &[ColumnDef],
258    expression: &mut Expr,
259) -> Result<(), SQLError> {
260    let binding = context.bindings.binding_scope()?;
261    crate::schema::constraints::validate_check_expression(
262        &SchemaBindingContext {
263            catalog: context.schema,
264            binding: &binding.context(),
265        },
266        table,
267        qualifier,
268        columns,
269        expression,
270    )
271}
272
273fn prepare_foreign_table_sequence_references(
274    references: &dyn SchemaReferenceCatalog,
275    sequences: &dyn StoredSequenceNames,
276    expression: &mut crate::ast::Expr,
277    stored: bool,
278) -> Result<(), SQLError> {
279    crate::schema::dependencies::regclass::bind_schema_regclass_constants(
280        references, expression, stored,
281    )
282    .map_err(SQLError::Internal)?;
283    let result = if stored {
284        crate::schema::dependencies::regclass::bind_legacy_sequence_regclass_constants(
285            references, sequences, expression,
286        )
287        .map_err(SQLError::Internal)?;
288        crate::schema::dependencies::rewrites::rewrite_sequence_function_references(
289            expression,
290            &mut |reference| {
291                *reference = sequences.stored_sequence_name(reference)?;
292                Ok(())
293            },
294        )
295    } else {
296        crate::schema::dependencies::regclass::bind_sequence_references_in_expr(
297            references, expression,
298        )
299    };
300    result.map_err(SQLError::Internal)
301}
302
303#[cfg(test)]
304mod tests;
305
306pub mod dependencies;