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