Skip to main content

uqa_sql/schema/indexes/
removal.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! DROP INDEX names, constraint dependencies and stored field references.
8use crate::{
9    ast::{ColumnType, ForeignKey},
10    catalog::resolution::RelationResolution,
11    SQLError,
12};
13use std::collections::BTreeSet;
14use uqa_core::RelationIdentity;
15
16pub fn resolve_drop_index_name(
17    resolution: RelationResolution,
18    requested: &str,
19    if_exists: bool,
20    notice: &mut dyn FnMut(&str),
21) -> Result<Option<String>, SQLError> {
22    match resolution {
23        RelationResolution::Found(canonical, "index") => Ok(Some(canonical)),
24        RelationResolution::Found(_, _) => Err(SQLError::Routine {
25            sqlstate: "42809".into(),
26            message: format!("\"{requested}\" is not an index"),
27        }),
28        RelationResolution::MissingSchema(schema) if if_exists => {
29            notice(&format!("schema \"{schema}\" does not exist, skipping"));
30            Ok(None)
31        }
32        RelationResolution::MissingSchema(schema) => Err(SQLError::Routine {
33            sqlstate: "3F000".into(),
34            message: format!("schema \"{schema}\" does not exist"),
35        }),
36        RelationResolution::MissingRelation if if_exists => {
37            let local = uqa_core::RelationIdentity::parse_reference(requested)
38                .map_err(SQLError::Internal)?
39                .1;
40            notice(&format!("index \"{local}\" does not exist, skipping"));
41            Ok(None)
42        }
43        RelationResolution::MissingRelation => {
44            let local = uqa_core::RelationIdentity::parse_reference(requested)
45                .map_err(SQLError::Internal)?
46                .1;
47            Err(SQLError::Routine {
48                sqlstate: "42704".into(),
49                message: format!("index \"{local}\" does not exist"),
50            })
51        }
52    }
53}
54pub fn ensure_index_not_constraint_owned(
55    relation: &RelationIdentity,
56    table: &str,
57    constraint_owned: bool,
58) -> Result<(), SQLError> {
59    if constraint_owned {
60        return Err(SQLError::Routine {
61            sqlstate: "2BP01".into(),
62            message: format!(
63                "cannot drop index {} because constraint {} on table {} requires it",
64                relation.name, relation.name, table
65            ),
66        });
67    }
68    Ok(())
69}
70pub fn catalog_index_columns(
71    relation: &RelationIdentity,
72    columns_json: &str,
73    action: &str,
74) -> Result<Vec<String>, SQLError> {
75    serde_json::from_str(columns_json).map_err(|e| {
76        SQLError::Internal(format!(
77            "{action} `{}`: invalid index column metadata: {e}",
78            relation.qualified_name()
79        ))
80    })
81}
82
83pub fn collect_index_dependents(
84    index_name: &str,
85    referrers: Vec<(String, ForeignKey)>,
86    cascade: bool,
87    dependents: &mut BTreeSet<(String, String)>,
88) -> Result<(), SQLError> {
89    for (table, foreign_key) in referrers {
90        if foreign_key.referenced_key.as_deref() != Some(index_name) {
91            continue;
92        }
93        let name = foreign_key
94            .name
95            .ok_or_else(|| SQLError::Internal("unnamed foreign-key dependency".into()))?;
96        if !cascade {
97            return Err(SQLError::Routine {
98                    sqlstate: "2BP01".into(),
99                    message: format!("cannot drop index {index_name} because constraint {name} on table {table} depends on it"),
100                });
101        }
102        dependents.insert((table, name));
103    }
104    Ok(())
105}
106/// Borrowed catalog metadata used to detect remaining references to a physical text field.
107pub struct IndexRemovalCandidate<'a> {
108    pub relation: &'a RelationIdentity,
109    pub table: &'a str,
110    pub method: &'a str,
111    pub columns_json: &'a str,
112}
113pub fn gin_field_is_referenced<'a>(
114    relation: &RelationIdentity,
115    table: &str,
116    field: &str,
117    candidates: impl IntoIterator<Item = IndexRemovalCandidate<'a>>,
118) -> Result<bool, SQLError> {
119    for candidate in candidates {
120        if candidate.relation == relation
121            || candidate.table != table
122            || !candidate.method.eq_ignore_ascii_case("gin")
123        {
124            continue;
125        }
126        if catalog_index_columns(candidate.relation, candidate.columns_json, "DROP INDEX")?
127            .iter()
128            .any(|candidate_field| candidate_field == field)
129        {
130            return Ok(true);
131        }
132    }
133    Ok(false)
134}
135pub fn vector_index_dimensions(
136    relation: &RelationIdentity,
137    table: &str,
138    column: &str,
139    column_type: Option<ColumnType>,
140) -> Result<u32, SQLError> {
141    match column_type {
142        Some(ColumnType::Vector(dim) | ColumnType::Tensor(dim)) => Ok(dim),
143        Some(other) => Err(SQLError::Unsupported(format!(
144            "DROP INDEX `{}`: vector-index column `{}`.`{column}` is no longer VECTOR or TENSOR, got {other:?}",
145            relation.qualified_name(), table
146        ))),
147        None => Err(SQLError::Unsupported(format!(
148            "DROP INDEX `{}`: column `{}`.`{column}` does not exist",
149            relation.qualified_name(), table
150        ))),
151    }
152}