hayson-engine 0.10.0

Storage engine for hayson document databases — the embeddable core behind hayson-server, hayson-cli and hayson-browser
Documentation
use std::collections::BTreeMap;

use serde_json::json;

use crate::schema::IndexKind;

use super::types::{CatalogFieldSpec, CatalogIndexKind, CatalogPredicate, IndexEntry, TableEntry};

#[derive(Debug, Clone, PartialEq)]
pub struct ExpandedIndex {
    pub name: String,
    pub fields: Vec<String>,
    pub kind: CatalogIndexKind,
    pub unique: bool,
    pub where_: Option<Vec<CatalogPredicate>>,
    pub expire_after_seconds: Option<u64>,
}

pub fn sugar_index_name(table: &str, field: &str, kind: &str) -> String {
    format!("{table}_{field}_{kind}")
}

fn catalog_kind_of(kind: IndexKind) -> CatalogIndexKind {
    match kind {
        IndexKind::Exact => CatalogIndexKind::Exact,
        IndexKind::Sorted => CatalogIndexKind::Sorted,
    }
}

pub fn sugar_indexes_of(table: &str, fields: &BTreeMap<String, CatalogFieldSpec>) -> Vec<ExpandedIndex> {
    let mut sugar = Vec::new();
    for (field, entry) in fields {
        if let Some(kind) = entry.spec.index {
            let name_part = match kind {
                IndexKind::Exact => "exact",
                IndexKind::Sorted => "sorted",
            };
            sugar.push(ExpandedIndex {
                name: sugar_index_name(table, field, name_part),
                fields: vec![field.clone()],
                kind: catalog_kind_of(kind),
                unique: entry.spec.unique,
                where_: None,
                expire_after_seconds: None,
            });
        }
        if entry.spec.fulltext {
            sugar.push(ExpandedIndex {
                name: sugar_index_name(table, field, "fulltext"),
                fields: vec![field.clone()],
                kind: CatalogIndexKind::Fulltext,
                unique: false,
                where_: None,
                expire_after_seconds: None,
            });
        }
    }
    sugar
}

fn from_declared(declared: &IndexEntry) -> ExpandedIndex {
    ExpandedIndex {
        name: declared.name.clone(),
        fields: declared.fields.clone(),
        kind: declared.kind,
        unique: declared.unique,
        where_: declared.where_.clone(),
        expire_after_seconds: declared.expire_after_seconds,
    }
}

pub fn index_identity(index: &ExpandedIndex) -> String {
    let mut clauses: Vec<(String, String, serde_json::Value)> = index
        .where_
        .as_ref()
        .map(|clauses| {
            clauses
                .iter()
                .map(|clause| (clause.field.clone(), clause.op.clone(), clause.value.clone()))
                .collect()
        })
        .unwrap_or_default();
    clauses.sort_by(|a, b| {
        let left = serde_json::to_string(a).unwrap_or_default();
        let right = serde_json::to_string(b).unwrap_or_default();
        left.cmp(&right)
    });
    json!([
        index.kind.as_str(),
        index.fields,
        index.unique,
        clauses,
        index.expire_after_seconds,
    ])
    .to_string()
}

/// Merges declared `indexes[]` with the sugar the field modifiers (`index`, `fulltext`) expand to,
/// deduplicated by `index_identity` — an explicit declaration wins over the name sugar generates.
pub fn expand_indexes(table: &str, entry: &TableEntry) -> Vec<ExpandedIndex> {
    let mut by_identity: BTreeMap<String, ExpandedIndex> = BTreeMap::new();
    for declared in entry.indexes.iter().flatten() {
        let expanded = from_declared(declared);
        let identity = index_identity(&expanded);
        by_identity.entry(identity).or_insert(expanded);
    }
    for expanded in sugar_indexes_of(table, &entry.fields) {
        let identity = index_identity(&expanded);
        by_identity.entry(identity).or_insert(expanded);
    }
    // NOTE: BTreeMap already walks in key (identity) order — same order TS gets from sorting by it.
    by_identity.into_values().collect()
}