reifydb_value/value/constraint/
mod.rs1use 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}