use crate::prelude::*;
use std::{
fmt::{self, Display},
ops::Not,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub enum IndexExpression {
Lower(&'static str),
Upper(&'static str),
Trim(&'static str),
LowerTrim(&'static str),
Date(&'static str),
Year(&'static str),
Month(&'static str),
Day(&'static str),
}
impl IndexExpression {
#[must_use]
pub const fn field(&self) -> &'static str {
match self {
Self::Lower(field)
| Self::Upper(field)
| Self::Trim(field)
| Self::LowerTrim(field)
| Self::Date(field)
| Self::Year(field)
| Self::Month(field)
| Self::Day(field) => field,
}
}
}
impl Display for IndexExpression {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Lower(field) => write!(f, "LOWER({field})"),
Self::Upper(field) => write!(f, "UPPER({field})"),
Self::Trim(field) => write!(f, "TRIM({field})"),
Self::LowerTrim(field) => write!(f, "LOWER(TRIM({field}))"),
Self::Date(field) => write!(f, "DATE({field})"),
Self::Year(field) => write!(f, "YEAR({field})"),
Self::Month(field) => write!(f, "MONTH({field})"),
Self::Day(field) => write!(f, "DAY({field})"),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub enum IndexKeyItem {
Field(&'static str),
Expression(IndexExpression),
}
impl IndexKeyItem {
#[must_use]
pub const fn field(&self) -> &'static str {
match self {
Self::Field(field) => field,
Self::Expression(expression) => expression.field(),
}
}
#[must_use]
pub fn canonical_text(&self) -> String {
match self {
Self::Field(field) => (*field).to_string(),
Self::Expression(expression) => expression.to_string(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IndexKeyItemsRef {
Fields(&'static [&'static str]),
Items(&'static [IndexKeyItem]),
}
#[derive(Clone, Debug, Serialize)]
pub struct Index {
fields: &'static [&'static str],
#[serde(skip_serializing_if = "Option::is_none")]
key_items: Option<&'static [IndexKeyItem]>,
#[serde(skip_serializing_if = "Not::not")]
unique: bool,
#[serde(skip_serializing_if = "Option::is_none")]
predicate: Option<&'static str>,
}
impl Index {
#[must_use]
pub const fn new(fields: &'static [&'static str], unique: bool) -> Self {
Self::new_with_key_items_and_predicate(fields, None, unique, None)
}
#[must_use]
pub const fn new_with_predicate(
fields: &'static [&'static str],
unique: bool,
predicate: Option<&'static str>,
) -> Self {
Self::new_with_key_items_and_predicate(fields, None, unique, predicate)
}
#[must_use]
pub const fn new_with_key_items(
fields: &'static [&'static str],
key_items: &'static [IndexKeyItem],
unique: bool,
) -> Self {
Self::new_with_key_items_and_predicate(fields, Some(key_items), unique, None)
}
#[must_use]
pub const fn new_with_key_items_and_predicate(
fields: &'static [&'static str],
key_items: Option<&'static [IndexKeyItem]>,
unique: bool,
predicate: Option<&'static str>,
) -> Self {
Self {
fields,
key_items,
unique,
predicate,
}
}
#[must_use]
pub const fn fields(&self) -> &'static [&'static str] {
self.fields
}
#[must_use]
pub const fn key_items(&self) -> IndexKeyItemsRef {
if let Some(items) = self.key_items {
IndexKeyItemsRef::Items(items)
} else {
IndexKeyItemsRef::Fields(self.fields)
}
}
#[must_use]
pub const fn has_expression_key_items(&self) -> bool {
let Some(items) = self.key_items else {
return false;
};
let mut index = 0usize;
while index < items.len() {
if matches!(items[index], IndexKeyItem::Expression(_)) {
return true;
}
index = index.saturating_add(1);
}
false
}
#[must_use]
pub const fn is_unique(&self) -> bool {
self.unique
}
#[must_use]
pub const fn predicate(&self) -> Option<&'static str> {
self.predicate
}
#[must_use]
pub fn is_prefix_of(&self, other: &Self) -> bool {
self.fields().len() < other.fields().len() && other.fields().starts_with(self.fields())
}
fn joined_key_items(&self) -> String {
match self.key_items() {
IndexKeyItemsRef::Fields(fields) => fields.join(", "),
IndexKeyItemsRef::Items(items) => {
let mut joined = String::new();
for item in items {
if !joined.is_empty() {
joined.push_str(", ");
}
joined.push_str(item.canonical_text().as_str());
}
joined
}
}
}
}
impl Display for Index {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let fields = self.joined_key_items();
if self.is_unique() {
if let Some(predicate) = self.predicate() {
write!(f, "UNIQUE ({fields}) WHERE {predicate}")
} else {
write!(f, "UNIQUE ({fields})")
}
} else if let Some(predicate) = self.predicate() {
write!(f, "({fields}) WHERE {predicate}")
} else {
write!(f, "({fields})")
}
}
}
impl MacroNode for Index {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl ValidateNode for Index {}
impl VisitableNode for Index {
fn route_key(&self) -> String {
self.joined_key_items()
}
}
#[cfg(test)]
mod tests {
use crate::node::index::{Index, IndexExpression, IndexKeyItem, IndexKeyItemsRef};
#[test]
fn index_with_predicate_reports_conditional_shape() {
let index = Index::new_with_predicate(&["email"], false, Some("active = true"));
assert_eq!(index.predicate(), Some("active = true"));
assert_eq!(index.to_string(), "(email) WHERE active = true");
}
#[test]
fn index_without_predicate_preserves_unconditional_shape() {
let index = Index::new(&["email"], true);
assert_eq!(index.predicate(), None);
assert_eq!(index.to_string(), "UNIQUE (email)");
}
#[test]
fn index_with_explicit_key_items_exposes_expression_items() {
static KEY_ITEMS: [IndexKeyItem; 2] = [
IndexKeyItem::Field("tenant_id"),
IndexKeyItem::Expression(IndexExpression::Lower("email")),
];
let index = Index::new_with_key_items(&["tenant_id"], &KEY_ITEMS, false);
assert!(index.has_expression_key_items());
assert_eq!(index.to_string(), "(tenant_id, LOWER(email))");
assert!(matches!(
index.key_items(),
IndexKeyItemsRef::Items(items)
if items == KEY_ITEMS.as_slice()
));
}
}