Skip to main content

uqa_sql/schema/indexes/
unique.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Unique-index access-method and partition-key declaration rules.
8use crate::{
9    ast::{CreateIndex, PartitionSpec, TableHierarchy, TableKeyConstraint, TableKeyConstraintKind},
10    SQLError,
11};
12pub fn validate_unique_index_method(statement: &CreateIndex) -> Result<(), SQLError> {
13    if !matches!(statement.access_method.as_str(), "" | "btree") {
14        return Err(SQLError::Routine {
15            sqlstate: "0A000".into(),
16            message: format!(
17                "access method \"{}\" does not support unique indexes",
18                statement.access_method
19            ),
20        });
21    }
22    Ok(())
23}
24pub fn validate_unique_partition_columns(
25    statement: &CreateIndex,
26    hierarchy: &TableHierarchy,
27) -> Result<(), SQLError> {
28    match &hierarchy.partition_spec {
29        Some(partition) => validate_unique_index_partition_key(statement, partition),
30        None => Ok(()),
31    }
32}
33
34/// A unique index of a partitioned table holds every partition key column among its key columns.
35pub fn validate_unique_index_partition_key(
36    statement: &CreateIndex,
37    partition: &PartitionSpec,
38) -> Result<(), SQLError> {
39    let columns = statement
40        .columns
41        .iter()
42        .map(crate::ast::IndexKey::column)
43        .collect::<Vec<_>>();
44    validate_partitioned_unique_key(
45        &statement.table,
46        &PartitionedUniqueKey {
47            constraint_type: TableKeyConstraintKind::Unique,
48            columns: &columns,
49            without_overlaps: false,
50        },
51        partition,
52    )
53}
54
55/// Validate a key constraint of a partitioned table by its key columns; its `INCLUDE` columns take no part.
56pub fn validate_partitioned_key_constraint(
57    table: &str,
58    constraint: &TableKeyConstraint,
59    partition: &PartitionSpec,
60) -> Result<(), SQLError> {
61    let columns = constraint
62        .columns
63        .iter()
64        .map(|column| Some(column.as_str()))
65        .collect::<Vec<_>>();
66    validate_partitioned_unique_key(
67        table,
68        &PartitionedUniqueKey {
69            constraint_type: constraint.kind,
70            columns: &columns,
71            without_overlaps: constraint.without_overlaps,
72        },
73        partition,
74    )
75}
76
77/// A unique key as `PostgreSQL`'s `DefineIndex` matches it against a partition key.
78pub struct PartitionedUniqueKey<'a> {
79    /// The constraint type the errors name; a unique index reports `UNIQUE`.
80    pub constraint_type: TableKeyConstraintKind,
81    /// The key columns in key order without `INCLUDE` columns, `None` for an expression key.
82    pub columns: &'a [Option<&'a str>],
83    /// The final key column is compared by overlap rather than equality.
84    pub without_overlaps: bool,
85}
86
87/// Every partition key column must be compared by equality in a unique key of a partitioned table, so that each partition's index enforces the key alone. As `PostgreSQL`'s `DefineIndex` does, the partition key is scanned in order: an expression cannot be matched at all, a column must be one of the key columns, and the `WITHOUT OVERLAPS` column is compared by `&&` rather than equality.
88pub fn validate_partitioned_unique_key(
89    table: &str,
90    key: &PartitionedUniqueKey<'_>,
91    partition: &PartitionSpec,
92) -> Result<(), SQLError> {
93    let constraint_type = key.constraint_type.sql_label();
94    for partition_key in &partition.keys {
95        let crate::ast::Expr::Column(column) = partition_key else {
96            return Err(SQLError::Diagnostic {
97                sqlstate: "0A000".into(),
98                message: format!(
99                    "unsupported {constraint_type} constraint with partition key definition"
100                ),
101                detail: Some(format!(
102                    "{constraint_type} constraints cannot be used when partition keys include expressions."
103                )),
104                hint: None,
105            });
106        };
107        match key
108            .columns
109            .iter()
110            .position(|candidate| *candidate == Some(column.as_str()))
111        {
112            Some(position) if key.without_overlaps && position + 1 == key.columns.len() => {
113                return Err(SQLError::Routine {
114                    sqlstate: "0A000".into(),
115                    message: format!(
116                        "cannot match partition key to index on column \"{column}\" using non-equal operator \"&&\""
117                    ),
118                });
119            }
120            Some(_) => {}
121            None => {
122                let relation = uqa_core::RelationIdentity::from_legacy_name(table)
123                    .map_err(SQLError::Internal)?;
124                return Err(SQLError::Diagnostic {
125                    sqlstate: "0A000".into(),
126                    message: "unique constraint on partitioned table must include all partitioning columns".into(),
127                    detail: Some(format!(
128                        "{constraint_type} constraint on table \"{}\" lacks column \"{column}\" which is part of the partition key.",
129                        relation.name
130                    )),
131                    hint: None,
132                });
133            }
134        }
135    }
136    Ok(())
137}