Skip to main content

uqa_sql/semantics/
membership.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Membership operands shared by access selection, cardinality and key reads.
8
9use crate::{ast::FunctionDispatch, ScalarExpr};
10use uqa_core::Value;
11
12/// An equality set or its SQL three-valued negation. The operands still require the consumer's ordinary safety and constant checks.
13pub struct MembershipOperands<'a> {
14    pub value: &'a ScalarExpr,
15    pub items: MembershipItems<'a>,
16    pub negated: bool,
17}
18
19/// Analyzed expressions or the borrowed values left after constant array folding.
20#[derive(Clone, Copy)]
21pub enum MembershipItems<'a> {
22    Expressions(&'a [ScalarExpr]),
23    Constants(&'a [Value]),
24}
25
26pub enum MembershipItem<'a> {
27    Expression(&'a ScalarExpr),
28    Constant(&'a Value),
29}
30
31impl<'a> MembershipItems<'a> {
32    #[must_use]
33    pub fn len(self) -> usize {
34        match self {
35            Self::Expressions(items) => items.len(),
36            Self::Constants(items) => items.len(),
37        }
38    }
39
40    #[must_use]
41    pub fn is_empty(self) -> bool {
42        self.len() == 0
43    }
44
45    pub fn iter(self) -> impl Iterator<Item = MembershipItem<'a>> {
46        let (expressions, constants): (&[ScalarExpr], &[Value]) = match self {
47            Self::Expressions(items) => (items, &[]),
48            Self::Constants(items) => (&[], items),
49        };
50        expressions
51            .iter()
52            .map(MembershipItem::Expression)
53            .chain(constants.iter().map(MembershipItem::Constant))
54    }
55}
56
57/// Borrow an IN list or its analyzed scalar-array comparison without dropping conversions. Empty ALL arrays stay scalar because they also accept a NULL left operand.
58#[must_use]
59pub fn membership_operands(expression: &ScalarExpr) -> Option<MembershipOperands<'_>> {
60    match expression {
61        ScalarExpr::InList {
62            expr,
63            list,
64            negated,
65        } => Some(MembershipOperands {
66            value: expr,
67            items: MembershipItems::Expressions(list),
68            negated: *negated,
69        }),
70        ScalarExpr::Func {
71            binding: Some(binding),
72            args,
73            ..
74        } => {
75            let [value, array, ScalarExpr::Literal(Value::Str(operator))] = args.as_slice() else {
76                return None;
77            };
78            let items = match array {
79                ScalarExpr::Array(items) => MembershipItems::Expressions(items),
80                ScalarExpr::Literal(Value::Array(array))
81                | ScalarExpr::TypedLiteral {
82                    value: Value::Array(array),
83                    ..
84                } if array.dimensions().len() <= 1 => MembershipItems::Constants(array.elements()),
85                _ => return None,
86            };
87            let negated = match (binding.dispatch, operator.as_str()) {
88                (Some(FunctionDispatch::AnyOperator), "=") => false,
89                (Some(FunctionDispatch::AllOperator), "<>") if !items.is_empty() => true,
90                _ => return None,
91            };
92            Some(MembershipOperands {
93                value,
94                items,
95                negated,
96            })
97        }
98        _ => None,
99    }
100}
101
102/// Query-local columns participate in IN-list partitioning and window placement; an enclosing query's columns do not. A declared schema without physical scopes treats every column as local.
103pub(crate) fn references_current_row(
104    expression: &ScalarExpr,
105    schema: Option<&crate::RowSchema>,
106) -> bool {
107    let mut found = false;
108    expression.visit(&mut |node| {
109        found |= match node {
110            ScalarExpr::Column(column) => {
111                schema.is_none_or(|schema| schema.resolves_local_column(None, column))
112            }
113            ScalarExpr::QualifiedColumn { qualifier, column } => {
114                schema.is_none_or(|schema| schema.resolves_local_column(Some(qualifier), column))
115            }
116            ScalarExpr::Position(position) => schema.is_none_or(|schema| {
117                schema
118                    .slot(*position)
119                    .is_some_and(|slot| schema.slot_is_local(slot))
120            }),
121            ScalarExpr::InternalColumn(column) => schema.is_none_or(|schema| {
122                schema
123                    .internal_slot(*column)
124                    .is_some_and(|slot| schema.slot_is_local(slot))
125            }),
126            ScalarExpr::QualifiedStar(qualifier) => schema.is_none_or(|schema| {
127                schema
128                    .identities()
129                    .iter()
130                    .any(|identity| identity.qualifier() == Some(qualifier.as_str()))
131            }),
132            _ => false,
133        };
134    });
135    found
136}