Skip to main content

hayson_engine/catalog/
indexes.rs

1use std::collections::BTreeMap;
2
3use serde_json::json;
4
5use crate::schema::IndexKind;
6
7use super::types::{CatalogFieldSpec, CatalogIndexKind, CatalogPredicate, IndexEntry, TableEntry};
8
9#[derive(Debug, Clone, PartialEq)]
10pub struct ExpandedIndex {
11    pub name: String,
12    pub fields: Vec<String>,
13    pub kind: CatalogIndexKind,
14    pub unique: bool,
15    pub where_: Option<Vec<CatalogPredicate>>,
16    pub expire_after_seconds: Option<u64>,
17}
18
19pub fn sugar_index_name(table: &str, field: &str, kind: &str) -> String {
20    format!("{table}_{field}_{kind}")
21}
22
23fn catalog_kind_of(kind: IndexKind) -> CatalogIndexKind {
24    match kind {
25        IndexKind::Exact => CatalogIndexKind::Exact,
26        IndexKind::Sorted => CatalogIndexKind::Sorted,
27    }
28}
29
30pub fn sugar_indexes_of(table: &str, fields: &BTreeMap<String, CatalogFieldSpec>) -> Vec<ExpandedIndex> {
31    let mut sugar = Vec::new();
32    for (field, entry) in fields {
33        if let Some(kind) = entry.spec.index {
34            let name_part = match kind {
35                IndexKind::Exact => "exact",
36                IndexKind::Sorted => "sorted",
37            };
38            sugar.push(ExpandedIndex {
39                name: sugar_index_name(table, field, name_part),
40                fields: vec![field.clone()],
41                kind: catalog_kind_of(kind),
42                unique: entry.spec.unique,
43                where_: None,
44                expire_after_seconds: None,
45            });
46        }
47        if entry.spec.fulltext {
48            sugar.push(ExpandedIndex {
49                name: sugar_index_name(table, field, "fulltext"),
50                fields: vec![field.clone()],
51                kind: CatalogIndexKind::Fulltext,
52                unique: false,
53                where_: None,
54                expire_after_seconds: None,
55            });
56        }
57    }
58    sugar
59}
60
61fn from_declared(declared: &IndexEntry) -> ExpandedIndex {
62    ExpandedIndex {
63        name: declared.name.clone(),
64        fields: declared.fields.clone(),
65        kind: declared.kind,
66        unique: declared.unique,
67        where_: declared.where_.clone(),
68        expire_after_seconds: declared.expire_after_seconds,
69    }
70}
71
72pub fn index_identity(index: &ExpandedIndex) -> String {
73    let mut clauses: Vec<(String, String, serde_json::Value)> = index
74        .where_
75        .as_ref()
76        .map(|clauses| {
77            clauses
78                .iter()
79                .map(|clause| (clause.field.clone(), clause.op.clone(), clause.value.clone()))
80                .collect()
81        })
82        .unwrap_or_default();
83    clauses.sort_by(|a, b| {
84        let left = serde_json::to_string(a).unwrap_or_default();
85        let right = serde_json::to_string(b).unwrap_or_default();
86        left.cmp(&right)
87    });
88    json!([
89        index.kind.as_str(),
90        index.fields,
91        index.unique,
92        clauses,
93        index.expire_after_seconds,
94    ])
95    .to_string()
96}
97
98/// Merges declared `indexes[]` with the sugar the field modifiers (`index`, `fulltext`) expand to,
99/// deduplicated by `index_identity` — an explicit declaration wins over the name sugar generates.
100pub fn expand_indexes(table: &str, entry: &TableEntry) -> Vec<ExpandedIndex> {
101    let mut by_identity: BTreeMap<String, ExpandedIndex> = BTreeMap::new();
102    for declared in entry.indexes.iter().flatten() {
103        let expanded = from_declared(declared);
104        let identity = index_identity(&expanded);
105        by_identity.entry(identity).or_insert(expanded);
106    }
107    for expanded in sugar_indexes_of(table, &entry.fields) {
108        let identity = index_identity(&expanded);
109        by_identity.entry(identity).or_insert(expanded);
110    }
111    // NOTE: BTreeMap already walks in key (identity) order — same order TS gets from sorting by it.
112    by_identity.into_values().collect()
113}