llkv_table/
metadata.rs

1//! Shared metadata manager that consolidates catalog I/O for tables, columns, and constraints.
2//!
3//! This module offers a single entry point for querying and mutating persisted metadata. It keeps
4//! an in-memory snapshot per table, performs diff-aware persistence, and always uses batch catalog
5//! operations to minimise I/O.
6
7#![forbid(unsafe_code)]
8
9use crate::catalog::TableCatalog;
10use crate::constraints::{
11    ConstraintId, ConstraintKind, ConstraintRecord, ConstraintState, ForeignKeyAction,
12    ForeignKeyConstraint, PrimaryKeyConstraint, UniqueConstraint,
13};
14use crate::constraints::{ForeignKeyTableInfo, ValidatedForeignKey, validate_foreign_keys};
15use crate::reserved;
16use crate::resolvers::resolve_table_name;
17use crate::sys_catalog::{ConstraintNameRecord, SysCatalog};
18use crate::sys_catalog::{MultiColumnIndexEntryMeta, SingleColumnIndexEntryMeta, TriggerEntryMeta};
19use crate::table::Table;
20use crate::types::{FieldId, TableColumn, TableId};
21use crate::view::{ForeignKeyView, TableView};
22use crate::{ColMeta, TableMeta, TableMultiColumnIndexMeta};
23use arrow::datatypes::DataType;
24use llkv_column_map::ColumnStore;
25use llkv_column_map::store::IndexKind;
26use llkv_column_map::types::LogicalFieldId;
27use llkv_plan::ForeignKeySpec;
28use llkv_result::{Error, Result as LlkvResult};
29use llkv_storage::pager::Pager;
30use rustc_hash::{FxHashMap, FxHashSet};
31use simd_r_drive_entry_handle::EntryHandle;
32use std::sync::{Arc, RwLock};
33
34#[derive(Clone, Debug, Default, PartialEq, Eq)]
35pub struct SingleColumnIndexEntry {
36    pub index_name: String,
37    pub canonical_name: String,
38    pub column_id: FieldId,
39    pub column_name: String,
40    pub unique: bool,
41    pub ascending: bool,
42    pub nulls_first: bool,
43}
44
45#[derive(Clone, Debug, Default)]
46struct TableSnapshot {
47    table_meta: Option<TableMeta>,
48    column_metas: FxHashMap<FieldId, ColMeta>,
49    constraints: FxHashMap<ConstraintId, ConstraintRecord>,
50    constraint_names: FxHashMap<ConstraintId, String>,
51    single_indexes: FxHashMap<String, SingleColumnIndexEntry>,
52    multi_column_indexes: FxHashMap<String, MultiColumnIndexEntryMeta>,
53    sort_indexes: FxHashSet<FieldId>,
54    triggers: FxHashMap<String, TriggerEntryMeta>,
55}
56
57#[derive(Clone, Debug)]
58struct TableState {
59    current: TableSnapshot,
60    persisted: TableSnapshot,
61}
62
63impl TableState {
64    fn from_snapshot(snapshot: TableSnapshot) -> Self {
65        Self {
66            current: snapshot.clone(),
67            persisted: snapshot,
68        }
69    }
70}
71
72#[derive(Default)]
73struct ReferencingIndex {
74    parent_to_children: FxHashMap<TableId, FxHashSet<(TableId, ConstraintId)>>,
75    child_to_parents: FxHashMap<TableId, FxHashSet<TableId>>,
76    initialized: bool,
77}
78
79impl ReferencingIndex {
80    fn remove_child(&mut self, child_id: TableId) {
81        if let Some(parents) = self.child_to_parents.remove(&child_id) {
82            for parent_id in parents {
83                if let Some(children) = self.parent_to_children.get_mut(&parent_id) {
84                    children.retain(|(entry_child, _)| *entry_child != child_id);
85                    if children.is_empty() {
86                        self.parent_to_children.remove(&parent_id);
87                    }
88                }
89            }
90        }
91    }
92
93    fn insert(&mut self, parent_id: TableId, child_id: TableId, constraint_id: ConstraintId) {
94        self.parent_to_children
95            .entry(parent_id)
96            .or_default()
97            .insert((child_id, constraint_id));
98        self.child_to_parents
99            .entry(child_id)
100            .or_default()
101            .insert(parent_id);
102        self.initialized = true;
103    }
104
105    fn children(&self, parent_id: TableId) -> Vec<(TableId, ConstraintId)> {
106        self.parent_to_children
107            .get(&parent_id)
108            .map(|set| set.iter().cloned().collect())
109            .unwrap_or_default()
110    }
111
112    fn mark_initialized(&mut self) {
113        self.initialized = true;
114    }
115
116    fn is_initialized(&self) -> bool {
117        self.initialized
118    }
119}
120
121/// Central metadata facade that hides the raw catalog implementation details.
122pub struct MetadataManager<P>
123where
124    P: Pager<Blob = EntryHandle> + Send + Sync,
125{
126    store: Arc<ColumnStore<P>>,
127    tables: RwLock<FxHashMap<TableId, TableState>>,
128    referencing_index: RwLock<ReferencingIndex>,
129}
130
131impl<P> MetadataManager<P>
132where
133    P: Pager<Blob = EntryHandle> + Send + Sync,
134{
135    /// Create a new metadata manager backed by the provided column store.
136    pub fn new(store: Arc<ColumnStore<P>>) -> Self {
137        Self {
138            store,
139            tables: RwLock::new(FxHashMap::default()),
140            referencing_index: RwLock::new(ReferencingIndex::default()),
141        }
142    }
143
144    /// Load metadata for a table from the catalog if not already cached.
145    fn ensure_table_state(&self, table_id: TableId) -> LlkvResult<()> {
146        if self.tables.read().unwrap().contains_key(&table_id) {
147            return Ok(());
148        }
149        let state = self.load_table_state(table_id)?;
150        {
151            let mut tables = self.tables.write().unwrap();
152            tables.entry(table_id).or_insert(state);
153        }
154        self.refresh_referencing_index_for_table(table_id);
155        Ok(())
156    }
157
158    fn load_table_state(&self, table_id: TableId) -> LlkvResult<TableState> {
159        let catalog = SysCatalog::new(&self.store);
160        let table_meta = catalog.get_table_meta(table_id);
161        let constraint_records = catalog.constraint_records_for_table(table_id)?;
162        let constraint_ids: Vec<ConstraintId> = constraint_records
163            .iter()
164            .map(|record| record.constraint_id)
165            .collect();
166        let constraint_name_entries = if constraint_ids.is_empty() {
167            Vec::new()
168        } else {
169            catalog.get_constraint_names(table_id, &constraint_ids)?
170        };
171        let multi_uniques = catalog.get_multi_column_indexes(table_id)?;
172        let single_index_metas = catalog.get_single_column_indexes(table_id)?;
173        let multi_column_index_metas = catalog.get_multi_column_indexes(table_id)?;
174        let trigger_metas = catalog.get_triggers(table_id)?;
175        let mut constraints = FxHashMap::default();
176        let mut constraint_names = FxHashMap::default();
177        let mut single_indexes = FxHashMap::default();
178        let mut multi_column_indexes = FxHashMap::default();
179        let mut sort_indexes = FxHashSet::default();
180        for meta in single_index_metas {
181            sort_indexes.insert(meta.column_id);
182            single_indexes.insert(
183                meta.canonical_name.clone(),
184                SingleColumnIndexEntry {
185                    index_name: meta.index_name,
186                    canonical_name: meta.canonical_name,
187                    column_id: meta.column_id,
188                    column_name: meta.column_name,
189                    unique: meta.unique,
190                    ascending: meta.ascending,
191                    nulls_first: meta.nulls_first,
192                },
193            );
194        }
195        for meta in multi_uniques {
196            multi_column_indexes.insert(meta.canonical_name.clone(), meta);
197        }
198        for meta in multi_column_index_metas {
199            multi_column_indexes.insert(meta.canonical_name.clone(), meta);
200        }
201        let mut triggers = FxHashMap::default();
202        for meta in trigger_metas {
203            triggers.insert(meta.canonical_name.clone(), meta);
204        }
205        for (record, name) in constraint_records
206            .into_iter()
207            .zip(constraint_name_entries.into_iter())
208        {
209            if let Some(name) = name {
210                constraint_names.insert(record.constraint_id, name);
211            }
212            constraints.insert(record.constraint_id, record);
213        }
214        let snapshot = TableSnapshot {
215            table_meta,
216            column_metas: FxHashMap::default(),
217            constraints,
218            constraint_names,
219            single_indexes,
220            multi_column_indexes,
221            sort_indexes,
222            triggers,
223        };
224        Ok(TableState::from_snapshot(snapshot))
225    }
226
227    fn refresh_referencing_index_for_table(&self, table_id: TableId) {
228        let foreign_keys: Vec<(TableId, ConstraintId)> = {
229            let tables = self.tables.read().unwrap();
230            match tables.get(&table_id) {
231                Some(state) => state
232                    .current
233                    .constraints
234                    .iter()
235                    .filter(|(_, record)| record.is_active())
236                    .filter_map(|(constraint_id, record)| {
237                        if let ConstraintKind::ForeignKey(fk) = &record.kind {
238                            Some((fk.referenced_table, *constraint_id))
239                        } else {
240                            None
241                        }
242                    })
243                    .collect(),
244                None => Vec::new(),
245            }
246        };
247
248        let mut index = self.referencing_index.write().unwrap();
249        index.remove_child(table_id);
250        for (parent_table, constraint_id) in foreign_keys {
251            index.insert(parent_table, table_id, constraint_id);
252        }
253    }
254
255    fn constraint_name_for(
256        &self,
257        table_id: TableId,
258        constraint_id: ConstraintId,
259    ) -> LlkvResult<Option<String>> {
260        self.ensure_table_state(table_id)?;
261        let tables = self.tables.read().unwrap();
262        let state = tables.get(&table_id).unwrap();
263        Ok(state.current.constraint_names.get(&constraint_id).cloned())
264    }
265
266    fn ensure_referencing_index_initialized(&self) -> LlkvResult<()> {
267        let needs_init = {
268            let index = self.referencing_index.read().unwrap();
269            !index.is_initialized()
270        };
271
272        if !needs_init {
273            return Ok(());
274        }
275
276        let metas = self.all_table_metas()?;
277        for (table_id, _) in metas {
278            self.ensure_table_state(table_id)?;
279            self.refresh_referencing_index_for_table(table_id);
280        }
281
282        let mut index = self.referencing_index.write().unwrap();
283        index.mark_initialized();
284        Ok(())
285    }
286
287    /// Retrieve the current table metadata snapshot (loaded lazily if required).
288    pub fn table_meta(&self, table_id: TableId) -> LlkvResult<Option<TableMeta>> {
289        self.ensure_table_state(table_id)?;
290        let tables = self.tables.read().unwrap();
291        Ok(tables
292            .get(&table_id)
293            .and_then(|state| state.current.table_meta.clone()))
294    }
295
296    /// Return the list of child table + constraint identifiers that reference the provided table.
297    pub fn foreign_keys_referencing(
298        &self,
299        referenced_table: TableId,
300    ) -> LlkvResult<Vec<(TableId, ConstraintId)>> {
301        self.ensure_referencing_index_initialized()?;
302        let index = self.referencing_index.read().unwrap();
303        Ok(index.children(referenced_table))
304    }
305
306    /// Update the in-memory table metadata. Changes are flushed on demand.
307    pub fn set_table_meta(&self, table_id: TableId, meta: TableMeta) -> LlkvResult<()> {
308        self.ensure_table_state(table_id)?;
309        let mut tables = self.tables.write().unwrap();
310        let state = tables.get_mut(&table_id).unwrap();
311        state.current.table_meta = Some(meta);
312        Ok(())
313    }
314
315    /// Fetch column metadata for the requested field identifiers, loading missing entries lazily.
316    pub fn column_metas(
317        &self,
318        table_id: TableId,
319        field_ids: &[FieldId],
320    ) -> LlkvResult<Vec<Option<ColMeta>>> {
321        self.ensure_table_state(table_id)?;
322
323        // Determine which columns still need to be loaded from the catalog.
324        let missing_ids = {
325            let tables = self.tables.read().unwrap();
326            let state = tables.get(&table_id).unwrap();
327            field_ids
328                .iter()
329                .copied()
330                .filter(|field_id| !state.current.column_metas.contains_key(field_id))
331                .collect::<Vec<_>>()
332        };
333
334        if !missing_ids.is_empty() {
335            let catalog = SysCatalog::new(&self.store);
336            let fetched = catalog.get_cols_meta(table_id, &missing_ids);
337            let mut tables = self.tables.write().unwrap();
338            let state = tables.get_mut(&table_id).unwrap();
339            for (idx, field_id) in missing_ids.iter().enumerate() {
340                if let Some(meta) = fetched[idx].clone() {
341                    state.current.column_metas.insert(*field_id, meta.clone());
342                    state
343                        .persisted
344                        .column_metas
345                        .entry(*field_id)
346                        .or_insert(meta);
347                }
348            }
349        }
350
351        let tables = self.tables.read().unwrap();
352        let state = tables.get(&table_id).unwrap();
353        Ok(field_ids
354            .iter()
355            .map(|field_id| state.current.column_metas.get(field_id).cloned())
356            .collect())
357    }
358
359    /// Upsert a single column metadata record in the in-memory snapshot.
360    pub fn set_column_meta(&self, table_id: TableId, meta: ColMeta) -> LlkvResult<()> {
361        self.ensure_table_state(table_id)?;
362        let mut tables = self.tables.write().unwrap();
363        let state = tables.get_mut(&table_id).unwrap();
364        state.current.column_metas.insert(meta.col_id, meta);
365        Ok(())
366    }
367
368    /// Return the multi-column UNIQUE definitions cached for the table.
369    pub fn multi_column_uniques(
370        &self,
371        table_id: TableId,
372    ) -> LlkvResult<Vec<MultiColumnIndexEntryMeta>> {
373        self.ensure_table_state(table_id)?;
374        let tables = self.tables.read().unwrap();
375        let state = tables.get(&table_id).unwrap();
376        Ok(state
377            .current
378            .multi_column_indexes
379            .values()
380            .filter(|entry| entry.unique)
381            .cloned()
382            .collect())
383    }
384
385    /// Replace the cached multi-column UNIQUE definitions for the table.
386    pub fn set_multi_column_uniques(
387        &self,
388        table_id: TableId,
389        uniques: Vec<MultiColumnIndexEntryMeta>,
390    ) -> LlkvResult<()> {
391        self.ensure_table_state(table_id)?;
392        let mut tables = self.tables.write().unwrap();
393        let state = tables.get_mut(&table_id).unwrap();
394        // Remove all existing unique multi-column indexes
395        state
396            .current
397            .multi_column_indexes
398            .retain(|_, entry| !entry.unique);
399        // Add the new unique indexes
400        for unique in uniques {
401            state
402                .current
403                .multi_column_indexes
404                .insert(unique.canonical_name.clone(), unique);
405        }
406        Ok(())
407    }
408
409    /// Return the named single-column indexes registered for a table.
410    pub fn single_column_indexes(
411        &self,
412        table_id: TableId,
413    ) -> LlkvResult<Vec<SingleColumnIndexEntry>> {
414        self.ensure_table_state(table_id)?;
415        let tables = self.tables.read().unwrap();
416        let state = tables.get(&table_id).unwrap();
417        Ok(state.current.single_indexes.values().cloned().collect())
418    }
419
420    /// Lookup a single-column index by canonical name.
421    pub fn single_column_index(
422        &self,
423        table_id: TableId,
424        canonical_index_name: &str,
425    ) -> LlkvResult<Option<SingleColumnIndexEntry>> {
426        self.ensure_table_state(table_id)?;
427        let tables = self.tables.read().unwrap();
428        let state = tables.get(&table_id).unwrap();
429        Ok(state
430            .current
431            .single_indexes
432            .get(canonical_index_name)
433            .cloned())
434    }
435
436    /// Register or replace a single-column index metadata entry in the cached snapshot.
437    pub fn put_single_column_index(
438        &self,
439        table_id: TableId,
440        entry: SingleColumnIndexEntry,
441    ) -> LlkvResult<()> {
442        self.ensure_table_state(table_id)?;
443        let mut tables = self.tables.write().unwrap();
444        let state = tables.get_mut(&table_id).unwrap();
445        let column_id = entry.column_id;
446        state
447            .current
448            .single_indexes
449            .insert(entry.canonical_name.clone(), entry);
450        state.current.sort_indexes.insert(column_id);
451        Ok(())
452    }
453
454    /// Remove a single-column index metadata entry from the cached snapshot.
455    pub fn remove_single_column_index(
456        &self,
457        table_id: TableId,
458        canonical_index_name: &str,
459    ) -> LlkvResult<Option<SingleColumnIndexEntry>> {
460        self.ensure_table_state(table_id)?;
461        let mut tables = self.tables.write().unwrap();
462        let state = tables.get_mut(&table_id).unwrap();
463        let removed = state.current.single_indexes.remove(canonical_index_name);
464        if let Some(ref entry) = removed {
465            let still_indexed = state
466                .current
467                .single_indexes
468                .values()
469                .any(|existing| existing.column_id == entry.column_id);
470            if !still_indexed {
471                state.current.sort_indexes.remove(&entry.column_id);
472            }
473        }
474        Ok(removed)
475    }
476
477    /// Register or replace a multi-column index metadata entry in the cached snapshot.
478    pub fn put_multi_column_index(
479        &self,
480        table_id: TableId,
481        entry: MultiColumnIndexEntryMeta,
482    ) -> LlkvResult<()> {
483        self.ensure_table_state(table_id)?;
484        let mut tables = self.tables.write().unwrap();
485        let state = tables.get_mut(&table_id).unwrap();
486        state
487            .current
488            .multi_column_indexes
489            .insert(entry.canonical_name.clone(), entry);
490        Ok(())
491    }
492
493    /// Remove a multi-column index metadata entry from the cached snapshot.
494    pub fn remove_multi_column_index(
495        &self,
496        table_id: TableId,
497        canonical_index_name: &str,
498    ) -> LlkvResult<Option<MultiColumnIndexEntryMeta>> {
499        self.ensure_table_state(table_id)?;
500        let mut tables = self.tables.write().unwrap();
501        let state = tables.get_mut(&table_id).unwrap();
502        Ok(state
503            .current
504            .multi_column_indexes
505            .remove(canonical_index_name))
506    }
507
508    /// Retrieve a multi-column index metadata entry by canonical name.
509    pub fn get_multi_column_index(
510        &self,
511        table_id: TableId,
512        canonical_index_name: &str,
513    ) -> LlkvResult<Option<MultiColumnIndexEntryMeta>> {
514        self.ensure_table_state(table_id)?;
515        let tables = self.tables.read().unwrap();
516        Ok(tables.get(&table_id).and_then(|state| {
517            state
518                .current
519                .multi_column_indexes
520                .get(canonical_index_name)
521                .cloned()
522        }))
523    }
524
525    /// Register a sort index for a column at the metadata level, staging the change for the next flush.
526    pub fn register_sort_index(&self, table_id: TableId, field_id: FieldId) -> LlkvResult<()> {
527        self.ensure_table_state(table_id)?;
528
529        {
530            let mut tables = self.tables.write().unwrap();
531            let state = tables.get_mut(&table_id).unwrap();
532            if state.persisted.sort_indexes.contains(&field_id)
533                || state.current.sort_indexes.contains(&field_id)
534            {
535                state.current.sort_indexes.insert(field_id);
536                return Ok(());
537            }
538        }
539
540        if self.field_has_sort_index(table_id, field_id)? {
541            let mut tables = self.tables.write().unwrap();
542            let state = tables.get_mut(&table_id).unwrap();
543            state.persisted.sort_indexes.insert(field_id);
544            state.current.sort_indexes.insert(field_id);
545            return Ok(());
546        }
547
548        let mut tables = self.tables.write().unwrap();
549        let state = tables.get_mut(&table_id).unwrap();
550        state.current.sort_indexes.insert(field_id);
551        Ok(())
552    }
553
554    /// Unregister a sort index for a column, staging removal for the next flush.
555    pub fn unregister_sort_index(&self, table_id: TableId, field_id: FieldId) -> LlkvResult<()> {
556        self.ensure_table_state(table_id)?;
557
558        let mut tables = self.tables.write().unwrap();
559        let state = tables.get_mut(&table_id).unwrap();
560        state.current.sort_indexes.remove(&field_id);
561
562        if !state.persisted.sort_indexes.contains(&field_id) {
563            drop(tables);
564            if self.field_has_sort_index(table_id, field_id)? {
565                let mut tables = self.tables.write().unwrap();
566                let state = tables.get_mut(&table_id).unwrap();
567                state.persisted.sort_indexes.insert(field_id);
568            }
569        }
570
571        Ok(())
572    }
573
574    /// Mutate the cached multi-column UNIQUE definitions for a table in-place.
575    pub fn update_multi_column_uniques<F, T>(&self, table_id: TableId, f: F) -> LlkvResult<T>
576    where
577        F: FnOnce(&mut Vec<MultiColumnIndexEntryMeta>) -> T,
578    {
579        self.ensure_table_state(table_id)?;
580        let mut tables = self.tables.write().unwrap();
581        let state = tables.get_mut(&table_id).unwrap();
582        let mut uniques: Vec<MultiColumnIndexEntryMeta> = state
583            .current
584            .multi_column_indexes
585            .values()
586            .filter(|entry| entry.unique)
587            .cloned()
588            .collect();
589        let result = f(&mut uniques);
590        // Remove all existing unique multi-column indexes
591        state
592            .current
593            .multi_column_indexes
594            .retain(|_, entry| !entry.unique);
595        // Add back the modified unique indexes
596        for unique in uniques {
597            state
598                .current
599                .multi_column_indexes
600                .insert(unique.canonical_name.clone(), unique);
601        }
602        Ok(result)
603    }
604
605    /// Return the trigger definitions cached for the table.
606    pub fn triggers(&self, table_id: TableId) -> LlkvResult<Vec<TriggerEntryMeta>> {
607        self.ensure_table_state(table_id)?;
608        let tables = self.tables.read().unwrap();
609        let state = tables.get(&table_id).unwrap();
610        Ok(state.current.triggers.values().cloned().collect())
611    }
612
613    /// Fetch a trigger definition by its canonical name.
614    pub fn trigger(
615        &self,
616        table_id: TableId,
617        canonical_name: &str,
618    ) -> LlkvResult<Option<TriggerEntryMeta>> {
619        self.ensure_table_state(table_id)?;
620        let tables = self.tables.read().unwrap();
621        let state = tables.get(&table_id).unwrap();
622        Ok(state
623            .current
624            .triggers
625            .get(&canonical_name.to_ascii_lowercase())
626            .cloned())
627    }
628
629    /// Insert or replace a trigger definition in the cached snapshot.
630    pub fn insert_trigger(&self, table_id: TableId, trigger: TriggerEntryMeta) -> LlkvResult<()> {
631        self.ensure_table_state(table_id)?;
632        let mut tables = self.tables.write().unwrap();
633        let state = tables.get_mut(&table_id).unwrap();
634        state
635            .current
636            .triggers
637            .insert(trigger.canonical_name.clone(), trigger);
638        Ok(())
639    }
640
641    /// Remove a trigger definition by canonical name. Returns true when the trigger existed.
642    pub fn remove_trigger(&self, table_id: TableId, canonical_name: &str) -> LlkvResult<bool> {
643        self.ensure_table_state(table_id)?;
644        let mut tables = self.tables.write().unwrap();
645        let state = tables.get_mut(&table_id).unwrap();
646        Ok(state
647            .current
648            .triggers
649            .remove(&canonical_name.to_ascii_lowercase())
650            .is_some())
651    }
652
653    /// Prepare the metadata state for dropping a table by clearing cached entries.
654    ///
655    /// Column metadata is loaded eagerly for the provided field identifiers so deletions
656    /// are persisted on the next flush.
657    pub fn prepare_table_drop(&self, table_id: TableId, column_ids: &[FieldId]) -> LlkvResult<()> {
658        if !column_ids.is_empty() {
659            let _ = self.column_metas(table_id, column_ids)?;
660        } else {
661            self.ensure_table_state(table_id)?;
662        }
663
664        let mut tables = self.tables.write().unwrap();
665        if let Some(state) = tables.get_mut(&table_id) {
666            state.current.table_meta = None;
667            state.current.column_metas.clear();
668            state.current.constraints.clear();
669            state.current.constraint_names.clear();
670            state.current.single_indexes.clear();
671            state.current.multi_column_indexes.clear();
672            state.current.sort_indexes.clear();
673            state.current.triggers.clear();
674        }
675        drop(tables);
676        self.refresh_referencing_index_for_table(table_id);
677        Ok(())
678    }
679
680    /// Remove any cached snapshots for the specified table.
681    pub fn remove_table_state(&self, table_id: TableId) {
682        self.tables.write().unwrap().remove(&table_id);
683        self.referencing_index
684            .write()
685            .unwrap()
686            .remove_child(table_id);
687    }
688
689    /// Return all constraint records currently cached for the table.
690    pub fn constraint_records(&self, table_id: TableId) -> LlkvResult<Vec<ConstraintRecord>> {
691        self.ensure_table_state(table_id)?;
692        let tables = self.tables.read().unwrap();
693        let state = tables.get(&table_id).unwrap();
694        Ok(state.current.constraints.values().cloned().collect())
695    }
696
697    /// Fetch a subset of constraint records by their identifiers.
698    pub fn constraint_records_by_id(
699        &self,
700        table_id: TableId,
701        constraint_ids: &[ConstraintId],
702    ) -> LlkvResult<Vec<Option<ConstraintRecord>>> {
703        self.ensure_table_state(table_id)?;
704        let tables = self.tables.read().unwrap();
705        let state = tables.get(&table_id).unwrap();
706        Ok(constraint_ids
707            .iter()
708            .map(|constraint_id| state.current.constraints.get(constraint_id).cloned())
709            .collect())
710    }
711
712    /// Upsert constraint records in the in-memory snapshot.
713    pub fn put_constraint_records(
714        &self,
715        table_id: TableId,
716        records: &[ConstraintRecord],
717    ) -> LlkvResult<()> {
718        self.ensure_table_state(table_id)?;
719        let mut tables = self.tables.write().unwrap();
720        let state = tables.get_mut(&table_id).unwrap();
721        for record in records {
722            state
723                .current
724                .constraints
725                .insert(record.constraint_id, record.clone());
726        }
727        drop(tables);
728        self.refresh_referencing_index_for_table(table_id);
729        Ok(())
730    }
731
732    /// Upsert constraint names in the in-memory snapshot.
733    pub fn put_constraint_names(
734        &self,
735        table_id: TableId,
736        names: &[(ConstraintId, Option<String>)],
737    ) -> LlkvResult<()> {
738        if names.is_empty() {
739            return Ok(());
740        }
741        self.ensure_table_state(table_id)?;
742        let mut tables = self.tables.write().unwrap();
743        if let Some(state) = tables.get_mut(&table_id) {
744            for (constraint_id, name) in names {
745                if let Some(name) = name {
746                    state
747                        .current
748                        .constraint_names
749                        .insert(*constraint_id, name.clone());
750                } else {
751                    state.current.constraint_names.remove(constraint_id);
752                }
753            }
754        }
755        Ok(())
756    }
757
758    /// Produce a map of constraint records keyed by identifier.
759    pub fn constraint_record_map(
760        &self,
761        table_id: TableId,
762    ) -> LlkvResult<FxHashMap<ConstraintId, ConstraintRecord>> {
763        self.ensure_table_state(table_id)?;
764        let tables = self.tables.read().unwrap();
765        let state = tables.get(&table_id).unwrap();
766        Ok(state.current.constraints.clone())
767    }
768
769    /// Persist changes for a single table to the underlying catalog, writing only the diffs.
770    pub fn flush_table(&self, table_id: TableId) -> LlkvResult<()> {
771        self.ensure_table_state(table_id)?;
772        let mut tables = self.tables.write().unwrap();
773        let state = tables.get_mut(&table_id).unwrap();
774
775        let catalog = SysCatalog::new(&self.store);
776
777        match (
778            state.current.table_meta.as_ref(),
779            state.persisted.table_meta.as_ref(),
780        ) {
781            (Some(meta), Some(existing)) if meta != existing => {
782                catalog.put_table_meta(meta);
783                state.persisted.table_meta = Some(meta.clone());
784            }
785            (Some(meta), None) => {
786                catalog.put_table_meta(meta);
787                state.persisted.table_meta = Some(meta.clone());
788            }
789            (None, Some(_)) => {
790                catalog.delete_table_meta(table_id)?;
791                state.persisted.table_meta = None;
792            }
793            _ => {}
794        }
795
796        let mut dirty_columns: Vec<(FieldId, ColMeta)> = Vec::new();
797        for (field_id, meta) in &state.current.column_metas {
798            match state.persisted.column_metas.get(field_id) {
799                Some(existing) if existing == meta => {}
800                _ => dirty_columns.push((*field_id, meta.clone())),
801            }
802        }
803        for (field_id, meta) in dirty_columns.iter() {
804            catalog.put_col_meta(table_id, meta);
805            state.persisted.column_metas.insert(*field_id, meta.clone());
806        }
807
808        let removed_columns: Vec<FieldId> = state
809            .persisted
810            .column_metas
811            .keys()
812            .copied()
813            .filter(|field_id| !state.current.column_metas.contains_key(field_id))
814            .collect();
815        if !removed_columns.is_empty() {
816            catalog.delete_col_meta(table_id, &removed_columns)?;
817            for field_id in removed_columns {
818                state.persisted.column_metas.remove(&field_id);
819            }
820        }
821
822        let mut dirty_constraints: Vec<ConstraintRecord> = Vec::new();
823        for (constraint_id, record) in &state.current.constraints {
824            match state.persisted.constraints.get(constraint_id) {
825                Some(existing) if existing == record => {}
826                _ => dirty_constraints.push(record.clone()),
827            }
828        }
829        if !dirty_constraints.is_empty() {
830            catalog.put_constraint_records(table_id, &dirty_constraints)?;
831            for record in dirty_constraints {
832                state
833                    .persisted
834                    .constraints
835                    .insert(record.constraint_id, record);
836            }
837        }
838
839        let removed_constraints: Vec<ConstraintId> = state
840            .persisted
841            .constraints
842            .keys()
843            .copied()
844            .filter(|constraint_id| !state.current.constraints.contains_key(constraint_id))
845            .collect();
846        if !removed_constraints.is_empty() {
847            catalog.delete_constraint_records(table_id, &removed_constraints)?;
848            for constraint_id in removed_constraints {
849                state.persisted.constraints.remove(&constraint_id);
850            }
851        }
852
853        let mut dirty_constraint_names: Vec<(ConstraintId, String)> = Vec::new();
854        for (constraint_id, name) in &state.current.constraint_names {
855            match state.persisted.constraint_names.get(constraint_id) {
856                Some(existing) if existing == name => {}
857                _ => dirty_constraint_names.push((*constraint_id, name.clone())),
858            }
859        }
860        if !dirty_constraint_names.is_empty() {
861            let records: Vec<ConstraintNameRecord> = dirty_constraint_names
862                .iter()
863                .map(|(constraint_id, name)| ConstraintNameRecord {
864                    constraint_id: *constraint_id,
865                    name: Some(name.clone()),
866                })
867                .collect();
868            catalog.put_constraint_names(table_id, &records)?;
869            for (constraint_id, name) in dirty_constraint_names {
870                state.persisted.constraint_names.insert(constraint_id, name);
871            }
872        }
873
874        let removed_constraint_names: Vec<ConstraintId> = state
875            .persisted
876            .constraint_names
877            .keys()
878            .copied()
879            .filter(|constraint_id| !state.current.constraint_names.contains_key(constraint_id))
880            .collect();
881        if !removed_constraint_names.is_empty() {
882            catalog.delete_constraint_names(table_id, &removed_constraint_names)?;
883            for constraint_id in removed_constraint_names {
884                state.persisted.constraint_names.remove(&constraint_id);
885            }
886        }
887
888        // Flush all multi-column indexes (both unique and non-unique) to the catalog
889        if state.current.multi_column_indexes != state.persisted.multi_column_indexes {
890            if state.current.multi_column_indexes.is_empty() {
891                catalog.delete_multi_column_indexes(table_id)?;
892            } else {
893                let mut entries: Vec<MultiColumnIndexEntryMeta> = state
894                    .current
895                    .multi_column_indexes
896                    .values()
897                    .cloned()
898                    .collect();
899                entries.sort_by(|a, b| a.canonical_name.cmp(&b.canonical_name));
900                catalog.put_multi_column_indexes(table_id, &entries)?;
901            }
902            state.persisted.multi_column_indexes = state.current.multi_column_indexes.clone();
903        }
904
905        if state.current.single_indexes != state.persisted.single_indexes {
906            if state.current.single_indexes.is_empty() {
907                catalog.delete_single_column_indexes(table_id)?;
908                state.persisted.single_indexes.clear();
909            } else {
910                let mut entries: Vec<SingleColumnIndexEntryMeta> = state
911                    .current
912                    .single_indexes
913                    .values()
914                    .cloned()
915                    .map(|entry| SingleColumnIndexEntryMeta {
916                        index_name: entry.index_name,
917                        canonical_name: entry.canonical_name,
918                        column_id: entry.column_id,
919                        column_name: entry.column_name,
920                        unique: entry.unique,
921                        ascending: entry.ascending,
922                        nulls_first: entry.nulls_first,
923                    })
924                    .collect();
925                entries.sort_by(|a, b| a.canonical_name.cmp(&b.canonical_name));
926                catalog.put_single_column_indexes(table_id, &entries)?;
927                state.persisted.single_indexes = state.current.single_indexes.clone();
928            }
929        }
930
931        if state.current.triggers != state.persisted.triggers {
932            if state.current.triggers.is_empty() {
933                if !state.persisted.triggers.is_empty() {
934                    catalog.delete_triggers(table_id)?;
935                }
936                state.persisted.triggers.clear();
937            } else {
938                let mut entries: Vec<TriggerEntryMeta> =
939                    state.current.triggers.values().cloned().collect();
940                entries.sort_by(|a, b| a.canonical_name.cmp(&b.canonical_name));
941                catalog.put_triggers(table_id, &entries)?;
942                state.persisted.triggers = state.current.triggers.clone();
943            }
944        }
945
946        let sort_adds: Vec<FieldId> = state
947            .current
948            .sort_indexes
949            .iter()
950            .copied()
951            .filter(|field_id| !state.persisted.sort_indexes.contains(field_id))
952            .collect();
953        let sort_removes: Vec<FieldId> = state
954            .persisted
955            .sort_indexes
956            .iter()
957            .copied()
958            .filter(|field_id| !state.current.sort_indexes.contains(field_id))
959            .collect();
960        if !sort_adds.is_empty() || !sort_removes.is_empty() {
961            let table = Table::from_id_and_store(table_id, Arc::clone(&self.store))?;
962            for field_id in &sort_adds {
963                table.register_sort_index(*field_id)?;
964                state.persisted.sort_indexes.insert(*field_id);
965            }
966            for field_id in &sort_removes {
967                table.unregister_sort_index(*field_id)?;
968                state.persisted.sort_indexes.remove(field_id);
969            }
970        }
971
972        Ok(())
973    }
974
975    /// Persist changes for all tracked tables.
976    pub fn flush_all(&self) -> LlkvResult<()> {
977        let table_ids: Vec<TableId> = {
978            let tables = self.tables.read().unwrap();
979            tables.keys().copied().collect()
980        };
981        for table_id in table_ids {
982            self.flush_table(table_id)?;
983        }
984        Ok(())
985    }
986
987    /// Return all persisted table metadata.
988    pub fn all_table_metas(&self) -> LlkvResult<Vec<(TableId, TableMeta)>> {
989        let catalog = SysCatalog::new(&self.store);
990        catalog.all_table_metas()
991    }
992
993    /// Return all persisted multi-column unique metadata.
994    pub fn all_multi_column_unique_metas(&self) -> LlkvResult<Vec<TableMultiColumnIndexMeta>> {
995        let catalog = SysCatalog::new(&self.store);
996        let all = catalog.all_multi_column_index_metas()?;
997        // Filter to unique indexes only
998        Ok(all
999            .into_iter()
1000            .map(|mut meta| {
1001                meta.indexes.retain(|idx| idx.unique);
1002                meta
1003            })
1004            .filter(|meta| !meta.indexes.is_empty())
1005            .collect())
1006    }
1007
1008    /// Assemble foreign key descriptors for the table using cached metadata.
1009    pub fn foreign_key_descriptors(
1010        &self,
1011        table_id: TableId,
1012    ) -> LlkvResult<Vec<ForeignKeyDescriptor>> {
1013        let records = self.constraint_records(table_id)?;
1014        let mut descriptors = Vec::new();
1015
1016        for record in records {
1017            if !record.is_active() {
1018                continue;
1019            }
1020
1021            let ConstraintKind::ForeignKey(fk) = record.kind else {
1022                continue;
1023            };
1024
1025            descriptors.push(ForeignKeyDescriptor {
1026                constraint_id: record.constraint_id,
1027                referencing_table_id: table_id,
1028                referencing_field_ids: fk.referencing_field_ids.clone(),
1029                referenced_table_id: fk.referenced_table,
1030                referenced_field_ids: fk.referenced_field_ids.clone(),
1031                on_delete: fk.on_delete,
1032                on_update: fk.on_update,
1033            });
1034        }
1035
1036        Ok(descriptors)
1037    }
1038
1039    /// Resolve foreign key descriptors into names suitable for runtime consumers.
1040    pub fn foreign_key_views(
1041        &self,
1042        catalog: &TableCatalog,
1043        table_id: TableId,
1044    ) -> LlkvResult<Vec<ForeignKeyView>> {
1045        let descriptors = self.foreign_key_descriptors(table_id)?;
1046
1047        if descriptors.is_empty() {
1048            return Ok(Vec::new());
1049        }
1050
1051        let (referencing_display, referencing_canonical) =
1052            resolve_table_name(catalog, self, table_id)?;
1053
1054        let mut details = Vec::with_capacity(descriptors.len());
1055        for descriptor in descriptors {
1056            let referenced_table_id = descriptor.referenced_table_id;
1057            let (referenced_display, referenced_canonical) =
1058                resolve_table_name(catalog, self, referenced_table_id)?;
1059
1060            let referencing_column_names =
1061                self.column_names(table_id, &descriptor.referencing_field_ids)?;
1062            let referenced_column_names =
1063                self.column_names(referenced_table_id, &descriptor.referenced_field_ids)?;
1064            let constraint_name = self.constraint_name_for(table_id, descriptor.constraint_id)?;
1065
1066            details.push(ForeignKeyView {
1067                constraint_id: descriptor.constraint_id,
1068                constraint_name,
1069                referencing_table_id: descriptor.referencing_table_id,
1070                referencing_table_display: referencing_display.clone(),
1071                referencing_table_canonical: referencing_canonical.clone(),
1072                referencing_field_ids: descriptor.referencing_field_ids.clone(),
1073                referencing_column_names,
1074                referenced_table_id,
1075                referenced_table_display: referenced_display.clone(),
1076                referenced_table_canonical: referenced_canonical.clone(),
1077                referenced_field_ids: descriptor.referenced_field_ids.clone(),
1078                referenced_column_names,
1079                on_delete: descriptor.on_delete,
1080                on_update: descriptor.on_update,
1081            });
1082        }
1083
1084        Ok(details)
1085    }
1086
1087    /// Assemble a consolidated read-only view of table metadata.
1088    pub fn table_view(
1089        &self,
1090        catalog: &TableCatalog,
1091        table_id: TableId,
1092        field_ids: &[FieldId],
1093    ) -> LlkvResult<TableView> {
1094        let table_meta = self.table_meta(table_id)?;
1095        let column_metas = self.column_metas(table_id, field_ids)?;
1096        let constraint_records = self.constraint_records(table_id)?;
1097        let multi_column_uniques = self.multi_column_uniques(table_id)?;
1098        let foreign_keys = self.foreign_key_views(catalog, table_id)?;
1099
1100        Ok(TableView {
1101            table_meta,
1102            column_metas,
1103            constraint_records,
1104            multi_column_uniques,
1105            foreign_keys,
1106        })
1107    }
1108
1109    /// Validate foreign key specifications and persist them for the referencing table.
1110    pub fn validate_and_register_foreign_keys<F>(
1111        &self,
1112        referencing_table: &ForeignKeyTableInfo,
1113        specs: &[ForeignKeySpec],
1114        lookup_table: F,
1115        timestamp_micros: u64,
1116    ) -> LlkvResult<Vec<ValidatedForeignKey>>
1117    where
1118        F: FnMut(&str) -> LlkvResult<ForeignKeyTableInfo>,
1119    {
1120        let validated = validate_foreign_keys(referencing_table, specs, lookup_table)?;
1121        self.register_foreign_keys(referencing_table.table_id, &validated, timestamp_micros)?;
1122        Ok(validated)
1123    }
1124
1125    /// Register validated foreign key definitions for a table.
1126    pub fn register_foreign_keys(
1127        &self,
1128        table_id: TableId,
1129        foreign_keys: &[ValidatedForeignKey],
1130        timestamp_micros: u64,
1131    ) -> LlkvResult<()> {
1132        if foreign_keys.is_empty() {
1133            return Ok(());
1134        }
1135
1136        let existing_constraints = self.constraint_record_map(table_id)?;
1137        let mut next_constraint_id = existing_constraints
1138            .keys()
1139            .copied()
1140            .max()
1141            .unwrap_or(0)
1142            .saturating_add(1);
1143
1144        let mut constraint_records = Vec::with_capacity(foreign_keys.len());
1145        let mut constraint_names: Vec<(ConstraintId, Option<String>)> =
1146            Vec::with_capacity(foreign_keys.len());
1147
1148        for fk in foreign_keys {
1149            let constraint_id = next_constraint_id;
1150            constraint_records.push(ConstraintRecord {
1151                constraint_id,
1152                kind: ConstraintKind::ForeignKey(ForeignKeyConstraint {
1153                    referencing_field_ids: fk.referencing_field_ids.clone(),
1154                    referenced_table: fk.referenced_table_id,
1155                    referenced_field_ids: fk.referenced_field_ids.clone(),
1156                    on_delete: fk.on_delete,
1157                    on_update: fk.on_update,
1158                }),
1159                state: ConstraintState::Active,
1160                revision: 1,
1161                last_modified_micros: timestamp_micros,
1162            });
1163            constraint_names.push((constraint_id, fk.name.clone()));
1164            next_constraint_id = next_constraint_id.saturating_add(1);
1165        }
1166
1167        self.put_constraint_records(table_id, &constraint_records)?;
1168        self.put_constraint_names(table_id, &constraint_names)?;
1169        self.flush_table(table_id)?;
1170
1171        Ok(())
1172    }
1173
1174    /// Register column metadata, physical storage columns, and primary/unique constraints.
1175    pub fn apply_column_definitions(
1176        &self,
1177        table_id: TableId,
1178        columns: &[TableColumn],
1179        timestamp_micros: u64,
1180    ) -> LlkvResult<()> {
1181        if columns.is_empty() {
1182            return Ok(());
1183        }
1184
1185        self.ensure_table_state(table_id)?;
1186
1187        for column in columns {
1188            let column_meta = ColMeta {
1189                col_id: column.field_id,
1190                name: Some(column.name.clone()),
1191                flags: 0,
1192                default: None,
1193            };
1194            self.set_column_meta(table_id, column_meta)?;
1195        }
1196
1197        let table = Table::from_id_and_store(table_id, Arc::clone(&self.store))?;
1198        let store = table.store();
1199
1200        for column in columns {
1201            let logical_field_id = LogicalFieldId::for_user(table_id, column.field_id);
1202            store.ensure_column_registered(logical_field_id, &column.data_type)?;
1203            store.data_type(logical_field_id)?;
1204        }
1205
1206        let created_by_lfid = LogicalFieldId::for_mvcc_created_by(table_id);
1207        store.ensure_column_registered(created_by_lfid, &DataType::UInt64)?;
1208
1209        let deleted_by_lfid = LogicalFieldId::for_mvcc_deleted_by(table_id);
1210        store.ensure_column_registered(deleted_by_lfid, &DataType::UInt64)?;
1211
1212        let existing = self.constraint_record_map(table_id)?;
1213        let mut next_constraint_id = existing
1214            .keys()
1215            .copied()
1216            .max()
1217            .unwrap_or(0)
1218            .saturating_add(1);
1219
1220        let mut constraints = Vec::new();
1221
1222        let primary_key_fields: Vec<FieldId> = columns
1223            .iter()
1224            .filter(|col| col.primary_key)
1225            .map(|col| col.field_id)
1226            .collect();
1227        if !primary_key_fields.is_empty() {
1228            constraints.push(ConstraintRecord {
1229                constraint_id: next_constraint_id,
1230                kind: ConstraintKind::PrimaryKey(PrimaryKeyConstraint {
1231                    field_ids: primary_key_fields,
1232                }),
1233                state: ConstraintState::Active,
1234                revision: 1,
1235                last_modified_micros: timestamp_micros,
1236            });
1237            next_constraint_id = next_constraint_id.saturating_add(1);
1238        }
1239
1240        for column in columns.iter().filter(|col| col.unique && !col.primary_key) {
1241            constraints.push(ConstraintRecord {
1242                constraint_id: next_constraint_id,
1243                kind: ConstraintKind::Unique(UniqueConstraint {
1244                    field_ids: vec![column.field_id],
1245                }),
1246                state: ConstraintState::Active,
1247                revision: 1,
1248                last_modified_micros: timestamp_micros,
1249            });
1250            next_constraint_id = next_constraint_id.saturating_add(1);
1251        }
1252
1253        if !constraints.is_empty() {
1254            self.put_constraint_records(table_id, &constraints)?;
1255        }
1256
1257        Ok(())
1258    }
1259
1260    pub fn column_data_type(&self, table_id: TableId, field_id: FieldId) -> LlkvResult<DataType> {
1261        let table = Table::from_id_and_store(table_id, Arc::clone(&self.store))?;
1262        let store = table.store();
1263        let logical_field_id = LogicalFieldId::for_user(table_id, field_id);
1264        store.data_type(logical_field_id)
1265    }
1266
1267    /// Register a multi-column UNIQUE definition for a table.
1268    pub fn register_multi_column_unique(
1269        &self,
1270        table_id: TableId,
1271        column_ids: &[FieldId],
1272        index_name: Option<String>,
1273    ) -> LlkvResult<MultiColumnUniqueRegistration> {
1274        let mut created = false;
1275        let mut existing_name: Option<Option<String>> = None;
1276        let column_vec: Vec<FieldId> = column_ids.to_vec();
1277
1278        // Generate canonical name from column IDs
1279        let canonical_name = format!(
1280            "__unique_{}_{}",
1281            table_id,
1282            column_vec
1283                .iter()
1284                .map(|id| id.to_string())
1285                .collect::<Vec<_>>()
1286                .join("_")
1287        );
1288
1289        self.update_multi_column_uniques(table_id, |entries| {
1290            if let Some(existing) = entries.iter().find(|entry| entry.column_ids == column_vec) {
1291                existing_name = Some(existing.index_name.clone());
1292            } else {
1293                entries.push(MultiColumnIndexEntryMeta {
1294                    index_name: index_name.clone(),
1295                    canonical_name: canonical_name.clone(),
1296                    column_ids: column_vec.clone(),
1297                    unique: true,
1298                });
1299                created = true;
1300            }
1301        })?;
1302
1303        if created {
1304            Ok(MultiColumnUniqueRegistration::Created)
1305        } else {
1306            Ok(MultiColumnUniqueRegistration::AlreadyExists {
1307                index_name: existing_name.unwrap_or(None),
1308            })
1309        }
1310    }
1311
1312    fn column_names(&self, table_id: TableId, field_ids: &[FieldId]) -> LlkvResult<Vec<String>> {
1313        if field_ids.is_empty() {
1314            return Ok(Vec::new());
1315        }
1316
1317        let metas = self.column_metas(table_id, field_ids)?;
1318        let mut names = Vec::with_capacity(field_ids.len());
1319        for (idx, field_id) in field_ids.iter().enumerate() {
1320            let name = metas
1321                .get(idx)
1322                .and_then(|meta| meta.as_ref())
1323                .and_then(|meta| meta.name.clone())
1324                .unwrap_or_else(|| format!("col_{}", field_id));
1325            names.push(name);
1326        }
1327        Ok(names)
1328    }
1329
1330    /// Reserve and return the next available table id.
1331    pub fn reserve_table_id(&self) -> LlkvResult<TableId> {
1332        let catalog = SysCatalog::new(&self.store);
1333
1334        let mut next = match catalog.get_next_table_id()? {
1335            Some(value) => value,
1336            None => {
1337                let seed = catalog
1338                    .max_table_id()?
1339                    .unwrap_or(reserved::CATALOG_TABLE_ID);
1340                let initial = seed.checked_add(1).ok_or_else(|| {
1341                    Error::InvalidArgumentError("exhausted available table ids".into())
1342                })?;
1343                catalog.put_next_table_id(initial)?;
1344                initial
1345            }
1346        };
1347
1348        while reserved::is_reserved_table_id(next) {
1349            next = next.checked_add(1).ok_or_else(|| {
1350                Error::InvalidArgumentError("exhausted available table ids".into())
1351            })?;
1352        }
1353
1354        let mut following = next
1355            .checked_add(1)
1356            .ok_or_else(|| Error::InvalidArgumentError("exhausted available table ids".into()))?;
1357
1358        while reserved::is_reserved_table_id(following) {
1359            following = following.checked_add(1).ok_or_else(|| {
1360                Error::InvalidArgumentError("exhausted available table ids".into())
1361            })?;
1362        }
1363
1364        catalog.put_next_table_id(following)?;
1365        Ok(next)
1366    }
1367
1368    /// Ensure the catalog's next_table_id counter is at least `minimum`.
1369    ///
1370    /// This is primarily used by in-memory namespaces (e.g., temporary tables)
1371    /// that share a catalog handle with persistent storage. By seeding their
1372    /// own metadata store with a higher starting point we avoid collisions on
1373    /// table IDs already allocated by the persistent catalog.
1374    pub fn ensure_next_table_id_at_least(&self, minimum: TableId) -> LlkvResult<()> {
1375        if reserved::is_reserved_table_id(minimum) {
1376            return Err(Error::InvalidArgumentError(
1377                reserved::reserved_table_id_message(minimum),
1378            ));
1379        }
1380
1381        let catalog = SysCatalog::new(&self.store);
1382        match catalog.get_next_table_id()? {
1383            Some(current) if current >= minimum => Ok(()),
1384            _ => catalog.put_next_table_id(minimum),
1385        }
1386    }
1387
1388    /// Check if a field has a sort index in the underlying store.
1389    ///
1390    /// Note: Creates a temporary Table instance to access index metadata.
1391    /// This is acceptable since Table::from_id_and_store is lightweight (just wraps
1392    /// table_id + `Arc<ColumnStore>`) and this method is only called during index
1393    /// registration/unregistration, not in query hot paths.
1394    fn field_has_sort_index(&self, table_id: TableId, field_id: FieldId) -> LlkvResult<bool> {
1395        let table = Table::from_id_and_store(table_id, Arc::clone(&self.store))?;
1396        let indexes = table.list_registered_indexes(field_id)?;
1397        Ok(indexes.contains(&IndexKind::Sort))
1398    }
1399}
1400
1401#[cfg(test)]
1402mod tests {
1403    use super::*;
1404    use crate::constraints::{ConstraintKind, ConstraintState, PrimaryKeyConstraint};
1405    use crate::{MultiColumnIndexEntryMeta, Table};
1406    use llkv_column_map::ColumnStore;
1407    use llkv_column_map::store::IndexKind;
1408    use llkv_storage::pager::MemPager;
1409    use std::sync::Arc;
1410
1411    #[test]
1412    fn metadata_manager_persists_and_loads() {
1413        let pager = Arc::new(MemPager::default());
1414        let store = Arc::new(ColumnStore::open(Arc::clone(&pager)).unwrap());
1415        let manager = MetadataManager::new(Arc::clone(&store));
1416
1417        let table_id: TableId = 42;
1418        let table_meta = TableMeta {
1419            table_id,
1420            name: Some("users".into()),
1421            created_at_micros: 123,
1422            flags: 0,
1423            epoch: 1,
1424            view_definition: None,
1425        };
1426        manager
1427            .set_table_meta(table_id, table_meta.clone())
1428            .unwrap();
1429
1430        {
1431            let tables = manager.tables.read().unwrap();
1432            let state = tables.get(&table_id).unwrap();
1433            assert!(state.current.table_meta.is_some());
1434        }
1435
1436        let column_meta = ColMeta {
1437            col_id: 1,
1438            name: Some("id".into()),
1439            flags: 0,
1440            default: None,
1441        };
1442        manager
1443            .set_column_meta(table_id, column_meta.clone())
1444            .unwrap();
1445
1446        let logical_field_id =
1447            llkv_column_map::types::LogicalFieldId::for_user(table_id, column_meta.col_id);
1448        store
1449            .ensure_column_registered(logical_field_id, &arrow::datatypes::DataType::Utf8)
1450            .unwrap();
1451
1452        manager
1453            .register_sort_index(table_id, column_meta.col_id)
1454            .unwrap();
1455
1456        let constraint = ConstraintRecord {
1457            constraint_id: 7,
1458            kind: ConstraintKind::PrimaryKey(PrimaryKeyConstraint {
1459                field_ids: vec![column_meta.col_id],
1460            }),
1461            state: ConstraintState::Active,
1462            revision: 1,
1463            last_modified_micros: 456,
1464        };
1465        manager
1466            .put_constraint_records(table_id, std::slice::from_ref(&constraint))
1467            .unwrap();
1468
1469        let multi_unique = MultiColumnIndexEntryMeta {
1470            index_name: Some("uniq_users_name".into()),
1471            canonical_name: "uniq_users_name".to_lowercase(),
1472            column_ids: vec![column_meta.col_id],
1473            unique: true,
1474        };
1475        manager
1476            .set_multi_column_uniques(table_id, vec![multi_unique.clone()])
1477            .unwrap();
1478
1479        assert_eq!(
1480            manager.table_meta(table_id).unwrap(),
1481            Some(table_meta.clone())
1482        );
1483
1484        manager.flush_table(table_id).unwrap();
1485
1486        let table = Table::from_id_and_store(table_id, Arc::clone(&store)).unwrap();
1487        let indexes = table.list_registered_indexes(column_meta.col_id).unwrap();
1488        assert!(indexes.contains(&IndexKind::Sort));
1489
1490        let verify_catalog = SysCatalog::new(&store);
1491        let column_roundtrip = verify_catalog.get_cols_meta(table_id, &[column_meta.col_id]);
1492        assert_eq!(column_roundtrip[0].as_ref(), Some(&column_meta));
1493        let constraints = verify_catalog
1494            .constraint_records_for_table(table_id)
1495            .unwrap();
1496        assert_eq!(constraints, vec![constraint.clone()]);
1497        let unique_roundtrip = verify_catalog.get_multi_column_indexes(table_id).unwrap();
1498        assert_eq!(unique_roundtrip, vec![multi_unique.clone()]);
1499
1500        let meta_from_cache = manager.table_meta(table_id).unwrap();
1501        assert_eq!(meta_from_cache, Some(table_meta.clone()));
1502
1503        let columns_from_cache = manager
1504            .column_metas(table_id, &[column_meta.col_id])
1505            .unwrap();
1506        assert_eq!(columns_from_cache[0].as_ref(), Some(&column_meta));
1507
1508        let constraints_from_cache = manager.constraint_records(table_id).unwrap();
1509        assert_eq!(constraints_from_cache, vec![constraint.clone()]);
1510
1511        let uniques_from_cache = manager.multi_column_uniques(table_id).unwrap();
1512        assert_eq!(uniques_from_cache, vec![multi_unique]);
1513
1514        // No additional writes should occur on subsequent flushes without modifications.
1515        manager.flush_table(table_id).unwrap();
1516    }
1517
1518    #[test]
1519    fn metadata_manager_lazy_loads_columns_and_constraints() {
1520        let pager = Arc::new(MemPager::default());
1521        let store = Arc::new(ColumnStore::open(Arc::clone(&pager)).unwrap());
1522        let manager = MetadataManager::new(Arc::clone(&store));
1523
1524        let table_id: TableId = 99;
1525        let column_meta = ColMeta {
1526            col_id: 3,
1527            name: Some("value".into()),
1528            flags: 0,
1529            default: None,
1530        };
1531        let initial_catalog = SysCatalog::new(&store);
1532        initial_catalog.put_col_meta(table_id, &column_meta);
1533
1534        let constraint = ConstraintRecord {
1535            constraint_id: 15,
1536            kind: ConstraintKind::PrimaryKey(PrimaryKeyConstraint {
1537                field_ids: vec![column_meta.col_id],
1538            }),
1539            state: ConstraintState::Active,
1540            revision: 1,
1541            last_modified_micros: 0,
1542        };
1543        initial_catalog
1544            .put_constraint_records(table_id, std::slice::from_ref(&constraint))
1545            .unwrap();
1546        let multi_unique = MultiColumnIndexEntryMeta {
1547            index_name: Some("uniq_value".into()),
1548            canonical_name: "uniq_value".to_lowercase(),
1549            column_ids: vec![column_meta.col_id],
1550            unique: true,
1551        };
1552        initial_catalog
1553            .put_multi_column_indexes(table_id, std::slice::from_ref(&multi_unique))
1554            .unwrap();
1555
1556        let columns = manager
1557            .column_metas(table_id, &[column_meta.col_id])
1558            .unwrap();
1559        assert_eq!(columns[0].as_ref(), Some(&column_meta));
1560
1561        let constraints = manager.constraint_records(table_id).unwrap();
1562        assert_eq!(constraints, vec![constraint]);
1563
1564        let uniques = manager.multi_column_uniques(table_id).unwrap();
1565        assert_eq!(uniques, vec![multi_unique]);
1566    }
1567}
1568
1569/// Descriptor describing a foreign key constraint scoped to field identifiers.
1570#[derive(Clone, Debug)]
1571pub struct ForeignKeyDescriptor {
1572    pub constraint_id: ConstraintId,
1573    pub referencing_table_id: TableId,
1574    pub referencing_field_ids: Vec<FieldId>,
1575    pub referenced_table_id: TableId,
1576    pub referenced_field_ids: Vec<FieldId>,
1577    pub on_delete: ForeignKeyAction,
1578    pub on_update: ForeignKeyAction,
1579}
1580
1581/// Result of attempting to register a multi-column unique definition.
1582#[derive(Debug, Clone, PartialEq, Eq)]
1583pub enum MultiColumnUniqueRegistration {
1584    Created,
1585    AlreadyExists { index_name: Option<String> },
1586}