Skip to main content

reifydb_type/value/constraint/
mod.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2025 ReifyDB
3
4use serde::{Deserialize, Serialize};
5
6use crate::{
7	error::{
8		Error,
9		diagnostic::constraint::{none_not_allowed, utf8_exceeds_max_bytes},
10	},
11	fragment::Fragment,
12	value::{
13		Value,
14		constraint::{bytes::MaxBytes, precision::Precision, scale::Scale},
15		dictionary::DictionaryId,
16		sumtype::SumTypeId,
17		r#type::Type,
18	},
19};
20
21pub mod bytes;
22pub mod precision;
23pub mod scale;
24
25/// Represents a type with optional constraints
26#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
27pub struct TypeConstraint {
28	base_type: Type,
29	constraint: Option<Constraint>,
30}
31
32/// Constraint types for different data types
33#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
34pub enum Constraint {
35	/// Maximum number of bytes for UTF8, BLOB, INT, UINT
36	MaxBytes(MaxBytes),
37	/// Precision and scale for DECIMAL
38	PrecisionScale(Precision, Scale),
39	/// Dictionary constraint: (catalog dictionary ID, id_type)
40	Dictionary(DictionaryId, Type),
41	/// Sum type constraint: links a logical column to a catalog SumTypeDef
42	SumType(SumTypeId),
43}
44
45/// FFI-safe representation of a TypeConstraint
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47#[repr(C)]
48pub struct FFITypeConstraint {
49	/// Base type code (Type::to_u8)
50	pub base_type: u8,
51	/// Constraint type: 0=None, 1=MaxBytes, 2=PrecisionScale, 3=Dictionary, 4=SumType
52	pub constraint_type: u8,
53	/// First constraint param: MaxBytes value OR precision OR dictionary_id low 32 bits
54	pub constraint_param1: u32,
55	/// Second constraint param: scale (PrecisionScale) OR id_type (Dictionary)
56	pub constraint_param2: u32,
57}
58
59impl TypeConstraint {
60	/// Create an unconstrained type
61	pub const fn unconstrained(ty: Type) -> Self {
62		Self {
63			base_type: ty,
64			constraint: None,
65		}
66	}
67
68	/// Create a type with a constraint
69	pub fn with_constraint(ty: Type, constraint: Constraint) -> Self {
70		Self {
71			base_type: ty,
72			constraint: Some(constraint),
73		}
74	}
75
76	/// Create a dictionary type constraint
77	pub fn dictionary(dictionary_id: DictionaryId, id_type: Type) -> Self {
78		Self {
79			base_type: Type::DictionaryId,
80			constraint: Some(Constraint::Dictionary(dictionary_id, id_type)),
81		}
82	}
83
84	/// Create a sum type constraint (tag stored as Uint1)
85	pub fn sumtype(id: SumTypeId) -> Self {
86		Self {
87			base_type: Type::Uint1,
88			constraint: Some(Constraint::SumType(id)),
89		}
90	}
91
92	/// Get the base type
93	pub fn get_type(&self) -> Type {
94		self.base_type.clone()
95	}
96
97	/// Get the storage type. For DictionaryId with a Dictionary constraint,
98	/// returns the id_type (e.g. Uint4). For all other types, returns the base_type.
99	pub fn storage_type(&self) -> Type {
100		match (&self.base_type, &self.constraint) {
101			(Type::DictionaryId, Some(Constraint::Dictionary(_, id_type))) => id_type.clone(),
102			_ => self.base_type.clone(),
103		}
104	}
105
106	/// Get the constraint
107	pub fn constraint(&self) -> &Option<Constraint> {
108		&self.constraint
109	}
110
111	/// Convert to FFI representation
112	pub fn to_ffi(&self) -> FFITypeConstraint {
113		let base_type = self.base_type.to_u8();
114		match &self.constraint {
115			None => FFITypeConstraint {
116				base_type,
117				constraint_type: 0,
118				constraint_param1: 0,
119				constraint_param2: 0,
120			},
121			Some(Constraint::MaxBytes(max)) => FFITypeConstraint {
122				base_type,
123				constraint_type: 1,
124				constraint_param1: max.value(),
125				constraint_param2: 0,
126			},
127			Some(Constraint::PrecisionScale(p, s)) => FFITypeConstraint {
128				base_type,
129				constraint_type: 2,
130				constraint_param1: p.value() as u32,
131				constraint_param2: s.value() as u32,
132			},
133			Some(Constraint::Dictionary(dict_id, id_type)) => FFITypeConstraint {
134				base_type,
135				constraint_type: 3,
136				constraint_param1: dict_id.to_u64() as u32,
137				constraint_param2: id_type.to_u8() as u32,
138			},
139			Some(Constraint::SumType(id)) => FFITypeConstraint {
140				base_type,
141				constraint_type: 4,
142				constraint_param1: id.to_u64() as u32,
143				constraint_param2: 0,
144			},
145		}
146	}
147
148	/// Create from FFI representation
149	pub fn from_ffi(ffi: FFITypeConstraint) -> Self {
150		let ty = Type::from_u8(ffi.base_type);
151		match ffi.constraint_type {
152			1 => Self::with_constraint(ty, Constraint::MaxBytes(MaxBytes::new(ffi.constraint_param1))),
153			2 => Self::with_constraint(
154				ty,
155				Constraint::PrecisionScale(
156					Precision::new(ffi.constraint_param1 as u8),
157					Scale::new(ffi.constraint_param2 as u8),
158				),
159			),
160			3 => Self::with_constraint(
161				ty,
162				Constraint::Dictionary(
163					DictionaryId::from(ffi.constraint_param1 as u64),
164					Type::from_u8(ffi.constraint_param2 as u8),
165				),
166			),
167			4 => Self::with_constraint(
168				ty,
169				Constraint::SumType(SumTypeId::from(ffi.constraint_param1 as u64)),
170			),
171			_ => Self::unconstrained(ty),
172		}
173	}
174
175	/// Validate a value against this type constraint
176	pub fn validate(&self, value: &Value) -> Result<(), Error> {
177		// First check type compatibility
178		let value_type = value.get_type();
179		if value_type != self.base_type && !matches!(value, Value::None { .. }) {
180			// For Option types, also accept values matching the inner type
181			if let Type::Option(inner) = &self.base_type {
182				if value_type != **inner {
183					unimplemented!()
184				}
185			} else {
186				unimplemented!()
187			}
188		}
189
190		// If None, only allow for Option types
191		if matches!(value, Value::None { .. }) {
192			if self.base_type.is_option() {
193				return Ok(());
194			} else {
195				return Err(crate::error!(none_not_allowed(Fragment::None, &self.base_type)));
196			}
197		}
198
199		// Check constraints if present
200		match (&self.base_type, &self.constraint) {
201			(Type::Utf8, Some(Constraint::MaxBytes(max))) => {
202				if let Value::Utf8(s) = value {
203					let byte_len = s.as_bytes().len();
204					let max_value: usize = (*max).into();
205					if byte_len > max_value {
206						return Err(crate::error!(utf8_exceeds_max_bytes(
207							Fragment::None,
208							byte_len,
209							max_value
210						)));
211					}
212				}
213			}
214			(Type::Blob, Some(Constraint::MaxBytes(max))) => {
215				if let Value::Blob(blob) = value {
216					let byte_len = blob.len();
217					let max_value: usize = (*max).into();
218					if byte_len > max_value {
219						return Err(crate::error!(
220							crate::error::diagnostic::constraint::blob_exceeds_max_bytes(
221								Fragment::None,
222								byte_len,
223								max_value
224							)
225						));
226					}
227				}
228			}
229			(Type::Int, Some(Constraint::MaxBytes(max))) => {
230				if let Value::Int(vi) = value {
231					// Calculate byte size of Int by
232					// converting to string and estimating
233					// This is a rough estimate: each
234					// decimal digit needs ~3.32 bits, so
235					// ~0.415 bytes
236					let str_len = vi.to_string().len();
237					let byte_len = (str_len * 415 / 1000) + 1; // Rough estimate
238					let max_value: usize = (*max).into();
239					if byte_len > max_value {
240						return Err(crate::error!(
241							crate::error::diagnostic::constraint::int_exceeds_max_bytes(
242								Fragment::None,
243								byte_len,
244								max_value
245							)
246						));
247					}
248				}
249			}
250			(Type::Uint, Some(Constraint::MaxBytes(max))) => {
251				if let Value::Uint(vu) = value {
252					// Calculate byte size of Uint by
253					// converting to string and estimating
254					// This is a rough estimate: each
255					// decimal digit needs ~3.32 bits, so
256					// ~0.415 bytes
257					let str_len = vu.to_string().len();
258					let byte_len = (str_len * 415 / 1000) + 1; // Rough estimate
259					let max_value: usize = (*max).into();
260					if byte_len > max_value {
261						return Err(crate::error!(
262							crate::error::diagnostic::constraint::uint_exceeds_max_bytes(
263								Fragment::None,
264								byte_len,
265								max_value
266							)
267						));
268					}
269				}
270			}
271			(Type::Decimal, Some(Constraint::PrecisionScale(precision, scale))) => {
272				if let Value::Decimal(decimal) = value {
273					// Calculate precision and scale from
274					// BigDecimal
275					let decimal_str = decimal.to_string();
276
277					// Calculate scale (digits after decimal
278					// point)
279					let decimal_scale: u8 = if let Some(dot_pos) = decimal_str.find('.') {
280						let after_dot = &decimal_str[dot_pos + 1..];
281						after_dot.len().min(255) as u8
282					} else {
283						0
284					};
285
286					// Calculate precision (total number of
287					// significant digits)
288					let decimal_precision: u8 =
289						decimal_str.chars().filter(|c| c.is_ascii_digit()).count().min(255)
290							as u8;
291
292					let scale_value: u8 = (*scale).into();
293					let precision_value: u8 = (*precision).into();
294
295					if decimal_scale > scale_value {
296						return Err(crate::error!(
297							crate::error::diagnostic::constraint::decimal_exceeds_scale(
298								Fragment::None,
299								decimal_scale,
300								scale_value
301							)
302						));
303					}
304					if decimal_precision > precision_value {
305						return Err(crate::error!(
306							crate::error::diagnostic::constraint::decimal_exceeds_precision(
307								Fragment::None,
308								decimal_precision,
309								precision_value
310							)
311						));
312					}
313				}
314			}
315			// No constraint or non-applicable constraint
316			_ => {}
317		}
318
319		Ok(())
320	}
321
322	/// Check if this type is unconstrained
323	pub fn is_unconstrained(&self) -> bool {
324		self.constraint.is_none()
325	}
326
327	/// Get a human-readable string representation
328	pub fn to_string(&self) -> String {
329		match &self.constraint {
330			None => format!("{}", self.base_type),
331			Some(Constraint::MaxBytes(max)) => {
332				format!("{}({})", self.base_type, max)
333			}
334			Some(Constraint::PrecisionScale(p, s)) => {
335				format!("{}({},{})", self.base_type, p, s)
336			}
337			Some(Constraint::Dictionary(dict_id, id_type)) => {
338				format!("DictionaryId(dict={}, {})", dict_id, id_type)
339			}
340			Some(Constraint::SumType(id)) => {
341				format!("SumType({})", id)
342			}
343		}
344	}
345}
346
347#[cfg(test)]
348pub mod tests {
349	use super::*;
350
351	#[test]
352	fn test_unconstrained_type() {
353		let tc = TypeConstraint::unconstrained(Type::Utf8);
354		assert_eq!(tc.base_type, Type::Utf8);
355		assert_eq!(tc.constraint, None);
356		assert!(tc.is_unconstrained());
357	}
358
359	#[test]
360	fn test_constrained_utf8() {
361		let tc = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(50)));
362		assert_eq!(tc.base_type, Type::Utf8);
363		assert_eq!(tc.constraint, Some(Constraint::MaxBytes(MaxBytes::new(50))));
364		assert!(!tc.is_unconstrained());
365	}
366
367	#[test]
368	fn test_constrained_decimal() {
369		let tc = TypeConstraint::with_constraint(
370			Type::Decimal,
371			Constraint::PrecisionScale(Precision::new(10), Scale::new(2)),
372		);
373		assert_eq!(tc.base_type, Type::Decimal);
374		assert_eq!(tc.constraint, Some(Constraint::PrecisionScale(Precision::new(10), Scale::new(2))));
375	}
376
377	#[test]
378	fn test_validate_utf8_within_limit() {
379		let tc = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(10)));
380		let value = Value::Utf8("hello".to_string());
381		assert!(tc.validate(&value).is_ok());
382	}
383
384	#[test]
385	fn test_validate_utf8_exceeds_limit() {
386		let tc = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(5)));
387		let value = Value::Utf8("hello world".to_string());
388		assert!(tc.validate(&value).is_err());
389	}
390
391	#[test]
392	fn test_validate_unconstrained() {
393		let tc = TypeConstraint::unconstrained(Type::Utf8);
394		let value = Value::Utf8("any length string is fine here".to_string());
395		assert!(tc.validate(&value).is_ok());
396	}
397
398	#[test]
399	fn test_validate_none_rejected_for_non_option() {
400		let tc = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(5)));
401		let value = Value::none();
402		assert!(tc.validate(&value).is_err());
403	}
404
405	#[test]
406	fn test_validate_none_accepted_for_option() {
407		let tc = TypeConstraint::unconstrained(Type::Option(Box::new(Type::Utf8)));
408		let value = Value::none();
409		assert!(tc.validate(&value).is_ok());
410	}
411
412	#[test]
413	fn test_to_string() {
414		let tc1 = TypeConstraint::unconstrained(Type::Utf8);
415		assert_eq!(tc1.to_string(), "Utf8");
416
417		let tc2 = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(50)));
418		assert_eq!(tc2.to_string(), "Utf8(50)");
419
420		let tc3 = TypeConstraint::with_constraint(
421			Type::Decimal,
422			Constraint::PrecisionScale(Precision::new(10), Scale::new(2)),
423		);
424		assert_eq!(tc3.to_string(), "Decimal(10,2)");
425	}
426}