Skip to main content

selene_graph/mutator/
catalog.rs

1//! Catalog mutation methods for the transaction mutator.
2
3use std::sync::Arc;
4
5use selene_core::{
6    ByteStringType, Change, CharacterStringType, DbString, EdgeEndpointDef as CoreEdgeEndpointDef,
7    GraphTypeId, LabelSet, PredefinedValueType, PropertyDef, PropertyValueType, SchemaChange,
8    ValueType,
9};
10use smallvec::SmallVec;
11
12use crate::{
13    DropBehavior, EdgeEndpointDef, EdgeTypeDef, GraphError, GraphResult, GraphTypeDef, Mutator,
14    NodeTypeDef, PropertyElementType, PropertyTypeDef, RecordFieldType, RecordFieldTypes,
15    ValidationMode,
16    graph_types::{MAX_LIST_TYPE_NESTING, MAX_RECORD_TYPE_NESTING},
17};
18
19const OPEN_GRAPH_CATALOG_DDL: &str =
20    "open graph (GG01) does not support catalog type DDL -- use a closed graph (GG02)";
21
22impl<'tx, 'g> Mutator<'tx, 'g> {
23    /// Add a node type to the transaction-local closed graph type.
24    ///
25    /// # Errors
26    ///
27    /// Returns [`GraphError::Inconsistent`] when the graph is open, the type
28    /// already exists, or the resulting graph type is structurally invalid.
29    pub fn create_node_type(
30        &mut self,
31        name: DbString,
32        key_labels: LabelSet,
33        properties: Vec<PropertyTypeDef>,
34        validation_mode: ValidationMode,
35    ) -> GraphResult<()> {
36        let mut graph_type = self.current_graph_type()?;
37        if graph_type
38            .node_types
39            .iter()
40            .any(|node_type| node_type.name == name)
41        {
42            return Err(GraphError::Inconsistent {
43                reason: format!("node type {name} already exists"),
44            });
45        }
46        let node_type = NodeTypeDef {
47            name: name.clone(),
48            key_labels,
49            properties,
50            validation_mode,
51        };
52        graph_type.node_types.push(node_type.clone());
53        graph_type.validate_ref()?;
54        let graph_id = self.txn.read().graph_id();
55        self.txn.guard_mut().meta.bound_type = Some(Arc::new(graph_type));
56        self.txn.changes.push(Change::SchemaChanged {
57            graph: graph_id,
58            change: SchemaChange::NodeTypeAddedV2 {
59                graph_type: implicit_graph_type_id(),
60                label: name,
61                def: core_node_type_def(&node_type)?,
62            },
63        });
64        Ok(())
65    }
66
67    /// Add an edge type to the transaction-local closed graph type.
68    ///
69    /// # Errors
70    ///
71    /// Returns [`GraphError::Inconsistent`] when the graph is open, the type
72    /// already exists, an endpoint index is invalid, or the resulting graph
73    /// type is structurally invalid.
74    pub fn create_edge_type(
75        &mut self,
76        name: DbString,
77        label: DbString,
78        source_node_type: EdgeEndpointDef,
79        target_node_type: EdgeEndpointDef,
80        properties: Vec<PropertyTypeDef>,
81        validation_mode: ValidationMode,
82    ) -> GraphResult<()> {
83        let mut graph_type = self.current_graph_type()?;
84        if graph_type
85            .edge_types
86            .iter()
87            .any(|edge_type| edge_type.name == name)
88        {
89            return Err(GraphError::Inconsistent {
90                reason: format!("edge type {name} already exists"),
91            });
92        }
93        let edge_type = EdgeTypeDef {
94            name,
95            label: label.clone(),
96            source_node_type,
97            target_node_type,
98            properties,
99            validation_mode,
100        };
101        graph_type.edge_types.push(edge_type.clone());
102        graph_type.validate_ref()?;
103        let graph_id = self.txn.read().graph_id();
104        self.txn.guard_mut().meta.bound_type = Some(Arc::new(graph_type.clone()));
105        self.txn.changes.push(Change::SchemaChanged {
106            graph: graph_id,
107            change: SchemaChange::EdgeTypeAddedV2 {
108                graph_type: implicit_graph_type_id(),
109                label,
110                def: core_edge_type_def(&graph_type, &edge_type)?,
111            },
112        });
113        Ok(())
114    }
115
116    /// Drop a node type from the transaction-local closed graph type.
117    ///
118    /// `behavior` selects the surviving-instance / inbound-dependency policy
119    /// (deletion-reclamation audit Item 3, Seam B):
120    ///
121    /// * [`DropBehavior::Restrict`] (the default) rejects the drop with
122    ///   [`GraphError::Inconsistent`] when any instance still carries `name` as
123    ///   its key label, or when an edge type still references the node type
124    ///   (dangling-endpoint guard). Nothing is removed — no `Change` is recorded
125    ///   and the bound graph type is left intact (no partial state).
126    /// * [`DropBehavior::Cascade`] (`IM_DROP_CASCADE`) truncates every instance
127    ///   first via [`Mutator::truncate_node_type`] (which also removes incident
128    ///   edges, so no dangling endpoints remain), then drops the type. Both the
129    ///   truncate change(s) and the [`SchemaChange::NodeTypeDropped`] land in the
130    ///   same transaction, so commit and WAL replay are atomic.
131    ///
132    /// # Errors
133    ///
134    /// Returns [`GraphError::Inconsistent`] when the graph is open, the type
135    /// does not exist, `Restrict` finds surviving instances or an inbound edge
136    /// dependency, or any edge endpoint would require positional endpoint
137    /// reindexing.
138    pub fn drop_node_type(&mut self, name: DbString, behavior: DropBehavior) -> GraphResult<()> {
139        let graph_type = self.current_graph_type()?;
140        let removed_index = graph_type
141            .node_type_index_for(name.clone())
142            .ok_or_else(|| GraphError::Inconsistent {
143                reason: format!("node type {name} does not exist"),
144            })?;
145        match behavior {
146            DropBehavior::Restrict => {
147                // Seam-B fix: a surviving instance whose declared type is being
148                // dropped would become an orphan on commit. Reject early with a
149                // message that blames the drop, not the instance.
150                let live = self
151                    .txn
152                    .read()
153                    .nodes_with_label(&name)
154                    .map_or(0, roaring::RoaringBitmap::len);
155                if live > 0 {
156                    return Err(GraphError::Inconsistent {
157                        reason: format!(
158                            "cannot drop node type {name}: {live} instance(s) still exist; use CASCADE to remove them"
159                        ),
160                    });
161                }
162                // Type-dependency: an edge type that directly references this
163                // node type would be left with a dangling endpoint. Recursive
164                // type cascade is out of scope (Item 3 is instance cascade only).
165                for edge_type in &graph_type.edge_types {
166                    if endpoint_references_node(&edge_type.source_node_type, removed_index)
167                        || endpoint_references_node(&edge_type.target_node_type, removed_index)
168                    {
169                        return Err(GraphError::Inconsistent {
170                            reason: format!(
171                                "cannot drop node type {name}: edge type {} still references it",
172                                edge_type.name
173                            ),
174                        });
175                    }
176                }
177            }
178            DropBehavior::Cascade => {
179                // Truncate instances FIRST (reuses the BRIEF-150 funnel); this
180                // also removes incident edges, so no dangling endpoint remains.
181                self.truncate_node_type(name.clone())?;
182            }
183        }
184        // Shared schema-drop step. The positional-reindexing guard still applies
185        // to BOTH paths: if a surviving edge type references a node-type index
186        // at or after the removed slot, the drop must reject (recursive type
187        // cascade is out of scope). After a CASCADE truncate of `name`'s own
188        // instances, an edge type that referenced `name` directly is structurally
189        // empty but still declared, so this guard governs the type relationship.
190        for edge_type in &graph_type.edge_types {
191            if endpoint_depends_on_shifted_node(&edge_type.source_node_type, removed_index)
192                || endpoint_depends_on_shifted_node(&edge_type.target_node_type, removed_index)
193            {
194                return Err(GraphError::Inconsistent {
195                    reason: format!(
196                        "cannot drop node type {name}: edge type {} still depends on node-type indexes that would require reindexing",
197                        edge_type.name
198                    ),
199                });
200            }
201        }
202        let next = graph_type
203            .without_node_type(name.clone())
204            .expect("node type existed above");
205        next.validate_ref()?;
206        let graph_id = self.txn.read().graph_id();
207        self.txn.guard_mut().meta.bound_type = Some(Arc::new(next));
208        self.txn.changes.push(Change::SchemaChanged {
209            graph: graph_id,
210            change: SchemaChange::NodeTypeDropped {
211                graph_type: implicit_graph_type_id(),
212                name,
213            },
214        });
215        Ok(())
216    }
217
218    /// Drop an edge type from the transaction-local closed graph type.
219    ///
220    /// `behavior` selects the surviving-instance policy (deletion-reclamation
221    /// audit Item 3, Seam B). Edge types have no inbound type dependency, so
222    /// only the instance check applies:
223    ///
224    /// * [`DropBehavior::Restrict`] (the default) rejects with
225    ///   [`GraphError::Inconsistent`] when any edge still carries `name`; nothing
226    ///   is removed and no `Change` is recorded.
227    /// * [`DropBehavior::Cascade`] (`IM_DROP_CASCADE`) truncates every edge of
228    ///   the type first via [`Mutator::truncate_edge_type`], then drops the type,
229    ///   atomically in one transaction.
230    ///
231    /// # Errors
232    ///
233    /// Returns [`GraphError::Inconsistent`] when the graph is open, the type
234    /// does not exist, `Restrict` finds surviving instances, or the resulting
235    /// graph type is structurally invalid.
236    pub fn drop_edge_type(&mut self, name: DbString, behavior: DropBehavior) -> GraphResult<()> {
237        let graph_type = self.current_graph_type()?;
238        if graph_type.edge_type_index_for(name.clone()).is_none() {
239            return Err(GraphError::Inconsistent {
240                reason: format!("edge type {name} does not exist"),
241            });
242        }
243        match behavior {
244            DropBehavior::Restrict => {
245                let live = self
246                    .txn
247                    .read()
248                    .edges_with_label(&name)
249                    .map_or(0, roaring::RoaringBitmap::len);
250                if live > 0 {
251                    return Err(GraphError::Inconsistent {
252                        reason: format!(
253                            "cannot drop edge type {name}: {live} instance(s) still exist; use CASCADE to remove them"
254                        ),
255                    });
256                }
257            }
258            DropBehavior::Cascade => {
259                self.truncate_edge_type(name.clone())?;
260            }
261        }
262        let next = graph_type
263            .without_edge_type(name.clone())
264            .expect("edge type existed above");
265        next.validate_ref()?;
266        let graph_id = self.txn.read().graph_id();
267        self.txn.guard_mut().meta.bound_type = Some(Arc::new(next));
268        self.txn.changes.push(Change::SchemaChanged {
269            graph: graph_id,
270            change: SchemaChange::EdgeTypeDropped {
271                graph_type: implicit_graph_type_id(),
272                name,
273            },
274        });
275        Ok(())
276    }
277
278    fn current_graph_type(&self) -> GraphResult<GraphTypeDef> {
279        self.txn
280            .read()
281            .meta
282            .bound_type
283            .as_deref()
284            .cloned()
285            .ok_or_else(|| GraphError::Inconsistent {
286                reason: OPEN_GRAPH_CATALOG_DDL.to_owned(),
287            })
288    }
289}
290
291/// Return the implicit graph type ID used while one bound type is allowed per graph.
292///
293/// Future multi-type-bound graph work should replace this sentinel with a real
294/// graph-type allocator and preserve the ID across WAL replay.
295fn implicit_graph_type_id() -> GraphTypeId {
296    GraphTypeId::new(1).expect("implicit graph type id")
297}
298
299/// Whether `endpoint` directly references the node-type at `removed_index`.
300///
301/// Distinct from [`endpoint_depends_on_shifted_node`], which is the broader
302/// positional-reindexing guard (>= removed_index). This narrower check powers
303/// the clear RESTRICT message "edge type E still references it" for the direct
304/// dangling-endpoint case.
305fn endpoint_references_node(endpoint: &EdgeEndpointDef, removed_index: u32) -> bool {
306    match endpoint {
307        EdgeEndpointDef::Any => false,
308        EdgeEndpointDef::NodeType(index) => *index == removed_index,
309        EdgeEndpointDef::OneOf(indices) => indices.contains(&removed_index),
310    }
311}
312
313fn endpoint_depends_on_shifted_node(endpoint: &EdgeEndpointDef, removed_index: u32) -> bool {
314    // Why: node_type_index() returns None for OneOf, so the prior helper would
315    // have silently let DROP NODE TYPE succeed when an OneOf endpoint depended
316    // on the removed (or shifted) node. Walk each candidate index explicitly.
317    match endpoint {
318        EdgeEndpointDef::Any => false,
319        EdgeEndpointDef::NodeType(index) => *index >= removed_index,
320        EdgeEndpointDef::OneOf(indices) => indices.iter().any(|index| *index >= removed_index),
321    }
322}
323
324fn core_node_type_def(node_type: &NodeTypeDef) -> GraphResult<selene_core::NodeTypeDef> {
325    Ok(selene_core::NodeTypeDef {
326        labels: node_type.key_labels.clone(),
327        properties: core_node_properties(&node_type.properties)?,
328        key: None,
329        validation_mode: core_validation_mode(node_type.validation_mode),
330    })
331}
332
333fn core_edge_type_def(
334    graph_type: &GraphTypeDef,
335    edge_type: &EdgeTypeDef,
336) -> GraphResult<selene_core::EdgeTypeDef> {
337    Ok(selene_core::EdgeTypeDef {
338        label: edge_type.label.clone(),
339        source_node_type: core_edge_endpoint_def(
340            graph_type,
341            edge_type.name.clone(),
342            &edge_type.source_node_type,
343        )?,
344        target_node_type: core_edge_endpoint_def(
345            graph_type,
346            edge_type.name.clone(),
347            &edge_type.target_node_type,
348        )?,
349        properties: core_edge_properties(&edge_type.properties)?,
350        validation_mode: core_validation_mode(edge_type.validation_mode),
351    })
352}
353
354fn core_edge_endpoint_def(
355    graph_type: &GraphTypeDef,
356    edge_name: DbString,
357    endpoint: &EdgeEndpointDef,
358) -> GraphResult<CoreEdgeEndpointDef> {
359    match endpoint {
360        EdgeEndpointDef::Any => Ok(CoreEdgeEndpointDef::Any),
361        EdgeEndpointDef::NodeType(index) => graph_type
362            .node_types
363            .get(*index as usize)
364            .map(|node_type| {
365                CoreEdgeEndpointDef::NodeType(selene_core::NodeTypeRef(node_type.name.clone()))
366            })
367            .ok_or_else(|| GraphError::Inconsistent {
368                reason: format!("edge type {edge_name} references invalid node type {index}"),
369            }),
370        EdgeEndpointDef::OneOf(indices) => {
371            let mut refs: SmallVec<[selene_core::NodeTypeRef; 4]> = SmallVec::new();
372            for index in indices {
373                let node_type = graph_type.node_types.get(*index as usize).ok_or_else(|| {
374                    GraphError::Inconsistent {
375                        reason: format!(
376                            "edge type {edge_name} OneOf endpoint references invalid node type {index}"
377                        ),
378                    }
379                })?;
380                refs.push(selene_core::NodeTypeRef(node_type.name.clone()));
381            }
382            Ok(CoreEdgeEndpointDef::OneOf(refs))
383        }
384    }
385}
386
387fn core_node_properties(properties: &[PropertyTypeDef]) -> GraphResult<SmallVec<[PropertyDef; 8]>> {
388    let mut out = SmallVec::new();
389    for property in properties {
390        out.push(PropertyDef {
391            name: property.name.clone(),
392            value_type: core_value_type(
393                property.value_type,
394                property.list_element_type.as_ref(),
395                property.decimal_type,
396                property.character_string_type,
397                property.byte_string_type,
398                property.required,
399            )?,
400            nullable: !property.required,
401            default: property
402                .default
403                .as_ref()
404                .map(|default| default.to_value())
405                .transpose()?,
406            immutable: property.immutable,
407            unique: property.unique,
408            record_fields: core_record_fields(
409                property.value_type,
410                property.record_field_types.as_ref(),
411            )?,
412        });
413    }
414    Ok(out)
415}
416
417fn core_edge_properties(properties: &[PropertyTypeDef]) -> GraphResult<SmallVec<[PropertyDef; 4]>> {
418    let mut out = SmallVec::new();
419    for property in properties {
420        out.push(PropertyDef {
421            name: property.name.clone(),
422            value_type: core_value_type(
423                property.value_type,
424                property.list_element_type.as_ref(),
425                property.decimal_type,
426                property.character_string_type,
427                property.byte_string_type,
428                property.required,
429            )?,
430            nullable: !property.required,
431            default: property
432                .default
433                .as_ref()
434                .map(|default| default.to_value())
435                .transpose()?,
436            immutable: property.immutable,
437            unique: property.unique,
438            record_fields: core_record_fields(
439                property.value_type,
440                property.record_field_types.as_ref(),
441            )?,
442        });
443    }
444    Ok(out)
445}
446
447const fn core_validation_mode(mode: ValidationMode) -> selene_core::ValidationMode {
448    match mode {
449        ValidationMode::Strict => selene_core::ValidationMode::Strict,
450        ValidationMode::Warn => selene_core::ValidationMode::Warn,
451    }
452}
453
454fn core_value_type(
455    value_type: PropertyValueType,
456    list_element_type: Option<&PropertyElementType>,
457    decimal_type: Option<selene_core::DecimalType>,
458    character_string_type: Option<CharacterStringType>,
459    byte_string_type: Option<ByteStringType>,
460    required: bool,
461) -> GraphResult<ValueType> {
462    let mut value_type = if value_type == PropertyValueType::List {
463        let element_type = list_element_type.ok_or_else(|| GraphError::Inconsistent {
464            reason: "LIST property definition is missing element type".to_owned(),
465        })?;
466        ValueType::list_of(core_element_value_type(element_type, 1)?)
467    } else {
468        core_scalar_value_type(
469            value_type,
470            decimal_type,
471            character_string_type,
472            byte_string_type,
473        )
474    };
475    value_type.not_null = required;
476    Ok(value_type)
477}
478
479fn core_element_value_type(
480    element_type: &PropertyElementType,
481    depth: u32,
482) -> GraphResult<ValueType> {
483    if depth > MAX_LIST_TYPE_NESTING {
484        return Err(GraphError::Inconsistent {
485            reason: "LIST property definition exceeds nesting limit".to_owned(),
486        });
487    }
488    match element_type {
489        PropertyElementType::Scalar(value_type) => {
490            Ok(core_scalar_value_type(*value_type, None, None, None))
491        }
492        PropertyElementType::CharacterString(character_string_type) => Ok(core_scalar_value_type(
493            PropertyValueType::String,
494            None,
495            Some(*character_string_type),
496            None,
497        )),
498        PropertyElementType::Decimal(decimal_type) => Ok(core_scalar_value_type(
499            PropertyValueType::Decimal,
500            Some(*decimal_type),
501            None,
502            None,
503        )),
504        PropertyElementType::ByteString(byte_string_type) => Ok(core_scalar_value_type(
505            PropertyValueType::Bytes,
506            None,
507            None,
508            Some(*byte_string_type),
509        )),
510        PropertyElementType::List(inner) => Ok(ValueType::list_of(core_element_value_type(
511            inner,
512            depth + 1,
513        )?)),
514        PropertyElementType::NotNull(inner) => {
515            let mut value_type = core_element_value_type(inner, depth)?;
516            value_type.not_null = true;
517            Ok(value_type)
518        }
519    }
520}
521
522fn core_scalar_value_type(
523    value_type: PropertyValueType,
524    decimal_type: Option<selene_core::DecimalType>,
525    character_string_type: Option<CharacterStringType>,
526    byte_string_type: Option<ByteStringType>,
527) -> ValueType {
528    let predefined = match value_type {
529        PropertyValueType::Bool => Some(PredefinedValueType::Bool),
530        PropertyValueType::Int => Some(PredefinedValueType::Int),
531        PropertyValueType::Uint => Some(PredefinedValueType::Uint),
532        PropertyValueType::Int128 => Some(PredefinedValueType::Int128),
533        PropertyValueType::Uint128 => Some(PredefinedValueType::Uint128),
534        PropertyValueType::Float => Some(PredefinedValueType::Float),
535        PropertyValueType::Float32 => Some(PredefinedValueType::Float32),
536        PropertyValueType::Decimal => Some(PredefinedValueType::Decimal),
537        PropertyValueType::String => Some(PredefinedValueType::String),
538        PropertyValueType::Bytes => Some(PredefinedValueType::Bytes),
539        PropertyValueType::Path => Some(PredefinedValueType::Path),
540        PropertyValueType::NodeRef => Some(PredefinedValueType::NodeRef),
541        PropertyValueType::EdgeRef => Some(PredefinedValueType::EdgeRef),
542        PropertyValueType::GraphRef => Some(PredefinedValueType::GraphRef),
543        PropertyValueType::TableRef => Some(PredefinedValueType::TableRef),
544        PropertyValueType::ZonedDateTime => Some(PredefinedValueType::ZonedDateTime),
545        PropertyValueType::LocalDateTime => Some(PredefinedValueType::LocalDateTime),
546        PropertyValueType::Date => Some(PredefinedValueType::Date),
547        PropertyValueType::ZonedTime => Some(PredefinedValueType::ZonedTime),
548        PropertyValueType::LocalTime => Some(PredefinedValueType::LocalTime),
549        PropertyValueType::Duration => Some(PredefinedValueType::Duration),
550        PropertyValueType::DurationYearToMonth => Some(PredefinedValueType::DurationYearToMonth),
551        PropertyValueType::DurationDayToSecond => Some(PredefinedValueType::DurationDayToSecond),
552        PropertyValueType::Uuid => Some(PredefinedValueType::Uuid),
553        PropertyValueType::Vector => Some(PredefinedValueType::Vector),
554        PropertyValueType::Json => Some(PredefinedValueType::Json),
555        PropertyValueType::List
556        | PropertyValueType::Record
557        | PropertyValueType::RecordTyped
558        | PropertyValueType::Null => None,
559    };
560    ValueType {
561        predefined,
562        decimal_type: if value_type == PropertyValueType::Decimal {
563            decimal_type
564        } else {
565            None
566        },
567        character_string_type: if value_type == PropertyValueType::String {
568            character_string_type
569        } else {
570            None
571        },
572        byte_string_type: if value_type == PropertyValueType::Bytes {
573            byte_string_type
574        } else {
575            None
576        },
577        union: None,
578        list_of: None,
579        record: None,
580        not_null: false,
581        cardinality: selene_core::ValueTypeCardinality::ExactlyOne,
582    }
583}
584
585/// Convert the rkyv-side typed-`RECORD` descriptor into the serde/WAL counterpart carried
586/// on [`PropertyDef::record_fields`]. Returns `None` for every non-record property,
587/// `Some(Open)` for an open/bare `RECORD` (no declared fields), and `Some(Closed(..))`
588/// for a closed/typed `RECORD{..}`.
589// Why: a RECORD property's record-ness must survive WAL replay; it rides
590// `PropertyDef.record_fields` (structural-inline), not `ValueType.record`. The open/bare
591// form carries no field list, so it persists as `Some(Open)` — without that marker WAL
592// recovery cannot tell an open record from a scalar `Null` and degrades it to `Null`.
593fn core_record_fields(
594    value_type: PropertyValueType,
595    fields: Option<&RecordFieldTypes>,
596) -> GraphResult<Option<Box<selene_core::RecordFieldStructure>>> {
597    match (value_type, fields) {
598        (PropertyValueType::RecordTyped, Some(fields)) => {
599            Ok(Some(Box::new(core_record_field_structure(fields, 1)?)))
600        }
601        (PropertyValueType::RecordTyped, None) => {
602            Ok(Some(Box::new(selene_core::RecordFieldStructure::Open)))
603        }
604        _ => Ok(None),
605    }
606}
607
608fn core_record_field_structure(
609    fields: &RecordFieldTypes,
610    depth: u32,
611) -> GraphResult<selene_core::RecordFieldStructure> {
612    if depth > MAX_RECORD_TYPE_NESTING {
613        return Err(GraphError::Inconsistent {
614            reason: "RECORD property definition exceeds nesting limit".to_owned(),
615        });
616    }
617    let defs = fields
618        .0
619        .iter()
620        .map(|field| {
621            Ok(selene_core::RecordFieldStructureDef {
622                name: field.name.clone(),
623                field_type: core_record_field_structure_type(&field.field_type, depth)?,
624                required: field.required,
625            })
626        })
627        .collect::<GraphResult<Vec<_>>>()?;
628    Ok(selene_core::RecordFieldStructure::Closed(defs))
629}
630
631fn core_record_field_structure_type(
632    field_type: &RecordFieldType,
633    depth: u32,
634) -> GraphResult<selene_core::RecordFieldStructureType> {
635    Ok(match field_type {
636        RecordFieldType::Scalar(value_type) => {
637            selene_core::RecordFieldStructureType::Scalar(*value_type)
638        }
639        RecordFieldType::CharacterString(character_string_type) => {
640            selene_core::RecordFieldStructureType::CharacterString(*character_string_type)
641        }
642        RecordFieldType::Decimal(decimal_type) => {
643            selene_core::RecordFieldStructureType::Decimal(*decimal_type)
644        }
645        RecordFieldType::ByteString(byte_string_type) => {
646            selene_core::RecordFieldStructureType::ByteString(*byte_string_type)
647        }
648        RecordFieldType::List(inner) => selene_core::RecordFieldStructureType::List(Box::new(
649            core_record_field_structure_type(inner, depth + 1)?,
650        )),
651        RecordFieldType::OpenRecord => selene_core::RecordFieldStructureType::Record(Box::new(
652            selene_core::RecordFieldStructure::Open,
653        )),
654        RecordFieldType::Record(inner) => selene_core::RecordFieldStructureType::Record(Box::new(
655            core_record_field_structure(inner, depth + 1)?,
656        )),
657        RecordFieldType::NotNull(inner) => selene_core::RecordFieldStructureType::NotNull(
658            Box::new(core_record_field_structure_type(inner, depth)?),
659        ),
660    })
661}
662
663#[cfg(test)]
664mod tests;