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        for column in columns.iter_mut() {
110            for expression in [column.default.as_mut(), column.check.as_mut()]
111                .into_iter()
112                .flatten()
113            {
114                expression.upgrade_legacy_serialized_dispatches();
115            }
116        }
117        for check in checks.iter_mut() {
118            check.expr.upgrade_legacy_serialized_dispatches();
119        }
120        self.prepare_foreign_table_schema_inner(
121            table_name,
122            columns,
123            checks,
124            Some(dropped_attributes),
125            allocate,
126            &crate::schema::constraint_metadata::ConstraintNameScope::default(),
127        )
128    }
129    fn prepare_foreign_table_schema_inner(
130        &self,
131        table_name: &str,
132        columns: &mut [ColumnDef],
133        checks: &mut Vec<TableCheck>,
134        stored_attributes: Option<&[crate::catalog::relation_attributes::DroppedAttribute]>,
135        allocate: &mut CatalogIdentityAllocator<'_>,
136        names: &crate::schema::constraint_metadata::ConstraintNameScope,
137    ) -> Result<(), SQLError> {
138        let relation = RelationIdentity::from_legacy_name(table_name).map_err(|error| {
139            SQLError::Internal(format!("decode foreign table `{table_name}`: {error}"))
140        })?;
141        let stored = stored_attributes.is_some();
142        let qualifier = relation.name.clone();
143        let check_columns = columns.to_vec();
144        for column in columns.iter_mut() {
145            if let Some(default) = &mut column.default {
146                if stored {
147                    prepare_foreign_table_sequence_references(
148                        self.references,
149                        self.sequences,
150                        default,
151                        true,
152                    )?;
153                }
154                if !validate_default_expression(self, default, &column.ty, &column.name)? {
155                    column.default = None;
156                }
157            }
158            if let Some(check) = &mut column.check {
159                if stored {
160                    prepare_foreign_table_sequence_references(
161                        self.references,
162                        self.sequences,
163                        check,
164                        true,
165                    )?;
166                }
167                validate_check_expression(self, table_name, &qualifier, &check_columns, check)?;
168                crate::catalog::regrole_dependencies::reject_stored_regrole_constants(
169                    self.schema,
170                    check,
171                    None,
172                )?;
173            }
174            if let Some(generated) = &mut column.generated {
175                prepare_foreign_table_sequence_references(
176                    self.references,
177                    self.sequences,
178                    &mut generated.expression,
179                    stored,
180                )?;
181            }
182        }
183        for check in checks.iter_mut() {
184            if stored {
185                prepare_foreign_table_sequence_references(
186                    self.references,
187                    self.sequences,
188                    &mut check.expr,
189                    true,
190                )?;
191            }
192            validate_check_expression(
193                self,
194                table_name,
195                &qualifier,
196                &check_columns,
197                &mut check.expr,
198            )?;
199            crate::catalog::regrole_dependencies::reject_stored_regrole_constants(
200                self.schema,
201                &check.expr,
202                None,
203            )?;
204        }
205        let binding = self.bindings.binding_scope()?;
206        crate::schema::generated::prepare_generated_columns(
207            &SchemaBindingContext {
208                catalog: self.schema,
209                binding: &binding.context(),
210            },
211            &qualifier,
212            columns,
213            &[],
214        )?;
215        let mut constraints = crate::ast::TableConstraintSet {
216            checks: std::mem::take(checks),
217            dropped_attributes: stored_attributes.unwrap_or_default().to_vec(),
218            ..crate::ast::TableConstraintSet::default()
219        };
220        crate::schema::constraint_metadata::materialize_constraint_metadata_with_names(
221            &relation,
222            columns,
223            &mut constraints,
224            allocate,
225            names,
226        )
227        .map_err(|error| {
228            crate::catalog::errors::storage_error("foreign table constraint identity", &error)
229        })?;
230        *checks = constraints.checks;
231        Ok(())
232    }
233}
234fn validate_default_expression(
235    context: &ForeignSchemaContext<'_>,
236    expression: &mut Expr,
237    target: &ColumnType,
238    column: &str,
239) -> Result<bool, SQLError> {
240    let binding = context.bindings.binding_scope()?;
241    crate::schema::defaults::validate_default_expression(
242        &SchemaBindingContext {
243            catalog: context.schema,
244            binding: &binding.context(),
245        },
246        expression,
247        target,
248        column,
249    )
250}
251fn validate_check_expression(
252    context: &ForeignSchemaContext<'_>,
253    table: &str,
254    qualifier: &str,
255    columns: &[ColumnDef],
256    expression: &mut Expr,
257) -> Result<(), SQLError> {
258    let binding = context.bindings.binding_scope()?;
259    crate::schema::constraints::validate_check_expression(
260        &SchemaBindingContext {
261            catalog: context.schema,
262            binding: &binding.context(),
263        },
264        table,
265        qualifier,
266        columns,
267        expression,
268    )
269}
270
271fn prepare_foreign_table_sequence_references(
272    references: &dyn SchemaReferenceCatalog,
273    sequences: &dyn StoredSequenceNames,
274    expression: &mut crate::ast::Expr,
275    stored: bool,
276) -> Result<(), SQLError> {
277    crate::schema::dependencies::regclass::bind_schema_regclass_constants(
278        references, expression, stored,
279    )
280    .map_err(SQLError::Internal)?;
281    let result = if stored {
282        crate::schema::dependencies::regclass::bind_legacy_sequence_regclass_constants(
283            references, sequences, expression,
284        )
285        .map_err(SQLError::Internal)?;
286        crate::schema::dependencies::rewrites::rewrite_sequence_function_references(
287            expression,
288            &mut |reference| {
289                *reference = sequences.stored_sequence_name(reference)?;
290                Ok(())
291            },
292        )
293    } else {
294        crate::schema::dependencies::regclass::bind_sequence_references_in_expr(
295            references, expression,
296        )
297    };
298    result.map_err(SQLError::Internal)
299}
300
301#[cfg(test)]
302mod tests;
303
304pub mod dependencies;