Skip to main content

selene_core/
schema.rs

1//! Schema model types per spec 02 section 6.
2//!
3//! These are structural data carriers. Runtime validation of graph mutations
4//! against a [`GraphType`] belongs to `selene-graph`.
5
6use std::collections::BTreeMap;
7use std::fmt;
8
9use serde::{Deserialize, Deserializer, Serialize};
10use smallvec::SmallVec;
11
12use crate::{
13    ByteStringType, CharacterStringType, CoreError, CoreResult, DbString, DecimalType,
14    ExtensionTypeId, LabelSet, PropertyValueType, RecordTypeId, Value,
15};
16
17/// Graph-type-scoped schema identifier.
18#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
19#[repr(transparent)]
20pub struct GraphTypeId(pub u64);
21
22impl<'de> Deserialize<'de> for GraphTypeId {
23    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
24    where
25        D: Deserializer<'de>,
26    {
27        let raw = u64::deserialize(deserializer)?;
28        Self::new(raw).map_err(serde::de::Error::custom)
29    }
30}
31
32impl GraphTypeId {
33    /// Construct a graph type ID, rejecting the reserved zero sentinel.
34    ///
35    /// # Errors
36    ///
37    /// Returns [`CoreError::ZeroIdentifier`] when `value` is `0`.
38    pub const fn new(value: u64) -> CoreResult<Self> {
39        if value == 0 {
40            Err(CoreError::ZeroIdentifier)
41        } else {
42            Ok(Self(value))
43        }
44    }
45
46    /// Return the raw `u64` value.
47    #[must_use]
48    pub const fn get(self) -> u64 {
49        self.0
50    }
51}
52
53impl fmt::Display for GraphTypeId {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        write!(f, "GraphTypeId({})", self.0)
56    }
57}
58
59/// Closed-graph schema definition.
60#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
61pub struct GraphType {
62    /// Stable graph type ID.
63    pub id: GraphTypeId,
64    /// Database-string graph type name.
65    pub name: DbString,
66    /// Node types keyed by node label.
67    pub node_types: BTreeMap<DbString, NodeTypeDef>,
68    /// Edge types keyed by edge label.
69    pub edge_types: BTreeMap<DbString, EdgeTypeDef>,
70    /// Record types keyed by record type ID.
71    pub record_types: BTreeMap<RecordTypeId, RecordTypeDef>,
72    /// Reserved policy for relationships between key label sets. **Not yet
73    /// consulted:** closed-graph element binding uses exact key-label-set
74    /// equality, and every type's key label set is currently a singleton
75    /// (cardinality 1), so no overlap/containment relationship can arise to
76    /// apply a policy to. See [`KeyLabelSetPolicy`].
77    pub key_label_set_policy: KeyLabelSetPolicy,
78}
79
80impl GraphType {
81    /// Construct an empty graph type.
82    #[must_use]
83    pub fn new(id: GraphTypeId, name: DbString) -> Self {
84        Self {
85            id,
86            name,
87            node_types: BTreeMap::new(),
88            edge_types: BTreeMap::new(),
89            record_types: BTreeMap::new(),
90            key_label_set_policy: KeyLabelSetPolicy::default(),
91        }
92    }
93}
94
95/// Node type definition.
96#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
97pub struct NodeTypeDef {
98    /// Label set required by this node type.
99    pub labels: LabelSet,
100    /// Property definitions in schema order.
101    pub properties: SmallVec<[PropertyDef; 8]>,
102    /// Optional property-name key.
103    pub key: Option<NodeKey>,
104    /// Closed-graph validation mode for this node type.
105    #[serde(default)]
106    pub validation_mode: ValidationMode,
107}
108
109impl NodeTypeDef {
110    /// Construct a node type definition with no properties.
111    #[must_use]
112    pub fn new(labels: LabelSet) -> Self {
113        Self {
114            labels,
115            properties: SmallVec::new(),
116            key: None,
117            validation_mode: ValidationMode::Strict,
118        }
119    }
120}
121
122/// Legacy WAL node type definition carried by [`SchemaChange::NodeTypeAdded`](crate::SchemaChange::NodeTypeAdded).
123///
124/// This freezes the pre-v1.1 catalog-DDL payload shape. New WAL entries use
125/// [`SchemaChange::NodeTypeAddedV2`](crate::SchemaChange::NodeTypeAddedV2)
126/// with [`NodeTypeDef`]; recovery upgrades this shape with
127/// [`ValidationMode::Strict`] plus non-immutable, non-unique properties.
128#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
129pub struct NodeTypeDefV1 {
130    /// Label set required by this node type.
131    pub labels: LabelSet,
132    /// Property definitions in schema order.
133    pub properties: SmallVec<[PropertyDefV1; 8]>,
134    /// Optional property-name key.
135    pub key: Option<NodeKey>,
136}
137
138impl NodeTypeDefV1 {
139    /// Construct a legacy node type definition with no properties.
140    #[must_use]
141    pub fn new(labels: LabelSet) -> Self {
142        Self {
143            labels,
144            properties: SmallVec::new(),
145            key: None,
146        }
147    }
148}
149
150impl From<NodeTypeDefV1> for NodeTypeDef {
151    fn from(value: NodeTypeDefV1) -> Self {
152        Self {
153            labels: value.labels,
154            properties: value.properties.into_iter().map(Into::into).collect(),
155            key: value.key,
156            validation_mode: ValidationMode::Strict,
157        }
158    }
159}
160
161/// Property-name list that forms a node key.
162#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
163pub struct NodeKey {
164    /// Property names participating in the key.
165    pub property_names: SmallVec<[DbString; 2]>,
166}
167
168/// Edge endpoint definition.
169///
170/// `OneOf` carries a sorted, deduplicated, length-≥-2 set of distinct
171/// [`NodeTypeRef`]s. Construct it via [`EdgeEndpointDef::one_of`] so the
172/// invariants are enforced (singleton inputs collapse to
173/// [`EdgeEndpointDef::NodeType`]). The WAL is permissive — recovery re-applies
174/// the constructor through the storage-side resolver, so direct struct
175/// construction in WAL paths is acceptable and replay canonicalizes.
176#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
177pub enum EdgeEndpointDef {
178    /// Accept any declared node type at this endpoint.
179    Any,
180    /// Reference one concrete node type.
181    NodeType(NodeTypeRef),
182    /// Reference any node type drawn from a sorted, deduplicated, length-≥-2
183    /// set of distinct node types.
184    OneOf(SmallVec<[NodeTypeRef; 4]>),
185}
186
187impl EdgeEndpointDef {
188    /// Construct an endpoint accepting `refs`, canonicalized.
189    ///
190    /// References are sorted by database-string identity and deduplicated. A
191    /// single resulting reference collapses to [`EdgeEndpointDef::NodeType`].
192    ///
193    /// # Panics
194    ///
195    /// Panics when the resulting set is empty; zero-label endpoints are a
196    /// caller bug and the upstream resolver must reject them before reaching
197    /// this constructor.
198    #[must_use]
199    pub fn one_of(refs: impl IntoIterator<Item = NodeTypeRef>) -> Self {
200        let mut buf: SmallVec<[NodeTypeRef; 4]> = refs.into_iter().collect();
201        buf.sort_unstable_by(|a, b| a.0.cmp(&b.0));
202        buf.dedup();
203        assert!(
204            !buf.is_empty(),
205            "EdgeEndpointDef::one_of called with empty NodeTypeRef set"
206        );
207        match buf.len() {
208            1 => Self::NodeType(buf[0].clone()),
209            _ => Self::OneOf(buf),
210        }
211    }
212}
213
214/// Edge type definition.
215#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
216pub struct EdgeTypeDef {
217    /// Single edge label.
218    pub label: DbString,
219    /// Source endpoint definition.
220    pub source_node_type: EdgeEndpointDef,
221    /// Target endpoint definition.
222    pub target_node_type: EdgeEndpointDef,
223    /// Property definitions in schema order.
224    pub properties: SmallVec<[PropertyDef; 4]>,
225    /// Closed-graph validation mode for this edge type.
226    #[serde(default)]
227    pub validation_mode: ValidationMode,
228}
229
230impl EdgeTypeDef {
231    /// Construct an edge type definition with no properties.
232    #[must_use]
233    pub fn new(label: DbString, source: NodeTypeRef, target: NodeTypeRef) -> Self {
234        Self::new_with_endpoints(
235            label,
236            EdgeEndpointDef::NodeType(source),
237            EdgeEndpointDef::NodeType(target),
238        )
239    }
240
241    /// Construct an edge type definition with explicit endpoints and no properties.
242    #[must_use]
243    pub fn new_with_endpoints(
244        label: DbString,
245        source: EdgeEndpointDef,
246        target: EdgeEndpointDef,
247    ) -> Self {
248        Self {
249            label,
250            source_node_type: source,
251            target_node_type: target,
252            properties: SmallVec::new(),
253            validation_mode: ValidationMode::Strict,
254        }
255    }
256}
257
258/// Legacy WAL edge type definition carried by [`SchemaChange::EdgeTypeAdded`](crate::SchemaChange::EdgeTypeAdded).
259///
260/// This freezes the pre-v1.1 catalog-DDL payload shape. New WAL entries use
261/// [`SchemaChange::EdgeTypeAddedV2`](crate::SchemaChange::EdgeTypeAddedV2)
262/// with [`EdgeTypeDef`]; recovery upgrades this shape with
263/// [`ValidationMode::Strict`] plus non-immutable, non-unique properties.
264#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
265pub struct EdgeTypeDefV1 {
266    /// Single edge label.
267    pub label: DbString,
268    /// Source node type reference.
269    pub source_node_type: NodeTypeRef,
270    /// Target node type reference.
271    pub target_node_type: NodeTypeRef,
272    /// Property definitions in schema order.
273    pub properties: SmallVec<[PropertyDefV1; 4]>,
274}
275
276impl EdgeTypeDefV1 {
277    /// Construct a legacy edge type definition with no properties.
278    #[must_use]
279    pub fn new(label: DbString, source: NodeTypeRef, target: NodeTypeRef) -> Self {
280        Self {
281            label,
282            source_node_type: source,
283            target_node_type: target,
284            properties: SmallVec::new(),
285        }
286    }
287}
288
289impl From<EdgeTypeDefV1> for EdgeTypeDef {
290    fn from(value: EdgeTypeDefV1) -> Self {
291        Self {
292            label: value.label,
293            source_node_type: EdgeEndpointDef::NodeType(value.source_node_type),
294            target_node_type: EdgeEndpointDef::NodeType(value.target_node_type),
295            properties: value.properties.into_iter().map(Into::into).collect(),
296            validation_mode: ValidationMode::Strict,
297        }
298    }
299}
300
301/// Closed-graph validation mode.
302#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
303pub enum ValidationMode {
304    /// Reject type-model violations.
305    #[default]
306    Strict,
307    /// Allow relaxed property-shape writes and report warnings.
308    Warn,
309}
310
311/// Node type reference by label.
312#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
313#[repr(transparent)]
314pub struct NodeTypeRef(pub DbString);
315
316/// Record type reference by ID.
317#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
318#[repr(transparent)]
319pub struct RecordTypeRef(pub RecordTypeId);
320
321/// Inline, recursively-nestable closed/typed RECORD field-type structure carried on
322/// the WAL change stream (postcard). This is the serde/WAL counterpart of the rkyv
323/// snapshot-side `selene_graph::graph_types::RecordFieldTypes`; the two carry the same
324/// structure and must round-trip into each other.
325///
326/// Record structure is inlined on [`PropertyDef::record_fields`] rather than on
327/// [`ValueType::record`] (a [`RecordTypeRef`] by-ID that cannot hold inline structure),
328/// symmetric to how `LIST` inlines its element type.
329// Why: Per ISO 39075:2024 §18.9 <record type> / <field types specification> and §18.10
330// <field type>; features GV46 (closed record types) / GV47 (open record types) / GV48
331// (nested record types).
332//
333// Three durable record states are encoded jointly with the `Option` on
334// [`PropertyDef::record_fields`]: absent (`None`) ⇒ the property is not a record;
335// `Some(Open)` ⇒ an open/bare `RECORD` (no declared fields, any record value conforms);
336// `Some(Closed(..))` ⇒ a closed/typed `RECORD{..}`. The open variant is what makes a bare
337// `RECORD` property survive WAL replay as `RecordTyped` rather than degrading to `Null`
338// (it carries no field list, so the absent/open distinction cannot ride the inner `Vec`).
339#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
340pub enum RecordFieldStructure {
341    /// Open/bare `RECORD` — no declared field types; any record value conforms (GV47).
342    Open,
343    /// Closed/typed `RECORD{..}` — the declared field-type list (GV46/GV48).
344    Closed(Vec<RecordFieldStructureDef>),
345}
346
347/// One declared field of a closed/typed RECORD: name, its (possibly nested) type, and
348/// whether the field is required (non-nullable).
349#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
350pub struct RecordFieldStructureDef {
351    /// Field name.
352    pub name: DbString,
353    /// Declared field type (recursively nestable).
354    pub field_type: RecordFieldStructureType,
355    /// `true` when the field is required (NOT NULL).
356    pub required: bool,
357}
358
359/// Recursively-nestable field-type for a closed/typed RECORD declaration (serde/WAL side).
360///
361/// Deliberately **not** `#[non_exhaustive]`: it is matched cross-crate by the
362/// selene-graph rkyv⇄serde conversions, where exhaustive matching is wanted so a future
363/// variant forces both conversion directions to be updated.
364// Why: Per ISO 39075:2024 §18.10 CR1 (GV48 nested record types) — a field type may itself
365// contain a list or record type.
366#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
367pub enum RecordFieldStructureType {
368    /// Scalar field type.
369    Scalar(PropertyValueType),
370    /// STRING field type with a user-specified length envelope.
371    CharacterString(CharacterStringType),
372    /// DECIMAL field type with a user-specified precision/scale envelope.
373    Decimal(DecimalType),
374    /// BYTES field type with a user-specified length envelope.
375    ByteString(ByteStringType),
376    /// LIST field type.
377    List(Box<RecordFieldStructureType>),
378    /// Nested RECORD field type.
379    Record(Box<RecordFieldStructure>),
380    /// Explicitly non-null field or nested element type.
381    NotNull(Box<RecordFieldStructureType>),
382}
383
384/// Property schema definition.
385#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
386pub struct PropertyDef {
387    /// Property name.
388    pub name: DbString,
389    /// Property value type.
390    pub value_type: ValueType,
391    /// Whether `Value::Null` is allowed.
392    pub nullable: bool,
393    /// Optional default value.
394    pub default: Option<Value>,
395    /// Whether updates to this property are forbidden after creation.
396    #[serde(default)]
397    pub immutable: bool,
398    /// Whether non-null property values must be unique within the declaring type.
399    #[serde(default)]
400    pub unique: bool,
401    /// Inline RECORD field structure when [`PropertyDef::value_type`] resolves to a
402    /// `RecordTyped` property. `None` for every non-record property; `Some(Open)` for an
403    /// open/bare `RECORD`; `Some(Closed(..))` for a closed/typed `RECORD{..}`. The
404    /// `None`-vs-`Some(Open)` distinction is load-bearing: it is the only durable signal
405    /// that a bare `RECORD` property is record-typed (its `ValueType` is otherwise
406    /// indistinguishable from a scalar `Null` on the WAL side), so without it WAL replay
407    /// would degrade an open record to `Null`. Carried for WAL durability, symmetric to
408    /// the rkyv snapshot-side
409    /// `selene_graph::graph_types::PropertyTypeDef::record_field_types`.
410    #[serde(default)]
411    pub record_fields: Option<Box<RecordFieldStructure>>,
412}
413
414/// Legacy WAL property definition carried by v1 catalog-DDL schema changes.
415#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
416pub struct PropertyDefV1 {
417    /// Property name.
418    pub name: DbString,
419    /// Property value type.
420    pub value_type: ValueType,
421    /// Whether `Value::Null` is allowed.
422    pub nullable: bool,
423    /// Optional default value.
424    pub default: Option<Value>,
425}
426
427impl From<PropertyDefV1> for PropertyDef {
428    fn from(value: PropertyDefV1) -> Self {
429        Self {
430            name: value.name,
431            value_type: value.value_type,
432            nullable: value.nullable,
433            default: value.default,
434            immutable: false,
435            unique: false,
436            record_fields: None,
437        }
438    }
439}
440
441/// Structural value type definition.
442#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
443pub struct ValueType {
444    /// Scalar predefined type.
445    pub predefined: Option<PredefinedValueType>,
446    /// User-specified decimal precision/scale descriptor.
447    ///
448    /// Only meaningful when [`Self::predefined`] is
449    /// [`PredefinedValueType::Decimal`].
450    #[serde(default)]
451    pub decimal_type: Option<DecimalType>,
452    /// User-specified character-string length descriptor.
453    ///
454    /// Only meaningful when [`Self::predefined`] is
455    /// [`PredefinedValueType::String`].
456    #[serde(default)]
457    pub character_string_type: Option<CharacterStringType>,
458    /// User-specified byte-string length descriptor.
459    ///
460    /// Only meaningful when [`Self::predefined`] is [`PredefinedValueType::Bytes`].
461    #[serde(default)]
462    pub byte_string_type: Option<ByteStringType>,
463    /// Union member types.
464    pub union: Option<Vec<ValueType>>,
465    /// List element type. When present, this takes precedence over scalar
466    /// fields for callers interpreting the type.
467    pub list_of: Option<Box<ValueType>>,
468    /// Closed record type reference.
469    pub record: Option<RecordTypeRef>,
470    /// Whether null is forbidden at this level.
471    pub not_null: bool,
472    /// Currently supported scalar cardinality.
473    pub cardinality: ValueTypeCardinality,
474}
475
476impl ValueType {
477    /// Construct a predefined scalar value type.
478    #[must_use]
479    pub const fn predefined(predefined: PredefinedValueType) -> Self {
480        Self {
481            predefined: Some(predefined),
482            decimal_type: None,
483            character_string_type: None,
484            byte_string_type: None,
485            union: None,
486            list_of: None,
487            record: None,
488            not_null: false,
489            cardinality: ValueTypeCardinality::ExactlyOne,
490        }
491    }
492
493    /// Construct a list value type.
494    #[must_use]
495    pub fn list_of(item: Self) -> Self {
496        Self {
497            predefined: None,
498            decimal_type: None,
499            character_string_type: None,
500            byte_string_type: None,
501            union: None,
502            list_of: Some(Box::new(item)),
503            record: None,
504            not_null: false,
505            cardinality: ValueTypeCardinality::ExactlyOne,
506        }
507    }
508}
509
510/// Predefined value types claimed by the D1 surface.
511#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
512pub enum PredefinedValueType {
513    /// Boolean.
514    Bool,
515    /// Default signed integer.
516    Int,
517    /// 8-bit signed integer.
518    Int8,
519    /// 16-bit signed integer.
520    Int16,
521    /// 32-bit signed integer.
522    Int32,
523    /// 64-bit signed integer.
524    Int64,
525    /// 128-bit signed integer.
526    Int128,
527    /// Default unsigned integer.
528    Uint,
529    /// 8-bit unsigned integer.
530    Uint8,
531    /// 16-bit unsigned integer.
532    Uint16,
533    /// 32-bit unsigned integer.
534    Uint32,
535    /// 64-bit unsigned integer.
536    Uint64,
537    /// 128-bit unsigned integer.
538    Uint128,
539    /// Default floating-point number.
540    Float,
541    /// 32-bit floating-point number.
542    Float32,
543    /// 64-bit floating-point number.
544    Float64,
545    /// Fixed-precision decimal.
546    Decimal,
547    /// Database string.
548    String,
549    /// Byte string.
550    Bytes,
551    /// Date.
552    Date,
553    /// Local time.
554    LocalTime,
555    /// Zoned time.
556    ZonedTime,
557    /// Local datetime.
558    LocalDateTime,
559    /// Zoned datetime.
560    ZonedDateTime,
561    /// Duration.
562    Duration,
563    /// `DURATION (YEAR TO MONTH)`.
564    DurationYearToMonth,
565    /// `DURATION (DAY TO SECOND)`.
566    DurationDayToSecond,
567    /// Node reference.
568    NodeRef,
569    /// Edge reference.
570    EdgeRef,
571    /// Graph reference.
572    GraphRef,
573    /// Binding-table reference.
574    TableRef,
575    /// Path.
576    Path,
577    /// UUID.
578    Uuid,
579    /// Extension-owned value type.
580    Extended(ExtensionTypeId),
581    /// Native dense vector.
582    Vector,
583    /// Native JSON.
584    Json,
585}
586
587/// Currently supported value cardinality.
588#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
589pub enum ValueTypeCardinality {
590    /// Exactly one value.
591    ExactlyOne,
592    /// Zero or one value.
593    ZeroOrOne,
594}
595
596/// Closed record type definition.
597#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
598pub struct RecordTypeDef {
599    /// Stable record type ID.
600    pub id: RecordTypeId,
601    /// Database-string record type name.
602    pub name: DbString,
603    /// Field definitions in schema order.
604    pub fields: SmallVec<[PropertyDef; 4]>,
605}
606
607/// Reserved policy for relationships between the key label sets of a closed
608/// graph type's element types.
609///
610/// **Currently inert.** This value is persisted on [`GraphType`] but is not yet
611/// consulted by any binding or validation path: closed-graph element binding
612/// uses exact key-label-set equality, and the catalog DDL only produces
613/// singleton key label sets (one label per type — see
614/// `selene-gql`'s `create_node_type` lowering), so no two key label sets can
615/// stand in an overlap/containment relationship. The variants reserve the two
616/// ISO postures that become meaningful once future multi-label key label set
617/// support lands.
618#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
619pub enum KeyLabelSetPolicy {
620    /// Reserved: key label sets must be pairwise disjoint.
621    NoOverlap,
622    /// Reserved: key label sets may contain one another, enabling ISO/IEC
623    /// 39075:2024 §4.13.2.7 *key label set implication consistency* — a type
624    /// whose key label set is a subset of another type's label set "implies"
625    /// that super-type. The current default; activated when multi-label key
626    /// label sets ship.
627    #[default]
628    Containment,
629}
630
631#[cfg(test)]
632#[path = "schema_tests.rs"]
633mod tests;