Skip to main content

reifydb_core/encoded/shape/
evolution.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use reifydb_type::value::constraint::TypeConstraint;
5
6use crate::encoded::shape::RowShape;
7
8/// Describes how to map a source field to a target field during shape evolution.
9#[derive(Debug, Clone)]
10pub enum FieldMapping {
11	/// Field exists in both shapes at the given source index
12	Direct {
13		source_index: usize,
14	},
15	/// Field is new in target shape, use default value
16	UseDefault,
17	/// Field was removed (source has it, target doesn't) - skip during read
18	Removed,
19}
20
21/// Resolves differences between source and target shapes.
22///
23/// Used when reading data that was written with an older shape version
24/// using a newer shape, or vice versa.
25#[derive(Debug)]
26pub struct ShapeResolver {
27	/// The shape the data was written with
28	source: RowShape,
29	/// The shape we want to read as
30	target: RowShape,
31	/// Mapping from target field index to source field
32	mappings: Vec<FieldMapping>,
33}
34
35impl ShapeResolver {
36	/// Create a resolver to read data from source shape as target shape.
37	///
38	/// Returns None if the shapes are incompatible (e.g., type mismatch
39	/// on same-named field without valid widening path).
40	pub fn new(source: RowShape, target: RowShape) -> Option<Self> {
41		// If fingerprints match, no resolution needed - shapes are identical
42		if source.fingerprint() == target.fingerprint() {
43			return Some(Self {
44				mappings: (0..target.field_count())
45					.map(|i| FieldMapping::Direct {
46						source_index: i,
47					})
48					.collect(),
49				source,
50				target,
51			});
52		}
53
54		let mut mappings = Vec::with_capacity(target.field_count());
55
56		for target_field in target.fields() {
57			if let Some((shape_idx, source_field)) =
58				source.fields().iter().enumerate().find(|(_, f)| f.name == target_field.name)
59			{
60				// Field exists in both - check type compatibility
61				if !Self::types_compatible(&source_field.constraint, &target_field.constraint) {
62					return None; // Incompatible types
63				}
64				mappings.push(FieldMapping::Direct {
65					source_index: shape_idx,
66				});
67			} else {
68				// Field only in target - needs default
69				mappings.push(FieldMapping::UseDefault);
70			}
71		}
72
73		Some(Self {
74			source,
75			target,
76			mappings,
77		})
78	}
79
80	/// Check if source constraint can be read as target constraint.
81	/// For now, just compares base types - constraint widening could be added later.
82	fn types_compatible(source: &TypeConstraint, target: &TypeConstraint) -> bool {
83		let shape_type = source.get_type();
84		let target_type = target.get_type();
85
86		if shape_type == target_type {
87			return true;
88		}
89
90		// Type widening would go here
91		// For now, only identical types are compatible
92		false
93	}
94
95	/// Get the source shape
96	pub fn source(&self) -> &RowShape {
97		&self.source
98	}
99
100	/// Get the target shape
101	pub fn target(&self) -> &RowShape {
102		&self.target
103	}
104
105	/// Get the field mappings
106	pub fn mappings(&self) -> &[FieldMapping] {
107		&self.mappings
108	}
109
110	/// Check if this is an identity mapping (source == target)
111	pub fn is_identity(&self) -> bool {
112		self.source.fingerprint() == self.target.fingerprint()
113	}
114}
115
116#[cfg(test)]
117mod tests {
118	use reifydb_type::value::r#type::Type;
119
120	use super::*;
121	use crate::encoded::shape::RowShapeField;
122
123	#[test]
124	fn test_resolver_identity() {
125		let fields = vec![
126			RowShapeField::unconstrained("a", Type::Int4),
127			RowShapeField::unconstrained("b", Type::Utf8),
128		];
129
130		let shape = RowShape::new(fields);
131		let resolver = ShapeResolver::new(shape.clone(), shape.clone()).unwrap();
132
133		assert!(resolver.is_identity());
134		assert_eq!(resolver.mappings().len(), 2);
135	}
136
137	#[test]
138	fn test_resolver_added_field() {
139		let source_fields = vec![RowShapeField::unconstrained("a", Type::Int4)];
140
141		let target_fields = vec![
142			RowShapeField::unconstrained("a", Type::Int4),
143			RowShapeField::unconstrained("b", Type::Utf8), // new field
144		];
145
146		let source = RowShape::new(source_fields);
147		let target = RowShape::new(target_fields);
148
149		let resolver = ShapeResolver::new(source, target).unwrap();
150
151		assert!(!resolver.is_identity());
152		assert!(matches!(
153			resolver.mappings()[0],
154			FieldMapping::Direct {
155				source_index: 0
156			}
157		));
158		assert!(matches!(resolver.mappings()[1], FieldMapping::UseDefault));
159	}
160
161	#[test]
162	fn test_resolver_incompatible_types() {
163		let source_fields = vec![RowShapeField::unconstrained("a", Type::Int4)];
164		let target_fields = vec![RowShapeField::unconstrained("a", Type::Utf8)]; // type changed
165
166		let source = RowShape::new(source_fields);
167		let target = RowShape::new(target_fields);
168
169		// Should return None due to incompatible types
170		assert!(ShapeResolver::new(source, target).is_none());
171	}
172}