#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Index {
pub fields: Vec<String>,
pub name: String,
pub db_tablespace: Option<String>,
pub opclasses: Vec<String>,
pub condition: Option<String>,
pub include: Vec<String>,
pub expressions: Vec<String>,
}
#[must_use]
fn format_condition(condition: Option<&str>) -> String {
condition
.filter(|value| !value.trim().is_empty())
.map_or_else(String::new, |value| format!(" WHERE {value}"))
}
#[must_use]
fn format_include(columns: &[String]) -> String {
if columns.is_empty() {
String::new()
} else {
format!(" INCLUDE ({})", columns.join(", "))
}
}
#[must_use]
fn format_tablespace(db_tablespace: Option<&str>) -> String {
db_tablespace
.filter(|value| !value.trim().is_empty())
.map_or_else(String::new, |value| format!(" TABLESPACE {value}"))
}
#[must_use]
fn join_fields_with_opclasses(fields: &[String], opclasses: &[String]) -> Vec<String> {
fields
.iter()
.enumerate()
.map(|(index, field)| match opclasses.get(index) {
Some(opclass) if !opclass.trim().is_empty() => format!("{field} {opclass}"),
_ => field.clone(),
})
.collect()
}
impl Index {
#[must_use]
pub fn create_sql(&self, table: &str) -> String {
let mut items = self.expressions.clone();
items.extend(join_fields_with_opclasses(&self.fields, &self.opclasses));
format!(
"CREATE INDEX {} ON {} ({}){}{}{}",
self.name,
table,
items.join(", "),
format_condition(self.condition.as_deref()),
format_include(&self.include),
format_tablespace(self.db_tablespace.as_deref()),
)
}
#[must_use]
pub fn remove_sql(&self, _table: &str) -> String {
format!("DROP INDEX IF EXISTS {}", self.name)
}
#[must_use]
pub fn describe(&self) -> String {
let mut items = self.expressions.clone();
items.extend(self.fields.iter().cloned());
let mut description = format!("Index {} on {}", self.name, items.join(", "));
if let Some(condition) = self.condition.as_deref() {
description.push_str(&format!(" where {condition}"));
}
if !self.include.is_empty() {
description.push_str(&format!(" include {}", self.include.join(", ")));
}
description
}
}
#[cfg(test)]
mod tests {
use super::Index;
fn base_index() -> Index {
Index {
fields: vec!["title".into()],
name: "book_title_idx".into(),
db_tablespace: None,
opclasses: Vec::new(),
condition: None,
include: Vec::new(),
expressions: Vec::new(),
}
}
#[test]
fn test_index_basic_create_sql() {
let index = base_index();
assert_eq!(
index.create_sql("books"),
"CREATE INDEX book_title_idx ON books (title)"
);
}
#[test]
fn test_index_with_condition() {
let mut index = base_index();
index.condition = Some("published = TRUE".into());
assert_eq!(
index.create_sql("books"),
"CREATE INDEX book_title_idx ON books (title) WHERE published = TRUE"
);
}
#[test]
fn test_index_with_include() {
let mut index = base_index();
index.include = vec!["id".into(), "slug".into()];
assert_eq!(
index.create_sql("books"),
"CREATE INDEX book_title_idx ON books (title) INCLUDE (id, slug)"
);
}
#[test]
fn test_index_remove_sql() {
let index = base_index();
assert_eq!(
index.remove_sql("books"),
"DROP INDEX IF EXISTS book_title_idx"
);
}
#[test]
fn test_index_describe() {
let mut index = base_index();
index.condition = Some("published = TRUE".into());
index.include = vec!["slug".into()];
assert_eq!(
index.describe(),
"Index book_title_idx on title where published = TRUE include slug"
);
}
#[test]
fn index_create_sql_supports_expressions_opclasses_and_tablespaces() {
let index = Index {
fields: vec!["title".into()],
name: "book_search_idx".into(),
db_tablespace: Some("fastspace".into()),
opclasses: vec!["text_pattern_ops".into()],
condition: None,
include: Vec::new(),
expressions: vec!["lower(title)".into()],
};
assert_eq!(
index.create_sql("books"),
"CREATE INDEX book_search_idx ON books (lower(title), title text_pattern_ops) TABLESPACE fastspace"
);
}
}