use crate::prelude::*;
use icydb_schema::{SchemaContractError, SourceCheckExpr};
use std::{
fmt::{self, Display},
ops::Not,
};
use crate::node::{Schema, SourceExpressionResolver};
#[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 {
name: &'static str,
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>,
#[serde(skip)]
predicate_expression: Option<SourceExpressionResolver>,
}
impl Index {
#[must_use]
pub const fn new(name: &'static str, fields: &'static [&'static str], unique: bool) -> Self {
Self::new_with_key_items_and_predicate(name, fields, None, unique, None, None)
}
#[must_use]
pub const fn new_with_predicate(
name: &'static str,
fields: &'static [&'static str],
unique: bool,
predicate: Option<&'static str>,
predicate_expression: Option<SourceExpressionResolver>,
) -> Self {
Self::new_with_key_items_and_predicate(
name,
fields,
None,
unique,
predicate,
predicate_expression,
)
}
#[must_use]
pub const fn new_with_key_items(
name: &'static str,
fields: &'static [&'static str],
key_items: &'static [IndexKeyItem],
unique: bool,
) -> Self {
Self::new_with_key_items_and_predicate(name, fields, Some(key_items), unique, None, None)
}
#[must_use]
pub const fn new_with_key_items_and_predicate(
name: &'static str,
fields: &'static [&'static str],
key_items: Option<&'static [IndexKeyItem]>,
unique: bool,
predicate: Option<&'static str>,
predicate_expression: Option<SourceExpressionResolver>,
) -> Self {
Self {
name,
fields,
key_items,
unique,
predicate,
predicate_expression,
}
}
#[must_use]
pub const fn name(&self) -> &'static str {
self.name
}
#[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
}
pub fn source_predicate(
&self,
schema: &Schema,
) -> Result<Option<SourceCheckExpr>, SchemaContractError> {
match (self.predicate, self.predicate_expression) {
(None, None) => Ok(None),
(Some(_), Some(resolve)) => resolve(schema).map(Some),
(None, Some(_)) | (Some(_), None) => Err(SchemaContractError::InvalidExpression),
}
}
#[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 {
fn validate(&self) -> Result<(), ErrorTree> {
let mut errs = ErrorTree::new();
validate_source_name(
&mut errs,
"index",
self.name(),
icydb_schema::IndexSourceKey::try_new,
);
errs.result()
}
}
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(
"idx_user__email",
&["email"],
false,
Some("active = true"),
None,
);
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("uidx_user__email", &["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(
"idx_user__tenant_id__lower_email",
&["tenant_id"],
&KEY_ITEMS,
false,
);
assert!(index.has_expression_key_items());
assert_eq!(index.to_string(), "(tenant_id, LOWER(email))");
std::assert_matches!(
index.key_items(),
IndexKeyItemsRef::Items(items)
if items == KEY_ITEMS.as_slice()
);
}
}