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;