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