Skip to main content

uqa_sql/schema/
name_binding.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Static SQL name and type binding over a physical row schema.
8
9use crate::ast::{ColumnType, InternalColumnRef};
10
11use super::{ColumnIdentity, RowSchema, SchemaBuildMetadata, ScoreSource, NULL_SLOT};
12
13impl RowSchema {
14    /// Bind every SQL-visible column to one relation qualifier while preserving executor-only internal attributes and rebinding carried retrieval scores to the same relation boundary.
15    pub fn with_relation_qualifier(input: &Self, qualifier: &str) -> Self {
16        let identities = input
17            .columns()
18            .iter()
19            .cloned()
20            .map(|column| ColumnIdentity::qualified(qualifier, column))
21            .collect();
22        let score_sources = input
23            .index
24            .cold
25            .score_sources
26            .iter()
27            .map(|source| ScoreSource {
28                qualifier: Some(Box::<str>::from(qualifier)),
29                column: source.column,
30            })
31            .collect();
32        Self::from_typed_parts_with_aliases_and_exact_precedence(
33            input.columns().to_vec(),
34            identities,
35            input.column_types().to_vec(),
36            input.index.slots.to_vec(),
37            input.physical_width(),
38            SchemaBuildMetadata {
39                internal: input.index.executor_attributes.clone(),
40                internal_types: input.index.cold.executor_attribute_types.clone(),
41                score_sources,
42                open_qualifiers: input
43                    .columns_are_open(None)
44                    .then(|| Some(Box::<str>::from(qualifier)))
45                    .into_iter()
46                    .collect(),
47                wildcard_hidden: input.index.cold.wildcard_hidden.clone(),
48                ..SchemaBuildMetadata::default()
49            },
50        )
51    }
52
53    /// Whether a visible or hidden lookup identity belongs to `qualifier`.
54    #[must_use]
55    pub fn has_qualifier(&self, qualifier: &str) -> bool {
56        self.index
57            .identities
58            .iter()
59            .chain(self.index.aliases.keys())
60            .chain(self.index.cold.binding_only.keys())
61            .any(|identity| identity.qualifier() == Some(qualifier))
62            || self.columns_are_open(Some(qualifier))
63    }
64
65    /// Whether a visible, aliased, or static binding-only identity contains this exact unqualified column, independently of its declared type.
66    #[must_use]
67    pub fn has_unqualified_column(&self, column: &str) -> bool {
68        let identity = ColumnIdentity::unqualified(column);
69        self.index.unqualified.contains_key(column)
70            || self.index.aliases.contains_key(&identity)
71            || self.index.cold.binding_only.contains_key(&identity)
72    }
73
74    /// Whether a visible or hidden lookup identity contains this exact qualified column, independently of its static type or ambiguity.
75    #[must_use]
76    pub fn has_qualified_column(&self, qualifier: &str, column: &str) -> bool {
77        let identity = ColumnIdentity::qualified(qualifier, column);
78        self.index.qualified.contains_key(&identity)
79            || self.index.aliases.contains_key(&identity)
80            || self.index.cold.binding_only.contains_key(&identity)
81    }
82
83    /// Iterate over static name-binding identities that deliberately have no physical value slot or wildcard visibility.
84    pub fn typed_virtual_identities(
85        &self,
86    ) -> impl Iterator<Item = (&ColumnIdentity, Option<&ColumnType>)> {
87        self.index
88            .cold
89            .binding_only
90            .iter()
91            .map(|(identity, ty)| (identity, ty.as_ref()))
92    }
93
94    /// Iterate over hidden lookup aliases that resolve to physical value slots while retaining their declared SQL types.
95    pub fn typed_physical_alias_identities(
96        &self,
97    ) -> impl Iterator<Item = (&ColumnIdentity, Option<&ColumnType>)> {
98        self.index.aliases.keys().map(|identity| {
99            (
100                identity,
101                self.index
102                    .cold
103                    .aliases
104                    .get(identity)
105                    .and_then(Option::as_ref),
106            )
107        })
108    }
109
110    /// Resolve an unqualified logical identity to its static type.
111    pub fn type_of(&self, name: &str) -> Option<&ColumnType> {
112        if self.index.ambiguous_unqualified.contains(name) {
113            return None;
114        }
115        self.index
116            .unqualified
117            .get(name)
118            .and_then(|logical| self.column_type(*logical))
119            .or_else(|| {
120                self.index
121                    .cold
122                    .aliases
123                    .get(&ColumnIdentity::unqualified(name))
124                    .and_then(Option::as_ref)
125            })
126            .or_else(|| {
127                self.index
128                    .cold
129                    .binding_only
130                    .get(&ColumnIdentity::unqualified(name))
131                    .and_then(Option::as_ref)
132            })
133    }
134
135    /// Resolve a qualified logical identity to its static type.
136    pub fn qualified_type(&self, qualifier: &str, column: &str) -> Option<&ColumnType> {
137        let identity = ColumnIdentity::qualified(qualifier, column);
138        if self.index.ambiguous_qualified.contains(&identity) {
139            return None;
140        }
141        self.index
142            .qualified
143            .get(&identity)
144            .and_then(|logical| self.column_type(*logical))
145            .or_else(|| {
146                self.index
147                    .cold
148                    .aliases
149                    .get(&identity)
150                    .and_then(Option::as_ref)
151            })
152            .or_else(|| {
153                self.index
154                    .cold
155                    .binding_only
156                    .get(&identity)
157                    .and_then(Option::as_ref)
158            })
159    }
160
161    pub fn column_is_ambiguous(&self, name: &str) -> bool {
162        self.index.ambiguous_unqualified.contains(name)
163    }
164
165    pub fn qualified_column_is_ambiguous(&self, qualifier: &str, column: &str) -> bool {
166        self.index
167            .ambiguous_qualified
168            .contains(&ColumnIdentity::qualified(qualifier, column))
169    }
170
171    /// Add hidden structured lookup identities for existing logical positions.
172    pub fn with_identity_aliases(input: &Self, aliases: &[(ColumnIdentity, usize)]) -> Self {
173        let mut lookup_aliases = input.index.aliases.clone();
174        let mut alias_types = input.index.cold.aliases.clone();
175        for (identity, logical) in aliases {
176            lookup_aliases.insert(identity.clone(), input.slot(*logical).unwrap_or(NULL_SLOT));
177            alias_types.insert(identity.clone(), input.column_type(*logical).cloned());
178        }
179        Self::from_typed_parts_with_aliases_and_exact_precedence(
180            input.columns().to_vec(),
181            input.identities().to_vec(),
182            input.column_types().to_vec(),
183            input.index.slots.to_vec(),
184            input.physical_width(),
185            SchemaBuildMetadata {
186                aliases: lookup_aliases,
187                alias_types,
188                internal: input.index.executor_attributes.clone(),
189                internal_types: input.index.cold.executor_attribute_types.clone(),
190                score_sources: input.index.cold.score_sources.clone(),
191                wildcard_hidden: input.index.cold.wildcard_hidden.clone(),
192                binding_only: input.index.cold.binding_only.clone(),
193                open_qualifiers: input.index.cold.open_qualifiers.clone(),
194                extra_ambiguous_unqualified: input.index.ambiguous_unqualified.clone(),
195                extra_ambiguous_qualified: input.index.ambiguous_qualified.clone(),
196                ..SchemaBuildMetadata::default()
197            },
198        )
199    }
200
201    /// Add hidden SQL lookup identities that point at explicit physical slots.
202    /// Unlike [`Self::with_identity_aliases`], these slots do not need a
203    /// SQL-visible logical column owner.
204    pub fn with_physical_identity_aliases(
205        input: &Self,
206        aliases: &[(ColumnIdentity, usize, Option<ColumnType>)],
207    ) -> Self {
208        let mut lookup_aliases = input.index.aliases.clone();
209        let mut alias_types = input.index.cold.aliases.clone();
210        for (identity, slot, ty) in aliases {
211            assert!(
212                *slot < input.physical_width(),
213                "physical identity alias is outside row width"
214            );
215            lookup_aliases.insert(identity.clone(), *slot);
216            alias_types.insert(identity.clone(), ty.clone());
217        }
218        Self::from_typed_parts_with_aliases_and_exact_precedence(
219            input.columns().to_vec(),
220            input.identities().to_vec(),
221            input.column_types().to_vec(),
222            input.index.slots.to_vec(),
223            input.physical_width(),
224            SchemaBuildMetadata {
225                aliases: lookup_aliases,
226                alias_types,
227                internal: input.index.executor_attributes.clone(),
228                internal_types: input.index.cold.executor_attribute_types.clone(),
229                score_sources: input.index.cold.score_sources.clone(),
230                wildcard_hidden: input.index.cold.wildcard_hidden.clone(),
231                binding_only: input.index.cold.binding_only.clone(),
232                open_qualifiers: input.index.cold.open_qualifiers.clone(),
233                ..SchemaBuildMetadata::default()
234            },
235        )
236    }
237
238    /// Add executor-only relation attributes at explicit physical slots.
239    pub fn with_physical_internal_aliases(
240        input: &Self,
241        aliases: &[(InternalColumnRef, usize, Option<ColumnType>)],
242    ) -> Self {
243        let mut internal = input.index.executor_attributes.clone();
244        let mut internal_types = input.index.cold.executor_attribute_types.clone();
245        for (column, slot, ty) in aliases {
246            assert!(
247                *slot < input.physical_width(),
248                "physical internal alias is outside row width"
249            );
250            internal.insert(*column, *slot);
251            internal_types.insert(*column, ty.clone());
252        }
253        Self::from_typed_parts_with_aliases_and_exact_precedence(
254            input.columns().to_vec(),
255            input.identities().to_vec(),
256            input.column_types().to_vec(),
257            input.index.slots.to_vec(),
258            input.physical_width(),
259            SchemaBuildMetadata {
260                aliases: input.index.aliases.clone(),
261                alias_types: input.index.cold.aliases.clone(),
262                internal,
263                internal_types,
264                score_sources: input.index.cold.score_sources.clone(),
265                wildcard_hidden: input.index.cold.wildcard_hidden.clone(),
266                binding_only: input.index.cold.binding_only.clone(),
267                open_qualifiers: input.index.cold.open_qualifiers.clone(),
268                extra_ambiguous_unqualified: input.index.ambiguous_unqualified.clone(),
269                extra_ambiguous_qualified: input.index.ambiguous_qualified.clone(),
270                ..SchemaBuildMetadata::default()
271            },
272        )
273    }
274
275    /// Restore structural identities from a row scope that occupies the trailing physical slots of this schema.
276    pub fn with_trailing_internal_aliases(input: &Self, trailing: &Self) -> Self {
277        let offset = input
278            .physical_width()
279            .checked_sub(trailing.physical_width())
280            .expect("trailing internal-alias scope exceeds input row width");
281        let mut internal = input.index.executor_attributes.clone();
282        let mut internal_types = input.index.cold.executor_attribute_types.clone();
283        for (column, slot) in &trailing.index.executor_attributes {
284            let slot = if *slot == NULL_SLOT {
285                NULL_SLOT
286            } else {
287                offset + *slot
288            };
289            if let Some(previous) = internal.insert(*column, slot) {
290                assert_eq!(
291                    previous, slot,
292                    "structural identity moved between row scopes"
293                );
294            }
295        }
296        for (column, ty) in &trailing.index.cold.executor_attribute_types {
297            if let Some(previous) = internal_types.insert(*column, ty.clone()) {
298                assert_eq!(
299                    previous, *ty,
300                    "structural identity type changed between row scopes"
301                );
302            }
303        }
304        Self::from_typed_parts_with_aliases_and_exact_precedence(
305            input.columns().to_vec(),
306            input.identities().to_vec(),
307            input.column_types().to_vec(),
308            input.index.slots.to_vec(),
309            input.physical_width(),
310            SchemaBuildMetadata {
311                aliases: input.index.aliases.clone(),
312                alias_types: input.index.cold.aliases.clone(),
313                internal,
314                internal_types,
315                score_sources: input.index.cold.score_sources.clone(),
316                wildcard_hidden: input.index.cold.wildcard_hidden.clone(),
317                binding_only: input.index.cold.binding_only.clone(),
318                open_qualifiers: input.index.cold.open_qualifiers.clone(),
319                extra_ambiguous_unqualified: input.index.ambiguous_unqualified.clone(),
320                extra_ambiguous_qualified: input.index.ambiguous_qualified.clone(),
321                ..SchemaBuildMetadata::default()
322            },
323        )
324    }
325
326    /// Mark source-owned logical metadata attributes as explicitly
327    /// addressable but absent from `*` expansion. The identity is positional,
328    /// so an ordinary user column with the same text remains visible.
329    pub fn with_wildcard_hidden_positions(
330        input: &Self,
331        positions: impl IntoIterator<Item = usize>,
332    ) -> Self {
333        let wildcard_hidden = positions.into_iter().collect();
334        Self::from_typed_parts_with_aliases_and_exact_precedence(
335            input.columns().to_vec(),
336            input.identities().to_vec(),
337            input.column_types().to_vec(),
338            input.index.slots.to_vec(),
339            input.physical_width(),
340            SchemaBuildMetadata {
341                aliases: input.index.aliases.clone(),
342                alias_types: input.index.cold.aliases.clone(),
343                internal: input.index.executor_attributes.clone(),
344                internal_types: input.index.cold.executor_attribute_types.clone(),
345                score_sources: input.index.cold.score_sources.clone(),
346                wildcard_hidden,
347                binding_only: input.index.cold.binding_only.clone(),
348                open_qualifiers: input.index.cold.open_qualifiers.clone(),
349                ..SchemaBuildMetadata::default()
350            },
351        )
352    }
353
354    #[must_use]
355    pub fn internal_slot(&self, column: InternalColumnRef) -> Option<usize> {
356        self.index
357            .executor_attributes
358            .get(&column)
359            .copied()
360            .filter(|slot| *slot != NULL_SLOT)
361    }
362
363    /// Return the sole executor-only identity attached to a physical slot. Slots with no structural identity or with multiple aliases are intentionally unresolved.
364    #[must_use]
365    pub fn unique_internal_column_for_slot(&self, slot: usize) -> Option<InternalColumnRef> {
366        let mut columns = self
367            .index
368            .executor_attributes
369            .iter()
370            .filter_map(|(column, candidate)| (*candidate == slot).then_some(*column));
371        let column = columns.next()?;
372        columns.next().is_none().then_some(column)
373    }
374
375    #[must_use]
376    pub fn internal_type(&self, column: InternalColumnRef) -> Option<&ColumnType> {
377        self.index
378            .cold
379            .executor_attribute_types
380            .get(&column)
381            .and_then(Option::as_ref)
382    }
383
384    /// Mark an existing internal attribute as the score carried by one retrieval relation. This semantic tag is schema metadata and never enters SQL name lookup or wildcard expansion.
385    pub fn with_score_source(
386        input: &Self,
387        qualifier: Option<&str>,
388        column: InternalColumnRef,
389    ) -> Self {
390        assert!(
391            input.index.executor_attributes.contains_key(&column),
392            "score source must reference an existing internal attribute"
393        );
394        let mut score_sources = input.index.cold.score_sources.clone();
395        score_sources.retain(|source| source.column != column);
396        score_sources.push(ScoreSource {
397            qualifier: qualifier.map(Box::<str>::from),
398            column,
399        });
400        Self::from_typed_parts_with_aliases_and_exact_precedence(
401            input.columns().to_vec(),
402            input.identities().to_vec(),
403            input.column_types().to_vec(),
404            input.index.slots.to_vec(),
405            input.physical_width(),
406            SchemaBuildMetadata {
407                aliases: input.index.aliases.clone(),
408                alias_types: input.index.cold.aliases.clone(),
409                internal: input.index.executor_attributes.clone(),
410                internal_types: input.index.cold.executor_attribute_types.clone(),
411                score_sources,
412                wildcard_hidden: input.index.cold.wildcard_hidden.clone(),
413                binding_only: input.index.cold.binding_only.clone(),
414                open_qualifiers: input.index.cold.open_qualifiers.clone(),
415                ..SchemaBuildMetadata::default()
416            },
417        )
418    }
419
420    /// Rebind every carried score source at a relation-alias boundary while retaining its opaque internal attribute identity.
421    pub fn with_rebound_score_sources(input: &Self, qualifier: Option<&str>) -> Self {
422        let score_sources = input
423            .index
424            .cold
425            .score_sources
426            .iter()
427            .map(|source| ScoreSource {
428                qualifier: qualifier.map(Box::<str>::from),
429                column: source.column,
430            })
431            .collect();
432        Self::from_typed_parts_with_aliases_and_exact_precedence(
433            input.columns().to_vec(),
434            input.identities().to_vec(),
435            input.column_types().to_vec(),
436            input.index.slots.to_vec(),
437            input.physical_width(),
438            SchemaBuildMetadata {
439                aliases: input.index.aliases.clone(),
440                alias_types: input.index.cold.aliases.clone(),
441                internal: input.index.executor_attributes.clone(),
442                internal_types: input.index.cold.executor_attribute_types.clone(),
443                score_sources,
444                wildcard_hidden: input.index.cold.wildcard_hidden.clone(),
445                binding_only: input.index.cold.binding_only.clone(),
446                open_qualifiers: input.index.cold.open_qualifiers.clone(),
447                ..SchemaBuildMetadata::default()
448            },
449        )
450    }
451
452    fn matching_score_sources<'a>(
453        &'a self,
454        qualifier: Option<&'a str>,
455    ) -> impl Iterator<Item = InternalColumnRef> + 'a {
456        self.index
457            .cold
458            .score_sources
459            .iter()
460            .filter(move |source| {
461                qualifier.is_none_or(|qualifier| source.qualifier.as_deref() == Some(qualifier))
462            })
463            .map(move |source| source.column)
464    }
465
466    #[must_use]
467    pub fn score_source_column(&self, qualifier: Option<&str>) -> Option<InternalColumnRef> {
468        let mut columns = self.matching_score_sources(qualifier);
469        let column = columns.next()?;
470        columns.next().is_none().then_some(column)
471    }
472
473    #[must_use]
474    pub fn score_source_is_ambiguous(&self, qualifier: Option<&str>) -> bool {
475        let mut columns = self.matching_score_sources(qualifier);
476        columns.next().is_some() && columns.next().is_some()
477    }
478
479    #[must_use]
480    pub fn score_source_slot(&self, qualifier: Option<&str>) -> Option<usize> {
481        self.score_source_column(qualifier)
482            .and_then(|column| self.internal_slot(column))
483    }
484
485    /// Add statically typed hidden lookup identities that have no physical value slot. Visible columns and star expansion remain unchanged, while binders can resolve the identities and their declared SQL types.
486    pub fn with_typed_virtual_identities(
487        input: &Self,
488        identities: &[(ColumnIdentity, Option<ColumnType>)],
489    ) -> Self {
490        let mut binding_only = input.index.cold.binding_only.clone();
491        for (identity, ty) in identities {
492            let already_visible = match identity.qualifier() {
493                Some(qualifier) => input.has_qualified_column(qualifier, identity.column()),
494                None => {
495                    input.column_is_ambiguous(identity.column())
496                        || input.has_unqualified_column(identity.column())
497                }
498            };
499            if already_visible {
500                continue;
501            }
502            binding_only
503                .entry(identity.clone())
504                .or_insert_with(|| ty.clone());
505        }
506        Self::from_typed_parts_with_aliases_and_exact_precedence(
507            input.columns().to_vec(),
508            input.identities().to_vec(),
509            input.column_types().to_vec(),
510            input.index.slots.to_vec(),
511            input.physical_width(),
512            SchemaBuildMetadata {
513                aliases: input.index.aliases.clone(),
514                alias_types: input.index.cold.aliases.clone(),
515                internal: input.index.executor_attributes.clone(),
516                internal_types: input.index.cold.executor_attribute_types.clone(),
517                score_sources: input.index.cold.score_sources.clone(),
518                wildcard_hidden: input.index.cold.wildcard_hidden.clone(),
519                binding_only,
520                open_qualifiers: input.index.cold.open_qualifiers.clone(),
521                extra_ambiguous_unqualified: input.index.ambiguous_unqualified.clone(),
522                extra_ambiguous_qualified: input.index.ambiguous_qualified.clone(),
523                ..SchemaBuildMetadata::default()
524            },
525        )
526    }
527
528    /// Add binding-only identities and preserve collisions as ambiguous names. This models SQL scopes that expose a hidden generated column for explicit lookup while deliberately excluding it from wildcard expansion.
529    pub fn with_typed_conflicting_virtual_identities(
530        input: &Self,
531        identities: &[(ColumnIdentity, Option<ColumnType>)],
532    ) -> Self {
533        let mut binding_only = input.index.cold.binding_only.clone();
534        let mut ambiguous_unqualified = input.index.ambiguous_unqualified.clone();
535        let mut ambiguous_qualified = input.index.ambiguous_qualified.clone();
536        for (identity, ty) in identities {
537            if input.column_is_ambiguous(identity.column())
538                || input.has_unqualified_column(identity.column())
539            {
540                ambiguous_unqualified.insert(Box::<str>::from(identity.column()));
541            }
542            if let Some(qualifier) = identity.qualifier() {
543                if input.has_qualified_column(qualifier, identity.column()) {
544                    ambiguous_qualified.insert(identity.clone());
545                }
546            }
547            binding_only
548                .entry(identity.clone())
549                .or_insert_with(|| ty.clone());
550        }
551        Self::from_typed_parts_with_aliases_and_exact_precedence(
552            input.columns().to_vec(),
553            input.identities().to_vec(),
554            input.column_types().to_vec(),
555            input.index.slots.to_vec(),
556            input.physical_width(),
557            SchemaBuildMetadata {
558                aliases: input.index.aliases.clone(),
559                alias_types: input.index.cold.aliases.clone(),
560                internal: input.index.executor_attributes.clone(),
561                internal_types: input.index.cold.executor_attribute_types.clone(),
562                score_sources: input.index.cold.score_sources.clone(),
563                wildcard_hidden: input.index.cold.wildcard_hidden.clone(),
564                binding_only,
565                open_qualifiers: input.index.cold.open_qualifiers.clone(),
566                extra_ambiguous_unqualified: ambiguous_unqualified,
567                extra_ambiguous_qualified: ambiguous_qualified,
568                ..SchemaBuildMetadata::default()
569            },
570        )
571    }
572}