Skip to main content

reifydb_value/value/constraint/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use serde::{Deserialize, Serialize};
5
6use crate::{
7	error::{ConstraintKind, Error, TypeError},
8	fragment::Fragment,
9	value::{
10		Value,
11		constraint::{bytes::MaxBytes, precision::Precision, scale::Scale},
12		dictionary::DictionaryId,
13		sumtype::SumTypeId,
14		value_type::ValueType,
15	},
16};
17
18pub mod bytes;
19pub mod precision;
20pub mod scale;
21
22#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
23pub struct TypeConstraint {
24	base_type: ValueType,
25	constraint: Option<Constraint>,
26}
27
28#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
29pub enum Constraint {
30	MaxBytes(MaxBytes),
31
32	PrecisionScale(Precision, Scale),
33
34	Dictionary(DictionaryId, ValueType),
35
36	SumType(SumTypeId),
37}
38
39impl TypeConstraint {
40	pub const fn unconstrained(ty: ValueType) -> Self {
41		Self {
42			base_type: ty,
43			constraint: None,
44		}
45	}
46
47	pub fn with_constraint(ty: ValueType, constraint: Constraint) -> Self {
48		Self {
49			base_type: ty,
50			constraint: Some(constraint),
51		}
52	}
53
54	pub fn dictionary(dictionary_id: DictionaryId, id_type: ValueType) -> Self {
55		Self {
56			base_type: ValueType::DictionaryId,
57			constraint: Some(Constraint::Dictionary(dictionary_id, id_type)),
58		}
59	}
60
61	pub fn sumtype(id: SumTypeId) -> Self {
62		Self {
63			base_type: ValueType::Uint1,
64			constraint: Some(Constraint::SumType(id)),
65		}
66	}
67
68	pub fn get_type(&self) -> ValueType {
69		self.base_type.clone()
70	}
71
72	pub fn storage_type(&self) -> ValueType {
73		match (&self.base_type, &self.constraint) {
74			(ValueType::DictionaryId, Some(Constraint::Dictionary(_, id_type))) => id_type.clone(),
75			_ => self.base_type.clone(),
76		}
77	}
78
79	pub fn constraint(&self) -> &Option<Constraint> {
80		&self.constraint
81	}
82
83	pub fn validate(&self, value: &Value) -> Result<(), Error> {
84		let value_type = value.get_type();
85		if value_type != self.base_type && !matches!(value, Value::None { .. }) {
86			if let ValueType::Option(inner) = &self.base_type {
87				if value_type != **inner {
88					unimplemented!()
89				}
90			} else {
91				unimplemented!()
92			}
93		}
94
95		if matches!(value, Value::None { .. }) {
96			if self.base_type.is_option() {
97				return Ok(());
98			} else {
99				return Err(TypeError::ConstraintViolation {
100					kind: ConstraintKind::NoneNotAllowed {
101						column_type: self.base_type.clone(),
102					},
103					message: format!(
104						"Cannot insert none into non-optional column of type {}. Declare the column as Option({}) to allow none values.",
105						self.base_type, self.base_type
106					),
107					fragment: Fragment::None,
108				}
109				.into());
110			}
111		}
112
113		match (&self.base_type, &self.constraint) {
114			(ValueType::Utf8, Some(Constraint::MaxBytes(max))) => {
115				if let Value::Utf8(s) = value {
116					let byte_len = s.len();
117					let max_value: usize = (*max).into();
118					if byte_len > max_value {
119						return Err(TypeError::ConstraintViolation {
120							kind: ConstraintKind::Utf8MaxBytes {
121								actual: byte_len,
122								max: max_value,
123							},
124							message: format!(
125								"UTF8 value exceeds maximum byte length: {} bytes (max: {} bytes)",
126								byte_len, max_value
127							),
128							fragment: Fragment::None,
129						}
130						.into());
131					}
132				}
133			}
134			(ValueType::Blob, Some(Constraint::MaxBytes(max))) => {
135				if let Value::Blob(blob) = value {
136					let byte_len = blob.len();
137					let max_value: usize = (*max).into();
138					if byte_len > max_value {
139						return Err(TypeError::ConstraintViolation {
140							kind: ConstraintKind::BlobMaxBytes {
141								actual: byte_len,
142								max: max_value,
143							},
144							message: format!(
145								"BLOB value exceeds maximum byte length: {} bytes (max: {} bytes)",
146								byte_len, max_value
147							),
148							fragment: Fragment::None,
149						}
150						.into());
151					}
152				}
153			}
154			(ValueType::Int, Some(Constraint::MaxBytes(max))) => {
155				if let Value::Int(vi) = value {
156					let str_len = vi.to_string().len();
157					let byte_len = (str_len * 415 / 1000) + 1;
158					let max_value: usize = (*max).into();
159					if byte_len > max_value {
160						return Err(TypeError::ConstraintViolation {
161							kind: ConstraintKind::IntMaxBytes {
162								actual: byte_len,
163								max: max_value,
164							},
165							message: format!(
166								"INT value exceeds maximum byte length: {} bytes (max: {} bytes)",
167								byte_len, max_value
168							),
169							fragment: Fragment::None,
170						}
171						.into());
172					}
173				}
174			}
175			(ValueType::Uint, Some(Constraint::MaxBytes(max))) => {
176				if let Value::Uint(vu) = value {
177					let str_len = vu.to_string().len();
178					let byte_len = (str_len * 415 / 1000) + 1;
179					let max_value: usize = (*max).into();
180					if byte_len > max_value {
181						return Err(TypeError::ConstraintViolation {
182							kind: ConstraintKind::UintMaxBytes {
183								actual: byte_len,
184								max: max_value,
185							},
186							message: format!(
187								"UINT value exceeds maximum byte length: {} bytes (max: {} bytes)",
188								byte_len, max_value
189							),
190							fragment: Fragment::None,
191						}
192						.into());
193					}
194				}
195			}
196			(ValueType::Decimal, Some(Constraint::PrecisionScale(precision, scale))) => {
197				if let Value::Decimal(decimal) = value {
198					let decimal_str = decimal.to_string();
199
200					let decimal_scale: u8 = if let Some(dot_pos) = decimal_str.find('.') {
201						let after_dot = &decimal_str[dot_pos + 1..];
202						after_dot.len().min(255) as u8
203					} else {
204						0
205					};
206
207					let decimal_precision: u8 =
208						decimal_str.chars().filter(|c| c.is_ascii_digit()).count().min(255)
209							as u8;
210
211					let scale_value: u8 = (*scale).into();
212					let precision_value: u8 = (*precision).into();
213
214					if decimal_scale > scale_value {
215						return Err(TypeError::ConstraintViolation {
216							kind: ConstraintKind::DecimalScale {
217								actual: decimal_scale,
218								max: scale_value,
219							},
220							message: format!(
221								"DECIMAL value exceeds maximum scale: {} decimal places (max: {} decimal places)",
222								decimal_scale, scale_value
223							),
224							fragment: Fragment::None,
225						}
226						.into());
227					}
228					if decimal_precision > precision_value {
229						return Err(TypeError::ConstraintViolation {
230							kind: ConstraintKind::DecimalPrecision {
231								actual: decimal_precision,
232								max: precision_value,
233							},
234							message: format!(
235								"DECIMAL value exceeds maximum precision: {} digits (max: {} digits)",
236								decimal_precision, precision_value
237							),
238							fragment: Fragment::None,
239						}
240						.into());
241					}
242				}
243			}
244
245			_ => {}
246		}
247
248		Ok(())
249	}
250
251	pub fn is_unconstrained(&self) -> bool {
252		self.constraint.is_none()
253	}
254
255	#[allow(clippy::inherent_to_string)]
256	pub fn to_string(&self) -> String {
257		match &self.constraint {
258			None => format!("{}", self.base_type),
259			Some(Constraint::MaxBytes(max)) => {
260				format!("{}({})", self.base_type, max)
261			}
262			Some(Constraint::PrecisionScale(p, s)) => {
263				format!("{}({},{})", self.base_type, p, s)
264			}
265			Some(Constraint::Dictionary(dict_id, id_type)) => {
266				format!("DictionaryId(dict={}, {})", dict_id, id_type)
267			}
268			Some(Constraint::SumType(id)) => {
269				format!("SumType({})", id)
270			}
271		}
272	}
273}
274
275#[cfg(test)]
276pub mod tests {
277	use super::*;
278
279	#[test]
280	fn test_unconstrained_type() {
281		let tc = TypeConstraint::unconstrained(ValueType::Utf8);
282		assert_eq!(tc.base_type, ValueType::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(ValueType::Utf8, Constraint::MaxBytes(MaxBytes::new(50)));
290		assert_eq!(tc.base_type, ValueType::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			ValueType::Decimal,
299			Constraint::PrecisionScale(Precision::new(10), Scale::new(2)),
300		);
301		assert_eq!(tc.base_type, ValueType::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(ValueType::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(ValueType::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(ValueType::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_none_rejected_for_non_option() {
328		let tc = TypeConstraint::with_constraint(ValueType::Utf8, Constraint::MaxBytes(MaxBytes::new(5)));
329		let value = Value::none();
330		assert!(tc.validate(&value).is_err());
331	}
332
333	#[test]
334	fn test_validate_none_accepted_for_option() {
335		let tc = TypeConstraint::unconstrained(ValueType::Option(Box::new(ValueType::Utf8)));
336		let value = Value::none();
337		assert!(tc.validate(&value).is_ok());
338	}
339
340	#[test]
341	fn test_to_string() {
342		let tc1 = TypeConstraint::unconstrained(ValueType::Utf8);
343		assert_eq!(tc1.to_string(), "Utf8");
344
345		let tc2 = TypeConstraint::with_constraint(ValueType::Utf8, Constraint::MaxBytes(MaxBytes::new(50)));
346		assert_eq!(tc2.to_string(), "Utf8(50)");
347
348		let tc3 = TypeConstraint::with_constraint(
349			ValueType::Decimal,
350			Constraint::PrecisionScale(Precision::new(10), Scale::new(2)),
351		);
352		assert_eq!(tc3.to_string(), "Decimal(10,2)");
353	}
354}