Skip to main content

cobble_data_structure/
structured_db.rs

1#[cfg(feature = "ffi")]
2use crate::list::encode_borrowed_list_for_write;
3use crate::list::{
4    LIST_OPERATOR_ID, ListConfig, decode_list_for_read, encode_list_for_write, list_operator,
5    list_operator_from_metadata, transform_list_elements,
6};
7use crate::priority_queue::{
8    PriorityQueue, priority_queue_column_family_name, priority_queue_column_family_options,
9    validate_priority_queue_column_family,
10};
11use arc_swap::ArcSwapOption;
12use bytes::Bytes;
13use cobble::{
14    BytesMergeOperator, ColumnEvolution, Config, Db, DbBuilder, DbIterator, Error, MemtableType,
15    MergeOperatorResolver, ReadOptions, RecoveryMode, Result, ScanOptions, Schema, SchemaBuilder,
16    ShardSnapshotMetadata, TransformSpec, WriteBatch, WriteOptions,
17};
18use serde::{Deserialize, Serialize};
19use serde_json::Value as JsonValue;
20use std::collections::BTreeMap;
21use std::ops::{Range, RangeInclusive};
22use std::sync::Arc;
23
24const DEFAULT_COLUMN_FAMILY_ID: u8 = 0;
25const DEFAULT_COLUMN_FAMILY_NAME: &str = "default";
26
27#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
28pub struct StructuredSchema {
29    pub column_family_ids: BTreeMap<String, u8>,
30    pub column_families: BTreeMap<u8, StructuredColumnFamilySchema>,
31}
32
33#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
34pub struct StructuredColumnFamilySchema {
35    pub columns: BTreeMap<u16, StructuredColumnType>,
36}
37
38#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(tag = "kind", rename_all = "snake_case")]
40pub enum StructuredColumnType {
41    #[default]
42    Bytes,
43    List(ListConfig),
44}
45
46impl StructuredColumnType {
47    /// Adapt an element transform into a raw List-column schema transform.
48    ///
49    /// The returned callback can be registered on writers, readers, or standalone
50    /// compactors. It preserves null/empty lists, element order/count, and original
51    /// expiration timestamps, without filtering expired elements or applying caps.
52    /// `config.preserve_element_ttl` must match the source and target List encoding;
53    /// the other config fields do not affect this adapter.
54    pub fn list_element_transform<F>(
55        config: ListConfig,
56        transform: F,
57    ) -> impl Fn(Option<Bytes>) -> Result<Option<Bytes>> + Send + Sync + 'static
58    where
59        F: Fn(Bytes) -> Result<Bytes> + Send + Sync + 'static,
60    {
61        move |value| {
62            value
63                .map(|payload| {
64                    transform_list_elements(payload, config.preserve_element_ttl, &transform)
65                })
66                .transpose()
67        }
68    }
69}
70
71#[derive(Clone, Debug, PartialEq, Eq)]
72pub enum StructuredColumnValue {
73    Bytes(Bytes),
74    List(Vec<Bytes>),
75}
76
77impl StructuredColumnFamilySchema {
78    fn structured_column_type(&self, column: u16) -> &StructuredColumnType {
79        self.columns
80            .get(&column)
81            .unwrap_or(&StructuredColumnType::Bytes)
82    }
83
84    fn insert_column(&mut self, column: u16, column_type: StructuredColumnType) {
85        let mut shifted = BTreeMap::new();
86        for (existing_column, existing_type) in std::mem::take(&mut self.columns) {
87            let shifted_column = if existing_column >= column {
88                existing_column + 1
89            } else {
90                existing_column
91            };
92            shifted.insert(shifted_column, existing_type);
93        }
94        if !matches!(column_type, StructuredColumnType::Bytes) {
95            shifted.insert(column, column_type);
96        }
97        self.columns = shifted;
98    }
99
100    fn delete_column(&mut self, column: u16) {
101        let mut shifted = BTreeMap::new();
102        for (existing_column, existing_type) in std::mem::take(&mut self.columns) {
103            if existing_column == column {
104                continue;
105            }
106            let shifted_column = if existing_column > column {
107                existing_column - 1
108            } else {
109                existing_column
110            };
111            shifted.insert(shifted_column, existing_type);
112        }
113        self.columns = shifted;
114    }
115
116    fn set_column_type(&mut self, column: u16, column_type: StructuredColumnType) {
117        if matches!(column_type, StructuredColumnType::Bytes) {
118            self.columns.remove(&column);
119        } else {
120            self.columns.insert(column, column_type);
121        }
122    }
123}
124
125impl StructuredSchema {
126    /// Returns structured column families keyed by public column-family name.
127    ///
128    /// The returned view always includes the default family, even when that family
129    /// only relies on implicit `Bytes` columns and therefore has no explicit
130    /// structured-column entries recorded internally.
131    pub fn column_families(&self) -> BTreeMap<String, StructuredColumnFamilySchema> {
132        let mut families = BTreeMap::new();
133        families.insert(
134            DEFAULT_COLUMN_FAMILY_NAME.to_string(),
135            self.column_families
136                .get(&DEFAULT_COLUMN_FAMILY_ID)
137                .cloned()
138                .unwrap_or_default(),
139        );
140        for (name, &id) in &self.column_family_ids {
141            if name == DEFAULT_COLUMN_FAMILY_NAME {
142                continue;
143            }
144            families.insert(
145                name.clone(),
146                self.column_families.get(&id).cloned().unwrap_or_default(),
147            );
148        }
149        families
150    }
151
152    pub(crate) fn resolve_column_family_id(&self, column_family: Option<&str>) -> Result<u8> {
153        match column_family {
154            None => Ok(DEFAULT_COLUMN_FAMILY_ID),
155            Some(column_family) if column_family == DEFAULT_COLUMN_FAMILY_NAME => {
156                Ok(DEFAULT_COLUMN_FAMILY_ID)
157            }
158            Some(column_family) => self
159                .column_family_ids
160                .get(column_family)
161                .copied()
162                .ok_or_else(|| {
163                    Error::IoError(format!("Unknown column family '{}'", column_family))
164                }),
165        }
166    }
167
168    pub(crate) fn projected(
169        &self,
170        column_family_id: u8,
171        column_indices: Option<&[usize]>,
172    ) -> StructuredColumnFamilySchema {
173        let columns = self
174            .column_families
175            .get(&column_family_id)
176            .map(|family| &family.columns);
177        let Some(indices) = column_indices else {
178            return columns
179                .cloned()
180                .map(|columns| StructuredColumnFamilySchema { columns })
181                .unwrap_or_default();
182        };
183        let mut projected = BTreeMap::new();
184        for (projected_idx, original_idx) in indices.iter().enumerate() {
185            if let Some(column_type) =
186                columns.and_then(|columns| columns.get(&(*original_idx as u16)))
187            {
188                projected.insert(projected_idx as u16, column_type.clone());
189            }
190        }
191        StructuredColumnFamilySchema { columns: projected }
192    }
193
194    fn insert_structured_column(
195        &mut self,
196        column_family_id: u8,
197        column: u16,
198        column_type: StructuredColumnType,
199    ) {
200        self.column_families
201            .entry(column_family_id)
202            .or_default()
203            .insert_column(column, column_type);
204    }
205
206    fn delete_structured_column(&mut self, column_family_id: u8, column: u16) {
207        self.column_families
208            .entry(column_family_id)
209            .or_default()
210            .delete_column(column);
211    }
212
213    pub(crate) fn project_structured_family(
214        &self,
215        column_family: Option<&str>,
216        column_indices: Option<&[usize]>,
217    ) -> Result<Arc<StructuredColumnFamilySchema>> {
218        let column_family_id = self.resolve_column_family_id(column_family)?;
219        Ok(Arc::new(self.projected(column_family_id, column_indices)))
220    }
221}
222
223impl Default for StructuredSchema {
224    fn default() -> Self {
225        Self {
226            column_family_ids: BTreeMap::from([(
227                DEFAULT_COLUMN_FAMILY_NAME.to_string(),
228                DEFAULT_COLUMN_FAMILY_ID,
229            )]),
230            column_families: BTreeMap::new(),
231        }
232    }
233}
234
235pub(crate) fn encode_for_write(
236    schema: &StructuredSchema,
237    column_family: Option<&str>,
238    now_seconds: u32,
239    column: u16,
240    value: StructuredColumnValue,
241    ttl_seconds: Option<u32>,
242) -> Result<Bytes> {
243    let column_family_id = schema.resolve_column_family_id(column_family)?;
244    match (
245        schema
246            .column_families
247            .get(&column_family_id)
248            .map(|family| family.structured_column_type(column))
249            .unwrap_or(&StructuredColumnType::Bytes),
250        value,
251    ) {
252        (StructuredColumnType::Bytes, StructuredColumnValue::Bytes(value)) => Ok(value),
253        (StructuredColumnType::List(config), StructuredColumnValue::List(elements)) => {
254            encode_list_for_write(elements, config, ttl_seconds, now_seconds)
255        }
256        (_, _) => Err(Error::InputError(format!(
257            "column {} expects a different type of value",
258            column,
259        ))),
260    }
261}
262
263#[cfg(feature = "ffi")]
264fn ensure_list_column(
265    schema: &StructuredSchema,
266    column_family: Option<&str>,
267    column: u16,
268) -> Result<()> {
269    let column_family_id = schema.resolve_column_family_id(column_family)?;
270    match schema
271        .column_families
272        .get(&column_family_id)
273        .map(|family| family.structured_column_type(column))
274        .unwrap_or(&StructuredColumnType::Bytes)
275    {
276        StructuredColumnType::List(_) => Ok(()),
277        StructuredColumnType::Bytes => Err(Error::InputError(format!(
278            "column {} is not a LIST column",
279            column,
280        ))),
281    }
282}
283
284pub(crate) fn ensure_bytes_column(
285    schema: &StructuredSchema,
286    column_family: Option<&str>,
287    column: u16,
288) -> Result<()> {
289    let column_family_id = schema.resolve_column_family_id(column_family)?;
290    match schema
291        .column_families
292        .get(&column_family_id)
293        .map(|family| family.structured_column_type(column))
294        .unwrap_or(&StructuredColumnType::Bytes)
295    {
296        StructuredColumnType::Bytes => Ok(()),
297        StructuredColumnType::List(_) => Err(Error::InputError(format!(
298            "column {} is not a BYTES column",
299            column,
300        ))),
301    }
302}
303
304#[cfg(feature = "ffi")]
305pub(crate) fn list_column_config(
306    schema: &StructuredSchema,
307    column_family: Option<&str>,
308    column: u16,
309) -> Result<ListConfig> {
310    let column_family_id = schema.resolve_column_family_id(column_family)?;
311    match schema
312        .column_families
313        .get(&column_family_id)
314        .map(|family| family.structured_column_type(column))
315        .unwrap_or(&StructuredColumnType::Bytes)
316    {
317        StructuredColumnType::List(config) => Ok(config.clone()),
318        StructuredColumnType::Bytes => Err(Error::InputError(format!(
319            "column {} is not a LIST column",
320            column,
321        ))),
322    }
323}
324
325pub(crate) fn decode_row(
326    schema: &StructuredColumnFamilySchema,
327    now_seconds: u32,
328    columns: Vec<Option<Bytes>>,
329) -> Result<Vec<Option<StructuredColumnValue>>> {
330    columns
331        .into_iter()
332        .enumerate()
333        .map(|(idx, column)| {
334            let Some(raw) = column else {
335                return Ok(None);
336            };
337            match schema.structured_column_type(idx as u16) {
338                StructuredColumnType::Bytes => Ok(Some(StructuredColumnValue::Bytes(raw))),
339                StructuredColumnType::List(config) => Ok(Some(StructuredColumnValue::List(
340                    decode_list_for_read(&raw, config, now_seconds)?,
341                ))),
342            }
343        })
344        .collect()
345}
346
347pub(crate) fn load_structured_schema_from_cobble_schema(
348    schema: &Schema,
349) -> Result<StructuredSchema> {
350    let mut structured_schema = StructuredSchema {
351        column_family_ids: schema.column_family_ids(),
352        ..StructuredSchema::default()
353    };
354    let column_family_ids = structured_schema.column_family_ids.clone();
355    for (column_family, num_columns) in schema.column_families() {
356        let column_family_id = column_family_ids
357            .get(&column_family)
358            .copied()
359            .ok_or_else(|| {
360                Error::FileFormatError(format!("missing column family id for {}", column_family))
361            })?;
362        let operator_ids = if column_family == DEFAULT_COLUMN_FAMILY_NAME {
363            schema.all_operator_ids()
364        } else {
365            schema.operator_ids_in_family(&column_family)?
366        };
367        let mut columns = BTreeMap::new();
368        for column_idx in 0..num_columns {
369            let operator_id = operator_ids
370                .get(column_idx)
371                .map(|s| s.as_str())
372                .unwrap_or("");
373            if operator_id == LIST_OPERATOR_ID {
374                let metadata_value = schema
375                    .column_metadata_at(Some(column_family.as_str()), column_idx)?
376                    .ok_or_else(|| {
377                        Error::FileFormatError(format!(
378                            "list column {} in column family {} missing metadata",
379                            column_idx, column_family
380                        ))
381                    })?;
382                let config = serde_json::from_value::<ListConfig>(metadata_value.clone()).map_err(
383                    |err| {
384                        Error::FileFormatError(format!(
385                            "failed to decode list config at column {} in column family {}: {}",
386                            column_idx, column_family, err
387                        ))
388                    },
389                )?;
390                columns.insert(column_idx as u16, StructuredColumnType::List(config));
391            }
392        }
393        structured_schema
394            .column_families
395            .insert(column_family_id, StructuredColumnFamilySchema { columns });
396    }
397    Ok(structured_schema)
398}
399
400pub(crate) fn combined_resolver(
401    custom: Option<Arc<dyn MergeOperatorResolver>>,
402) -> Arc<dyn MergeOperatorResolver> {
403    Arc::new(move |id: &str, metadata: Option<&JsonValue>| {
404        if let Some(operator) = list_operator_from_metadata(id, metadata) {
405            return Some(operator);
406        }
407        custom
408            .as_ref()
409            .and_then(|resolver| resolver.resolve(id, metadata))
410    })
411}
412
413/// Returns a `MergeOperatorResolver` that can resolve all structured data type
414/// merge operators (e.g. list) from their metadata.
415pub fn structured_merge_operator_resolver() -> Arc<dyn MergeOperatorResolver> {
416    combined_resolver(None)
417}
418
419/// Returns the merge operator IDs that `structured_merge_operator_resolver` can resolve.
420pub fn structured_resolvable_operator_ids() -> Vec<String> {
421    vec![LIST_OPERATOR_ID.to_string()]
422}
423
424// ── StructuredWriteBatch ────────────────────────────────────────────────────
425
426/// Structured write batch wrapper.
427///
428/// Each operation is encoded and written into the inner `cobble::WriteBatch` immediately, so we
429/// avoid a second typed-op staging buffer and an extra conversion pass at flush time.
430pub struct StructuredWriteBatch {
431    structured_schema: Arc<StructuredSchema>,
432    now_seconds: u32,
433    inner: WriteBatch,
434}
435
436impl StructuredWriteBatch {
437    pub(crate) fn new(structured_schema: Arc<StructuredSchema>, now_seconds: u32) -> Self {
438        Self {
439            structured_schema,
440            now_seconds,
441            inner: WriteBatch::new(),
442        }
443    }
444
445    pub fn put<K, V>(&mut self, bucket: u16, key: K, column: u16, value: V) -> Result<()>
446    where
447        K: AsRef<[u8]>,
448        V: Into<StructuredColumnValue>,
449    {
450        self.put_with_options(
451            bucket,
452            key,
453            column,
454            value,
455            &StructuredWriteOptions::default(),
456        )
457    }
458
459    pub fn put_with_options<K, V>(
460        &mut self,
461        bucket: u16,
462        key: K,
463        column: u16,
464        value: V,
465        options: &StructuredWriteOptions,
466    ) -> Result<()>
467    where
468        K: AsRef<[u8]>,
469        V: Into<StructuredColumnValue>,
470    {
471        let encoded = encode_for_write(
472            &self.structured_schema,
473            options.column_family(),
474            self.now_seconds,
475            column,
476            value.into(),
477            options.ttl_seconds(),
478        )?;
479        self.inner
480            .put_with_options(bucket, key, column, encoded, options.as_cobble());
481        Ok(())
482    }
483
484    #[cfg(feature = "ffi")]
485    pub(crate) fn put_borrowed_bytes_with_options<K>(
486        &mut self,
487        bucket: u16,
488        key: K,
489        column: u16,
490        value: &[u8],
491        options: &StructuredWriteOptions,
492    ) -> Result<()>
493    where
494        K: AsRef<[u8]>,
495    {
496        ensure_bytes_column(&self.structured_schema, options.column_family(), column)?;
497        self.inner
498            .put_with_options(bucket, key, column, value, options.as_cobble());
499        Ok(())
500    }
501
502    #[cfg(feature = "ffi")]
503    pub(crate) fn put_borrowed_list_with_options<K>(
504        &mut self,
505        bucket: u16,
506        key: K,
507        column: u16,
508        elements: &[&[u8]],
509        options: &StructuredWriteOptions,
510    ) -> Result<()>
511    where
512        K: AsRef<[u8]>,
513    {
514        let config = list_column_config(&self.structured_schema, options.column_family(), column)?;
515        let encoded = encode_borrowed_list_for_write(
516            elements,
517            &config,
518            options.ttl_seconds(),
519            self.now_seconds,
520        )?;
521        self.inner
522            .put_with_options(bucket, key, column, encoded, options.as_cobble());
523        Ok(())
524    }
525
526    pub fn delete<K>(&mut self, bucket: u16, key: K, column: u16)
527    where
528        K: AsRef<[u8]>,
529    {
530        self.delete_with_options(bucket, key, column, &StructuredWriteOptions::default());
531    }
532
533    pub fn delete_with_options<K>(
534        &mut self,
535        bucket: u16,
536        key: K,
537        column: u16,
538        options: &StructuredWriteOptions,
539    ) where
540        K: AsRef<[u8]>,
541    {
542        self.inner
543            .delete_with_options(bucket, key, column, options.as_cobble());
544    }
545
546    pub fn merge<K, V>(&mut self, bucket: u16, key: K, column: u16, value: V) -> Result<()>
547    where
548        K: AsRef<[u8]>,
549        V: Into<StructuredColumnValue>,
550    {
551        self.merge_with_options(
552            bucket,
553            key,
554            column,
555            value,
556            &StructuredWriteOptions::default(),
557        )
558    }
559
560    pub fn merge_with_options<K, V>(
561        &mut self,
562        bucket: u16,
563        key: K,
564        column: u16,
565        value: V,
566        options: &StructuredWriteOptions,
567    ) -> Result<()>
568    where
569        K: AsRef<[u8]>,
570        V: Into<StructuredColumnValue>,
571    {
572        let encoded = encode_for_write(
573            &self.structured_schema,
574            options.column_family(),
575            self.now_seconds,
576            column,
577            value.into(),
578            options.ttl_seconds(),
579        )?;
580        self.inner
581            .merge_with_options(bucket, key, column, encoded, options.as_cobble());
582        Ok(())
583    }
584
585    #[cfg(feature = "ffi")]
586    pub(crate) fn merge_borrowed_bytes_with_options<K>(
587        &mut self,
588        bucket: u16,
589        key: K,
590        column: u16,
591        value: &[u8],
592        options: &StructuredWriteOptions,
593    ) -> Result<()>
594    where
595        K: AsRef<[u8]>,
596    {
597        ensure_bytes_column(&self.structured_schema, options.column_family(), column)?;
598        self.inner
599            .merge_with_options(bucket, key, column, value, options.as_cobble());
600        Ok(())
601    }
602
603    #[cfg(feature = "ffi")]
604    pub(crate) fn merge_borrowed_list_with_options<K>(
605        &mut self,
606        bucket: u16,
607        key: K,
608        column: u16,
609        elements: &[&[u8]],
610        options: &StructuredWriteOptions,
611    ) -> Result<()>
612    where
613        K: AsRef<[u8]>,
614    {
615        let config = list_column_config(&self.structured_schema, options.column_family(), column)?;
616        let encoded = encode_borrowed_list_for_write(
617            elements,
618            &config,
619            options.ttl_seconds(),
620            self.now_seconds,
621        )?;
622        self.inner
623            .merge_with_options(bucket, key, column, encoded, options.as_cobble());
624        Ok(())
625    }
626
627    pub(crate) fn into_inner(self) -> WriteBatch {
628        self.inner
629    }
630}
631
632// ── StructuredColumnValue conversions ───────────────────────────────────────
633
634impl From<Bytes> for StructuredColumnValue {
635    fn from(value: Bytes) -> Self {
636        Self::Bytes(value)
637    }
638}
639
640impl From<Vec<u8>> for StructuredColumnValue {
641    fn from(value: Vec<u8>) -> Self {
642        Self::Bytes(Bytes::from(value))
643    }
644}
645
646impl From<Vec<Bytes>> for StructuredColumnValue {
647    fn from(value: Vec<Bytes>) -> Self {
648        Self::List(value)
649    }
650}
651
652impl From<Vec<Vec<u8>>> for StructuredColumnValue {
653    fn from(value: Vec<Vec<u8>>) -> Self {
654        Self::List(value.into_iter().map(Bytes::from).collect())
655    }
656}
657
658// ── StructuredDb (formerly DataStructureDb) ─────────────────────────────────
659
660pub struct StructuredDb {
661    db: Db,
662    structured_schema: Arc<StructuredSchema>,
663    default_write_options: StructuredWriteOptions,
664    default_read_options: StructuredReadOptions,
665    default_scan_options: StructuredScanOptions,
666}
667
668/// Builder for opening a structured database with runtime schema wiring.
669pub struct StructuredDbBuilder {
670    inner: DbBuilder,
671}
672
673impl StructuredDbBuilder {
674    pub fn new(config: Config) -> Self {
675        Self {
676            inner: DbBuilder::new(config).merge_operator_resolver(combined_resolver(None)),
677        }
678    }
679
680    pub fn bucket_ranges(mut self, bucket_ranges: Vec<RangeInclusive<u16>>) -> Self {
681        self.inner = self.inner.bucket_ranges(bucket_ranges);
682        self
683    }
684
685    pub fn db_id(mut self, db_id: impl Into<String>) -> Self {
686        self.inner = self.inner.db_id(db_id);
687        self
688    }
689
690    pub fn merge_operator_resolver(mut self, resolver: Arc<dyn MergeOperatorResolver>) -> Self {
691        self.inner = self
692            .inner
693            .merge_operator_resolver(combined_resolver(Some(resolver)));
694        self
695    }
696
697    /// Register a factory for raw single-column transform specifications.
698    pub fn register_schema_transform<F, T>(
699        mut self,
700        transform_type: impl Into<String>,
701        factory: F,
702    ) -> Result<Self>
703    where
704        F: Fn(&[u8]) -> Result<T> + Send + Sync + 'static,
705        T: Fn(Option<Bytes>) -> Result<Option<Bytes>> + Send + Sync + 'static,
706    {
707        self.inner = self
708            .inner
709            .register_schema_transform(transform_type, factory)?;
710        Ok(self)
711    }
712
713    pub fn open(self) -> Result<StructuredDb> {
714        StructuredDb::from_db(self.inner.open()?)
715    }
716
717    pub fn open_from_snapshot(self, snapshot_id: u64) -> Result<StructuredDb> {
718        StructuredDb::from_db(self.inner.open_from_snapshot(snapshot_id)?)
719    }
720
721    pub fn open_from_snapshot_with_recovery_mode(
722        self,
723        snapshot_id: u64,
724        recovery_mode: RecoveryMode,
725    ) -> Result<StructuredDb> {
726        StructuredDb::from_db(
727            self.inner
728                .open_from_snapshot_with_recovery_mode(snapshot_id, recovery_mode)?,
729        )
730    }
731
732    pub fn resume(self) -> Result<StructuredDb> {
733        StructuredDb::from_db(self.inner.resume()?)
734    }
735
736    pub fn resume_with_recovery_mode(self, recovery_mode: RecoveryMode) -> Result<StructuredDb> {
737        StructuredDb::from_db(self.inner.resume_with_recovery_mode(recovery_mode)?)
738    }
739
740    pub fn resume_from_snapshot(self, snapshot_id: u64) -> Result<StructuredDb> {
741        StructuredDb::from_db(self.inner.resume_from_snapshot(snapshot_id)?)
742    }
743
744    pub fn resume_from_snapshot_with_recovery_mode(
745        self,
746        snapshot_id: u64,
747        recovery_mode: RecoveryMode,
748    ) -> Result<StructuredDb> {
749        StructuredDb::from_db(
750            self.inner
751                .resume_from_snapshot_with_recovery_mode(snapshot_id, recovery_mode)?,
752        )
753    }
754}
755
756#[derive(Clone, Debug)]
757struct StructuredProjectionCacheEntry {
758    schema: Arc<StructuredSchema>,
759    projected_schema: Arc<StructuredColumnFamilySchema>,
760}
761
762fn resolve_structured_projection_cached(
763    cache: &Arc<ArcSwapOption<StructuredProjectionCacheEntry>>,
764    structured_schema: &Arc<StructuredSchema>,
765    column_family: Option<&str>,
766    column_indices: Option<&[usize]>,
767) -> Result<Arc<StructuredColumnFamilySchema>> {
768    if let Some(entry) = cache.load_full()
769        && Arc::ptr_eq(&entry.schema, structured_schema)
770    {
771        return Ok(Arc::clone(&entry.projected_schema));
772    }
773    let projected = structured_schema.project_structured_family(column_family, column_indices)?;
774    cache.store(Some(Arc::new(StructuredProjectionCacheEntry {
775        schema: Arc::clone(structured_schema),
776        projected_schema: Arc::clone(&projected),
777    })));
778    Ok(projected)
779}
780
781#[derive(Clone, Debug, Default)]
782pub struct StructuredWriteOptions {
783    inner: WriteOptions,
784}
785
786impl StructuredWriteOptions {
787    pub fn with_ttl(ttl_seconds: u32) -> Self {
788        Self {
789            inner: WriteOptions::with_ttl(ttl_seconds),
790        }
791    }
792
793    pub fn with_column_family(column_family: impl Into<String>) -> Self {
794        Self {
795            inner: WriteOptions::with_column_family(column_family),
796        }
797    }
798
799    pub fn with_await_durable(mut self, await_durable: bool) -> Self {
800        self.inner = self.inner.with_await_durable(await_durable);
801        self
802    }
803
804    pub fn as_cobble(&self) -> &WriteOptions {
805        &self.inner
806    }
807
808    pub fn into_cobble(self) -> WriteOptions {
809        self.inner
810    }
811
812    pub fn ttl_seconds(&self) -> Option<u32> {
813        self.inner.ttl_seconds
814    }
815
816    pub fn column_family(&self) -> Option<&str> {
817        self.inner.column_family.as_deref()
818    }
819
820    pub fn await_durable(&self) -> bool {
821        self.inner.await_durable
822    }
823}
824
825impl From<WriteOptions> for StructuredWriteOptions {
826    fn from(value: WriteOptions) -> Self {
827        Self { inner: value }
828    }
829}
830
831impl From<StructuredWriteOptions> for WriteOptions {
832    fn from(value: StructuredWriteOptions) -> Self {
833        value.inner
834    }
835}
836
837#[derive(Clone, Debug)]
838pub struct StructuredReadOptions {
839    inner: ReadOptions,
840    projected_schema_cache: Arc<ArcSwapOption<StructuredProjectionCacheEntry>>,
841}
842
843impl Default for StructuredReadOptions {
844    fn default() -> Self {
845        Self::from(ReadOptions::default())
846    }
847}
848
849impl StructuredReadOptions {
850    pub fn for_column(column_index: usize) -> Self {
851        Self::from(ReadOptions::for_column(column_index))
852    }
853
854    pub fn for_columns(column_indices: Vec<usize>) -> Self {
855        Self::from(ReadOptions::for_columns(column_indices))
856    }
857
858    pub fn for_column_in_family(column_family: impl Into<String>, column_index: usize) -> Self {
859        Self::from(ReadOptions::for_column_in_family(
860            column_family,
861            column_index,
862        ))
863    }
864
865    pub fn for_columns_in_family(
866        column_family: impl Into<String>,
867        column_indices: Vec<usize>,
868    ) -> Self {
869        Self::from(ReadOptions::for_columns_in_family(
870            column_family,
871            column_indices,
872        ))
873    }
874
875    pub fn with_column_family(mut self, column_family: impl Into<String>) -> Self {
876        self.inner = self.inner.with_column_family(column_family);
877        self.projected_schema_cache = Arc::new(ArcSwapOption::empty());
878        self
879    }
880
881    pub fn as_cobble(&self) -> &ReadOptions {
882        &self.inner
883    }
884
885    pub fn into_cobble(self) -> ReadOptions {
886        self.inner
887    }
888
889    pub(crate) fn resolve_projected_schema_cached(
890        &self,
891        structured_schema: &Arc<StructuredSchema>,
892    ) -> Result<Arc<StructuredColumnFamilySchema>> {
893        resolve_structured_projection_cached(
894            &self.projected_schema_cache,
895            structured_schema,
896            self.inner.column_family.as_deref(),
897            self.inner.column_indices.as_deref(),
898        )
899    }
900}
901
902impl From<ReadOptions> for StructuredReadOptions {
903    fn from(value: ReadOptions) -> Self {
904        Self {
905            inner: value,
906            projected_schema_cache: Arc::new(ArcSwapOption::empty()),
907        }
908    }
909}
910
911impl From<StructuredReadOptions> for ReadOptions {
912    fn from(value: StructuredReadOptions) -> Self {
913        value.inner
914    }
915}
916
917#[derive(Clone, Debug)]
918pub struct StructuredScanOptions {
919    inner: ScanOptions,
920    projected_schema_cache: Arc<ArcSwapOption<StructuredProjectionCacheEntry>>,
921}
922
923impl Default for StructuredScanOptions {
924    fn default() -> Self {
925        Self::from(ScanOptions::default())
926    }
927}
928
929impl StructuredScanOptions {
930    pub fn for_column(column_index: usize) -> Self {
931        Self::from(ScanOptions::for_column(column_index))
932    }
933
934    pub fn for_columns(column_indices: Vec<usize>) -> Self {
935        Self::from(ScanOptions::for_columns(column_indices))
936    }
937
938    pub fn with_column_family(mut self, column_family: impl Into<String>) -> Self {
939        self.inner = self.inner.with_column_family(column_family);
940        self.projected_schema_cache = Arc::new(ArcSwapOption::empty());
941        self
942    }
943
944    pub fn with_preload_scan_cursor_block(mut self, enabled: bool) -> Self {
945        self.inner = self.inner.with_preload_scan_cursor_block(enabled);
946        self
947    }
948
949    /// Structured wrapper around `ScanOptions::with_stop_at_block_boundary`.
950    ///
951    /// When enabled, scans pause after crossing the next physical storage
952    /// boundary and expose that pause through the underlying iterator's
953    /// `stopped_at_block_boundary()` signal. Callers can then return the
954    /// partial batch, clear the stop, and continue scanning from the same
955    /// position.
956    pub fn with_stop_at_block_boundary(mut self, enabled: bool) -> Self {
957        self.inner = self.inner.with_stop_at_block_boundary(enabled);
958        self
959    }
960
961    pub fn as_cobble(&self) -> &ScanOptions {
962        &self.inner
963    }
964
965    pub fn into_cobble(self) -> ScanOptions {
966        self.inner
967    }
968
969    pub(crate) fn resolve_projected_schema_cached(
970        &self,
971        structured_schema: &Arc<StructuredSchema>,
972    ) -> Result<Arc<StructuredColumnFamilySchema>> {
973        resolve_structured_projection_cached(
974            &self.projected_schema_cache,
975            structured_schema,
976            self.inner.column_family.as_deref(),
977            self.inner.column_indices.as_deref(),
978        )
979    }
980}
981
982impl From<ScanOptions> for StructuredScanOptions {
983    fn from(value: ScanOptions) -> Self {
984        Self {
985            inner: value,
986            projected_schema_cache: Arc::new(ArcSwapOption::empty()),
987        }
988    }
989}
990
991impl From<StructuredScanOptions> for ScanOptions {
992    fn from(value: StructuredScanOptions) -> Self {
993        value.inner
994    }
995}
996
997pub trait StructuredSchemaOwner {
998    fn current_structured_schema(&self) -> StructuredSchema;
999    fn begin_core_schema_update(&self) -> SchemaBuilder;
1000    fn install_committed_structured_schema(&mut self, schema: StructuredSchema)
1001    -> StructuredSchema;
1002}
1003
1004pub struct StructuredSchemaBuilder<'a, O: StructuredSchemaOwner> {
1005    owner: &'a mut O,
1006    schema: StructuredSchema,
1007    inner: Option<SchemaBuilder>,
1008    pending_error: Option<Error>,
1009}
1010
1011impl<'a, O: StructuredSchemaOwner> StructuredSchemaBuilder<'a, O> {
1012    pub fn new(owner: &'a mut O) -> Self {
1013        let schema = owner.current_structured_schema();
1014        let inner = owner.begin_core_schema_update();
1015        Self {
1016            owner,
1017            schema,
1018            inner: Some(inner),
1019            pending_error: None,
1020        }
1021    }
1022
1023    pub fn add_bytes_column(&mut self, column_family: Option<String>, column: u16) -> &mut Self {
1024        let Some((column_family_name, core_column_family)) =
1025            self.normalize_column_family_or_record_error(column_family)
1026        else {
1027            return self;
1028        };
1029        let Some(column_family_id) = self.apply_inner(|inner| {
1030            let column_family_id = match core_column_family.as_ref() {
1031                Some(column_family) => inner.ensure_column_family_exists(column_family.clone())?,
1032                None => DEFAULT_COLUMN_FAMILY_ID,
1033            };
1034            inner.add_column(
1035                column as usize,
1036                Some(Arc::new(BytesMergeOperator)),
1037                None,
1038                core_column_family.clone(),
1039            )?;
1040            Ok(column_family_id)
1041        }) else {
1042            return self;
1043        };
1044        self.sync_structured_column_family_id(&column_family_name, column_family_id);
1045        if let Some(column_family_id) =
1046            self.structured_column_family_id_or_record_error(&column_family_name)
1047        {
1048            self.schema.insert_structured_column(
1049                column_family_id,
1050                column,
1051                StructuredColumnType::Bytes,
1052            );
1053        }
1054        self
1055    }
1056
1057    pub fn add_list_column(
1058        &mut self,
1059        column_family: Option<String>,
1060        column: u16,
1061        config: ListConfig,
1062    ) -> &mut Self {
1063        let Some((column_family_name, core_column_family)) =
1064            self.normalize_column_family_or_record_error(column_family)
1065        else {
1066            return self;
1067        };
1068        let Some(column_family_id) = self.apply_inner(|inner| {
1069            let column_family_id = match core_column_family.as_ref() {
1070                Some(column_family) => inner.ensure_column_family_exists(column_family.clone())?,
1071                None => DEFAULT_COLUMN_FAMILY_ID,
1072            };
1073            inner.add_column(
1074                column as usize,
1075                Some(list_operator(config.clone())),
1076                None,
1077                core_column_family.clone(),
1078            )?;
1079            Ok(column_family_id)
1080        }) else {
1081            return self;
1082        };
1083        self.sync_structured_column_family_id(&column_family_name, column_family_id);
1084        if let Some(column_family_id) =
1085            self.structured_column_family_id_or_record_error(&column_family_name)
1086        {
1087            self.schema.insert_structured_column(
1088                column_family_id,
1089                column,
1090                StructuredColumnType::List(config),
1091            );
1092        }
1093        self
1094    }
1095
1096    pub fn delete_column(&mut self, column_family: Option<String>, column: u16) -> &mut Self {
1097        let Some((column_family_name, core_column_family)) =
1098            self.normalize_column_family_or_record_error(column_family)
1099        else {
1100            return self;
1101        };
1102        let Some(()) = self
1103            .apply_inner(|inner| inner.delete_column(core_column_family.clone(), column as usize))
1104        else {
1105            return self;
1106        };
1107        if let Some(column_family_id) =
1108            self.structured_column_family_id_or_record_error(&column_family_name)
1109        {
1110            self.schema
1111                .delete_structured_column(column_family_id, column);
1112        }
1113        self
1114    }
1115
1116    /// Transform one existing column into `target_type` during this schema transition.
1117    ///
1118    /// The factory registered for `transform_id` receives an empty specification and
1119    /// returns a callback for the source column's raw optional bytes. The callback must
1120    /// return bytes valid for `target_type`. In particular,
1121    /// list transforms must preserve or produce Cobble's complete encoded list
1122    /// payload, including any element TTL information.
1123    pub fn transform_column(
1124        &mut self,
1125        column_family: Option<String>,
1126        column: u16,
1127        target_type: StructuredColumnType,
1128        transform_id: impl Into<String>,
1129    ) -> &mut Self {
1130        let Some((column_family_name, core_column_family)) =
1131            self.normalize_column_family_or_record_error(column_family)
1132        else {
1133            return self;
1134        };
1135        let transform_id = transform_id.into();
1136        let Some(()) = self.apply_inner(|inner| {
1137            let column_count = inner.num_columns_in_family(core_column_family.clone())?;
1138            let column_index = usize::from(column);
1139            if column_index >= column_count {
1140                return Err(Error::InvalidState(format!(
1141                    "Cannot transform column {} in column family {} with {} columns",
1142                    column, column_family_name, column_count
1143                )));
1144            }
1145            let columns = (0..column_count)
1146                .map(|source_index| ColumnEvolution::Source {
1147                    source_index,
1148                    transform: (source_index == column_index).then(|| TransformSpec {
1149                        transform_type: transform_id.clone(),
1150                        spec: Bytes::new(),
1151                    }),
1152                })
1153                .collect();
1154            inner.remap_columns(core_column_family.clone(), columns)?;
1155            apply_structured_column_type(
1156                inner,
1157                core_column_family.clone(),
1158                column_index,
1159                &target_type,
1160            )
1161        }) else {
1162            return self;
1163        };
1164        let Some(column_family_id) =
1165            self.structured_column_family_id_or_record_error(&column_family_name)
1166        else {
1167            return self;
1168        };
1169        self.schema
1170            .column_families
1171            .entry(column_family_id)
1172            .or_default()
1173            .set_column_type(column, target_type);
1174        self
1175    }
1176
1177    pub fn set_column_family_options(
1178        &mut self,
1179        column_family: Option<String>,
1180        options: cobble::ColumnFamilyOptions,
1181    ) -> &mut Self {
1182        let Some((column_family_name, core_column_family)) =
1183            self.normalize_column_family_or_record_error(column_family)
1184        else {
1185            return self;
1186        };
1187        let Some(column_family_id) = self.apply_inner(|inner| {
1188            let column_family_id = match core_column_family.as_ref() {
1189                Some(column_family) => inner.ensure_column_family_exists(column_family.clone())?,
1190                None => DEFAULT_COLUMN_FAMILY_ID,
1191            };
1192            inner.set_column_family_options(core_column_family.clone(), options.clone())?;
1193            Ok(column_family_id)
1194        }) else {
1195            return self;
1196        };
1197        self.sync_structured_column_family_id(&column_family_name, column_family_id);
1198        self
1199    }
1200
1201    pub fn current_schema(&self) -> &StructuredSchema {
1202        &self.schema
1203    }
1204
1205    fn ensure_structured_column_family_id(&self, column_family: &str) -> Result<u8> {
1206        if column_family == DEFAULT_COLUMN_FAMILY_NAME {
1207            return Ok(DEFAULT_COLUMN_FAMILY_ID);
1208        }
1209        self.schema
1210            .column_family_ids
1211            .get(column_family)
1212            .copied()
1213            .ok_or_else(|| {
1214                Error::InvalidState(format!(
1215                    "structured schema missing column family id for {}",
1216                    column_family
1217                ))
1218            })
1219    }
1220
1221    fn sync_structured_column_family_id(&mut self, column_family: &str, column_family_id: u8) {
1222        if column_family == DEFAULT_COLUMN_FAMILY_NAME {
1223            return;
1224        }
1225        self.schema
1226            .column_family_ids
1227            .insert(column_family.to_string(), column_family_id);
1228        self.schema
1229            .column_families
1230            .entry(column_family_id)
1231            .or_default();
1232    }
1233
1234    fn normalize_column_family_or_record_error(
1235        &mut self,
1236        column_family: Option<String>,
1237    ) -> Option<(String, Option<String>)> {
1238        match normalize_structured_column_family_name(column_family) {
1239            Ok(names) => Some(names),
1240            Err(err) => {
1241                self.pending_error = Some(err);
1242                None
1243            }
1244        }
1245    }
1246
1247    fn structured_column_family_id_or_record_error(&mut self, column_family: &str) -> Option<u8> {
1248        match self.ensure_structured_column_family_id(column_family) {
1249            Ok(column_family_id) => Some(column_family_id),
1250            Err(err) => {
1251                self.pending_error = Some(err);
1252                None
1253            }
1254        }
1255    }
1256
1257    pub fn commit(&mut self) -> Result<StructuredSchema> {
1258        if let Some(err) = self.pending_error.take() {
1259            // Drop the inner schema builder immediately so its DB access guard
1260            // is released even when commit fails.
1261            self.inner.take();
1262            return Err(err);
1263        }
1264        let inner = self
1265            .inner
1266            .take()
1267            .ok_or_else(|| Error::InvalidState("schema builder already committed".to_string()))?;
1268        inner.commit();
1269        Ok(self
1270            .owner
1271            .install_committed_structured_schema(self.schema.clone()))
1272    }
1273
1274    fn apply_inner<T, F>(&mut self, f: F) -> Option<T>
1275    where
1276        F: FnOnce(&mut SchemaBuilder) -> Result<T>,
1277    {
1278        if self.pending_error.is_some() {
1279            return None;
1280        }
1281        let Some(inner) = self.inner.as_mut() else {
1282            self.pending_error = Some(Error::InvalidState(
1283                "schema builder already committed".to_string(),
1284            ));
1285            return None;
1286        };
1287        match f(inner) {
1288            Ok(value) => Some(value),
1289            Err(err) => {
1290                self.pending_error = Some(err);
1291                None
1292            }
1293        }
1294    }
1295}
1296
1297fn normalize_structured_column_family_name(
1298    column_family: Option<String>,
1299) -> Result<(String, Option<String>)> {
1300    let normalized = match column_family {
1301        Some(column_family) => {
1302            let normalized = column_family.trim().to_string();
1303            if normalized.is_empty() {
1304                return Err(Error::InvalidState(
1305                    "column family name cannot be empty".to_string(),
1306                ));
1307            }
1308            normalized
1309        }
1310        None => DEFAULT_COLUMN_FAMILY_NAME.to_string(),
1311    };
1312    let core_column_family = if normalized == DEFAULT_COLUMN_FAMILY_NAME {
1313        None
1314    } else {
1315        Some(normalized.clone())
1316    };
1317    Ok((normalized, core_column_family))
1318}
1319
1320impl StructuredDb {
1321    fn from_db(db: Db) -> Result<Self> {
1322        let structured_schema = Arc::new(load_structured_schema_from_cobble_schema(
1323            &db.current_schema(),
1324        )?);
1325        Ok(Self {
1326            db,
1327            structured_schema,
1328            default_write_options: StructuredWriteOptions::default(),
1329            default_read_options: StructuredReadOptions::default(),
1330            default_scan_options: StructuredScanOptions::default(),
1331        })
1332    }
1333
1334    fn reset_default_options(&mut self) {
1335        self.default_write_options = StructuredWriteOptions::default();
1336        self.default_read_options = StructuredReadOptions::default();
1337        self.default_scan_options = StructuredScanOptions::default();
1338    }
1339
1340    pub fn open(config: Config, bucket_ranges: Vec<RangeInclusive<u16>>) -> Result<Self> {
1341        Self::from_db(Db::open(config, bucket_ranges)?)
1342    }
1343
1344    pub fn open_from_snapshot(
1345        config: Config,
1346        snapshot_id: u64,
1347        db_id: impl Into<String>,
1348    ) -> Result<Self> {
1349        Self::open_from_snapshot_with_resolver(config, snapshot_id, db_id, None)
1350    }
1351
1352    pub fn open_from_snapshot_with_resolver(
1353        config: Config,
1354        snapshot_id: u64,
1355        db_id: impl Into<String>,
1356        resolver: Option<Arc<dyn MergeOperatorResolver>>,
1357    ) -> Result<Self> {
1358        Self::from_db(Db::open_from_snapshot_with_resolver(
1359            config,
1360            snapshot_id,
1361            db_id,
1362            Some(combined_resolver(resolver)),
1363        )?)
1364    }
1365
1366    /// Open a selected snapshot using the requested WAL recovery behavior.
1367    pub fn open_from_snapshot_with_recovery_mode(
1368        config: Config,
1369        snapshot_id: u64,
1370        db_id: impl Into<String>,
1371        recovery_mode: RecoveryMode,
1372    ) -> Result<Self> {
1373        Self::from_db(Db::open_from_snapshot_with_recovery_mode_and_resolver(
1374            config,
1375            snapshot_id,
1376            db_id,
1377            recovery_mode,
1378            Some(combined_resolver(None)),
1379        )?)
1380    }
1381
1382    pub fn open_new_with_snapshot(
1383        config: Config,
1384        snapshot_id: u64,
1385        db_id: impl Into<String>,
1386    ) -> Result<Self> {
1387        Self::open_new_with_snapshot_with_resolver(config, snapshot_id, db_id, None)
1388    }
1389
1390    pub fn open_new_with_snapshot_with_resolver(
1391        config: Config,
1392        snapshot_id: u64,
1393        db_id: impl Into<String>,
1394        resolver: Option<Arc<dyn MergeOperatorResolver>>,
1395    ) -> Result<Self> {
1396        let db_id = db_id.into();
1397        Self::from_db(Db::open_new_with_snapshot_with_resolver(
1398            config,
1399            snapshot_id,
1400            &db_id,
1401            Some(combined_resolver(resolver)),
1402        )?)
1403    }
1404
1405    pub fn open_new_with_manifest_path(
1406        config: Config,
1407        manifest_path: impl Into<String>,
1408    ) -> Result<Self> {
1409        Self::open_new_with_manifest_path_with_resolver(config, manifest_path, None)
1410    }
1411
1412    pub fn open_new_with_manifest_path_with_resolver(
1413        config: Config,
1414        manifest_path: impl Into<String>,
1415        resolver: Option<Arc<dyn MergeOperatorResolver>>,
1416    ) -> Result<Self> {
1417        Self::from_db(Db::open_new_with_manifest_path_with_resolver(
1418            config,
1419            manifest_path,
1420            Some(combined_resolver(resolver)),
1421        )?)
1422    }
1423
1424    pub fn resume(config: Config, db_id: impl Into<String>) -> Result<Self> {
1425        Self::resume_with_resolver(config, db_id, None)
1426    }
1427
1428    pub fn resume_with_resolver(
1429        config: Config,
1430        db_id: impl Into<String>,
1431        resolver: Option<Arc<dyn MergeOperatorResolver>>,
1432    ) -> Result<Self> {
1433        Self::from_db(Db::resume_with_resolver(
1434            config,
1435            db_id,
1436            Some(combined_resolver(resolver)),
1437        )?)
1438    }
1439
1440    /// Resume the latest snapshot using the requested WAL recovery behavior.
1441    pub fn resume_with_recovery_mode(
1442        config: Config,
1443        db_id: impl Into<String>,
1444        recovery_mode: RecoveryMode,
1445    ) -> Result<Self> {
1446        Self::from_db(Db::resume_with_recovery_mode_and_resolver(
1447            config,
1448            db_id,
1449            recovery_mode,
1450            Some(combined_resolver(None)),
1451        )?)
1452    }
1453
1454    /// Resume an exact historical snapshot without replaying WAL.
1455    pub fn resume_from_snapshot(
1456        config: Config,
1457        snapshot_id: u64,
1458        db_id: impl Into<String>,
1459    ) -> Result<Self> {
1460        Self::resume_from_snapshot_with_resolver(config, snapshot_id, db_id, None)
1461    }
1462
1463    /// Resume an exact historical snapshot with a custom merge-operator resolver.
1464    pub fn resume_from_snapshot_with_resolver(
1465        config: Config,
1466        snapshot_id: u64,
1467        db_id: impl Into<String>,
1468        resolver: Option<Arc<dyn MergeOperatorResolver>>,
1469    ) -> Result<Self> {
1470        Self::from_db(Db::resume_from_snapshot_with_resolver(
1471            config,
1472            snapshot_id,
1473            db_id,
1474            Some(combined_resolver(resolver)),
1475        )?)
1476    }
1477
1478    /// Resume a selected snapshot using the requested WAL recovery behavior.
1479    pub fn resume_from_snapshot_with_recovery_mode(
1480        config: Config,
1481        snapshot_id: u64,
1482        db_id: impl Into<String>,
1483        recovery_mode: RecoveryMode,
1484    ) -> Result<Self> {
1485        Self::from_db(Db::resume_from_snapshot_with_recovery_mode_and_resolver(
1486            config,
1487            snapshot_id,
1488            db_id,
1489            recovery_mode,
1490            Some(combined_resolver(None)),
1491        )?)
1492    }
1493
1494    pub fn expand_bucket(
1495        &self,
1496        source_db_id: impl Into<String>,
1497        snapshot_id: Option<u64>,
1498        ranges: Option<Vec<RangeInclusive<u16>>>,
1499    ) -> Result<u64> {
1500        self.db.expand_bucket(source_db_id, snapshot_id, ranges)
1501    }
1502
1503    pub fn expand_bucket_with_storage_mode(
1504        &self,
1505        source_db_id: impl Into<String>,
1506        snapshot_id: Option<u64>,
1507        ranges: Option<Vec<RangeInclusive<u16>>>,
1508        storage_mode: cobble::ExpandStorageMode,
1509    ) -> Result<u64> {
1510        self.db
1511            .expand_bucket_with_storage_mode(source_db_id, snapshot_id, ranges, storage_mode)
1512    }
1513
1514    pub fn wait_for_expand_adoption(&self, timeout: std::time::Duration) -> Result<()> {
1515        self.db.wait_for_expand_adoption(timeout)
1516    }
1517
1518    pub fn shrink_bucket(&self, ranges: Vec<RangeInclusive<u16>>) -> Result<u64> {
1519        self.db.shrink_bucket(ranges)
1520    }
1521
1522    pub fn id(&self) -> &str {
1523        self.db.id()
1524    }
1525
1526    /// Returns the native Cobble metric samples for this structured database.
1527    pub fn metrics(&self) -> Vec<cobble::MetricSample> {
1528        self.db.metrics()
1529    }
1530
1531    pub fn current_schema(&self) -> StructuredSchema {
1532        self.structured_schema.as_ref().clone()
1533    }
1534
1535    /// Register a factory for raw single-column transform specifications.
1536    pub fn register_schema_transform<F, T>(
1537        &self,
1538        transform_type: impl Into<String>,
1539        factory: F,
1540    ) -> Result<()>
1541    where
1542        F: Fn(&[u8]) -> Result<T> + Send + Sync + 'static,
1543        T: Fn(Option<Bytes>) -> Result<Option<Bytes>> + Send + Sync + 'static,
1544    {
1545        self.db.register_schema_transform(transform_type, factory)
1546    }
1547
1548    pub fn update_schema(&mut self) -> StructuredSchemaBuilder<'_, Self> {
1549        StructuredSchemaBuilder::new(self)
1550    }
1551
1552    pub fn new_priority_queue<'a>(
1553        &'a mut self,
1554        name: impl Into<String>,
1555    ) -> Result<PriorityQueue<'a>> {
1556        let normalized_name = priority_queue_column_family_name(name.into())?;
1557        if self
1558            .db
1559            .current_schema()
1560            .column_family_ids()
1561            .contains_key(normalized_name.as_str())
1562        {
1563            return Err(Error::InvalidState(format!(
1564                "priority queue '{}' already exists",
1565                normalized_name
1566            )));
1567        }
1568
1569        let mut builder = self.update_schema();
1570        builder.add_bytes_column(Some(normalized_name.clone()), 0);
1571        builder.set_column_family_options(
1572            Some(normalized_name.clone()),
1573            priority_queue_column_family_options(),
1574        );
1575        builder.commit()?;
1576        let column_family_id = self
1577            .current_schema()
1578            .resolve_column_family_id(Some(normalized_name.as_str()))?;
1579
1580        Ok(PriorityQueue::from_column_family(
1581            self,
1582            normalized_name,
1583            column_family_id,
1584        ))
1585    }
1586
1587    pub fn get_priority_queue<'a>(&'a self, name: impl Into<String>) -> Result<PriorityQueue<'a>> {
1588        let normalized_name = priority_queue_column_family_name(name.into())?;
1589        let column_family_id = validate_priority_queue_column_family(
1590            self.db.current_schema().as_ref(),
1591            normalized_name.as_str(),
1592        )?;
1593        Ok(PriorityQueue::from_column_family(
1594            self,
1595            normalized_name,
1596            column_family_id,
1597        ))
1598    }
1599
1600    pub fn get_or_new_priority_queue<'a>(
1601        &'a mut self,
1602        name: impl Into<String>,
1603    ) -> Result<PriorityQueue<'a>> {
1604        let normalized_name = priority_queue_column_family_name(name.into())?;
1605        if self
1606            .db
1607            .current_schema()
1608            .column_family_ids()
1609            .contains_key(normalized_name.as_str())
1610        {
1611            let column_family_id = validate_priority_queue_column_family(
1612                self.db.current_schema().as_ref(),
1613                normalized_name.as_str(),
1614            )?;
1615            return Ok(PriorityQueue::from_column_family(
1616                self,
1617                normalized_name,
1618                column_family_id,
1619            ));
1620        } else {
1621            let mut builder = self.update_schema();
1622            builder.add_bytes_column(Some(normalized_name.clone()), 0);
1623            builder.set_column_family_options(
1624                Some(normalized_name.clone()),
1625                priority_queue_column_family_options(),
1626            );
1627            builder.commit()?;
1628        }
1629        let column_family_id = self
1630            .current_schema()
1631            .resolve_column_family_id(Some(normalized_name.as_str()))?;
1632
1633        Ok(PriorityQueue::from_column_family(
1634            self,
1635            normalized_name,
1636            column_family_id,
1637        ))
1638    }
1639
1640    pub fn reload_schema(&mut self) -> Result<()> {
1641        let schema = load_structured_schema_from_cobble_schema(&self.db.current_schema())?;
1642        self.structured_schema = Arc::new(schema);
1643        self.reset_default_options();
1644        Ok(())
1645    }
1646
1647    pub fn apply_schema(
1648        &mut self,
1649        structured_schema: StructuredSchema,
1650    ) -> Result<StructuredSchema> {
1651        persist_structured_schema_on_db(&self.db, &structured_schema)?;
1652        let reloaded = load_structured_schema_from_cobble_schema(&self.db.current_schema())?;
1653        self.structured_schema = Arc::new(reloaded.clone());
1654        self.reset_default_options();
1655        Ok(reloaded)
1656    }
1657
1658    pub fn put<K, V>(&self, bucket: u16, key: K, column: u16, value: V) -> Result<()>
1659    where
1660        K: AsRef<[u8]>,
1661        V: Into<StructuredColumnValue>,
1662    {
1663        self.put_with_options(bucket, key, column, value, &self.default_write_options)
1664    }
1665
1666    pub fn put_with_options<K, V>(
1667        &self,
1668        bucket: u16,
1669        key: K,
1670        column: u16,
1671        value: V,
1672        options: &StructuredWriteOptions,
1673    ) -> Result<()>
1674    where
1675        K: AsRef<[u8]>,
1676        V: Into<StructuredColumnValue>,
1677    {
1678        let encoded = encode_for_write(
1679            &self.structured_schema,
1680            options.column_family(),
1681            self.db.now_seconds(),
1682            column,
1683            value.into(),
1684            options.ttl_seconds(),
1685        )?;
1686        self.db
1687            .put_with_options(bucket, key, column, encoded, options.as_cobble())
1688    }
1689
1690    #[cfg(feature = "ffi")]
1691    pub(crate) fn put_borrowed_bytes_with_options<K>(
1692        &self,
1693        bucket: u16,
1694        key: K,
1695        column: u16,
1696        value: &[u8],
1697        options: &StructuredWriteOptions,
1698    ) -> Result<()>
1699    where
1700        K: AsRef<[u8]>,
1701    {
1702        ensure_bytes_column(&self.structured_schema, options.column_family(), column)?;
1703        self.db
1704            .put_with_options(bucket, key, column, value, options.as_cobble())
1705    }
1706
1707    /// Writes byte-column entries through the core put-batch fast path.
1708    #[cfg(feature = "ffi")]
1709    pub(crate) fn put_bytes_batch_with_options<'a, I>(
1710        &self,
1711        bucket: u16,
1712        column: u16,
1713        entries: I,
1714        options: &StructuredWriteOptions,
1715    ) -> Result<()>
1716    where
1717        I: IntoIterator<Item = (&'a [u8], &'a [u8])>,
1718    {
1719        let column_family_id = self
1720            .structured_schema
1721            .resolve_column_family_id(options.column_family())?;
1722        if !matches!(
1723            self.structured_schema
1724                .column_families
1725                .get(&column_family_id)
1726                .map(|family| family.structured_column_type(column))
1727                .unwrap_or(&StructuredColumnType::Bytes),
1728            StructuredColumnType::Bytes
1729        ) {
1730            return Err(Error::InputError(format!(
1731                "column {} expects a different type of value",
1732                column,
1733            )));
1734        }
1735        self.db
1736            .put_column_batch_with_options(bucket, column, entries, options.as_cobble())
1737    }
1738
1739    #[cfg(feature = "ffi")]
1740    pub(crate) fn put_encoded_list<K, B>(
1741        &self,
1742        bucket: u16,
1743        key: K,
1744        column: u16,
1745        encoded: B,
1746    ) -> Result<()>
1747    where
1748        K: AsRef<[u8]>,
1749        B: Into<Bytes>,
1750    {
1751        self.put_encoded_list_with_options(
1752            bucket,
1753            key,
1754            column,
1755            encoded,
1756            &self.default_write_options,
1757        )
1758    }
1759
1760    #[cfg(feature = "ffi")]
1761    pub(crate) fn put_encoded_list_with_options<K, B>(
1762        &self,
1763        bucket: u16,
1764        key: K,
1765        column: u16,
1766        encoded: B,
1767        options: &StructuredWriteOptions,
1768    ) -> Result<()>
1769    where
1770        K: AsRef<[u8]>,
1771        B: Into<Bytes>,
1772    {
1773        ensure_list_column(&self.structured_schema, options.column_family(), column)?;
1774        self.db
1775            .put_with_options(bucket, key, column, encoded.into(), options.as_cobble())
1776    }
1777
1778    #[cfg(feature = "ffi")]
1779    pub(crate) fn put_borrowed_list_with_options<K>(
1780        &self,
1781        bucket: u16,
1782        key: K,
1783        column: u16,
1784        elements: &[&[u8]],
1785        options: &StructuredWriteOptions,
1786    ) -> Result<()>
1787    where
1788        K: AsRef<[u8]>,
1789    {
1790        let config = list_column_config(&self.structured_schema, options.column_family(), column)?;
1791        let encoded = encode_borrowed_list_for_write(
1792            elements,
1793            &config,
1794            options.ttl_seconds(),
1795            self.db.now_seconds(),
1796        )?;
1797        self.db
1798            .put_with_options(bucket, key, column, encoded, options.as_cobble())
1799    }
1800
1801    pub fn merge<K, V>(&self, bucket: u16, key: K, column: u16, value: V) -> Result<()>
1802    where
1803        K: AsRef<[u8]>,
1804        V: Into<StructuredColumnValue>,
1805    {
1806        self.merge_with_options(bucket, key, column, value, &self.default_write_options)
1807    }
1808
1809    pub fn merge_with_options<K, V>(
1810        &self,
1811        bucket: u16,
1812        key: K,
1813        column: u16,
1814        value: V,
1815        options: &StructuredWriteOptions,
1816    ) -> Result<()>
1817    where
1818        K: AsRef<[u8]>,
1819        V: Into<StructuredColumnValue>,
1820    {
1821        let encoded = encode_for_write(
1822            &self.structured_schema,
1823            options.column_family(),
1824            self.db.now_seconds(),
1825            column,
1826            value.into(),
1827            options.ttl_seconds(),
1828        )?;
1829        self.db
1830            .merge_with_options(bucket, key, column, encoded, options.as_cobble())
1831    }
1832
1833    pub(crate) fn merge_borrowed_bytes_with_options<K>(
1834        &self,
1835        bucket: u16,
1836        key: K,
1837        column: u16,
1838        value: &[u8],
1839        options: &StructuredWriteOptions,
1840    ) -> Result<()>
1841    where
1842        K: AsRef<[u8]>,
1843    {
1844        ensure_bytes_column(&self.structured_schema, options.column_family(), column)?;
1845        self.db
1846            .merge_with_options(bucket, key, column, value, options.as_cobble())
1847    }
1848
1849    #[cfg(feature = "ffi")]
1850    pub(crate) fn merge_encoded_list<K, B>(
1851        &self,
1852        bucket: u16,
1853        key: K,
1854        column: u16,
1855        encoded: B,
1856    ) -> Result<()>
1857    where
1858        K: AsRef<[u8]>,
1859        B: Into<Bytes>,
1860    {
1861        self.merge_encoded_list_with_options(
1862            bucket,
1863            key,
1864            column,
1865            encoded,
1866            &self.default_write_options,
1867        )
1868    }
1869
1870    #[cfg(feature = "ffi")]
1871    pub(crate) fn merge_encoded_list_with_options<K, B>(
1872        &self,
1873        bucket: u16,
1874        key: K,
1875        column: u16,
1876        encoded: B,
1877        options: &StructuredWriteOptions,
1878    ) -> Result<()>
1879    where
1880        K: AsRef<[u8]>,
1881        B: Into<Bytes>,
1882    {
1883        ensure_list_column(&self.structured_schema, options.column_family(), column)?;
1884        self.db
1885            .merge_with_options(bucket, key, column, encoded.into(), options.as_cobble())
1886    }
1887
1888    #[cfg(feature = "ffi")]
1889    pub(crate) fn merge_borrowed_list_with_options<K>(
1890        &self,
1891        bucket: u16,
1892        key: K,
1893        column: u16,
1894        elements: &[&[u8]],
1895        options: &StructuredWriteOptions,
1896    ) -> Result<()>
1897    where
1898        K: AsRef<[u8]>,
1899    {
1900        let config = list_column_config(&self.structured_schema, options.column_family(), column)?;
1901        let encoded = encode_borrowed_list_for_write(
1902            elements,
1903            &config,
1904            options.ttl_seconds(),
1905            self.db.now_seconds(),
1906        )?;
1907        self.db
1908            .merge_with_options(bucket, key, column, encoded, options.as_cobble())
1909    }
1910
1911    pub fn delete<K>(&self, bucket: u16, key: K, column: u16) -> Result<()>
1912    where
1913        K: AsRef<[u8]>,
1914    {
1915        self.delete_with_options(bucket, key, column, &self.default_write_options)
1916    }
1917
1918    pub fn delete_with_options<K>(
1919        &self,
1920        bucket: u16,
1921        key: K,
1922        column: u16,
1923        options: &StructuredWriteOptions,
1924    ) -> Result<()>
1925    where
1926        K: AsRef<[u8]>,
1927    {
1928        self.db
1929            .delete_with_options(bucket, key, column, options.as_cobble())
1930    }
1931
1932    pub fn new_write_batch(&self) -> StructuredWriteBatch {
1933        StructuredWriteBatch::new(Arc::clone(&self.structured_schema), self.db.now_seconds())
1934    }
1935
1936    pub fn write_batch(&self, batch: StructuredWriteBatch) -> Result<()> {
1937        self.db.write_batch(batch.into_inner())
1938    }
1939
1940    pub fn write_batch_with_options(
1941        &self,
1942        batch: StructuredWriteBatch,
1943        options: &StructuredWriteOptions,
1944    ) -> Result<()> {
1945        self.db
1946            .write_batch_with_options(batch.into_inner(), options.as_cobble())
1947    }
1948
1949    pub fn get<K>(&self, bucket: u16, key: K) -> Result<Option<Vec<Option<StructuredColumnValue>>>>
1950    where
1951        K: AsRef<[u8]>,
1952    {
1953        self.get_with_options(bucket, key, &self.default_read_options)
1954    }
1955
1956    pub fn multi_get<K>(
1957        &self,
1958        keys: &[(u16, K)],
1959    ) -> Result<Vec<Option<Vec<Option<StructuredColumnValue>>>>>
1960    where
1961        K: AsRef<[u8]>,
1962    {
1963        self.multi_get_with_options(keys, &self.default_read_options)
1964    }
1965
1966    pub fn multi_get_with_options<K>(
1967        &self,
1968        keys: &[(u16, K)],
1969        options: &StructuredReadOptions,
1970    ) -> Result<Vec<Option<Vec<Option<StructuredColumnValue>>>>>
1971    where
1972        K: AsRef<[u8]>,
1973    {
1974        let raw_keys = keys
1975            .iter()
1976            .map(|(bucket, key)| (*bucket, key.as_ref()))
1977            .collect::<Vec<_>>();
1978        let projected_schema = options.resolve_projected_schema_cached(&self.structured_schema)?;
1979        self.db
1980            .multi_get_with_options(&raw_keys, options.as_cobble())?
1981            .into_iter()
1982            .map(|raw| {
1983                raw.map(|columns| decode_row(&projected_schema, 0, columns))
1984                    .transpose()
1985            })
1986            .collect()
1987    }
1988
1989    pub fn get_with_options<K>(
1990        &self,
1991        bucket: u16,
1992        key: K,
1993        options: &StructuredReadOptions,
1994    ) -> Result<Option<Vec<Option<StructuredColumnValue>>>>
1995    where
1996        K: AsRef<[u8]>,
1997    {
1998        let raw = self
1999            .db
2000            .get_with_options(bucket, key.as_ref(), options.as_cobble())?;
2001        let projected_schema = options.resolve_projected_schema_cached(&self.structured_schema)?;
2002        raw.map(|columns| decode_row(&projected_schema, 0, columns))
2003            .transpose()
2004    }
2005
2006    pub fn scan(&self, bucket: u16, range: Range<&[u8]>) -> Result<StructuredDbIterator> {
2007        self.scan_with_options(bucket, range, &self.default_scan_options)
2008    }
2009
2010    pub fn scan_with_options(
2011        &self,
2012        bucket: u16,
2013        range: Range<&[u8]>,
2014        options: &StructuredScanOptions,
2015    ) -> Result<StructuredDbIterator> {
2016        let inner = self
2017            .db
2018            .scan_with_options(bucket, range, options.as_cobble())?;
2019        let projected_schema = options.resolve_projected_schema_cached(&self.structured_schema)?;
2020        Ok(StructuredDbIterator::new(inner, projected_schema, 0))
2021    }
2022
2023    pub fn scan_bounds(
2024        &self,
2025        bucket: u16,
2026        start_key_inclusive: Option<&[u8]>,
2027        end_key_exclusive: Option<&[u8]>,
2028    ) -> Result<StructuredDbIterator> {
2029        self.scan_with_options_bounds(
2030            bucket,
2031            start_key_inclusive,
2032            end_key_exclusive,
2033            &self.default_scan_options,
2034        )
2035    }
2036
2037    pub fn scan_with_options_bounds(
2038        &self,
2039        bucket: u16,
2040        start_key_inclusive: Option<&[u8]>,
2041        end_key_exclusive: Option<&[u8]>,
2042        options: &StructuredScanOptions,
2043    ) -> Result<StructuredDbIterator> {
2044        let inner = self.db.scan_with_options_bounds(
2045            bucket,
2046            start_key_inclusive,
2047            end_key_exclusive,
2048            options.as_cobble(),
2049        )?;
2050        let projected_schema = options.resolve_projected_schema_cached(&self.structured_schema)?;
2051        Ok(StructuredDbIterator::new(inner, projected_schema, 0))
2052    }
2053
2054    pub fn snapshot(&self) -> Result<u64> {
2055        self.db.snapshot()
2056    }
2057
2058    /// Switch this running handle to an exact historical snapshot.
2059    pub fn switch_to_snapshot(&mut self, snapshot_id: u64) -> Result<()> {
2060        self.db
2061            .switch_to_snapshot_with_resolver(snapshot_id, Some(combined_resolver(None)))?;
2062        self.reload_schema()
2063    }
2064
2065    /// Change the memtable implementation for future rotations.
2066    ///
2067    /// `flush_current = false` leaves the active memtable untouched; `true` rotates a non-empty
2068    /// active memtable now through the normal flush path.
2069    pub fn switch_memtable_type(
2070        &self,
2071        memtable_type: MemtableType,
2072        flush_current: bool,
2073    ) -> Result<()> {
2074        self.db.switch_memtable_type(memtable_type, flush_current)
2075    }
2076
2077    /// Mark all currently referenced READONLY files for asynchronous loading into primary
2078    /// storage.
2079    pub fn load_readonly_files_to_primary(&self) -> Result<usize> {
2080        self.db.load_readonly_files_to_primary()
2081    }
2082
2083    pub fn snapshot_with_callback<F>(&self, callback: F) -> Result<u64>
2084    where
2085        F: Fn(Result<ShardSnapshotMetadata>) + Send + Sync + 'static,
2086    {
2087        self.db.snapshot_with_callback(callback)
2088    }
2089
2090    pub fn cancel_snapshot(&self, snapshot_id: u64) -> Result<bool> {
2091        self.db.cancel_snapshot(snapshot_id)
2092    }
2093
2094    pub fn expire_snapshot(&self, snapshot_id: u64) -> Result<bool> {
2095        self.db.expire_snapshot(snapshot_id)
2096    }
2097
2098    pub fn retain_snapshot(&self, snapshot_id: u64) -> bool {
2099        self.db.retain_snapshot(snapshot_id)
2100    }
2101
2102    pub fn shard_snapshot_metadata(&self, snapshot_id: u64) -> Result<ShardSnapshotMetadata> {
2103        self.db.shard_snapshot_metadata(snapshot_id)
2104    }
2105
2106    pub fn set_time(&self, next: u32) {
2107        self.db.set_time(next);
2108    }
2109
2110    pub fn now_seconds(&self) -> u32 {
2111        self.db.now_seconds()
2112    }
2113
2114    #[cfg(test)]
2115    pub(crate) fn get_raw_with_options(
2116        &self,
2117        bucket: u16,
2118        key: &[u8],
2119        options: &StructuredReadOptions,
2120    ) -> Result<Option<Vec<Option<Bytes>>>> {
2121        self.db.get_with_options(bucket, key, options.as_cobble())
2122    }
2123
2124    pub(crate) fn scan_raw_bounds(
2125        &self,
2126        bucket: u16,
2127        start_key_inclusive: Option<&[u8]>,
2128        end_key_exclusive: Option<&[u8]>,
2129        options: &StructuredScanOptions,
2130    ) -> Result<DbIterator> {
2131        self.db.scan_with_options_bounds(
2132            bucket,
2133            start_key_inclusive,
2134            end_key_exclusive,
2135            options.as_cobble(),
2136        )
2137    }
2138
2139    pub(crate) fn advance_column_family_truncation_cursor_by_id(
2140        &self,
2141        bucket: u16,
2142        column_family_id: u8,
2143        key: &[u8],
2144    ) -> Result<()> {
2145        self.db
2146            .advance_truncation_cursor_by_id(bucket, column_family_id, key)
2147    }
2148
2149    pub(crate) fn column_family_truncation_cursor_by_id(
2150        &self,
2151        bucket: u16,
2152        column_family_id: u8,
2153    ) -> Result<Option<Vec<u8>>> {
2154        self.db.truncation_cursor_by_id(bucket, column_family_id)
2155    }
2156
2157    pub fn close(&self) -> Result<()> {
2158        self.db.close()
2159    }
2160
2161    #[cfg(feature = "ffi")]
2162    pub(crate) fn jni_direct_buffer_pool_config(&self) -> Result<(usize, usize)> {
2163        cobble::ffi::db_direct_buffer_pool_config(&self.db)
2164    }
2165}
2166
2167impl StructuredSchemaOwner for StructuredDb {
2168    fn current_structured_schema(&self) -> StructuredSchema {
2169        self.current_schema()
2170    }
2171
2172    fn begin_core_schema_update(&self) -> SchemaBuilder {
2173        self.db.update_schema()
2174    }
2175
2176    fn install_committed_structured_schema(
2177        &mut self,
2178        schema: StructuredSchema,
2179    ) -> StructuredSchema {
2180        self.structured_schema = Arc::new(schema.clone());
2181        self.reset_default_options();
2182        schema
2183    }
2184}
2185
2186/// Type alias for backward compatibility.
2187pub type DataStructureDb = StructuredDb;
2188
2189// ── StructuredDbIterator ────────────────────────────────────────────────────
2190
2191pub struct StructuredDbIterator {
2192    inner: DbIterator,
2193    structured_schema: Arc<StructuredColumnFamilySchema>,
2194    now_seconds: u32,
2195}
2196
2197impl StructuredDbIterator {
2198    pub(crate) fn new(
2199        inner: DbIterator,
2200        structured_schema: Arc<StructuredColumnFamilySchema>,
2201        now_seconds: u32,
2202    ) -> Self {
2203        Self {
2204            inner,
2205            structured_schema,
2206            now_seconds,
2207        }
2208    }
2209
2210    pub fn consume_next_row<T, F>(&mut self, mut consumer: F) -> Result<Option<T>>
2211    where
2212        F: FnMut(&Bytes, &[Option<StructuredColumnValue>]) -> Result<T>,
2213    {
2214        let structured_schema = Arc::clone(&self.structured_schema);
2215        let now_seconds = self.now_seconds;
2216        self.inner.consume_next_row(|key, columns| {
2217            let decoded = decode_row(&structured_schema, now_seconds, columns.to_vec())?;
2218            consumer(key, &decoded)
2219        })
2220    }
2221
2222    pub fn consume_next_row_with_bucket<T, F>(&mut self, mut consumer: F) -> Result<Option<T>>
2223    where
2224        F: FnMut(u16, &Bytes, &[Option<StructuredColumnValue>]) -> Result<T>,
2225    {
2226        let structured_schema = Arc::clone(&self.structured_schema);
2227        let now_seconds = self.now_seconds;
2228        self.inner
2229            .consume_next_row_with_bucket(|bucket, key, columns| {
2230                let decoded = decode_row(&structured_schema, now_seconds, columns.to_vec())?;
2231                consumer(bucket, key, &decoded)
2232            })
2233    }
2234
2235    #[cfg(feature = "ffi")]
2236    pub(crate) fn stopped_at_block_boundary(&self) -> bool {
2237        self.inner.stopped_at_block_boundary()
2238    }
2239
2240    #[cfg(feature = "ffi")]
2241    pub(crate) fn clear_stop_at_block_boundary(&mut self) {
2242        self.inner.clear_stop_at_block_boundary();
2243    }
2244}
2245
2246impl Iterator for StructuredDbIterator {
2247    type Item = Result<(Bytes, Vec<Option<StructuredColumnValue>>)>;
2248
2249    fn next(&mut self) -> Option<Self::Item> {
2250        self.inner.next().map(|item| {
2251            let (key, columns) = item?;
2252            let decoded = decode_row(&self.structured_schema, self.now_seconds, columns)?;
2253            Ok((key, decoded))
2254        })
2255    }
2256}
2257
2258// ── Internal helpers ────────────────────────────────────────────────────────
2259
2260pub(crate) fn persist_structured_schema_on_db(
2261    db: &Db,
2262    structured_schema: &StructuredSchema,
2263) -> Result<()> {
2264    let mut schema = db.update_schema();
2265    apply_structured_schema(&mut schema, structured_schema)?;
2266    schema.commit();
2267    Ok(())
2268}
2269
2270fn apply_structured_schema(
2271    schema: &mut SchemaBuilder,
2272    structured_schema: &StructuredSchema,
2273) -> Result<()> {
2274    let column_family_names_by_id = structured_schema
2275        .column_family_ids
2276        .iter()
2277        .map(|(name, &id)| (id, name.clone()))
2278        .collect::<BTreeMap<_, _>>();
2279    for (column_family_id, family_schema) in &structured_schema.column_families {
2280        let column_family = if *column_family_id == DEFAULT_COLUMN_FAMILY_ID {
2281            None
2282        } else {
2283            Some(
2284                column_family_names_by_id
2285                    .get(column_family_id)
2286                    .cloned()
2287                    .ok_or_else(|| {
2288                        Error::InvalidState(format!(
2289                            "unknown structured column family id {}",
2290                            column_family_id
2291                        ))
2292                    })?,
2293            )
2294        };
2295        apply_structured_family(schema, column_family, &family_schema.columns)?;
2296    }
2297    Ok(())
2298}
2299
2300fn apply_structured_family(
2301    schema: &mut SchemaBuilder,
2302    column_family: Option<String>,
2303    columns: &BTreeMap<u16, StructuredColumnType>,
2304) -> Result<()> {
2305    if let Some(column_family) = column_family.as_ref()
2306        && !columns.is_empty()
2307    {
2308        schema.ensure_column_family_exists(column_family.clone())?;
2309    }
2310    for (column, column_type) in columns {
2311        match column_type {
2312            StructuredColumnType::Bytes => {
2313                schema.set_column_operator(
2314                    column_family.clone(),
2315                    *column as usize,
2316                    Arc::new(BytesMergeOperator),
2317                )?;
2318                schema.clear_column_metadata(column_family.clone(), *column as usize)?;
2319            }
2320            StructuredColumnType::List(config) => {
2321                schema.set_column_operator(
2322                    column_family.clone(),
2323                    *column as usize,
2324                    list_operator(config.clone()),
2325                )?;
2326                schema.set_column_metadata(
2327                    column_family.clone(),
2328                    *column as usize,
2329                    serde_json::to_value(config).map_err(|err| {
2330                        Error::FileFormatError(format!(
2331                            "failed to encode list config metadata: {}",
2332                            err
2333                        ))
2334                    })?,
2335                )?;
2336            }
2337        }
2338    }
2339    Ok(())
2340}
2341
2342fn apply_structured_column_type(
2343    schema: &mut SchemaBuilder,
2344    column_family: Option<String>,
2345    column: usize,
2346    column_type: &StructuredColumnType,
2347) -> Result<()> {
2348    let operator = match column_type {
2349        StructuredColumnType::Bytes => Arc::new(BytesMergeOperator) as Arc<_>,
2350        StructuredColumnType::List(config) => list_operator(config.clone()),
2351    };
2352    schema.replace_column(column_family, column, operator)
2353}
2354
2355#[cfg(test)]
2356#[path = "../tests/unit/structured_db.rs"]
2357mod tests;