Skip to main content

reifydb_type/value/constraint/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 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		r#type::Type,
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: Type,
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, Type),
35
36	SumType(SumTypeId),
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40#[repr(C)]
41pub struct FFITypeConstraint {
42	pub base_type: u8,
43
44	pub constraint_type: u8,
45
46	pub constraint_param1: u32,
47
48	pub constraint_param2: u32,
49}
50
51impl TypeConstraint {
52	pub const fn unconstrained(ty: Type) -> Self {
53		Self {
54			base_type: ty,
55			constraint: None,
56		}
57	}
58
59	pub fn with_constraint(ty: Type, constraint: Constraint) -> Self {
60		Self {
61			base_type: ty,
62			constraint: Some(constraint),
63		}
64	}
65
66	pub fn dictionary(dictionary_id: DictionaryId, id_type: Type) -> Self {
67		Self {
68			base_type: Type::DictionaryId,
69			constraint: Some(Constraint::Dictionary(dictionary_id, id_type)),
70		}
71	}
72
73	pub fn sumtype(id: SumTypeId) -> Self {
74		Self {
75			base_type: Type::Uint1,
76			constraint: Some(Constraint::SumType(id)),
77		}
78	}
79
80	pub fn get_type(&self) -> Type {
81		self.base_type.clone()
82	}
83
84	pub fn storage_type(&self) -> Type {
85		match (&self.base_type, &self.constraint) {
86			(Type::DictionaryId, Some(Constraint::Dictionary(_, id_type))) => id_type.clone(),
87			_ => self.base_type.clone(),
88		}
89	}
90
91	pub fn constraint(&self) -> &Option<Constraint> {
92		&self.constraint
93	}
94
95	pub fn to_ffi(&self) -> FFITypeConstraint {
96		let base_type = self.base_type.to_u8();
97		match &self.constraint {
98			None => FFITypeConstraint {
99				base_type,
100				constraint_type: 0,
101				constraint_param1: 0,
102				constraint_param2: 0,
103			},
104			Some(Constraint::MaxBytes(max)) => FFITypeConstraint {
105				base_type,
106				constraint_type: 1,
107				constraint_param1: max.value(),
108				constraint_param2: 0,
109			},
110			Some(Constraint::PrecisionScale(p, s)) => FFITypeConstraint {
111				base_type,
112				constraint_type: 2,
113				constraint_param1: p.value() as u32,
114				constraint_param2: s.value() as u32,
115			},
116			Some(Constraint::Dictionary(dict_id, id_type)) => FFITypeConstraint {
117				base_type,
118				constraint_type: 3,
119				constraint_param1: dict_id.to_u64() as u32,
120				constraint_param2: id_type.to_u8() as u32,
121			},
122			Some(Constraint::SumType(id)) => FFITypeConstraint {
123				base_type,
124				constraint_type: 4,
125				constraint_param1: id.to_u64() as u32,
126				constraint_param2: 0,
127			},
128		}
129	}
130
131	pub fn from_ffi(ffi: FFITypeConstraint) -> Self {
132		let ty = Type::from_u8(ffi.base_type);
133		match ffi.constraint_type {
134			1 => Self::with_constraint(ty, Constraint::MaxBytes(MaxBytes::new(ffi.constraint_param1))),
135			2 => Self::with_constraint(
136				ty,
137				Constraint::PrecisionScale(
138					Precision::new(ffi.constraint_param1 as u8),
139					Scale::new(ffi.constraint_param2 as u8),
140				),
141			),
142			3 => Self::with_constraint(
143				ty,
144				Constraint::Dictionary(
145					DictionaryId::from(ffi.constraint_param1 as u64),
146					Type::from_u8(ffi.constraint_param2 as u8),
147				),
148			),
149			4 => Self::with_constraint(
150				ty,
151				Constraint::SumType(SumTypeId::from(ffi.constraint_param1 as u64)),
152			),
153			_ => Self::unconstrained(ty),
154		}
155	}
156
157	pub fn validate(&self, value: &Value) -> Result<(), Error> {
158		let value_type = value.get_type();
159		if value_type != self.base_type && !matches!(value, Value::None { .. }) {
160			if let Type::Option(inner) = &self.base_type {
161				if value_type != **inner {
162					unimplemented!()
163				}
164			} else {
165				unimplemented!()
166			}
167		}
168
169		if matches!(value, Value::None { .. }) {
170			if self.base_type.is_option() {
171				return Ok(());
172			} else {
173				return Err(TypeError::ConstraintViolation {
174					kind: ConstraintKind::NoneNotAllowed {
175						column_type: self.base_type.clone(),
176					},
177					message: format!(
178						"Cannot insert none into non-optional column of type {}. Declare the column as Option({}) to allow none values.",
179						self.base_type, self.base_type
180					),
181					fragment: Fragment::None,
182				}
183				.into());
184			}
185		}
186
187		match (&self.base_type, &self.constraint) {
188			(Type::Utf8, Some(Constraint::MaxBytes(max))) => {
189				if let Value::Utf8(s) = value {
190					let byte_len = s.len();
191					let max_value: usize = (*max).into();
192					if byte_len > max_value {
193						return Err(TypeError::ConstraintViolation {
194							kind: ConstraintKind::Utf8MaxBytes {
195								actual: byte_len,
196								max: max_value,
197							},
198							message: format!(
199								"UTF8 value exceeds maximum byte length: {} bytes (max: {} bytes)",
200								byte_len, max_value
201							),
202							fragment: Fragment::None,
203						}
204						.into());
205					}
206				}
207			}
208			(Type::Blob, Some(Constraint::MaxBytes(max))) => {
209				if let Value::Blob(blob) = value {
210					let byte_len = blob.len();
211					let max_value: usize = (*max).into();
212					if byte_len > max_value {
213						return Err(TypeError::ConstraintViolation {
214							kind: ConstraintKind::BlobMaxBytes {
215								actual: byte_len,
216								max: max_value,
217							},
218							message: format!(
219								"BLOB value exceeds maximum byte length: {} bytes (max: {} bytes)",
220								byte_len, max_value
221							),
222							fragment: Fragment::None,
223						}
224						.into());
225					}
226				}
227			}
228			(Type::Int, Some(Constraint::MaxBytes(max))) => {
229				if let Value::Int(vi) = value {
230					let str_len = vi.to_string().len();
231					let byte_len = (str_len * 415 / 1000) + 1;
232					let max_value: usize = (*max).into();
233					if byte_len > max_value {
234						return Err(TypeError::ConstraintViolation {
235							kind: ConstraintKind::IntMaxBytes {
236								actual: byte_len,
237								max: max_value,
238							},
239							message: format!(
240								"INT value exceeds maximum byte length: {} bytes (max: {} bytes)",
241								byte_len, max_value
242							),
243							fragment: Fragment::None,
244						}
245						.into());
246					}
247				}
248			}
249			(Type::Uint, Some(Constraint::MaxBytes(max))) => {
250				if let Value::Uint(vu) = value {
251					let str_len = vu.to_string().len();
252					let byte_len = (str_len * 415 / 1000) + 1;
253					let max_value: usize = (*max).into();
254					if byte_len > max_value {
255						return Err(TypeError::ConstraintViolation {
256							kind: ConstraintKind::UintMaxBytes {
257								actual: byte_len,
258								max: max_value,
259							},
260							message: format!(
261								"UINT value exceeds maximum byte length: {} bytes (max: {} bytes)",
262								byte_len, max_value
263							),
264							fragment: Fragment::None,
265						}
266						.into());
267					}
268				}
269			}
270			(Type::Decimal, Some(Constraint::PrecisionScale(precision, scale))) => {
271				if let Value::Decimal(decimal) = value {
272					let decimal_str = decimal.to_string();
273
274					let decimal_scale: u8 = if let Some(dot_pos) = decimal_str.find('.') {
275						let after_dot = &decimal_str[dot_pos + 1..];
276						after_dot.len().min(255) as u8
277					} else {
278						0
279					};
280
281					let decimal_precision: u8 =
282						decimal_str.chars().filter(|c| c.is_ascii_digit()).count().min(255)
283							as u8;
284
285					let scale_value: u8 = (*scale).into();
286					let precision_value: u8 = (*precision).into();
287
288					if decimal_scale > scale_value {
289						return Err(TypeError::ConstraintViolation {
290							kind: ConstraintKind::DecimalScale {
291								actual: decimal_scale,
292								max: scale_value,
293							},
294							message: format!(
295								"DECIMAL value exceeds maximum scale: {} decimal places (max: {} decimal places)",
296								decimal_scale, scale_value
297							),
298							fragment: Fragment::None,
299						}
300						.into());
301					}
302					if decimal_precision > precision_value {
303						return Err(TypeError::ConstraintViolation {
304							kind: ConstraintKind::DecimalPrecision {
305								actual: decimal_precision,
306								max: precision_value,
307							},
308							message: format!(
309								"DECIMAL value exceeds maximum precision: {} digits (max: {} digits)",
310								decimal_precision, precision_value
311							),
312							fragment: Fragment::None,
313						}
314						.into());
315					}
316				}
317			}
318
319			_ => {}
320		}
321
322		Ok(())
323	}
324
325	pub fn is_unconstrained(&self) -> bool {
326		self.constraint.is_none()
327	}
328
329	#[allow(clippy::inherent_to_string)]
330	pub fn to_string(&self) -> String {
331		match &self.constraint {
332			None => format!("{}", self.base_type),
333			Some(Constraint::MaxBytes(max)) => {
334				format!("{}({})", self.base_type, max)
335			}
336			Some(Constraint::PrecisionScale(p, s)) => {
337				format!("{}({},{})", self.base_type, p, s)
338			}
339			Some(Constraint::Dictionary(dict_id, id_type)) => {
340				format!("DictionaryId(dict={}, {})", dict_id, id_type)
341			}
342			Some(Constraint::SumType(id)) => {
343				format!("SumType({})", id)
344			}
345		}
346	}
347}
348
349#[cfg(test)]
350pub mod tests {
351	use super::*;
352
353	#[test]
354	fn test_unconstrained_type() {
355		let tc = TypeConstraint::unconstrained(Type::Utf8);
356		assert_eq!(tc.base_type, Type::Utf8);
357		assert_eq!(tc.constraint, None);
358		assert!(tc.is_unconstrained());
359	}
360
361	#[test]
362	fn test_constrained_utf8() {
363		let tc = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(50)));
364		assert_eq!(tc.base_type, Type::Utf8);
365		assert_eq!(tc.constraint, Some(Constraint::MaxBytes(MaxBytes::new(50))));
366		assert!(!tc.is_unconstrained());
367	}
368
369	#[test]
370	fn test_constrained_decimal() {
371		let tc = TypeConstraint::with_constraint(
372			Type::Decimal,
373			Constraint::PrecisionScale(Precision::new(10), Scale::new(2)),
374		);
375		assert_eq!(tc.base_type, Type::Decimal);
376		assert_eq!(tc.constraint, Some(Constraint::PrecisionScale(Precision::new(10), Scale::new(2))));
377	}
378
379	#[test]
380	fn test_validate_utf8_within_limit() {
381		let tc = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(10)));
382		let value = Value::Utf8("hello".to_string());
383		assert!(tc.validate(&value).is_ok());
384	}
385
386	#[test]
387	fn test_validate_utf8_exceeds_limit() {
388		let tc = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(5)));
389		let value = Value::Utf8("hello world".to_string());
390		assert!(tc.validate(&value).is_err());
391	}
392
393	#[test]
394	fn test_validate_unconstrained() {
395		let tc = TypeConstraint::unconstrained(Type::Utf8);
396		let value = Value::Utf8("any length string is fine here".to_string());
397		assert!(tc.validate(&value).is_ok());
398	}
399
400	#[test]
401	fn test_validate_none_rejected_for_non_option() {
402		let tc = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(5)));
403		let value = Value::none();
404		assert!(tc.validate(&value).is_err());
405	}
406
407	#[test]
408	fn test_validate_none_accepted_for_option() {
409		let tc = TypeConstraint::unconstrained(Type::Option(Box::new(Type::Utf8)));
410		let value = Value::none();
411		assert!(tc.validate(&value).is_ok());
412	}
413
414	#[test]
415	fn test_to_string() {
416		let tc1 = TypeConstraint::unconstrained(Type::Utf8);
417		assert_eq!(tc1.to_string(), "Utf8");
418
419		let tc2 = TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(50)));
420		assert_eq!(tc2.to_string(), "Utf8(50)");
421
422		let tc3 = TypeConstraint::with_constraint(
423			Type::Decimal,
424			Constraint::PrecisionScale(Precision::new(10), Scale::new(2)),
425		);
426		assert_eq!(tc3.to_string(), "Decimal(10,2)");
427	}
428}