1use std::borrow::Cow;
6
7use crate::model::Id;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11#[repr(u8)]
12pub enum DataType {
13 Bool = 1,
14 Int64 = 2,
15 Float64 = 3,
16 Decimal = 4,
17 Text = 5,
18 Bytes = 6,
19 Date = 7,
20 Time = 8,
21 Datetime = 9,
22 Schedule = 10,
23 Point = 11,
24 Embedding = 12,
25}
26
27impl DataType {
28 pub fn from_u8(v: u8) -> Option<DataType> {
30 match v {
31 1 => Some(DataType::Bool),
32 2 => Some(DataType::Int64),
33 3 => Some(DataType::Float64),
34 4 => Some(DataType::Decimal),
35 5 => Some(DataType::Text),
36 6 => Some(DataType::Bytes),
37 7 => Some(DataType::Date),
38 8 => Some(DataType::Time),
39 9 => Some(DataType::Datetime),
40 10 => Some(DataType::Schedule),
41 11 => Some(DataType::Point),
42 12 => Some(DataType::Embedding),
43 _ => None,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50#[repr(u8)]
51pub enum EmbeddingSubType {
52 Float32 = 0,
54 Int8 = 1,
56 Binary = 2,
58}
59
60impl EmbeddingSubType {
61 pub fn from_u8(v: u8) -> Option<EmbeddingSubType> {
63 match v {
64 0 => Some(EmbeddingSubType::Float32),
65 1 => Some(EmbeddingSubType::Int8),
66 2 => Some(EmbeddingSubType::Binary),
67 _ => None,
68 }
69 }
70
71 pub fn bytes_for_dims(self, dims: usize) -> usize {
73 match self {
74 EmbeddingSubType::Float32 => dims * 4,
75 EmbeddingSubType::Int8 => dims,
76 EmbeddingSubType::Binary => dims.div_ceil(8),
77 }
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Hash)]
85pub enum DecimalMantissa<'a> {
86 I64(i64),
88 Big(Cow<'a, [u8]>),
90}
91
92impl DecimalMantissa<'_> {
93 pub fn has_trailing_zeros(&self) -> bool {
95 match self {
96 DecimalMantissa::I64(v) => *v != 0 && *v % 10 == 0,
97 DecimalMantissa::Big(bytes) => {
98 !bytes.is_empty() && bytes[bytes.len() - 1] == 0
101 }
102 }
103 }
104
105 pub fn is_zero(&self) -> bool {
107 match self {
108 DecimalMantissa::I64(v) => *v == 0,
109 DecimalMantissa::Big(bytes) => bytes.iter().all(|b| *b == 0),
110 }
111 }
112}
113
114#[derive(Debug, Clone, PartialEq)]
116pub enum Value<'a> {
117 Bool(bool),
119
120 Int64 {
122 value: i64,
123 unit: Option<Id>,
125 },
126
127 Float64 {
129 value: f64,
130 unit: Option<Id>,
132 },
133
134 Decimal {
136 exponent: i32,
137 mantissa: DecimalMantissa<'a>,
138 unit: Option<Id>,
140 },
141
142 Text {
144 value: Cow<'a, str>,
145 language: Option<Id>,
147 },
148
149 Bytes(Cow<'a, [u8]>),
151
152 Date(Cow<'a, str>),
154
155 Time(Cow<'a, str>),
157
158 Datetime(Cow<'a, str>),
160
161 Schedule(Cow<'a, str>),
163
164 Point {
166 lon: f64,
168 lat: f64,
170 alt: Option<f64>,
172 },
173
174 Embedding {
176 sub_type: EmbeddingSubType,
177 dims: usize,
178 data: Cow<'a, [u8]>,
180 },
181}
182
183impl Value<'_> {
184 pub fn data_type(&self) -> DataType {
186 match self {
187 Value::Bool(_) => DataType::Bool,
188 Value::Int64 { .. } => DataType::Int64,
189 Value::Float64 { .. } => DataType::Float64,
190 Value::Decimal { .. } => DataType::Decimal,
191 Value::Text { .. } => DataType::Text,
192 Value::Bytes(_) => DataType::Bytes,
193 Value::Date(_) => DataType::Date,
194 Value::Time(_) => DataType::Time,
195 Value::Datetime(_) => DataType::Datetime,
196 Value::Schedule(_) => DataType::Schedule,
197 Value::Point { .. } => DataType::Point,
198 Value::Embedding { .. } => DataType::Embedding,
199 }
200 }
201
202 pub fn validate(&self) -> Option<&'static str> {
206 match self {
207 Value::Float64 { value, .. } => {
208 if value.is_nan() {
209 return Some("NaN is not allowed in Float64");
210 }
211 }
212 Value::Decimal { exponent, mantissa, .. } => {
213 if mantissa.is_zero() && *exponent != 0 {
215 return Some("zero DECIMAL must have exponent 0");
216 }
217 if !mantissa.is_zero() && mantissa.has_trailing_zeros() {
219 return Some("DECIMAL mantissa has trailing zeros (not normalized)");
220 }
221 }
222 Value::Point { lon, lat, alt } => {
223 if *lon < -180.0 || *lon > 180.0 {
224 return Some("longitude out of range [-180, +180]");
225 }
226 if *lat < -90.0 || *lat > 90.0 {
227 return Some("latitude out of range [-90, +90]");
228 }
229 if lon.is_nan() || lat.is_nan() {
230 return Some("NaN is not allowed in Point coordinates");
231 }
232 if let Some(a) = alt {
233 if a.is_nan() {
234 return Some("NaN is not allowed in Point altitude");
235 }
236 }
237 }
238 Value::Embedding {
239 sub_type,
240 dims,
241 data,
242 } => {
243 let expected = sub_type.bytes_for_dims(*dims);
244 if data.len() != expected {
245 return Some("embedding data length doesn't match dims");
246 }
247 if *sub_type == EmbeddingSubType::Float32 {
249 for chunk in data.chunks_exact(4) {
250 let f = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
251 if f.is_nan() {
252 return Some("NaN is not allowed in float32 embedding");
253 }
254 }
255 }
256 }
257 _ => {}
258 }
259 None
260 }
261}
262
263#[derive(Debug, Clone, PartialEq)]
265pub struct PropertyValue<'a> {
266 pub property: Id,
268 pub value: Value<'a>,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq)]
274pub struct Property {
275 pub id: Id,
277 pub data_type: DataType,
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn test_embedding_bytes_for_dims() {
287 assert_eq!(EmbeddingSubType::Float32.bytes_for_dims(10), 40);
288 assert_eq!(EmbeddingSubType::Int8.bytes_for_dims(10), 10);
289 assert_eq!(EmbeddingSubType::Binary.bytes_for_dims(10), 2);
290 assert_eq!(EmbeddingSubType::Binary.bytes_for_dims(8), 1);
291 assert_eq!(EmbeddingSubType::Binary.bytes_for_dims(9), 2);
292 }
293
294 #[test]
295 fn test_value_validation_nan() {
296 assert!(Value::Float64 { value: f64::NAN, unit: None }.validate().is_some());
297 assert!(Value::Float64 { value: f64::INFINITY, unit: None }.validate().is_none());
298 assert!(Value::Float64 { value: -f64::INFINITY, unit: None }.validate().is_none());
299 assert!(Value::Float64 { value: 42.0, unit: None }.validate().is_none());
300 }
301
302 #[test]
303 fn test_value_validation_point() {
304 assert!(Value::Point { lon: 0.0, lat: 91.0, alt: None }.validate().is_some());
305 assert!(Value::Point { lon: 0.0, lat: -91.0, alt: None }.validate().is_some());
306 assert!(Value::Point { lon: 181.0, lat: 0.0, alt: None }.validate().is_some());
307 assert!(Value::Point { lon: -181.0, lat: 0.0, alt: None }.validate().is_some());
308 assert!(Value::Point { lon: 180.0, lat: 90.0, alt: None }.validate().is_none());
309 assert!(Value::Point { lon: -180.0, lat: -90.0, alt: None }.validate().is_none());
310 assert!(Value::Point { lon: 0.0, lat: 0.0, alt: Some(1000.0) }.validate().is_none());
312 assert!(Value::Point { lon: 0.0, lat: 0.0, alt: Some(f64::NAN) }.validate().is_some());
313 }
314
315 #[test]
316 fn test_decimal_normalization() {
317 let zero_bad = Value::Decimal {
319 exponent: 1,
320 mantissa: DecimalMantissa::I64(0),
321 unit: None,
322 };
323 assert!(zero_bad.validate().is_some());
324
325 let trailing = Value::Decimal {
327 exponent: 0,
328 mantissa: DecimalMantissa::I64(1230),
329 unit: None,
330 };
331 assert!(trailing.validate().is_some());
332
333 let valid = Value::Decimal {
335 exponent: -2,
336 mantissa: DecimalMantissa::I64(1234),
337 unit: None,
338 };
339 assert!(valid.validate().is_none());
340 }
341}