reifydb_type/value/constraint/
mod.rs

1// Copyright (c) reifydb.com 2025
2// This file is licensed under the MIT, see license.md file
3
4use serde::{Deserialize, Serialize};
5
6use crate::{
7	Error, OwnedFragment, Type, Value,
8	value::constraint::{bytes::MaxBytes, precision::Precision, scale::Scale},
9};
10
11pub mod bytes;
12pub mod precision;
13pub mod scale;
14
15/// Represents a type with optional constraints
16#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
17pub struct TypeConstraint {
18	base_type: Type,
19	constraint: Option<Constraint>,
20}
21
22/// Constraint types for different data types
23#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
24pub enum Constraint {
25	/// Maximum number of bytes for UTF8, BLOB, INT, UINT
26	MaxBytes(MaxBytes),
27	/// Precision and scale for DECIMAL
28	PrecisionScale(Precision, Scale),
29}
30
31impl TypeConstraint {
32	/// Create an unconstrained type
33	pub fn unconstrained(ty: Type) -> Self {
34		Self {
35			base_type: ty,
36			constraint: None,
37		}
38	}
39
40	/// Create a type with a constraint
41	pub fn with_constraint(ty: Type, constraint: Constraint) -> Self {
42		Self {
43			base_type: ty,
44			constraint: Some(constraint),
45		}
46	}
47
48	/// Get the base type
49	pub fn get_type(&self) -> Type {
50		self.base_type
51	}
52
53	/// Get the constraint
54	pub fn constraint(&self) -> &Option<Constraint> {
55		&self.constraint
56	}
57
58	/// Validate a value against this type constraint
59	pub fn validate(&self, value: &Value) -> Result<(), Error> {
60		// First check type compatibility
61		let value_type = value.get_type();
62		if value_type != self.base_type && value_type != Type::Undefined {
63			// For now, return a simple error - we'll create proper
64			// diagnostics later
65			return Err(crate::error!(crate::error::diagnostic::internal::internal(format!(
66				"Type mismatch: expected {}, got {}",
67				self.base_type, value_type
68			))));
69		}
70
71		// If undefined, no further validation needed
72		if matches!(value, Value::Undefined) {
73			return Ok(());
74		}
75
76		// Check constraints if present
77		match (&self.base_type, &self.constraint) {
78			(Type::Utf8, Some(Constraint::MaxBytes(max))) => {
79				if let Value::Utf8(s) = value {
80					let byte_len = s.as_bytes().len();
81					let max_value: usize = (*max).into();
82					if byte_len > max_value {
83						return Err(crate::error!(
84							crate::error::diagnostic::constraint::utf8_exceeds_max_bytes(
85								OwnedFragment::None,
86								byte_len,
87								max_value
88							)
89						));
90					}
91				}
92			}
93			(Type::Blob, Some(Constraint::MaxBytes(max))) => {
94				if let Value::Blob(blob) = value {
95					let byte_len = blob.len();
96					let max_value: usize = (*max).into();
97					if byte_len > max_value {
98						return Err(crate::error!(
99							crate::error::diagnostic::constraint::blob_exceeds_max_bytes(
100								OwnedFragment::None,
101								byte_len,
102								max_value
103							)
104						));
105					}
106				}
107			}
108			(Type::Int, Some(Constraint::MaxBytes(max))) => {
109				if let Value::Int(vi) = value {
110					// Calculate byte size of Int by
111					// converting to string and estimating
112					// This is a rough estimate: each
113					// decimal digit needs ~3.32 bits, so
114					// ~0.415 bytes
115					let str_len = vi.to_string().len();
116					let byte_len = (str_len * 415 / 1000) + 1; // Rough estimate
117					let max_value: usize = (*max).into();
118					if byte_len > max_value {
119						return Err(crate::error!(
120							crate::error::diagnostic::constraint::int_exceeds_max_bytes(
121								OwnedFragment::None,
122								byte_len,
123								max_value
124							)
125						));
126					}
127				}
128			}
129			(Type::Uint, Some(Constraint::MaxBytes(max))) => {
130				if let Value::Uint(vu) = value {
131					// Calculate byte size of Uint by
132					// converting to string and estimating
133					// This is a rough estimate: each
134					// decimal digit needs ~3.32 bits, so
135					// ~0.415 bytes
136					let str_len = vu.to_string().len();
137					let byte_len = (str_len * 415 / 1000) + 1; // Rough estimate
138					let max_value: usize = (*max).into();
139					if byte_len > max_value {
140						return Err(crate::error!(
141							crate::error::diagnostic::constraint::uint_exceeds_max_bytes(
142								OwnedFragment::None,
143								byte_len,
144								max_value
145							)
146						));
147					}
148				}
149			}
150			(Type::Decimal, Some(Constraint::PrecisionScale(precision, scale))) => {
151				if let Value::Decimal(decimal) = value {
152					// Calculate precision and scale from
153					// BigDecimal
154					let decimal_str = decimal.to_string();
155
156					// Calculate scale (digits after decimal
157					// point)
158					let decimal_scale: u8 = if let Some(dot_pos) = decimal_str.find('.') {
159						let after_dot = &decimal_str[dot_pos + 1..];
160						after_dot.len().min(255) as u8
161					} else {
162						0
163					};
164
165					// Calculate precision (total number of
166					// significant digits)
167					let decimal_precision: u8 =
168						decimal_str.chars().filter(|c| c.is_ascii_digit()).count().min(255)
169							as u8;
170
171					let scale_value: u8 = (*scale).into();
172					let precision_value: u8 = (*precision).into();
173
174					if decimal_scale > scale_value {
175						return Err(crate::error!(
176							crate::error::diagnostic::constraint::decimal_exceeds_scale(
177								OwnedFragment::None,
178								decimal_scale,
179								scale_value
180							)
181						));
182					}
183					if decimal_precision > precision_value {
184						return Err(crate::error!(
185							crate::error::diagnostic::constraint::decimal_exceeds_precision(
186								OwnedFragment::None,
187								decimal_precision,
188								precision_value
189							)
190						));
191					}
192				}
193			}
194			// No constraint or non-applicable constraint
195			_ => {}
196		}
197
198		Ok(())
199	}
200
201	/// Check if this type is unconstrained
202	pub fn is_unconstrained(&self) -> bool {
203		self.constraint.is_none()
204	}
205
206	/// Get a human-readable string representation
207	pub fn to_string(&self) -> String {
208		match &self.constraint {
209			None => format!("{}", self.base_type),
210			Some(Constraint::MaxBytes(max)) => {
211				format!("{}({})", self.base_type, max)
212			}
213			Some(Constraint::PrecisionScale(p, s)) => {
214				format!("{}({},{})", self.base_type, p, s)
215			}
216		}
217	}
218}
219
220#[cfg(test)]
221mod tests {
222	use super::*;
223
224	#[test]
225	fn test_unconstrained_type() {
226		let tc = TypeConstraint::unconstrained(Type::Utf8);
227		assert_eq!(tc.base_type, Type::Utf8);
228		assert_eq!(tc.constraint, None);
229		assert!(tc.is_unconstrained());
230	}
231
232	#[test]
233	fn test_constrained_utf8() {
234		let tc = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(50)));
235		assert_eq!(tc.base_type, Type::Utf8);
236		assert_eq!(tc.constraint, Some(Constraint::MaxBytes(MaxBytes::new(50))));
237		assert!(!tc.is_unconstrained());
238	}
239
240	#[test]
241	fn test_constrained_decimal() {
242		let tc = TypeConstraint::with_constraint(
243			Type::Decimal,
244			Constraint::PrecisionScale(Precision::new(10), Scale::new(2)),
245		);
246		assert_eq!(tc.base_type, Type::Decimal);
247		assert_eq!(tc.constraint, Some(Constraint::PrecisionScale(Precision::new(10), Scale::new(2))));
248	}
249
250	#[test]
251	fn test_validate_utf8_within_limit() {
252		let tc = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(10)));
253		let value = Value::Utf8("hello".to_string());
254		assert!(tc.validate(&value).is_ok());
255	}
256
257	#[test]
258	fn test_validate_utf8_exceeds_limit() {
259		let tc = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(5)));
260		let value = Value::Utf8("hello world".to_string());
261		assert!(tc.validate(&value).is_err());
262	}
263
264	#[test]
265	fn test_validate_unconstrained() {
266		let tc = TypeConstraint::unconstrained(Type::Utf8);
267		let value = Value::Utf8("any length string is fine here".to_string());
268		assert!(tc.validate(&value).is_ok());
269	}
270
271	#[test]
272	fn test_validate_undefined() {
273		let tc = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(5)));
274		let value = Value::Undefined;
275		assert!(tc.validate(&value).is_ok());
276	}
277
278	#[test]
279	fn test_to_string() {
280		let tc1 = TypeConstraint::unconstrained(Type::Utf8);
281		assert_eq!(tc1.to_string(), "Utf8");
282
283		let tc2 = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(50)));
284		assert_eq!(tc2.to_string(), "Utf8(50)");
285
286		let tc3 = TypeConstraint::with_constraint(
287			Type::Decimal,
288			Constraint::PrecisionScale(Precision::new(10), Scale::new(2)),
289		);
290		assert_eq!(tc3.to_string(), "Decimal(10,2)");
291	}
292}