Skip to main content

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