llkv_table/
sys_catalog.rs

1//! System catalog for storing table and column metadata.
2//!
3//! The system catalog uses table 0 (reserved) to store metadata about all tables
4//! and columns in the database. This metadata includes:
5//!
6//! - **Table metadata** ([`TableMeta`]): Table ID, name, creation time, flags
7//! - **Column metadata** ([`ColMeta`]): Column ID, name, flags, default values
8//! - **Multi-column index metadata** ([`TableMultiColumnIndexMeta`])
9//!
10//! # Storage Format
11//!
12//! The catalog stores metadata as serialized [`bitcode`] blobs in special catalog
13//! columns within table 0. See [`CATALOG_TABLE_ID`] and related constants in the
14//! [`reserved`](crate::reserved) module.
15//!
16//! # Usage
17//!
18//! The [`SysCatalog`] provides methods to:
19//! - Insert/update table metadata ([`put_table_meta`](SysCatalog::put_table_meta))
20//! - Query table metadata ([`get_table_meta`](SysCatalog::get_table_meta))
21//! - Manage column metadata similarly
22//!
23//! This metadata is used by higher-level components to validate schemas, assign
24//! field IDs, and enforce table constraints.
25
26use std::collections::HashMap;
27use std::mem;
28use std::sync::Arc;
29
30use arrow::array::{Array, BinaryArray, BinaryBuilder, UInt64Array};
31use arrow::datatypes::{DataType, Field, Schema};
32use arrow::record_batch::RecordBatch;
33use bitcode::{Decode, Encode};
34
35use crate::constants::CONSTRAINT_SCAN_CHUNK_SIZE;
36use crate::constraints::{
37    ConstraintId, ConstraintRecord, decode_constraint_row_id, encode_constraint_row_id,
38};
39use crate::types::{FieldId, ROW_ID_FIELD_ID, RowId, TableId};
40use llkv_column_map::store::scan::{
41    PrimitiveSortedVisitor, PrimitiveSortedWithRowIdsVisitor, PrimitiveVisitor,
42    PrimitiveWithRowIdsVisitor, ScanBuilder, ScanOptions,
43};
44
45use llkv_column_map::types::LogicalFieldId;
46use llkv_column_map::{
47    ColumnStore,
48    store::{GatherNullPolicy, ROW_ID_COLUMN_NAME, rowid_fid},
49};
50use llkv_result::{self, Result as LlkvResult};
51use llkv_storage::pager::{MemPager, Pager};
52use simd_r_drive_entry_handle::EntryHandle;
53
54// Import all reserved constants and validation functions
55use crate::reserved::*;
56
57// ----- Namespacing helpers -----
58
59// TODO: Dedupe with llkv_column_map::types::lfid()
60#[inline]
61fn lfid(table_id: TableId, col_id: u32) -> LogicalFieldId {
62    LogicalFieldId::for_user(table_id, col_id)
63}
64
65// TODO: Migrate to llkv_column_map::types::rid_table()
66#[inline]
67fn rid_table(table_id: TableId) -> u64 {
68    LogicalFieldId::for_user(table_id, ROW_ID_FIELD_ID).into()
69}
70
71// TODO: Migrate to llkv_column_map::types::rid_col()
72#[inline]
73fn rid_col(table_id: TableId, col_id: u32) -> u64 {
74    rowid_fid(lfid(table_id, col_id)).into()
75}
76
77#[inline]
78fn constraint_meta_lfid() -> LogicalFieldId {
79    lfid(CATALOG_TABLE_ID, CATALOG_FIELD_CONSTRAINT_META_ID)
80}
81
82#[inline]
83fn constraint_name_lfid() -> LogicalFieldId {
84    lfid(CATALOG_TABLE_ID, CATALOG_FIELD_CONSTRAINT_NAME_ID)
85}
86
87#[inline]
88fn constraint_row_lfid() -> LogicalFieldId {
89    rowid_fid(constraint_meta_lfid())
90}
91
92#[derive(Clone, Debug, Encode, Decode)]
93pub struct ConstraintNameRecord {
94    pub constraint_id: ConstraintId,
95    pub name: Option<String>,
96}
97
98fn decode_constraint_record(bytes: &[u8]) -> LlkvResult<ConstraintRecord> {
99    bitcode::decode(bytes).map_err(|err| {
100        llkv_result::Error::Internal(format!("failed to decode constraint metadata: {err}"))
101    })
102}
103
104struct ConstraintRowCollector<'a, P, F>
105where
106    P: Pager<Blob = EntryHandle> + Send + Sync,
107    F: FnMut(Vec<ConstraintRecord>),
108{
109    store: &'a ColumnStore<P>,
110    lfid: LogicalFieldId,
111    table_id: TableId,
112    on_batch: &'a mut F,
113    buffer: Vec<RowId>,
114    error: Option<llkv_result::Error>,
115}
116
117impl<'a, P, F> ConstraintRowCollector<'a, P, F>
118where
119    P: Pager<Blob = EntryHandle> + Send + Sync,
120    F: FnMut(Vec<ConstraintRecord>),
121{
122    fn flush_buffer(&mut self) -> LlkvResult<()> {
123        if self.buffer.is_empty() {
124            return Ok(());
125        }
126
127        let row_ids = mem::take(&mut self.buffer);
128        let batch =
129            self.store
130                .gather_rows(&[self.lfid], &row_ids, GatherNullPolicy::IncludeNulls)?;
131
132        if batch.num_columns() == 0 {
133            return Ok(());
134        }
135
136        let array = batch
137            .column(0)
138            .as_any()
139            .downcast_ref::<BinaryArray>()
140            .ok_or_else(|| {
141                llkv_result::Error::Internal(
142                    "constraint metadata column stored unexpected type".into(),
143                )
144            })?;
145
146        let mut records = Vec::with_capacity(row_ids.len());
147        for (idx, row_id) in row_ids.into_iter().enumerate() {
148            if array.is_null(idx) {
149                continue;
150            }
151
152            let record = decode_constraint_record(array.value(idx))?;
153            let (table_from_id, constraint_id) = decode_constraint_row_id(row_id);
154            if table_from_id != self.table_id {
155                continue;
156            }
157            if record.constraint_id != constraint_id {
158                return Err(llkv_result::Error::Internal(
159                    "constraint metadata id mismatch".into(),
160                ));
161            }
162            records.push(record);
163        }
164
165        if !records.is_empty() {
166            (self.on_batch)(records);
167        }
168
169        Ok(())
170    }
171
172    fn finish(&mut self) -> LlkvResult<()> {
173        if let Some(err) = self.error.take() {
174            return Err(err);
175        }
176        self.flush_buffer()
177    }
178}
179
180impl<'a, P, F> PrimitiveVisitor for ConstraintRowCollector<'a, P, F>
181where
182    P: Pager<Blob = EntryHandle> + Send + Sync,
183    F: FnMut(Vec<ConstraintRecord>),
184{
185    fn u64_chunk(&mut self, values: &UInt64Array) {
186        if self.error.is_some() {
187            return;
188        }
189
190        for idx in 0..values.len() {
191            let row_id = values.value(idx);
192            let (table_id, _) = decode_constraint_row_id(row_id);
193            if table_id != self.table_id {
194                continue;
195            }
196            self.buffer.push(row_id);
197            if self.buffer.len() >= CONSTRAINT_SCAN_CHUNK_SIZE
198                && let Err(err) = self.flush_buffer()
199            {
200                self.error = Some(err);
201                return;
202            }
203        }
204    }
205}
206
207impl<'a, P, F> PrimitiveWithRowIdsVisitor for ConstraintRowCollector<'a, P, F>
208where
209    P: Pager<Blob = EntryHandle> + Send + Sync,
210    F: FnMut(Vec<ConstraintRecord>),
211{
212}
213
214impl<'a, P, F> PrimitiveSortedVisitor for ConstraintRowCollector<'a, P, F>
215where
216    P: Pager<Blob = EntryHandle> + Send + Sync,
217    F: FnMut(Vec<ConstraintRecord>),
218{
219}
220
221impl<'a, P, F> PrimitiveSortedWithRowIdsVisitor for ConstraintRowCollector<'a, P, F>
222where
223    P: Pager<Blob = EntryHandle> + Send + Sync,
224    F: FnMut(Vec<ConstraintRecord>),
225{
226}
227
228// ----- Public catalog types -----
229
230/// Metadata about a table.
231///
232/// Stored in the system catalog (table 0) and serialized using [`bitcode`].
233#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)]
234pub struct TableMeta {
235    /// Unique identifier for this table.
236    pub table_id: TableId,
237    /// Optional human-readable name for the table.
238    pub name: Option<String>,
239    /// When the table was created (microseconds since epoch).
240    pub created_at_micros: u64,
241    /// Bitflags for table properties (e.g., temporary, system).
242    pub flags: u32,
243    /// Schema version or modification counter.
244    pub epoch: u64,
245    /// If this is a view, contains the SQL definition (SELECT statement).
246    /// If None, this is a regular table.
247    pub view_definition: Option<String>,
248}
249
250/// Metadata about a column.
251///
252/// Stored in the system catalog (table 0) and serialized using [`bitcode`].
253#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)]
254pub struct ColMeta {
255    /// Unique identifier for this column within its table.
256    pub col_id: u32,
257    /// Optional human-readable name for the column.
258    pub name: Option<String>,
259    /// Bitflags for column properties (e.g., nullable, indexed).
260    pub flags: u32,
261    /// Optional serialized default value for the column.
262    pub default: Option<Vec<u8>>,
263}
264
265/// Metadata about a schema.
266///
267/// Stored in the system catalog (table 0) and serialized using [`bitcode`].
268#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)]
269pub struct SchemaMeta {
270    /// Human-readable schema name (case-preserved).
271    pub name: String,
272    /// When the schema was created (microseconds since epoch).
273    pub created_at_micros: u64,
274    /// Bitflags for schema properties (reserved for future use).
275    pub flags: u32,
276}
277
278/// Metadata about a custom type (CREATE TYPE/DOMAIN).
279///
280/// Stored in the system catalog (table 0) and serialized using [`bitcode`].
281/// Represents type aliases like `CREATE TYPE custom_type AS integer`.
282#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)]
283pub struct CustomTypeMeta {
284    /// Human-readable type name (case-preserved, stored lowercase in catalog).
285    pub name: String,
286    /// SQL representation of the base data type (e.g., "INTEGER", "VARCHAR(100)").
287    /// Stored as string to avoid Arrow DataType serialization complexity.
288    pub base_type_sql: String,
289    /// When the type was created (microseconds since epoch).
290    pub created_at_micros: u64,
291}
292
293/// Metadata describing a single multi-column index (unique or non-unique).
294///
295/// Used to track both named CREATE INDEX statements and UNIQUE constraints over multiple columns.
296#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)]
297pub struct MultiColumnIndexEntryMeta {
298    /// Optional human-readable index name (None for unnamed UNIQUE constraints).
299    pub index_name: Option<String>,
300    /// Normalized lowercase name used as map key.
301    pub canonical_name: String,
302    /// Field IDs participating in this index.
303    pub column_ids: Vec<FieldId>,
304    /// Whether this index enforces uniqueness.
305    pub unique: bool,
306}
307
308/// Metadata describing a single named single-column index.
309#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)]
310pub struct SingleColumnIndexEntryMeta {
311    /// Human-readable index name (case preserved).
312    pub index_name: String,
313    /// Lower-cased canonical index name for case-insensitive lookups.
314    pub canonical_name: String,
315    /// Identifier of the column the index covers.
316    pub column_id: FieldId,
317    /// Human-readable column name (case preserved).
318    pub column_name: String,
319    /// Whether this index enforces uniqueness for the column.
320    pub unique: bool,
321    /// Whether the index is sorted in ascending order (true) or descending (false).
322    pub ascending: bool,
323    /// Whether NULL values appear first (true) or last (false) in the index.
324    pub nulls_first: bool,
325}
326
327/// Metadata describing all single-column indexes registered for a table.
328#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)]
329pub struct TableSingleColumnIndexMeta {
330    /// Owning table identifier.
331    pub table_id: TableId,
332    /// Definitions for each named single-column index on the table.
333    pub indexes: Vec<SingleColumnIndexEntryMeta>,
334}
335
336/// Metadata describing all multi-column indexes (unique and non-unique) for a table.
337#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)]
338pub struct TableMultiColumnIndexMeta {
339    /// Table identifier these indexes belong to.
340    pub table_id: TableId,
341    /// Definitions of each persisted multi-column index.
342    pub indexes: Vec<MultiColumnIndexEntryMeta>,
343}
344
345/// Timing information for a trigger (BEFORE, AFTER, INSTEAD OF).
346#[derive(Encode, Decode, Clone, Copy, Debug, PartialEq, Eq)]
347pub enum TriggerTimingMeta {
348    Before,
349    After,
350    InsteadOf,
351}
352
353/// Trigger event metadata describing which operation fires the trigger.
354#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)]
355pub enum TriggerEventMeta {
356    Insert,
357    Update { columns: Vec<String> },
358    Delete,
359}
360
361/// Persisted trigger definition stored alongside table metadata.
362#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)]
363pub struct TriggerEntryMeta {
364    /// Display name preserving original casing.
365    pub name: String,
366    /// Canonical lowercase trigger name for case-insensitive lookups.
367    pub canonical_name: String,
368    /// Timing phase indicating when the trigger executes relative to the mutation.
369    pub timing: TriggerTimingMeta,
370    /// Mutation event that fires the trigger.
371    pub event: TriggerEventMeta,
372    /// Whether the trigger fires per affected row (true) or per statement (false).
373    pub for_each_row: bool,
374    /// Optional SQL expression from the WHEN clause.
375    pub condition: Option<String>,
376    /// Trigger body stored as SQL string (BEGIN/END block or single statement).
377    pub body_sql: String,
378}
379
380/// Collection of triggers registered for a table.
381#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)]
382pub struct TableTriggerMeta {
383    /// Owning table identifier.
384    pub table_id: TableId,
385    /// Trigger definitions associated with the table.
386    pub triggers: Vec<TriggerEntryMeta>,
387}
388
389// ----- SysCatalog -----
390
391/// Interface to the system catalog (table 0).
392///
393/// The system catalog stores metadata about all tables and columns in the database.
394/// It uses special reserved columns within table 0 to persist [`TableMeta`] and
395/// [`ColMeta`] structures.
396///
397/// # Lifetime
398///
399/// `SysCatalog` borrows a reference to the [`ColumnStore`] and does not own it.
400/// This allows multiple catalog instances to coexist with the same storage.
401pub struct SysCatalog<'a, P = MemPager>
402where
403    P: Pager<Blob = EntryHandle> + Send + Sync,
404{
405    store: &'a ColumnStore<P>,
406}
407
408impl<'a, P> SysCatalog<'a, P>
409where
410    P: Pager<Blob = EntryHandle> + Send + Sync,
411{
412    fn write_null_entries(&self, meta_field: LogicalFieldId, row_ids: &[RowId]) -> LlkvResult<()> {
413        if row_ids.is_empty() {
414            return Ok(());
415        }
416
417        let lfid_val: u64 = meta_field.into();
418        let schema = Arc::new(Schema::new(vec![
419            Field::new(ROW_ID_COLUMN_NAME, DataType::UInt64, false),
420            Field::new("meta", DataType::Binary, true).with_metadata(HashMap::from([(
421                crate::constants::FIELD_ID_META_KEY.to_string(),
422                lfid_val.to_string(),
423            )])),
424        ]));
425
426        let row_array = Arc::new(UInt64Array::from(row_ids.to_vec()));
427        let mut builder = BinaryBuilder::new();
428        for _ in row_ids {
429            builder.append_null();
430        }
431        let meta_array = Arc::new(builder.finish());
432
433        let batch = RecordBatch::try_new(schema, vec![row_array, meta_array])?;
434        self.store.append(&batch)?;
435        Ok(())
436    }
437
438    /// Create a new system catalog interface using the provided column store.
439    pub fn new(store: &'a ColumnStore<P>) -> Self {
440        Self { store }
441    }
442
443    /// Insert or update table metadata.
444    ///
445    /// This persists the table's metadata to the system catalog. If metadata for
446    /// this table ID already exists, it is overwritten (last-write-wins).
447    pub fn put_table_meta(&self, meta: &TableMeta) {
448        let lfid_val: u64 = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_TABLE_META_ID).into();
449        let schema = Arc::new(Schema::new(vec![
450            Field::new(ROW_ID_COLUMN_NAME, DataType::UInt64, false),
451            Field::new("meta", DataType::Binary, false).with_metadata(HashMap::from([(
452                crate::constants::FIELD_ID_META_KEY.to_string(),
453                lfid_val.to_string(),
454            )])),
455        ]));
456
457        let row_id = Arc::new(UInt64Array::from(vec![rid_table(meta.table_id)]));
458        let meta_encoded = bitcode::encode(meta);
459        let meta_bytes = Arc::new(BinaryArray::from(vec![meta_encoded.as_slice()]));
460
461        let batch = RecordBatch::try_new(schema, vec![row_id, meta_bytes]).unwrap();
462        self.store.append(&batch).unwrap();
463    }
464
465    /// Retrieve table metadata by table ID.
466    ///
467    /// Returns `None` if no metadata exists for the given table ID.
468    pub fn get_table_meta(&self, table_id: TableId) -> Option<TableMeta> {
469        let row_id = rid_table(table_id);
470        let catalog_field = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_TABLE_META_ID);
471        let batch = self
472            .store
473            .gather_rows(&[catalog_field], &[row_id], GatherNullPolicy::IncludeNulls)
474            .ok()?;
475
476        if batch.num_rows() == 0 || batch.num_columns() == 0 {
477            return None;
478        }
479
480        let array = batch
481            .column(0)
482            .as_any()
483            .downcast_ref::<BinaryArray>()
484            .expect("table meta column must be BinaryArray");
485
486        if array.is_null(0) {
487            return None;
488        }
489
490        bitcode::decode(array.value(0)).ok()
491    }
492
493    /// Upsert a single column’s metadata.
494    pub fn put_col_meta(&self, table_id: TableId, meta: &ColMeta) {
495        let lfid_val: u64 = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_COL_META_ID).into();
496        let schema = Arc::new(Schema::new(vec![
497            Field::new(ROW_ID_COLUMN_NAME, DataType::UInt64, false),
498            Field::new("meta", DataType::Binary, false).with_metadata(HashMap::from([(
499                crate::constants::FIELD_ID_META_KEY.to_string(),
500                lfid_val.to_string(),
501            )])),
502        ]));
503
504        let rid_value = rid_col(table_id, meta.col_id);
505        let row_id = Arc::new(UInt64Array::from(vec![rid_value]));
506        let meta_encoded = bitcode::encode(meta);
507        let meta_bytes = Arc::new(BinaryArray::from(vec![meta_encoded.as_slice()]));
508
509        let batch = RecordBatch::try_new(schema, vec![row_id, meta_bytes]).unwrap();
510        self.store.append(&batch).unwrap();
511    }
512
513    /// Batch fetch specific column metas by col_id using a shared keyset.
514    pub fn get_cols_meta(&self, table_id: TableId, col_ids: &[u32]) -> Vec<Option<ColMeta>> {
515        if col_ids.is_empty() {
516            return Vec::new();
517        }
518
519        let row_ids: Vec<RowId> = col_ids.iter().map(|&cid| rid_col(table_id, cid)).collect();
520        let catalog_field = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_COL_META_ID);
521
522        let batch =
523            match self
524                .store
525                .gather_rows(&[catalog_field], &row_ids, GatherNullPolicy::IncludeNulls)
526            {
527                Ok(batch) => batch,
528                Err(_) => return vec![None; col_ids.len()],
529            };
530
531        let meta_col = batch
532            .column(0)
533            .as_any()
534            .downcast_ref::<BinaryArray>()
535            .expect("catalog meta column should be Binary");
536
537        col_ids
538            .iter()
539            .enumerate()
540            .map(|(idx, _)| {
541                if meta_col.is_null(idx) {
542                    None
543                } else {
544                    bitcode::decode(meta_col.value(idx)).ok()
545                }
546            })
547            .collect()
548    }
549
550    /// Delete metadata rows for the specified column identifiers.
551    pub fn delete_col_meta(&self, table_id: TableId, col_ids: &[FieldId]) -> LlkvResult<()> {
552        if col_ids.is_empty() {
553            return Ok(());
554        }
555
556        let meta_field = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_COL_META_ID);
557        let row_ids: Vec<RowId> = col_ids
558            .iter()
559            .map(|&col_id| rid_col(table_id, col_id))
560            .collect();
561        self.write_null_entries(meta_field, &row_ids)
562    }
563
564    /// Remove the persisted table metadata record, if present.
565    pub fn delete_table_meta(&self, table_id: TableId) -> LlkvResult<()> {
566        let meta_field = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_TABLE_META_ID);
567        let row_id = rid_table(table_id);
568        self.write_null_entries(meta_field, &[row_id])
569    }
570
571    /// Delete constraint records for the provided identifiers.
572    pub fn delete_constraint_records(
573        &self,
574        table_id: TableId,
575        constraint_ids: &[ConstraintId],
576    ) -> LlkvResult<()> {
577        if constraint_ids.is_empty() {
578            return Ok(());
579        }
580
581        let meta_field = constraint_meta_lfid();
582        let row_ids: Vec<RowId> = constraint_ids
583            .iter()
584            .map(|&constraint_id| encode_constraint_row_id(table_id, constraint_id))
585            .collect();
586        self.write_null_entries(meta_field, &row_ids)
587    }
588
589    /// Delete persisted constraint names for the provided identifiers.
590    pub fn delete_constraint_names(
591        &self,
592        table_id: TableId,
593        constraint_ids: &[ConstraintId],
594    ) -> LlkvResult<()> {
595        if constraint_ids.is_empty() {
596            return Ok(());
597        }
598
599        let lfid = constraint_name_lfid();
600        let row_ids: Vec<RowId> = constraint_ids
601            .iter()
602            .map(|&constraint_id| encode_constraint_row_id(table_id, constraint_id))
603            .collect();
604        self.write_null_entries(lfid, &row_ids)
605    }
606
607    /// Delete the multi-column index metadata record for a table, if any.
608    pub fn delete_multi_column_indexes(&self, table_id: TableId) -> LlkvResult<()> {
609        let meta_field = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_MULTI_COLUMN_UNIQUE_META_ID);
610        let row_id = rid_table(table_id);
611        self.write_null_entries(meta_field, &[row_id])
612    }
613
614    /// Delete the single-column index metadata record for a table, if any exists.
615    pub fn delete_single_column_indexes(&self, table_id: TableId) -> LlkvResult<()> {
616        let meta_field = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_SINGLE_COLUMN_INDEX_META_ID);
617        let row_id = rid_table(table_id);
618        self.write_null_entries(meta_field, &[row_id])
619    }
620
621    /// Persist the complete set of multi-column index definitions for a table.
622    pub fn put_multi_column_indexes(
623        &self,
624        table_id: TableId,
625        indexes: &[MultiColumnIndexEntryMeta],
626    ) -> LlkvResult<()> {
627        let lfid_val: u64 =
628            lfid(CATALOG_TABLE_ID, CATALOG_FIELD_MULTI_COLUMN_UNIQUE_META_ID).into();
629        let schema = Arc::new(Schema::new(vec![
630            Field::new(ROW_ID_COLUMN_NAME, DataType::UInt64, false),
631            Field::new("meta", DataType::Binary, false).with_metadata(HashMap::from([(
632                crate::constants::FIELD_ID_META_KEY.to_string(),
633                lfid_val.to_string(),
634            )])),
635        ]));
636
637        let row_id = Arc::new(UInt64Array::from(vec![rid_table(table_id)]));
638        let meta = TableMultiColumnIndexMeta {
639            table_id,
640            indexes: indexes.to_vec(),
641        };
642        let encoded = bitcode::encode(&meta);
643        let meta_bytes = Arc::new(BinaryArray::from(vec![encoded.as_slice()]));
644
645        let batch = RecordBatch::try_new(schema, vec![row_id, meta_bytes])?;
646        self.store.append(&batch)?;
647        Ok(())
648    }
649
650    /// Persist the complete set of single-column index definitions for a table.
651    pub fn put_single_column_indexes(
652        &self,
653        table_id: TableId,
654        indexes: &[SingleColumnIndexEntryMeta],
655    ) -> LlkvResult<()> {
656        let lfid_val: u64 =
657            lfid(CATALOG_TABLE_ID, CATALOG_FIELD_SINGLE_COLUMN_INDEX_META_ID).into();
658        let schema = Arc::new(Schema::new(vec![
659            Field::new(ROW_ID_COLUMN_NAME, DataType::UInt64, false),
660            Field::new("meta", DataType::Binary, false).with_metadata(HashMap::from([(
661                crate::constants::FIELD_ID_META_KEY.to_string(),
662                lfid_val.to_string(),
663            )])),
664        ]));
665
666        let row_id = Arc::new(UInt64Array::from(vec![rid_table(table_id)]));
667        let meta = TableSingleColumnIndexMeta {
668            table_id,
669            indexes: indexes.to_vec(),
670        };
671        let encoded = bitcode::encode(&meta);
672        let meta_bytes = Arc::new(BinaryArray::from(vec![encoded.as_slice()]));
673
674        let batch = RecordBatch::try_new(schema, vec![row_id, meta_bytes])?;
675        self.store.append(&batch)?;
676        Ok(())
677    }
678
679    /// Retrieve all persisted multi-column index definitions for a table.
680    pub fn get_multi_column_indexes(
681        &self,
682        table_id: TableId,
683    ) -> LlkvResult<Vec<MultiColumnIndexEntryMeta>> {
684        let lfid = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_MULTI_COLUMN_UNIQUE_META_ID);
685        let row_id = rid_table(table_id);
686        let batch = match self
687            .store
688            .gather_rows(&[lfid], &[row_id], GatherNullPolicy::IncludeNulls)
689        {
690            Ok(batch) => batch,
691            Err(llkv_result::Error::NotFound) => return Ok(Vec::new()),
692            Err(err) => return Err(err),
693        };
694
695        if batch.num_columns() == 0 || batch.num_rows() == 0 {
696            return Ok(Vec::new());
697        }
698
699        let array = batch
700            .column(0)
701            .as_any()
702            .downcast_ref::<BinaryArray>()
703            .ok_or_else(|| {
704                llkv_result::Error::Internal(
705                    "catalog multi-column index column stored unexpected type".into(),
706                )
707            })?;
708
709        if array.is_null(0) {
710            return Ok(Vec::new());
711        }
712
713        let meta: TableMultiColumnIndexMeta = bitcode::decode(array.value(0)).map_err(|err| {
714            llkv_result::Error::Internal(format!(
715                "failed to decode multi-column index metadata: {err}"
716            ))
717        })?;
718
719        Ok(meta.indexes)
720    }
721
722    /// Retrieve all persisted single-column index definitions for a table.
723    pub fn get_single_column_indexes(
724        &self,
725        table_id: TableId,
726    ) -> LlkvResult<Vec<SingleColumnIndexEntryMeta>> {
727        let lfid = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_SINGLE_COLUMN_INDEX_META_ID);
728        let row_id = rid_table(table_id);
729        let batch = match self
730            .store
731            .gather_rows(&[lfid], &[row_id], GatherNullPolicy::IncludeNulls)
732        {
733            Ok(batch) => batch,
734            Err(llkv_result::Error::NotFound) => return Ok(Vec::new()),
735            Err(err) => return Err(err),
736        };
737
738        if batch.num_columns() == 0 || batch.num_rows() == 0 {
739            return Ok(Vec::new());
740        }
741
742        let array = batch
743            .column(0)
744            .as_any()
745            .downcast_ref::<BinaryArray>()
746            .ok_or_else(|| {
747                llkv_result::Error::Internal(
748                    "catalog single-column index column stored unexpected type".into(),
749                )
750            })?;
751
752        if array.is_null(0) {
753            return Ok(Vec::new());
754        }
755
756        let meta: TableSingleColumnIndexMeta = bitcode::decode(array.value(0)).map_err(|err| {
757            llkv_result::Error::Internal(format!(
758                "failed to decode single-column index metadata: {err}"
759            ))
760        })?;
761
762        Ok(meta.indexes)
763    }
764
765    /// Persist the trigger definitions for a table.
766    pub fn put_triggers(&self, table_id: TableId, triggers: &[TriggerEntryMeta]) -> LlkvResult<()> {
767        let lfid_val: u64 = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_TRIGGER_META_ID).into();
768        let schema = Arc::new(Schema::new(vec![
769            Field::new(ROW_ID_COLUMN_NAME, DataType::UInt64, false),
770            Field::new("meta", DataType::Binary, false).with_metadata(HashMap::from([(
771                crate::constants::FIELD_ID_META_KEY.to_string(),
772                lfid_val.to_string(),
773            )])),
774        ]));
775
776        let row_id = Arc::new(UInt64Array::from(vec![rid_table(table_id)]));
777        let meta = TableTriggerMeta {
778            table_id,
779            triggers: triggers.to_vec(),
780        };
781        let encoded = bitcode::encode(&meta);
782        let meta_bytes = Arc::new(BinaryArray::from(vec![encoded.as_slice()]));
783
784        let batch = RecordBatch::try_new(schema, vec![row_id, meta_bytes])?;
785        self.store.append(&batch)?;
786        Ok(())
787    }
788
789    /// Remove all trigger definitions for the provided table.
790    pub fn delete_triggers(&self, table_id: TableId) -> LlkvResult<()> {
791        let meta_field = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_TRIGGER_META_ID);
792        let row_id = rid_table(table_id);
793        self.write_null_entries(meta_field, &[row_id])
794    }
795
796    /// Retrieve persisted trigger definitions for a table.
797    pub fn get_triggers(&self, table_id: TableId) -> LlkvResult<Vec<TriggerEntryMeta>> {
798        let lfid = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_TRIGGER_META_ID);
799        let row_id = rid_table(table_id);
800        let batch = match self
801            .store
802            .gather_rows(&[lfid], &[row_id], GatherNullPolicy::IncludeNulls)
803        {
804            Ok(batch) => batch,
805            Err(llkv_result::Error::NotFound) => return Ok(Vec::new()),
806            Err(err) => return Err(err),
807        };
808
809        if batch.num_columns() == 0 || batch.num_rows() == 0 {
810            return Ok(Vec::new());
811        }
812
813        let array = batch
814            .column(0)
815            .as_any()
816            .downcast_ref::<BinaryArray>()
817            .ok_or_else(|| {
818                llkv_result::Error::Internal(
819                    "catalog trigger metadata column stored unexpected type".into(),
820                )
821            })?;
822
823        if array.is_null(0) {
824            return Ok(Vec::new());
825        }
826
827        let meta: TableTriggerMeta = bitcode::decode(array.value(0)).map_err(|err| {
828            llkv_result::Error::Internal(format!("failed to decode trigger metadata: {err}"))
829        })?;
830
831        Ok(meta.triggers)
832    }
833
834    /// Retrieve all persisted multi-column index definitions across tables.
835    pub fn all_multi_column_index_metas(&self) -> LlkvResult<Vec<TableMultiColumnIndexMeta>> {
836        let meta_field = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_MULTI_COLUMN_UNIQUE_META_ID);
837        let row_field = rowid_fid(meta_field);
838
839        struct RowIdCollector {
840            row_ids: Vec<RowId>,
841        }
842
843        impl PrimitiveVisitor for RowIdCollector {
844            fn u64_chunk(&mut self, values: &UInt64Array) {
845                for i in 0..values.len() {
846                    self.row_ids.push(values.value(i));
847                }
848            }
849        }
850        impl PrimitiveWithRowIdsVisitor for RowIdCollector {}
851        impl PrimitiveSortedVisitor for RowIdCollector {}
852        impl PrimitiveSortedWithRowIdsVisitor for RowIdCollector {}
853
854        let mut collector = RowIdCollector {
855            row_ids: Vec::new(),
856        };
857        match ScanBuilder::new(self.store, row_field)
858            .options(ScanOptions::default())
859            .run(&mut collector)
860        {
861            Ok(()) => {}
862            Err(llkv_result::Error::NotFound) => return Ok(Vec::new()),
863            Err(err) => return Err(err),
864        }
865
866        if collector.row_ids.is_empty() {
867            return Ok(Vec::new());
868        }
869
870        let batch = match self.store.gather_rows(
871            &[meta_field],
872            &collector.row_ids,
873            GatherNullPolicy::IncludeNulls,
874        ) {
875            Ok(batch) => batch,
876            Err(llkv_result::Error::NotFound) => return Ok(Vec::new()),
877            Err(err) => return Err(err),
878        };
879
880        if batch.num_columns() == 0 {
881            return Ok(Vec::new());
882        }
883
884        let array = batch
885            .column(0)
886            .as_any()
887            .downcast_ref::<BinaryArray>()
888            .ok_or_else(|| {
889                llkv_result::Error::Internal(
890                    "catalog multi-column index column stored unexpected type".into(),
891                )
892            })?;
893
894        let mut metas = Vec::with_capacity(batch.num_rows());
895        for idx in 0..batch.num_rows() {
896            if array.is_null(idx) {
897                continue;
898            }
899            let meta: TableMultiColumnIndexMeta =
900                bitcode::decode(array.value(idx)).map_err(|err| {
901                    llkv_result::Error::Internal(format!(
902                        "failed to decode multi-column index metadata: {err}"
903                    ))
904                })?;
905            metas.push(meta);
906        }
907
908        Ok(metas)
909    }
910
911    /// Persist or update multiple constraint records for a table in a single batch.
912    pub fn put_constraint_records(
913        &self,
914        table_id: TableId,
915        records: &[ConstraintRecord],
916    ) -> LlkvResult<()> {
917        if records.is_empty() {
918            return Ok(());
919        }
920
921        let lfid_val: u64 = constraint_meta_lfid().into();
922        let schema = Arc::new(Schema::new(vec![
923            Field::new(ROW_ID_COLUMN_NAME, DataType::UInt64, false),
924            Field::new("constraint", DataType::Binary, false).with_metadata(HashMap::from([(
925                crate::constants::FIELD_ID_META_KEY.to_string(),
926                lfid_val.to_string(),
927            )])),
928        ]));
929
930        let row_ids: Vec<RowId> = records
931            .iter()
932            .map(|record| encode_constraint_row_id(table_id, record.constraint_id))
933            .collect();
934
935        let row_ids_array = Arc::new(UInt64Array::from(row_ids));
936        let payload_array = Arc::new(BinaryArray::from_iter_values(
937            records.iter().map(bitcode::encode),
938        ));
939
940        let batch = RecordBatch::try_new(schema, vec![row_ids_array, payload_array])?;
941        self.store.append(&batch)?;
942        Ok(())
943    }
944
945    /// Persist or update constraint names for the specified identifiers.
946    pub fn put_constraint_names(
947        &self,
948        table_id: TableId,
949        names: &[ConstraintNameRecord],
950    ) -> LlkvResult<()> {
951        if names.is_empty() {
952            return Ok(());
953        }
954
955        let lfid_val: u64 = constraint_name_lfid().into();
956        let schema = Arc::new(Schema::new(vec![
957            Field::new(ROW_ID_COLUMN_NAME, DataType::UInt64, false),
958            Field::new("constraint_name", DataType::Binary, false).with_metadata(HashMap::from([
959                (
960                    crate::constants::FIELD_ID_META_KEY.to_string(),
961                    lfid_val.to_string(),
962                ),
963            ])),
964        ]));
965
966        let row_ids: Vec<RowId> = names
967            .iter()
968            .map(|record| encode_constraint_row_id(table_id, record.constraint_id))
969            .collect();
970        let row_ids_array = Arc::new(UInt64Array::from(row_ids));
971        let payload_array = Arc::new(BinaryArray::from_iter_values(
972            names.iter().map(bitcode::encode),
973        ));
974
975        let batch = RecordBatch::try_new(schema, vec![row_ids_array, payload_array])?;
976        self.store.append(&batch)?;
977        Ok(())
978    }
979
980    /// Fetch multiple constraint records for a table in a single batch.
981    pub fn get_constraint_records(
982        &self,
983        table_id: TableId,
984        constraint_ids: &[ConstraintId],
985    ) -> LlkvResult<Vec<Option<ConstraintRecord>>> {
986        if constraint_ids.is_empty() {
987            return Ok(Vec::new());
988        }
989
990        let lfid = constraint_meta_lfid();
991        let row_ids: Vec<RowId> = constraint_ids
992            .iter()
993            .map(|&constraint_id| encode_constraint_row_id(table_id, constraint_id))
994            .collect();
995
996        let batch = match self
997            .store
998            .gather_rows(&[lfid], &row_ids, GatherNullPolicy::IncludeNulls)
999        {
1000            Ok(batch) => batch,
1001            Err(llkv_result::Error::NotFound) => {
1002                return Ok(vec![None; constraint_ids.len()]);
1003            }
1004            Err(err) => return Err(err),
1005        };
1006
1007        if batch.num_columns() == 0 || batch.num_rows() == 0 {
1008            return Ok(vec![None; constraint_ids.len()]);
1009        }
1010
1011        let array = batch
1012            .column(0)
1013            .as_any()
1014            .downcast_ref::<BinaryArray>()
1015            .ok_or_else(|| {
1016                llkv_result::Error::Internal(
1017                    "constraint metadata column stored unexpected type".into(),
1018                )
1019            })?;
1020
1021        let mut results = Vec::with_capacity(constraint_ids.len());
1022        for (idx, &constraint_id) in constraint_ids.iter().enumerate() {
1023            if array.is_null(idx) {
1024                results.push(None);
1025                continue;
1026            }
1027            let record = decode_constraint_record(array.value(idx))?;
1028            if record.constraint_id != constraint_id {
1029                return Err(llkv_result::Error::Internal(
1030                    "constraint metadata id mismatch".into(),
1031                ));
1032            }
1033            results.push(Some(record));
1034        }
1035
1036        Ok(results)
1037    }
1038
1039    /// Fetch constraint names for a table in a single batch.
1040    pub fn get_constraint_names(
1041        &self,
1042        table_id: TableId,
1043        constraint_ids: &[ConstraintId],
1044    ) -> LlkvResult<Vec<Option<String>>> {
1045        if constraint_ids.is_empty() {
1046            return Ok(Vec::new());
1047        }
1048
1049        let lfid = constraint_name_lfid();
1050        let row_ids: Vec<RowId> = constraint_ids
1051            .iter()
1052            .map(|&constraint_id| encode_constraint_row_id(table_id, constraint_id))
1053            .collect();
1054
1055        let batch = match self
1056            .store
1057            .gather_rows(&[lfid], &row_ids, GatherNullPolicy::IncludeNulls)
1058        {
1059            Ok(batch) => batch,
1060            Err(llkv_result::Error::NotFound) => {
1061                return Ok(vec![None; constraint_ids.len()]);
1062            }
1063            Err(err) => return Err(err),
1064        };
1065
1066        if batch.num_columns() == 0 {
1067            return Ok(vec![None; constraint_ids.len()]);
1068        }
1069
1070        let array = batch
1071            .column(0)
1072            .as_any()
1073            .downcast_ref::<BinaryArray>()
1074            .ok_or_else(|| {
1075                llkv_result::Error::Internal(
1076                    "constraint name metadata column stored unexpected type".into(),
1077                )
1078            })?;
1079
1080        let mut results = Vec::with_capacity(row_ids.len());
1081        for idx in 0..row_ids.len() {
1082            if array.is_null(idx) {
1083                results.push(None);
1084            } else {
1085                let record: ConstraintNameRecord =
1086                    bitcode::decode(array.value(idx)).map_err(|err| {
1087                        llkv_result::Error::Internal(format!(
1088                            "failed to decode constraint name metadata: {err}"
1089                        ))
1090                    })?;
1091                results.push(record.name);
1092            }
1093        }
1094
1095        Ok(results)
1096    }
1097
1098    /// Stream constraint records for a table in batches.
1099    pub fn scan_constraint_records_for_table<F>(
1100        &self,
1101        table_id: TableId,
1102        mut on_batch: F,
1103    ) -> LlkvResult<()>
1104    where
1105        F: FnMut(Vec<ConstraintRecord>),
1106    {
1107        let row_field = constraint_row_lfid();
1108        let mut visitor = ConstraintRowCollector {
1109            store: self.store,
1110            lfid: constraint_meta_lfid(),
1111            table_id,
1112            on_batch: &mut on_batch,
1113            buffer: Vec::with_capacity(CONSTRAINT_SCAN_CHUNK_SIZE),
1114            error: None,
1115        };
1116
1117        match ScanBuilder::new(self.store, row_field)
1118            .options(ScanOptions::default())
1119            .run(&mut visitor)
1120        {
1121            Ok(()) => {}
1122            Err(llkv_result::Error::NotFound) => return Ok(()),
1123            Err(err) => return Err(err),
1124        }
1125
1126        visitor.finish()
1127    }
1128
1129    /// Load all constraint records for a table into a vector.
1130    pub fn constraint_records_for_table(
1131        &self,
1132        table_id: TableId,
1133    ) -> LlkvResult<Vec<ConstraintRecord>> {
1134        let mut all = Vec::new();
1135        self.scan_constraint_records_for_table(table_id, |mut chunk| {
1136            all.append(&mut chunk);
1137        })?;
1138        Ok(all)
1139    }
1140
1141    pub fn put_next_table_id(&self, next_id: TableId) -> LlkvResult<()> {
1142        let lfid_val: u64 = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_NEXT_TABLE_ID).into();
1143        let schema = Arc::new(Schema::new(vec![
1144            Field::new(ROW_ID_COLUMN_NAME, DataType::UInt64, false),
1145            Field::new("next_table_id", DataType::UInt64, false).with_metadata(HashMap::from([(
1146                crate::constants::FIELD_ID_META_KEY.to_string(),
1147                lfid_val.to_string(),
1148            )])),
1149        ]));
1150
1151        let row_id = Arc::new(UInt64Array::from(vec![CATALOG_NEXT_TABLE_ROW_ID]));
1152        let value_array = Arc::new(UInt64Array::from(vec![next_id as u64]));
1153        let batch = RecordBatch::try_new(schema, vec![row_id, value_array])?;
1154        self.store.append(&batch)?;
1155        Ok(())
1156    }
1157
1158    pub fn get_next_table_id(&self) -> LlkvResult<Option<TableId>> {
1159        let lfid = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_NEXT_TABLE_ID);
1160        let batch = match self.store.gather_rows(
1161            &[lfid],
1162            &[CATALOG_NEXT_TABLE_ROW_ID],
1163            GatherNullPolicy::IncludeNulls,
1164        ) {
1165            Ok(batch) => batch,
1166            Err(llkv_result::Error::NotFound) => return Ok(None),
1167            Err(err) => return Err(err),
1168        };
1169
1170        if batch.num_columns() == 0 || batch.num_rows() == 0 {
1171            return Ok(None);
1172        }
1173
1174        let array = batch
1175            .column(0)
1176            .as_any()
1177            .downcast_ref::<UInt64Array>()
1178            .ok_or_else(|| {
1179                llkv_result::Error::Internal(
1180                    "catalog next_table_id column stored unexpected type".into(),
1181                )
1182            })?;
1183        if array.is_empty() || array.is_null(0) {
1184            return Ok(None);
1185        }
1186
1187        let value = array.value(0);
1188        if value > TableId::MAX as u64 {
1189            return Err(llkv_result::Error::InvalidArgumentError(
1190                "persisted next_table_id exceeds TableId range".into(),
1191            ));
1192        }
1193
1194        Ok(Some(value as TableId))
1195    }
1196
1197    pub fn max_table_id(&self) -> LlkvResult<Option<TableId>> {
1198        let meta_field = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_TABLE_META_ID);
1199        let row_field = rowid_fid(meta_field);
1200
1201        let mut collector = MaxRowIdCollector { max: None };
1202        match ScanBuilder::new(self.store, row_field)
1203            .options(ScanOptions::default())
1204            .run(&mut collector)
1205        {
1206            Ok(()) => {}
1207            Err(llkv_result::Error::NotFound) => return Ok(None),
1208            Err(err) => return Err(err),
1209        }
1210
1211        let max_value = match collector.max {
1212            Some(value) => value,
1213            None => return Ok(None),
1214        };
1215
1216        let logical: LogicalFieldId = max_value.into();
1217        Ok(Some(logical.table_id()))
1218    }
1219
1220    /// Scan all table metadata entries from the catalog.
1221    /// Returns a vector of (table_id, TableMeta) pairs for all persisted tables.
1222    ///
1223    /// This method first scans for all row IDs in the table metadata column,
1224    /// then uses gather_rows to retrieve the actual metadata.
1225    pub fn all_table_metas(&self) -> LlkvResult<Vec<(TableId, TableMeta)>> {
1226        let meta_field = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_TABLE_META_ID);
1227        let row_field = rowid_fid(meta_field);
1228
1229        // Collect all row IDs that have table metadata
1230        struct RowIdCollector {
1231            row_ids: Vec<RowId>,
1232        }
1233
1234        impl PrimitiveVisitor for RowIdCollector {
1235            fn u64_chunk(&mut self, values: &UInt64Array) {
1236                for i in 0..values.len() {
1237                    self.row_ids.push(values.value(i));
1238                }
1239            }
1240        }
1241        impl PrimitiveWithRowIdsVisitor for RowIdCollector {}
1242        impl PrimitiveSortedVisitor for RowIdCollector {}
1243        impl PrimitiveSortedWithRowIdsVisitor for RowIdCollector {}
1244
1245        let mut collector = RowIdCollector {
1246            row_ids: Vec::new(),
1247        };
1248        match ScanBuilder::new(self.store, row_field)
1249            .options(ScanOptions::default())
1250            .run(&mut collector)
1251        {
1252            Ok(()) => {}
1253            Err(llkv_result::Error::NotFound) => return Ok(Vec::new()),
1254            Err(err) => return Err(err),
1255        }
1256
1257        if collector.row_ids.is_empty() {
1258            return Ok(Vec::new());
1259        }
1260
1261        // Gather all table metadata using the collected row IDs
1262        let batch = self.store.gather_rows(
1263            &[meta_field],
1264            &collector.row_ids,
1265            GatherNullPolicy::IncludeNulls,
1266        )?;
1267
1268        let meta_col = batch
1269            .column(0)
1270            .as_any()
1271            .downcast_ref::<BinaryArray>()
1272            .ok_or_else(|| {
1273                llkv_result::Error::Internal("catalog table_meta column should be Binary".into())
1274            })?;
1275
1276        let mut result = Vec::new();
1277        for (idx, &row_id) in collector.row_ids.iter().enumerate() {
1278            if !meta_col.is_null(idx) {
1279                let bytes = meta_col.value(idx);
1280                if let Ok(meta) = bitcode::decode::<TableMeta>(bytes) {
1281                    let logical: LogicalFieldId = row_id.into();
1282                    let table_id = logical.table_id();
1283                    result.push((table_id, meta));
1284                }
1285            }
1286        }
1287
1288        Ok(result)
1289    }
1290
1291    /// Persist the next transaction id to the catalog.
1292    pub fn put_next_txn_id(&self, next_txn_id: u64) -> LlkvResult<()> {
1293        let lfid_val: u64 = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_NEXT_TXN_ID).into();
1294        let schema = Arc::new(Schema::new(vec![
1295            Field::new(ROW_ID_COLUMN_NAME, DataType::UInt64, false),
1296            Field::new("next_txn_id", DataType::UInt64, false).with_metadata(HashMap::from([(
1297                crate::constants::FIELD_ID_META_KEY.to_string(),
1298                lfid_val.to_string(),
1299            )])),
1300        ]));
1301
1302        let row_id = Arc::new(UInt64Array::from(vec![CATALOG_NEXT_TXN_ROW_ID]));
1303        let value_array = Arc::new(UInt64Array::from(vec![next_txn_id]));
1304        let batch = RecordBatch::try_new(schema, vec![row_id, value_array])?;
1305        self.store.append(&batch)?;
1306        Ok(())
1307    }
1308
1309    /// Load the next transaction id from the catalog.
1310    pub fn get_next_txn_id(&self) -> LlkvResult<Option<u64>> {
1311        let lfid = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_NEXT_TXN_ID);
1312        let batch = match self.store.gather_rows(
1313            &[lfid],
1314            &[CATALOG_NEXT_TXN_ROW_ID],
1315            GatherNullPolicy::IncludeNulls,
1316        ) {
1317            Ok(batch) => batch,
1318            Err(llkv_result::Error::NotFound) => return Ok(None),
1319            Err(err) => return Err(err),
1320        };
1321
1322        if batch.num_columns() == 0 || batch.num_rows() == 0 {
1323            return Ok(None);
1324        }
1325
1326        let array = batch
1327            .column(0)
1328            .as_any()
1329            .downcast_ref::<UInt64Array>()
1330            .ok_or_else(|| {
1331                llkv_result::Error::Internal(
1332                    "catalog next_txn_id column stored unexpected type".into(),
1333                )
1334            })?;
1335        if array.is_empty() || array.is_null(0) {
1336            return Ok(None);
1337        }
1338
1339        let value = array.value(0);
1340        Ok(Some(value))
1341    }
1342
1343    /// Persist the last committed transaction id to the catalog.
1344    pub fn put_last_committed_txn_id(&self, last_committed: u64) -> LlkvResult<()> {
1345        let lfid_val: u64 = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_LAST_COMMITTED_TXN_ID).into();
1346        let schema = Arc::new(Schema::new(vec![
1347            Field::new(ROW_ID_COLUMN_NAME, DataType::UInt64, false),
1348            Field::new("last_committed_txn_id", DataType::UInt64, false).with_metadata(
1349                HashMap::from([(
1350                    crate::constants::FIELD_ID_META_KEY.to_string(),
1351                    lfid_val.to_string(),
1352                )]),
1353            ),
1354        ]));
1355
1356        let row_id = Arc::new(UInt64Array::from(vec![CATALOG_LAST_COMMITTED_TXN_ROW_ID]));
1357        let value_array = Arc::new(UInt64Array::from(vec![last_committed]));
1358        let batch = RecordBatch::try_new(schema, vec![row_id, value_array])?;
1359        self.store.append(&batch)?;
1360        Ok(())
1361    }
1362
1363    /// Load the last committed transaction id from the catalog.
1364    pub fn get_last_committed_txn_id(&self) -> LlkvResult<Option<u64>> {
1365        let lfid = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_LAST_COMMITTED_TXN_ID);
1366        let batch = match self.store.gather_rows(
1367            &[lfid],
1368            &[CATALOG_LAST_COMMITTED_TXN_ROW_ID],
1369            GatherNullPolicy::IncludeNulls,
1370        ) {
1371            Ok(batch) => batch,
1372            Err(llkv_result::Error::NotFound) => return Ok(None),
1373            Err(err) => return Err(err),
1374        };
1375
1376        if batch.num_columns() == 0 || batch.num_rows() == 0 {
1377            return Ok(None);
1378        }
1379
1380        let array = batch
1381            .column(0)
1382            .as_any()
1383            .downcast_ref::<UInt64Array>()
1384            .ok_or_else(|| {
1385                llkv_result::Error::Internal(
1386                    "catalog last_committed_txn_id column stored unexpected type".into(),
1387                )
1388            })?;
1389        if array.is_empty() || array.is_null(0) {
1390            return Ok(None);
1391        }
1392
1393        let value = array.value(0);
1394        Ok(Some(value))
1395    }
1396
1397    /// Persist the catalog state to the system catalog.
1398    ///
1399    /// Stores the complete catalog state (all tables and fields) as a binary blob
1400    /// using bitcode serialization.
1401    pub fn put_catalog_state(&self, state: &crate::catalog::TableCatalogState) -> LlkvResult<()> {
1402        let lfid_val: u64 = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_CATALOG_STATE).into();
1403        let schema = Arc::new(Schema::new(vec![
1404            Field::new(ROW_ID_COLUMN_NAME, DataType::UInt64, false),
1405            Field::new("catalog_state", DataType::Binary, false).with_metadata(HashMap::from([(
1406                crate::constants::FIELD_ID_META_KEY.to_string(),
1407                lfid_val.to_string(),
1408            )])),
1409        ]));
1410
1411        let row_id = Arc::new(UInt64Array::from(vec![CATALOG_STATE_ROW_ID]));
1412        let encoded = bitcode::encode(state);
1413        let state_bytes = Arc::new(BinaryArray::from(vec![encoded.as_slice()]));
1414
1415        let batch = RecordBatch::try_new(schema, vec![row_id, state_bytes])?;
1416        self.store.append(&batch)?;
1417        Ok(())
1418    }
1419
1420    /// Load the catalog state from the system catalog.
1421    ///
1422    /// Retrieves the complete catalog state including all table and field mappings.
1423    pub fn get_catalog_state(&self) -> LlkvResult<Option<crate::catalog::TableCatalogState>> {
1424        let lfid = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_CATALOG_STATE);
1425        let batch = match self.store.gather_rows(
1426            &[lfid],
1427            &[CATALOG_STATE_ROW_ID],
1428            GatherNullPolicy::IncludeNulls,
1429        ) {
1430            Ok(batch) => batch,
1431            Err(llkv_result::Error::NotFound) => return Ok(None),
1432            Err(err) => return Err(err),
1433        };
1434
1435        if batch.num_columns() == 0 || batch.num_rows() == 0 {
1436            return Ok(None);
1437        }
1438
1439        let array = batch
1440            .column(0)
1441            .as_any()
1442            .downcast_ref::<BinaryArray>()
1443            .ok_or_else(|| {
1444                llkv_result::Error::Internal("catalog state column stored unexpected type".into())
1445            })?;
1446        if array.is_empty() || array.is_null(0) {
1447            return Ok(None);
1448        }
1449
1450        let bytes = array.value(0);
1451        let state = bitcode::decode(bytes).map_err(|e| {
1452            llkv_result::Error::Internal(format!("Failed to decode catalog state: {}", e))
1453        })?;
1454        Ok(Some(state))
1455    }
1456
1457    /// Persist schema metadata to the catalog.
1458    ///
1459    /// Stores schema metadata at a row ID derived from the schema name hash.
1460    /// This allows efficient lookup and prevents collisions.
1461    pub fn put_schema_meta(&self, meta: &SchemaMeta) -> LlkvResult<()> {
1462        let lfid_val: u64 = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_SCHEMA_META_ID).into();
1463        let schema = Arc::new(Schema::new(vec![
1464            Field::new(ROW_ID_COLUMN_NAME, DataType::UInt64, false),
1465            Field::new("meta", DataType::Binary, false).with_metadata(HashMap::from([(
1466                crate::constants::FIELD_ID_META_KEY.to_string(),
1467                lfid_val.to_string(),
1468            )])),
1469        ]));
1470
1471        // Use hash of canonical (lowercase) schema name as row ID
1472        let canonical = meta.name.to_ascii_lowercase();
1473        let row_id_val = schema_name_to_row_id(&canonical);
1474        let row_id = Arc::new(UInt64Array::from(vec![row_id_val]));
1475        let meta_encoded = bitcode::encode(meta);
1476        let meta_bytes = Arc::new(BinaryArray::from(vec![meta_encoded.as_slice()]));
1477
1478        let batch = RecordBatch::try_new(schema, vec![row_id, meta_bytes])?;
1479        self.store.append(&batch)?;
1480        Ok(())
1481    }
1482
1483    /// Retrieve schema metadata by name.
1484    ///
1485    /// Returns `None` if the schema does not exist.
1486    pub fn get_schema_meta(&self, schema_name: &str) -> LlkvResult<Option<SchemaMeta>> {
1487        let canonical = schema_name.to_ascii_lowercase();
1488        let row_id = schema_name_to_row_id(&canonical);
1489        let lfid = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_SCHEMA_META_ID);
1490
1491        let batch = match self
1492            .store
1493            .gather_rows(&[lfid], &[row_id], GatherNullPolicy::IncludeNulls)
1494        {
1495            Ok(batch) => batch,
1496            Err(llkv_result::Error::NotFound) => return Ok(None),
1497            Err(err) => return Err(err),
1498        };
1499
1500        if batch.num_columns() == 0 || batch.num_rows() == 0 {
1501            return Ok(None);
1502        }
1503
1504        let array = batch
1505            .column(0)
1506            .as_any()
1507            .downcast_ref::<BinaryArray>()
1508            .ok_or_else(|| {
1509                llkv_result::Error::Internal("catalog schema_meta column should be Binary".into())
1510            })?;
1511
1512        if array.is_empty() || array.is_null(0) {
1513            return Ok(None);
1514        }
1515
1516        let bytes = array.value(0);
1517        let meta = bitcode::decode(bytes).map_err(|e| {
1518            llkv_result::Error::Internal(format!("Failed to decode schema metadata: {}", e))
1519        })?;
1520        Ok(Some(meta))
1521    }
1522
1523    /// Scan all schema metadata entries from the catalog.
1524    ///
1525    /// Returns a vector of all persisted schemas.
1526    pub fn all_schema_metas(&self) -> LlkvResult<Vec<SchemaMeta>> {
1527        let meta_field = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_SCHEMA_META_ID);
1528        let row_field = rowid_fid(meta_field);
1529
1530        // Collect all row IDs that have schema metadata
1531        struct RowIdCollector {
1532            row_ids: Vec<RowId>,
1533        }
1534
1535        impl PrimitiveVisitor for RowIdCollector {
1536            fn u64_chunk(&mut self, values: &UInt64Array) {
1537                for i in 0..values.len() {
1538                    self.row_ids.push(values.value(i));
1539                }
1540            }
1541        }
1542        impl PrimitiveWithRowIdsVisitor for RowIdCollector {}
1543        impl PrimitiveSortedVisitor for RowIdCollector {}
1544        impl PrimitiveSortedWithRowIdsVisitor for RowIdCollector {}
1545
1546        let mut collector = RowIdCollector {
1547            row_ids: Vec::new(),
1548        };
1549        match ScanBuilder::new(self.store, row_field)
1550            .options(ScanOptions::default())
1551            .run(&mut collector)
1552        {
1553            Ok(()) => {}
1554            Err(llkv_result::Error::NotFound) => return Ok(Vec::new()),
1555            Err(err) => return Err(err),
1556        }
1557
1558        if collector.row_ids.is_empty() {
1559            return Ok(Vec::new());
1560        }
1561
1562        // Gather all schema metadata using the collected row IDs
1563        let batch = self.store.gather_rows(
1564            &[meta_field],
1565            &collector.row_ids,
1566            GatherNullPolicy::IncludeNulls,
1567        )?;
1568
1569        let meta_col = batch
1570            .column(0)
1571            .as_any()
1572            .downcast_ref::<BinaryArray>()
1573            .ok_or_else(|| {
1574                llkv_result::Error::Internal("catalog schema_meta column should be Binary".into())
1575            })?;
1576
1577        let mut result = Vec::new();
1578        for idx in 0..collector.row_ids.len() {
1579            if !meta_col.is_null(idx) {
1580                let bytes = meta_col.value(idx);
1581                if let Ok(meta) = bitcode::decode::<SchemaMeta>(bytes) {
1582                    result.push(meta);
1583                }
1584            }
1585        }
1586
1587        Ok(result)
1588    }
1589
1590    /// Persist custom type metadata to the catalog.
1591    ///
1592    /// Stores custom type metadata at a row ID derived from the type name hash.
1593    pub fn put_custom_type_meta(&self, meta: &CustomTypeMeta) -> LlkvResult<()> {
1594        let lfid_val: u64 = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_CUSTOM_TYPE_META_ID).into();
1595        let schema = Arc::new(Schema::new(vec![
1596            Field::new(ROW_ID_COLUMN_NAME, DataType::UInt64, false),
1597            Field::new("meta", DataType::Binary, false).with_metadata(HashMap::from([(
1598                crate::constants::FIELD_ID_META_KEY.to_string(),
1599                lfid_val.to_string(),
1600            )])),
1601        ]));
1602
1603        // Use hash of canonical (lowercase) type name as row ID
1604        let canonical = meta.name.to_ascii_lowercase();
1605        let row_id_val = schema_name_to_row_id(&canonical); // Reuse same hash function
1606        let row_id = Arc::new(UInt64Array::from(vec![row_id_val]));
1607        let meta_encoded = bitcode::encode(meta);
1608        let meta_bytes = Arc::new(BinaryArray::from(vec![meta_encoded.as_slice()]));
1609
1610        let batch = RecordBatch::try_new(schema, vec![row_id, meta_bytes])?;
1611        self.store.append(&batch)?;
1612        Ok(())
1613    }
1614
1615    /// Retrieve custom type metadata by name.
1616    ///
1617    /// Returns `None` if the type does not exist.
1618    pub fn get_custom_type_meta(&self, type_name: &str) -> LlkvResult<Option<CustomTypeMeta>> {
1619        let canonical = type_name.to_ascii_lowercase();
1620        let row_id = schema_name_to_row_id(&canonical);
1621        let lfid = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_CUSTOM_TYPE_META_ID);
1622
1623        let batch = match self
1624            .store
1625            .gather_rows(&[lfid], &[row_id], GatherNullPolicy::IncludeNulls)
1626        {
1627            Ok(batch) => batch,
1628            Err(llkv_result::Error::NotFound) => return Ok(None),
1629            Err(err) => return Err(err),
1630        };
1631
1632        let meta_col = batch
1633            .column(0)
1634            .as_any()
1635            .downcast_ref::<BinaryArray>()
1636            .ok_or_else(|| {
1637                llkv_result::Error::Internal(
1638                    "catalog custom_type_meta column should be Binary".into(),
1639                )
1640            })?;
1641
1642        if meta_col.is_null(0) {
1643            return Ok(None);
1644        }
1645
1646        let bytes = meta_col.value(0);
1647        let meta = bitcode::decode(bytes).map_err(|err| {
1648            llkv_result::Error::Internal(format!("failed to decode custom type metadata: {err}"))
1649        })?;
1650        Ok(Some(meta))
1651    }
1652
1653    /// Delete custom type metadata by name.
1654    ///
1655    /// Returns `Ok(())` regardless of whether the type existed.
1656    pub fn delete_custom_type_meta(&self, type_name: &str) -> LlkvResult<()> {
1657        let canonical = type_name.to_ascii_lowercase();
1658        let row_id = schema_name_to_row_id(&canonical);
1659        let lfid = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_CUSTOM_TYPE_META_ID);
1660
1661        // Delete by writing null value
1662        let lfid_val: u64 = lfid.into();
1663        let schema = Arc::new(Schema::new(vec![
1664            Field::new(ROW_ID_COLUMN_NAME, DataType::UInt64, false),
1665            Field::new("meta", DataType::Binary, true).with_metadata(HashMap::from([(
1666                crate::constants::FIELD_ID_META_KEY.to_string(),
1667                lfid_val.to_string(),
1668            )])),
1669        ]));
1670
1671        let row_id_arr = Arc::new(UInt64Array::from(vec![row_id]));
1672        let mut meta_builder = BinaryBuilder::new();
1673        meta_builder.append_null();
1674        let meta_arr = Arc::new(meta_builder.finish());
1675
1676        let batch = RecordBatch::try_new(schema, vec![row_id_arr, meta_arr])?;
1677        self.store.append(&batch)?;
1678        Ok(())
1679    }
1680
1681    /// Retrieve all custom type metadata.
1682    ///
1683    /// Returns all persisted custom types.
1684    pub fn all_custom_type_metas(&self) -> LlkvResult<Vec<CustomTypeMeta>> {
1685        let meta_field = lfid(CATALOG_TABLE_ID, CATALOG_FIELD_CUSTOM_TYPE_META_ID);
1686        let row_field = rowid_fid(meta_field);
1687
1688        // Collect all row IDs that have custom type metadata
1689        struct RowIdCollector {
1690            row_ids: Vec<RowId>,
1691        }
1692
1693        impl PrimitiveVisitor for RowIdCollector {
1694            fn u64_chunk(&mut self, values: &UInt64Array) {
1695                for i in 0..values.len() {
1696                    self.row_ids.push(values.value(i));
1697                }
1698            }
1699        }
1700        impl PrimitiveWithRowIdsVisitor for RowIdCollector {}
1701        impl PrimitiveSortedVisitor for RowIdCollector {}
1702        impl PrimitiveSortedWithRowIdsVisitor for RowIdCollector {}
1703
1704        let mut collector = RowIdCollector {
1705            row_ids: Vec::new(),
1706        };
1707        match ScanBuilder::new(self.store, row_field)
1708            .options(ScanOptions::default())
1709            .run(&mut collector)
1710        {
1711            Ok(()) => {}
1712            Err(llkv_result::Error::NotFound) => return Ok(Vec::new()),
1713            Err(err) => return Err(err),
1714        }
1715
1716        if collector.row_ids.is_empty() {
1717            return Ok(Vec::new());
1718        }
1719
1720        // Gather all custom type metadata using the collected row IDs
1721        let batch = self.store.gather_rows(
1722            &[meta_field],
1723            &collector.row_ids,
1724            GatherNullPolicy::IncludeNulls,
1725        )?;
1726
1727        let meta_col = batch
1728            .column(0)
1729            .as_any()
1730            .downcast_ref::<BinaryArray>()
1731            .ok_or_else(|| {
1732                llkv_result::Error::Internal(
1733                    "catalog custom_type_meta column should be Binary".into(),
1734                )
1735            })?;
1736
1737        let mut result = Vec::new();
1738        for idx in 0..collector.row_ids.len() {
1739            if !meta_col.is_null(idx) {
1740                let bytes = meta_col.value(idx);
1741                if let Ok(meta) = bitcode::decode::<CustomTypeMeta>(bytes) {
1742                    result.push(meta);
1743                }
1744            }
1745        }
1746
1747        Ok(result)
1748    }
1749}
1750
1751/// Generate a row ID for schema metadata based on schema name.
1752///
1753/// Uses a simple hash to map schema names to row IDs. This is deterministic
1754/// and allows direct lookup without scanning.
1755fn schema_name_to_row_id(canonical_name: &str) -> RowId {
1756    // Use a simple 64-bit FNV-1a hash for deterministic IDs across platforms and releases
1757    const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
1758    const FNV_PRIME: u64 = 0x1000_0000_01b3;
1759
1760    let mut hash = FNV_OFFSET;
1761    for byte in canonical_name.as_bytes() {
1762        hash ^= u64::from(*byte);
1763        hash = hash.wrapping_mul(FNV_PRIME);
1764    }
1765
1766    // Use high bit to avoid collision with reserved catalog row IDs (0-3) and table metadata rows
1767    hash | (1u64 << 63)
1768}
1769
1770struct MaxRowIdCollector {
1771    max: Option<RowId>,
1772}
1773
1774impl PrimitiveVisitor for MaxRowIdCollector {
1775    fn u64_chunk(&mut self, values: &UInt64Array) {
1776        for i in 0..values.len() {
1777            let value = values.value(i);
1778            self.max = match self.max {
1779                Some(curr) if curr >= value => Some(curr),
1780                _ => Some(value),
1781            };
1782        }
1783    }
1784}
1785
1786impl PrimitiveWithRowIdsVisitor for MaxRowIdCollector {}
1787impl PrimitiveSortedVisitor for MaxRowIdCollector {}
1788impl PrimitiveSortedWithRowIdsVisitor for MaxRowIdCollector {}
1789
1790#[cfg(test)]
1791mod tests {
1792    use super::*;
1793    use crate::constraints::{
1794        ConstraintKind, ConstraintState, PrimaryKeyConstraint, UniqueConstraint,
1795    };
1796    use llkv_column_map::ColumnStore;
1797    use std::sync::Arc;
1798
1799    #[test]
1800    fn constraint_records_roundtrip() {
1801        let pager = Arc::new(MemPager::default());
1802        let store = ColumnStore::open(Arc::clone(&pager)).unwrap();
1803        let catalog = SysCatalog::new(&store);
1804
1805        let table_id: TableId = 42;
1806        let record1 = ConstraintRecord {
1807            constraint_id: 1,
1808            kind: ConstraintKind::PrimaryKey(PrimaryKeyConstraint {
1809                field_ids: vec![1, 2],
1810            }),
1811            state: ConstraintState::Active,
1812            revision: 1,
1813            last_modified_micros: 100,
1814        };
1815        let record2 = ConstraintRecord {
1816            constraint_id: 2,
1817            kind: ConstraintKind::Unique(UniqueConstraint { field_ids: vec![3] }),
1818            state: ConstraintState::Active,
1819            revision: 2,
1820            last_modified_micros: 200,
1821        };
1822        catalog
1823            .put_constraint_records(table_id, &[record1.clone(), record2.clone()])
1824            .unwrap();
1825
1826        let other_table_record = ConstraintRecord {
1827            constraint_id: 1,
1828            kind: ConstraintKind::Unique(UniqueConstraint { field_ids: vec![5] }),
1829            state: ConstraintState::Active,
1830            revision: 1,
1831            last_modified_micros: 150,
1832        };
1833        catalog
1834            .put_constraint_records(7, &[other_table_record])
1835            .unwrap();
1836
1837        let mut fetched = catalog.constraint_records_for_table(table_id).unwrap();
1838        fetched.sort_by_key(|record| record.constraint_id);
1839
1840        assert_eq!(fetched.len(), 2);
1841        assert_eq!(fetched[0], record1);
1842        assert_eq!(fetched[1], record2);
1843
1844        let single = catalog
1845            .get_constraint_records(table_id, &[record1.constraint_id])
1846            .unwrap();
1847        assert_eq!(single.len(), 1);
1848        assert_eq!(single[0].as_ref(), Some(&record1));
1849
1850        let missing = catalog.get_constraint_records(table_id, &[999]).unwrap();
1851        assert_eq!(missing.len(), 1);
1852        assert!(missing[0].is_none());
1853    }
1854}