Skip to main content

uqa_sql/schema/
collations.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Selected collation metadata follows logical bindings independently of value slots.
8
9use super::{
10    ColumnIdentity, ColumnType, HashMap, InternalColumnRef, RowSchema, SchemaLayoutError,
11    SchemaLayoutResult,
12};
13use crate::catalog::type_metadata::pg_type_collation_oid;
14use std::collections::BTreeMap;
15
16#[derive(Clone, Debug, Default, PartialEq, Eq)]
17pub(super) struct Collations(Option<Box<Bindings>>);
18
19#[derive(Clone, Debug, Default, PartialEq, Eq)]
20struct Bindings {
21    columns: BTreeMap<usize, u32>,
22    aliases: BTreeMap<ColumnIdentity, u32>,
23    internal: BTreeMap<InternalColumnRef, u32>,
24    virtual_columns: BTreeMap<ColumnIdentity, u32>,
25}
26
27/// A selected collation belongs to a logical identity even when its physical value is NULL or shared with another identity.
28#[derive(Clone, Copy, Debug)]
29pub enum CollationBinding<'a> {
30    Column(usize),
31    Alias(&'a ColumnIdentity),
32    Internal(InternalColumnRef),
33}
34
35fn type_default(ty: Option<&ColumnType>) -> Option<u32> {
36    ty.map(|ty| u32::try_from(pg_type_collation_oid(ty)).expect("type collation identity"))
37}
38
39fn override_value(oid: Option<u32>, ty: Option<&ColumnType>) -> Option<u32> {
40    oid.filter(|oid| {
41        ty.is_some_and(|ty| {
42            crate::schema::composites::type_is_collatable(ty)
43                && type_default(Some(ty)) != Some(*oid)
44        })
45    })
46}
47
48impl Collations {
49    pub(super) fn same_physical_bindings(&self, other: &Self) -> bool {
50        let empty = Bindings::default();
51        let left = self.0.as_deref().unwrap_or(&empty);
52        let right = other.0.as_deref().unwrap_or(&empty);
53        left.columns == right.columns
54            && left.aliases == right.aliases
55            && left.internal == right.internal
56    }
57
58    pub(super) fn joined(left: &RowSchema, right: &RowSchema) -> Self {
59        let mut result = left.index.cold.collations.clone();
60        if let Some(target) = &mut result.0 {
61            if !target.aliases.is_empty() {
62                for identity in right.index.cold.aliases.keys() {
63                    target.aliases.remove(identity);
64                }
65            }
66            if !target.virtual_columns.is_empty() {
67                for identity in right.index.cold.binding_only.keys() {
68                    target.virtual_columns.remove(identity);
69                }
70            }
71        }
72        if let Some(source) = &right.index.cold.collations.0 {
73            let target = result.0.get_or_insert_with(Default::default);
74            target.columns.extend(
75                source
76                    .columns
77                    .iter()
78                    .map(|(position, oid)| (left.len() + position, *oid)),
79            );
80            target.aliases.extend(
81                source
82                    .aliases
83                    .iter()
84                    .map(|(identity, oid)| (identity.clone(), *oid)),
85            );
86            target
87                .internal
88                .extend(source.internal.iter().map(|(column, oid)| (*column, *oid)));
89            target.virtual_columns.extend(
90                source
91                    .virtual_columns
92                    .iter()
93                    .map(|(identity, oid)| (identity.clone(), *oid)),
94            );
95        }
96        result.compact();
97        result
98    }
99
100    fn compact(&mut self) {
101        if self.0.as_ref().is_some_and(|bindings| {
102            bindings.columns.is_empty()
103                && bindings.aliases.is_empty()
104                && bindings.internal.is_empty()
105                && bindings.virtual_columns.is_empty()
106        }) {
107            self.0 = None;
108        }
109    }
110
111    pub(super) fn column(&mut self, position: usize, oid: Option<u32>, ty: Option<&ColumnType>) {
112        let value = override_value(oid, ty);
113        if let Some(value) = value {
114            self.0
115                .get_or_insert_with(Default::default)
116                .columns
117                .insert(position, value);
118        } else if let Some(bindings) = &mut self.0 {
119            bindings.columns.remove(&position);
120        }
121    }
122
123    pub(super) fn alias(
124        &mut self,
125        identity: &ColumnIdentity,
126        oid: Option<u32>,
127        ty: Option<&ColumnType>,
128    ) {
129        let value = override_value(oid, ty);
130        if let Some(value) = value {
131            self.0
132                .get_or_insert_with(Default::default)
133                .aliases
134                .insert(identity.clone(), value);
135        } else if let Some(bindings) = &mut self.0 {
136            bindings.aliases.remove(identity);
137        }
138    }
139
140    pub(super) fn internal(
141        &mut self,
142        column: InternalColumnRef,
143        oid: Option<u32>,
144        ty: Option<&ColumnType>,
145    ) {
146        let value = override_value(oid, ty);
147        if let Some(value) = value {
148            self.0
149                .get_or_insert_with(Default::default)
150                .internal
151                .insert(column, value);
152        } else if let Some(bindings) = &mut self.0 {
153            bindings.internal.remove(&column);
154        }
155    }
156
157    pub(super) fn virtual_column(
158        &mut self,
159        identity: &ColumnIdentity,
160        oid: Option<u32>,
161        ty: Option<&ColumnType>,
162    ) {
163        let value = override_value(oid, ty);
164        if let Some(value) = value {
165            self.0
166                .get_or_insert_with(Default::default)
167                .virtual_columns
168                .insert(identity.clone(), value);
169        } else if let Some(bindings) = &mut self.0 {
170            bindings.virtual_columns.remove(identity);
171        }
172    }
173
174    pub(super) fn clear_columns(&mut self) {
175        if let Some(bindings) = &mut self.0 {
176            bindings.columns.clear();
177        }
178    }
179
180    /// Schema construction discards metadata for removed identities and defaults. Schemas with only type-default collations have no extra allocation.
181    pub(super) fn retain(
182        &mut self,
183        columns: &[Option<ColumnType>],
184        aliases: &HashMap<ColumnIdentity, Option<ColumnType>>,
185        internal: &HashMap<InternalColumnRef, Option<ColumnType>>,
186        virtual_columns: &HashMap<ColumnIdentity, Option<ColumnType>>,
187    ) {
188        let Some(bindings) = &mut self.0 else {
189            return;
190        };
191        bindings.columns.retain(|position, oid| {
192            override_value(Some(*oid), columns.get(*position).and_then(Option::as_ref)).is_some()
193        });
194        bindings.aliases.retain(|identity, oid| {
195            override_value(Some(*oid), aliases.get(identity).and_then(Option::as_ref)).is_some()
196        });
197        bindings.internal.retain(|column, oid| {
198            override_value(Some(*oid), internal.get(column).and_then(Option::as_ref)).is_some()
199        });
200        bindings.virtual_columns.retain(|identity, oid| {
201            override_value(
202                Some(*oid),
203                virtual_columns.get(identity).and_then(Option::as_ref),
204            )
205            .is_some()
206        });
207        if bindings.columns.is_empty()
208            && bindings.aliases.is_empty()
209            && bindings.internal.is_empty()
210            && bindings.virtual_columns.is_empty()
211        {
212            self.0 = None;
213        }
214    }
215}
216
217impl RowSchema {
218    /// Observe retained expression metadata or a direct logical reference, including selected clauses, static bindings and executor attributes without a visible output position.
219    pub fn input_collation(&self, expression: &crate::ScalarExpr) -> Option<u32> {
220        match expression {
221            crate::ScalarExpr::TypedLiteral { collation_oid, .. } => *collation_oid,
222            crate::ScalarExpr::Collated { collation, .. } => Some(collation.oid()),
223            crate::ScalarExpr::Func {
224                binding: Some(binding),
225                ..
226            } => match binding.dispatch {
227                Some(crate::ast::FunctionDispatch::Collate { oid }) => oid,
228                _ => None,
229            },
230            crate::ScalarExpr::Column(name) => self.collation_of(name),
231            crate::ScalarExpr::QualifiedColumn { qualifier, column } => {
232                self.qualified_collation(qualifier, column)
233            }
234            crate::ScalarExpr::Position(position) => self.column_collation(*position),
235            crate::ScalarExpr::InternalColumn(column) => self.internal_collation(*column),
236            _ => None,
237        }
238    }
239
240    /// Bind a direct scalar reference without confusing its SQL identity with another logical column that shares the same physical value. Static binding-only names remain with expression evaluation.
241    pub fn projection_input(&self, expression: &crate::ScalarExpr) -> Option<super::ProjectedSlot> {
242        let (logical, alias) = match expression {
243            crate::ScalarExpr::Column(name) => {
244                if self.column_is_ambiguous(name) {
245                    return None;
246                }
247                let logical = self.unqualified_position(name);
248                (
249                    logical,
250                    logical.is_none().then(|| ColumnIdentity::unqualified(name)),
251                )
252            }
253            crate::ScalarExpr::QualifiedColumn { qualifier, column } => {
254                if self.qualified_column_is_ambiguous(qualifier, column) {
255                    return None;
256                }
257                let logical = self.qualified_position(qualifier, column);
258                (
259                    logical,
260                    logical
261                        .is_none()
262                        .then(|| ColumnIdentity::qualified(qualifier, column)),
263                )
264            }
265            crate::ScalarExpr::Position(position) if *position < self.len() => {
266                (Some(*position), None)
267            }
268            _ => return None,
269        };
270        let (physical, collation) = if let Some(position) = logical {
271            (
272                self.physical_slot(position),
273                self.column_collation(position),
274            )
275        } else {
276            let identity = alias?;
277            let slot = *self.index.aliases.get(&identity)?;
278            (
279                (slot != super::NULL_SLOT).then_some(slot),
280                self.alias_collation(&identity),
281            )
282        };
283        Some(super::ProjectedSlot::BoundInput {
284            physical,
285            collation,
286        })
287    }
288
289    /// Attach already-selected column collations to an owned schema. The width and collatability must match its declared types; catalog name resolution belongs to the caller's SQL analysis.
290    pub fn with_column_collations(
291        mut self,
292        collations: impl IntoIterator<Item = Option<u32>>,
293    ) -> Self {
294        let mut width = 0;
295        let index = std::sync::Arc::make_mut(&mut self.index);
296        for (position, oid) in collations.into_iter().enumerate() {
297            assert!(
298                position < index.columns.len(),
299                "row schema column/collation width mismatch"
300            );
301            let ty = index.cold.columns[position].as_ref();
302            assert!(
303                oid.is_none_or(
304                    |oid| oid == 0 || ty.is_some_and(crate::schema::composites::type_is_collatable)
305                ),
306                "collation requires a collatable column type"
307            );
308            index.cold.collations.column(position, oid, ty);
309            width += 1;
310        }
311        assert_eq!(
312            width,
313            index.columns.len(),
314            "row schema column/collation width mismatch"
315        );
316        index.cold.collations.compact();
317        self
318    }
319
320    /// Select a collation for an existing static binding without adding a physical slot or wildcard output.
321    pub fn with_virtual_collation(
322        mut self,
323        identity: &ColumnIdentity,
324        oid: u32,
325    ) -> SchemaLayoutResult<Self> {
326        let ty = self
327            .index
328            .cold
329            .binding_only
330            .get(identity)
331            .and_then(Option::as_ref);
332        if ty.is_none_or(|ty| !crate::schema::composites::type_is_collatable(ty)) {
333            return Err(SchemaLayoutError(
334                "collation metadata requires an existing collatable virtual binding".into(),
335            ));
336        }
337        let index = std::sync::Arc::make_mut(&mut self.index);
338        index.cold.collations.virtual_column(
339            identity,
340            Some(oid),
341            index
342                .cold
343                .binding_only
344                .get(identity)
345                .and_then(Option::as_ref),
346        );
347        index.cold.collations.compact();
348        Ok(self)
349    }
350
351    pub fn column_collation(&self, position: usize) -> Option<u32> {
352        self.index
353            .cold
354            .collations
355            .0
356            .as_ref()
357            .and_then(|bindings| bindings.columns.get(&position).copied())
358            .or_else(|| type_default(self.column_type(position)))
359    }
360
361    pub fn internal_collation(&self, column: InternalColumnRef) -> Option<u32> {
362        self.index
363            .cold
364            .collations
365            .0
366            .as_ref()
367            .and_then(|bindings| bindings.internal.get(&column).copied())
368            .or_else(|| type_default(self.internal_type(column)))
369    }
370
371    pub fn collation_of(&self, name: &str) -> Option<u32> {
372        if self.column_is_ambiguous(name) {
373            return None;
374        }
375        self.index
376            .unqualified
377            .get(name)
378            .and_then(|position| self.column_collation(*position))
379            .or_else(|| self.hidden_collation(&ColumnIdentity::unqualified(name)))
380    }
381
382    pub fn qualified_collation(&self, qualifier: &str, name: &str) -> Option<u32> {
383        let identity = ColumnIdentity::qualified(qualifier, name);
384        if self.index.ambiguous_qualified.contains(&identity) {
385            return None;
386        }
387        self.index
388            .qualified
389            .get(&identity)
390            .and_then(|position| self.column_collation(*position))
391            .or_else(|| self.hidden_collation(&identity))
392    }
393
394    fn hidden_collation(&self, identity: &ColumnIdentity) -> Option<u32> {
395        self.alias_collation(identity)
396            .or_else(|| self.virtual_collation(identity))
397    }
398
399    pub(super) fn virtual_collation(&self, identity: &ColumnIdentity) -> Option<u32> {
400        self.index
401            .cold
402            .collations
403            .0
404            .as_ref()
405            .and_then(|bindings| bindings.virtual_columns.get(identity).copied())
406            .or_else(|| {
407                type_default(
408                    self.index
409                        .cold
410                        .binding_only
411                        .get(identity)
412                        .and_then(Option::as_ref),
413                )
414            })
415    }
416
417    pub(super) fn alias_collation(&self, identity: &ColumnIdentity) -> Option<u32> {
418        self.index
419            .cold
420            .collations
421            .0
422            .as_ref()
423            .and_then(|bindings| bindings.aliases.get(identity).copied())
424            .or_else(|| {
425                type_default(
426                    self.index
427                        .cold
428                        .aliases
429                        .get(identity)
430                        .and_then(Option::as_ref),
431                )
432            })
433    }
434
435    pub(super) fn inherited_column_collation(&self, position: usize) -> Option<u32> {
436        self.column_type(position)
437            .filter(|ty| crate::schema::composites::type_is_collatable(ty))
438            .and_then(|_| self.column_collation(position))
439    }
440
441    pub(super) fn inherited_physical_collation(&self, physical: usize) -> Option<u32> {
442        if physical == super::NULL_SLOT {
443            return None;
444        }
445        self.index
446            .slots
447            .iter()
448            .position(|slot| *slot == physical)
449            .and_then(|position| self.inherited_column_collation(position))
450            .or_else(|| {
451                self.index.aliases.iter().find_map(|(identity, slot)| {
452                    (*slot == physical
453                        && self
454                            .index
455                            .cold
456                            .aliases
457                            .get(identity)
458                            .and_then(Option::as_ref)
459                            .is_some_and(crate::schema::composites::type_is_collatable))
460                    .then(|| self.alias_collation(identity))
461                    .flatten()
462                })
463            })
464            .or_else(|| {
465                self.index
466                    .executor_attributes
467                    .iter()
468                    .find_map(|(column, slot)| {
469                        (*slot == physical
470                            && self
471                                .internal_type(*column)
472                                .is_some_and(crate::schema::composites::type_is_collatable))
473                        .then(|| self.internal_collation(*column))
474                        .flatten()
475                    })
476            })
477    }
478
479    /// Physical spill metadata excludes static name-binding-only identities, like the existing spill type layout. Type defaults need no extra record payload.
480    pub fn collation_overrides(&self) -> impl Iterator<Item = (CollationBinding<'_>, u32)> {
481        self.index.cold.collations.0.iter().flat_map(|bindings| {
482            bindings
483                .columns
484                .iter()
485                .map(|(column, oid)| (CollationBinding::Column(*column), *oid))
486                .chain(
487                    bindings
488                        .aliases
489                        .iter()
490                        .map(|(identity, oid)| (CollationBinding::Alias(identity), *oid)),
491                )
492                .chain(
493                    bindings
494                        .internal
495                        .iter()
496                        .map(|(column, oid)| (CollationBinding::Internal(*column), *oid)),
497                )
498        })
499    }
500
501    /// Restore one selected collation only for a binding that exists in this layout. Invalid spill metadata cannot invent columns or attach a collation to a noncollatable type.
502    pub fn with_collation_override(
503        mut self,
504        binding: CollationBinding<'_>,
505        oid: u32,
506    ) -> SchemaLayoutResult<Self> {
507        let ty = match binding {
508            CollationBinding::Column(position) => self.column_type(position),
509            CollationBinding::Alias(identity) => self
510                .index
511                .cold
512                .aliases
513                .get(identity)
514                .and_then(Option::as_ref),
515            CollationBinding::Internal(column) => self.internal_type(column),
516        };
517        if ty.is_none_or(|ty| !crate::schema::composites::type_is_collatable(ty)) {
518            return Err(SchemaLayoutError(
519                "collation metadata requires an existing collatable binding".into(),
520            ));
521        }
522        let value = override_value(Some(oid), ty);
523        let index = std::sync::Arc::make_mut(&mut self.index);
524        match binding {
525            CollationBinding::Column(position) => {
526                index.cold.collations.column(
527                    position,
528                    value,
529                    index.cold.columns[position].as_ref(),
530                );
531            }
532            CollationBinding::Alias(identity) => index.cold.collations.alias(
533                identity,
534                value,
535                index.cold.aliases.get(identity).and_then(Option::as_ref),
536            ),
537            CollationBinding::Internal(column) => index.cold.collations.internal(
538                column,
539                value,
540                index
541                    .cold
542                    .executor_attribute_types
543                    .get(&column)
544                    .and_then(Option::as_ref),
545            ),
546        }
547        index.cold.collations.compact();
548        Ok(self)
549    }
550}
551
552#[cfg(test)]
553mod tests;