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    index_id: [u8; 16],
86    referrers: Vec<(String, ForeignKey)>,
87    cascade: bool,
88    dependents: &mut BTreeSet<(String, String)>,
89) -> Result<(), SQLError> {
90    for (table, foreign_key) in referrers {
91        if foreign_key.referenced_index != Some(index_id) {
92            continue;
93        }
94        let name = foreign_key
95            .name
96            .ok_or_else(|| SQLError::Internal("unnamed foreign-key dependency".into()))?;
97        if !cascade {
98            return Err(SQLError::Routine {
99                    sqlstate: "2BP01".into(),
100                    message: format!("cannot drop index {index_name} because constraint {name} on table {table} depends on it"),
101                });
102        }
103        dependents.insert((table, name));
104    }
105    Ok(())
106}
107/// Borrowed catalog metadata used to detect remaining references to a physical text field.
108pub struct IndexRemovalCandidate<'a> {
109    pub relation: &'a RelationIdentity,
110    pub table: &'a str,
111    pub method: &'a str,
112    pub columns_json: &'a str,
113}
114pub fn gin_field_is_referenced<'a>(
115    relation: &RelationIdentity,
116    table: &str,
117    field: &str,
118    candidates: impl IntoIterator<Item = IndexRemovalCandidate<'a>>,
119) -> Result<bool, SQLError> {
120    for candidate in candidates {
121        if candidate.relation == relation
122            || candidate.table != table
123            || !candidate.method.eq_ignore_ascii_case("gin")
124        {
125            continue;
126        }
127        if catalog_index_columns(candidate.relation, candidate.columns_json, "DROP INDEX")?
128            .iter()
129            .any(|candidate_field| candidate_field == field)
130        {
131            return Ok(true);
132        }
133    }
134    Ok(false)
135}
136pub fn vector_index_dimensions(
137    relation: &RelationIdentity,
138    table: &str,
139    column: &str,
140    column_type: Option<ColumnType>,
141) -> Result<u32, SQLError> {
142    match column_type {
143        Some(ColumnType::Vector(dim) | ColumnType::Tensor(dim)) => Ok(dim),
144        Some(other) => Err(SQLError::Unsupported(format!(
145            "DROP INDEX `{}`: vector-index column `{}`.`{column}` is no longer VECTOR or TENSOR, got {other:?}",
146            relation.qualified_name(), table
147        ))),
148        None => Err(SQLError::Unsupported(format!(
149            "DROP INDEX `{}`: column `{}`.`{column}` does not exist",
150            relation.qualified_name(), table
151        ))),
152    }
153}