Skip to main content

uqa_sql/schema/
indexes.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Immutable index-expression binding in the indexed table's declared row type.
8
9use super::{generated::bind_schema_column_references, SchemaExpressionCatalog};
10use crate::plan::ExpressionPlan;
11use crate::RowSchema;
12use crate::{ast::Expr, binding::context::BindingContext, ColumnType, SQLError};
13
14pub fn prepare_index_expression(
15    engine: &dyn SchemaExpressionCatalog,
16    binding: &BindingContext<'_>,
17    table: &str,
18    expression: &mut Expr,
19) -> Result<ColumnType, SQLError> {
20    let ty = bind_index_expression(engine, binding, table, expression, false)?;
21    validate_index_expression_immutability(engine, table, expression, false)?;
22    if let Some(ty) = ty {
23        return Ok(ty);
24    }
25    *expression = Expr::Cast {
26        implicit: true,
27        expr: Box::new(expression.clone()),
28        ty: "text".into(),
29    };
30    Ok(ColumnType::Text)
31}
32
33pub fn prepare_index_predicate(
34    engine: &dyn SchemaExpressionCatalog,
35    binding: &BindingContext<'_>,
36    table: &str,
37    expression: &mut Expr,
38) -> Result<(), SQLError> {
39    bind_index_predicate(engine, binding, table, expression)?;
40    validate_index_expression_immutability(engine, table, expression, true)
41}
42
43pub(super) fn bind_index_predicate(
44    engine: &dyn SchemaExpressionCatalog,
45    binding: &BindingContext<'_>,
46    table: &str,
47    expression: &mut Expr,
48) -> Result<(), SQLError> {
49    match bind_index_expression(engine, binding, table, expression, true)? {
50        Some(ColumnType::Boolean) => Ok(()),
51        None => {
52            if let Expr::Literal(value) = expression {
53                *value = crate::expr::cast_value(value, "boolean")?;
54            }
55            Ok(())
56        }
57        Some(_) => Err(SQLError::TypeMismatch(
58            "argument of WHERE must be type boolean".into(),
59        )),
60    }
61}
62
63pub(super) fn bind_index_expression(
64    engine: &dyn SchemaExpressionCatalog,
65    binding: &BindingContext<'_>,
66    table: &str,
67    expression: &mut Expr,
68    predicate: bool,
69) -> Result<Option<ColumnType>, SQLError> {
70    let context = if predicate {
71        "index predicate"
72    } else {
73        "index expression"
74    };
75    let mut plan = ExpressionPlan::lower(expression.clone());
76    if !plan.subqueries.is_empty() {
77        return Err(index_error(
78            "0A000",
79            format!("cannot use subquery in {context}"),
80        ));
81    }
82    if crate::semantics::aggregates::contains_aggregate(engine, &plan.scalar) {
83        return Err(index_error(
84            "42803",
85            format!("aggregate functions are not allowed in {context}s"),
86        ));
87    }
88    if crate::semantics::windows::expr_has_window(&plan.scalar) {
89        return Err(index_error(
90            "42P20",
91            format!("window functions are not allowed in {context}s"),
92        ));
93    }
94    let columns = engine
95        .schema_expression_columns(table)?
96        .ok_or_else(|| SQLError::UnknownTable(table.into()))?;
97    let relation =
98        uqa_core::RelationIdentity::from_legacy_name(table).map_err(SQLError::Internal)?;
99    bind_schema_column_references(expression, &relation.name);
100    bind_schema_column_references(expression, table);
101    plan.scalar = ExpressionPlan::lower(expression.clone()).scalar;
102    let schema = RowSchema::with_types(
103        columns.iter().map(|column| column.name.clone()).collect(),
104        columns
105            .iter()
106            .map(|column| Some(column.ty.clone()))
107            .collect(),
108    );
109    if crate::semantics::sets::validation::expression_may_return_set(
110        engine,
111        engine,
112        &plan.scalar,
113        &schema,
114        &[],
115    )? {
116        return Err(index_error(
117            "0A000",
118            format!("set-returning functions are not allowed in {context}s"),
119        ));
120    }
121    let lowered = plan.clone();
122    let ty = crate::binding::bind_expression_plan_routines_for_storage(
123        engine,
124        &mut plan,
125        &[],
126        binding,
127        &schema,
128    )?;
129    let sites = crate::binding::syntax_sites::expression_syntax_sites(&lowered, &plan)?;
130    crate::catalog::stored_ast::bind_stored_expression_sites(expression, &sites)?;
131    Ok(ty)
132}
133
134pub(super) fn validate_index_expression_immutability(
135    engine: &dyn SchemaExpressionCatalog,
136    table: &str,
137    expression: &mut Expr,
138    predicate: bool,
139) -> Result<(), SQLError> {
140    let columns = engine
141        .schema_expression_columns(table)?
142        .ok_or_else(|| SQLError::UnknownTable(table.into()))?;
143    let planned = engine.plan_schema_expression(expression, &columns)?;
144    if !planned.immutable {
145        let context = if predicate { "predicate" } else { "expression" };
146        return Err(index_error(
147            "42P17",
148            format!("functions in index {context} must be marked IMMUTABLE"),
149        ));
150    }
151    *expression = planned.expression;
152    Ok(())
153}
154
155fn index_error(sqlstate: &str, message: String) -> SQLError {
156    SQLError::Routine {
157        sqlstate: sqlstate.into(),
158        message,
159    }
160}
161
162pub mod keys;
163
164pub mod names;
165
166pub mod unique;
167
168pub mod options;
169pub mod system_columns;
170pub mod vectors;
171
172pub mod removal;
173pub mod routines;