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, Copy, 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, Copy, 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
31/// FFI-safe representation of a TypeConstraint
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33#[repr(C)]
34pub struct FFITypeConstraint {
35	/// Base type code (Type::to_u8)
36	pub base_type: u8,
37	/// Constraint type: 0=None, 1=MaxBytes, 2=PrecisionScale
38	pub constraint_type: u8,
39	/// First constraint param: MaxBytes value OR precision
40	pub constraint_param1: u32,
41	/// Second constraint param: scale (only for PrecisionScale)
42	pub constraint_param2: u32,
43}
44
45impl TypeConstraint {
46	/// Create an unconstrained type (const for use in static contexts)
47	pub const fn unconstrained(ty: Type) -> Self {
48		Self {
49			base_type: ty,
50			constraint: None,
51		}
52	}
53
54	/// Create a type with a constraint
55	pub fn with_constraint(ty: Type, constraint: Constraint) -> Self {
56		Self {
57			base_type: ty,
58			constraint: Some(constraint),
59		}
60	}
61
62	/// Get the base type
63	pub fn get_type(&self) -> Type {
64		self.base_type
65	}
66
67	/// Get the constraint
68	pub fn constraint(&self) -> &Option<Constraint> {
69		&self.constraint
70	}
71
72	/// Convert to FFI representation
73	pub fn to_ffi(&self) -> FFITypeConstraint {
74		let base_type = self.base_type.to_u8();
75		match &self.constraint {
76			None => FFITypeConstraint {
77				base_type,
78				constraint_type: 0,
79				constraint_param1: 0,
80				constraint_param2: 0,
81			},
82			Some(Constraint::MaxBytes(max)) => FFITypeConstraint {
83				base_type,
84				constraint_type: 1,
85				constraint_param1: max.value(),
86				constraint_param2: 0,
87			},
88			Some(Constraint::PrecisionScale(p, s)) => FFITypeConstraint {
89				base_type,
90				constraint_type: 2,
91				constraint_param1: p.value() as u32,
92				constraint_param2: s.value() as u32,
93			},
94		}
95	}
96
97	/// Create from FFI representation
98	pub fn from_ffi(ffi: FFITypeConstraint) -> Self {
99		let ty = Type::from_u8(ffi.base_type);
100		match ffi.constraint_type {
101			1 => Self::with_constraint(ty, Constraint::MaxBytes(MaxBytes::new(ffi.constraint_param1))),
102			2 => Self::with_constraint(
103				ty,
104				Constraint::PrecisionScale(
105					Precision::new(ffi.constraint_param1 as u8),
106					Scale::new(ffi.constraint_param2 as u8),
107				),
108			),
109			_ => Self::unconstrained(ty),
110		}
111	}
112
113	/// Validate a value against this type constraint
114	pub fn validate(&self, value: &Value) -> Result<(), Error> {
115		// First check type compatibility
116		let value_type = value.get_type();
117		if value_type != self.base_type && value_type != Type::Undefined {
118			// For now, return a simple error - we'll create proper
119			// diagnostics later
120			return Err(crate::error!(crate::error::diagnostic::internal::internal(format!(
121				"Type mismatch: expected {}, got {}",
122				self.base_type, value_type
123			))));
124		}
125
126		// If undefined, no further validation needed
127		if matches!(value, Value::Undefined) {
128			return Ok(());
129		}
130
131		// Check constraints if present
132		match (&self.base_type, &self.constraint) {
133			(Type::Utf8, Some(Constraint::MaxBytes(max))) => {
134				if let Value::Utf8(s) = value {
135					let byte_len = s.as_bytes().len();
136					let max_value: usize = (*max).into();
137					if byte_len > max_value {
138						return Err(crate::error!(
139							crate::error::diagnostic::constraint::utf8_exceeds_max_bytes(
140								OwnedFragment::None,
141								byte_len,
142								max_value
143							)
144						));
145					}
146				}
147			}
148			(Type::Blob, Some(Constraint::MaxBytes(max))) => {
149				if let Value::Blob(blob) = value {
150					let byte_len = blob.len();
151					let max_value: usize = (*max).into();
152					if byte_len > max_value {
153						return Err(crate::error!(
154							crate::error::diagnostic::constraint::blob_exceeds_max_bytes(
155								OwnedFragment::None,
156								byte_len,
157								max_value
158							)
159						));
160					}
161				}
162			}
163			(Type::Int, Some(Constraint::MaxBytes(max))) => {
164				if let Value::Int(vi) = value {
165					// Calculate byte size of Int by
166					// converting to string and estimating
167					// This is a rough estimate: each
168					// decimal digit needs ~3.32 bits, so
169					// ~0.415 bytes
170					let str_len = vi.to_string().len();
171					let byte_len = (str_len * 415 / 1000) + 1; // Rough estimate
172					let max_value: usize = (*max).into();
173					if byte_len > max_value {
174						return Err(crate::error!(
175							crate::error::diagnostic::constraint::int_exceeds_max_bytes(
176								OwnedFragment::None,
177								byte_len,
178								max_value
179							)
180						));
181					}
182				}
183			}
184			(Type::Uint, Some(Constraint::MaxBytes(max))) => {
185				if let Value::Uint(vu) = value {
186					// Calculate byte size of Uint by
187					// converting to string and estimating
188					// This is a rough estimate: each
189					// decimal digit needs ~3.32 bits, so
190					// ~0.415 bytes
191					let str_len = vu.to_string().len();
192					let byte_len = (str_len * 415 / 1000) + 1; // Rough estimate
193					let max_value: usize = (*max).into();
194					if byte_len > max_value {
195						return Err(crate::error!(
196							crate::error::diagnostic::constraint::uint_exceeds_max_bytes(
197								OwnedFragment::None,
198								byte_len,
199								max_value
200							)
201						));
202					}
203				}
204			}
205			(Type::Decimal, Some(Constraint::PrecisionScale(precision, scale))) => {
206				if let Value::Decimal(decimal) = value {
207					// Calculate precision and scale from
208					// BigDecimal
209					let decimal_str = decimal.to_string();
210
211					// Calculate scale (digits after decimal
212					// point)
213					let decimal_scale: u8 = if let Some(dot_pos) = decimal_str.find('.') {
214						let after_dot = &decimal_str[dot_pos + 1..];
215						after_dot.len().min(255) as u8
216					} else {
217						0
218					};
219
220					// Calculate precision (total number of
221					// significant digits)
222					let decimal_precision: u8 =
223						decimal_str.chars().filter(|c| c.is_ascii_digit()).count().min(255)
224							as u8;
225
226					let scale_value: u8 = (*scale).into();
227					let precision_value: u8 = (*precision).into();
228
229					if decimal_scale > scale_value {
230						return Err(crate::error!(
231							crate::error::diagnostic::constraint::decimal_exceeds_scale(
232								OwnedFragment::None,
233								decimal_scale,
234								scale_value
235							)
236						));
237					}
238					if decimal_precision > precision_value {
239						return Err(crate::error!(
240							crate::error::diagnostic::constraint::decimal_exceeds_precision(
241								OwnedFragment::None,
242								decimal_precision,
243								precision_value
244							)
245						));
246					}
247				}
248			}
249			// No constraint or non-applicable constraint
250			_ => {}
251		}
252
253		Ok(())
254	}
255
256	/// Check if this type is unconstrained
257	pub fn is_unconstrained(&self) -> bool {
258		self.constraint.is_none()
259	}
260
261	/// Get a human-readable string representation
262	pub fn to_string(&self) -> String {
263		match &self.constraint {
264			None => format!("{}", self.base_type),
265			Some(Constraint::MaxBytes(max)) => {
266				format!("{}({})", self.base_type, max)
267			}
268			Some(Constraint::PrecisionScale(p, s)) => {
269				format!("{}({},{})", self.base_type, p, s)
270			}
271		}
272	}
273}
274
275#[cfg(test)]
276mod tests {
277	use super::*;
278
279	#[test]
280	fn test_unconstrained_type() {
281		let tc = TypeConstraint::unconstrained(Type::Utf8);
282		assert_eq!(tc.base_type, Type::Utf8);
283		assert_eq!(tc.constraint, None);
284		assert!(tc.is_unconstrained());
285	}
286
287	#[test]
288	fn test_constrained_utf8() {
289		let tc = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(50)));
290		assert_eq!(tc.base_type, Type::Utf8);
291		assert_eq!(tc.constraint, Some(Constraint::MaxBytes(MaxBytes::new(50))));
292		assert!(!tc.is_unconstrained());
293	}
294
295	#[test]
296	fn test_constrained_decimal() {
297		let tc = TypeConstraint::with_constraint(
298			Type::Decimal,
299			Constraint::PrecisionScale(Precision::new(10), Scale::new(2)),
300		);
301		assert_eq!(tc.base_type, Type::Decimal);
302		assert_eq!(tc.constraint, Some(Constraint::PrecisionScale(Precision::new(10), Scale::new(2))));
303	}
304
305	#[test]
306	fn test_validate_utf8_within_limit() {
307		let tc = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(10)));
308		let value = Value::Utf8("hello".to_string());
309		assert!(tc.validate(&value).is_ok());
310	}
311
312	#[test]
313	fn test_validate_utf8_exceeds_limit() {
314		let tc = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(5)));
315		let value = Value::Utf8("hello world".to_string());
316		assert!(tc.validate(&value).is_err());
317	}
318
319	#[test]
320	fn test_validate_unconstrained() {
321		let tc = TypeConstraint::unconstrained(Type::Utf8);
322		let value = Value::Utf8("any length string is fine here".to_string());
323		assert!(tc.validate(&value).is_ok());
324	}
325
326	#[test]
327	fn test_validate_undefined() {
328		let tc = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(5)));
329		let value = Value::Undefined;
330		assert!(tc.validate(&value).is_ok());
331	}
332
333	#[test]
334	fn test_to_string() {
335		let tc1 = TypeConstraint::unconstrained(Type::Utf8);
336		assert_eq!(tc1.to_string(), "Utf8");
337
338		let tc2 = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(50)));
339		assert_eq!(tc2.to_string(), "Utf8(50)");
340
341		let tc3 = TypeConstraint::with_constraint(
342			Type::Decimal,
343			Constraint::PrecisionScale(Precision::new(10), Scale::new(2)),
344		);
345		assert_eq!(tc3.to_string(), "Decimal(10,2)");
346	}
347}