use serde::{Deserialize, Serialize};
use crate::identifier::{Identifier, QualifiedName};
use crate::ir::default_expr::NormalizedExpr;
use crate::ir::difference::Difference;
use crate::ir::eq::{Equiv, field_difference};
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum IndexParent {
Table(QualifiedName),
Mv(QualifiedName),
}
impl IndexParent {
pub const fn qname(&self) -> &QualifiedName {
match self {
Self::Table(q) | Self::Mv(q) => q,
}
}
pub const fn is_mv(&self) -> bool {
matches!(self, Self::Mv(_))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Index {
pub qname: QualifiedName,
pub on: IndexParent,
pub method: IndexMethod,
pub columns: Vec<IndexColumn>,
pub include: Vec<Identifier>,
pub unique: bool,
pub nulls_not_distinct: bool,
pub predicate: Option<NormalizedExpr>,
pub tablespace: Option<Identifier>,
pub comment: Option<String>,
pub storage: crate::ir::reloptions::IndexStorageOptions,
}
impl Equiv for Index {
fn differences(&self, other: &Self) -> Vec<Difference> {
let Self {
qname: _,
on: _,
method: _,
columns: _,
include: _,
unique: _,
nulls_not_distinct: _,
predicate: _,
tablespace: _,
comment: _,
storage: _,
} = self;
let mut out = Vec::new();
out.extend(field_difference("qname", &self.qname, &other.qname));
out.extend(field_difference(
"on",
&format!("{:?}", self.on),
&format!("{:?}", other.on),
));
out.extend(field_difference(
"method",
&format!("{:?}", self.method),
&format!("{:?}", other.method),
));
out.extend(field_difference(
"columns",
&format!("{:?}", self.columns),
&format!("{:?}", other.columns),
));
out.extend(field_difference(
"include",
&format!("{:?}", self.include),
&format!("{:?}", other.include),
));
out.extend(field_difference("unique", &self.unique, &other.unique));
out.extend(field_difference(
"nulls_not_distinct",
&self.nulls_not_distinct,
&other.nulls_not_distinct,
));
out.extend(field_difference(
"predicate",
&format!("{:?}", self.predicate),
&format!("{:?}", other.predicate),
));
out.extend(field_difference(
"tablespace",
&format!("{:?}", self.tablespace),
&format!("{:?}", other.tablespace),
));
out.extend(field_difference(
"comment",
&format!("{:?}", self.comment),
&format!("{:?}", other.comment),
));
out.extend(field_difference(
"storage",
&format!("{:?}", self.storage),
&format!("{:?}", other.storage),
));
out
}
}
impl Index {
#[must_use]
pub fn structurally_eq(&self, other: &Self) -> bool {
self.qname == other.qname
&& self.on == other.on
&& self.method == other.method
&& self.columns == other.columns
&& self.include == other.include
&& self.unique == other.unique
&& self.nulls_not_distinct == other.nulls_not_distinct
&& self.predicate == other.predicate
&& self.tablespace == other.tablespace
&& self.comment == other.comment
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IndexColumn {
pub expr: IndexColumnExpr,
pub collation: Option<QualifiedName>,
pub opclass: Option<QualifiedName>,
pub sort_order: SortOrder,
pub nulls_order: NullsOrder,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IndexColumnExpr {
Column(Identifier),
Expression(NormalizedExpr),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IndexMethod {
BTree,
Hash,
Gin,
Gist,
Brin,
Spgist,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SortOrder {
Asc,
Desc,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NullsOrder {
NullsFirst,
NullsLast,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::eq::Equiv;
fn id(s: &str) -> Identifier {
Identifier::from_unquoted(s).unwrap()
}
fn qn(schema: &str, name: &str) -> QualifiedName {
QualifiedName::new(id(schema), id(name))
}
fn col(name: &str) -> IndexColumn {
IndexColumn {
expr: IndexColumnExpr::Column(id(name)),
collation: None,
opclass: None,
sort_order: SortOrder::Asc,
nulls_order: NullsOrder::NullsLast,
}
}
fn base() -> Index {
Index {
qname: qn("app", "users_email_idx"),
on: IndexParent::Table(qn("app", "users")),
method: IndexMethod::BTree,
columns: vec![col("email")],
include: vec![],
unique: true,
nulls_not_distinct: false,
predicate: None,
tablespace: None,
comment: None,
storage: crate::ir::reloptions::IndexStorageOptions::default(),
}
}
#[test]
fn equal_indexes_have_no_diff() {
assert!(base().canonical_eq(&base()));
}
#[test]
fn unique_change_diffs() {
let mut b = base();
b.unique = false;
assert!(base().differences(&b).iter().any(|x| x.path == "unique"));
}
#[test]
fn include_columns_diff() {
let mut b = base();
b.include = vec![id("name")];
assert!(base().differences(&b).iter().any(|x| x.path == "include"));
}
#[test]
fn predicate_change_diffs() {
let mut b = base();
b.predicate = Some(NormalizedExpr::from_text("deleted_at is null"));
assert!(base().differences(&b).iter().any(|x| x.path == "predicate"));
}
#[test]
fn opclass_change_diffs() {
let mut b = base();
b.columns[0].opclass = Some(qn("pg_catalog", "text_pattern_ops"));
assert!(base().differences(&b).iter().any(|x| x.path == "columns"));
}
#[test]
fn column_order_matters() {
let a = Index {
columns: vec![col("a"), col("b")],
..base()
};
let b = Index {
columns: vec![col("b"), col("a")],
..base()
};
assert!(!a.canonical_eq(&b));
}
#[test]
fn storage_change_diffs() {
let mut b = base();
b.storage = crate::ir::reloptions::IndexStorageOptions {
fillfactor: Some(70),
..Default::default()
};
assert!(base().differences(&b).iter().any(|x| x.path == "storage"));
}
#[test]
fn index_can_target_a_materialized_view() {
let mv_idx = Index {
qname: qn("app", "mv_email_idx"),
on: IndexParent::Mv(qn("app", "users_mv")),
method: IndexMethod::BTree,
columns: vec![col("email")],
include: vec![],
unique: true,
nulls_not_distinct: false,
predicate: None,
tablespace: None,
comment: None,
storage: crate::ir::reloptions::IndexStorageOptions::default(),
};
assert!(mv_idx.on.is_mv());
assert_eq!(mv_idx.on.qname().to_string(), "app.users_mv");
assert!(mv_idx.canonical_eq(&mv_idx));
let tbl_idx = base();
assert!(!tbl_idx.on.is_mv());
assert_eq!(tbl_idx.on.qname().to_string(), "app.users");
let tbl_idx_same_name = Index {
qname: qn("app", "mv_email_idx"),
on: IndexParent::Table(qn("app", "users_mv")),
..mv_idx.clone()
};
assert!(!mv_idx.canonical_eq(&tbl_idx_same_name));
let diffs = mv_idx.differences(&tbl_idx_same_name);
assert!(diffs.iter().any(|d| d.path == "on"));
}
}