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 {
154 days: i32,
156 offset_min: i16,
158 },
159
160 Time {
162 time_us: i64,
164 offset_min: i16,
166 },
167
168 Datetime {
170 epoch_us: i64,
172 offset_min: i16,
174 },
175
176 Schedule(Cow<'a, str>),
178
179 Point {
181 lon: f64,
183 lat: f64,
185 alt: Option<f64>,
187 },
188
189 Embedding {
191 sub_type: EmbeddingSubType,
192 dims: usize,
193 data: Cow<'a, [u8]>,
195 },
196}
197
198impl Value<'_> {
199 pub fn data_type(&self) -> DataType {
201 match self {
202 Value::Bool(_) => DataType::Bool,
203 Value::Int64 { .. } => DataType::Int64,
204 Value::Float64 { .. } => DataType::Float64,
205 Value::Decimal { .. } => DataType::Decimal,
206 Value::Text { .. } => DataType::Text,
207 Value::Bytes(_) => DataType::Bytes,
208 Value::Date { .. } => DataType::Date,
209 Value::Time { .. } => DataType::Time,
210 Value::Datetime { .. } => DataType::Datetime,
211 Value::Schedule(_) => DataType::Schedule,
212 Value::Point { .. } => DataType::Point,
213 Value::Embedding { .. } => DataType::Embedding,
214 }
215 }
216
217 pub fn validate(&self) -> Option<&'static str> {
221 match self {
222 Value::Float64 { value, .. } => {
223 if value.is_nan() {
224 return Some("NaN is not allowed in Float64");
225 }
226 }
227 Value::Decimal { exponent, mantissa, .. } => {
228 if mantissa.is_zero() && *exponent != 0 {
230 return Some("zero DECIMAL must have exponent 0");
231 }
232 if !mantissa.is_zero() && mantissa.has_trailing_zeros() {
234 return Some("DECIMAL mantissa has trailing zeros (not normalized)");
235 }
236 }
237 Value::Point { lon, lat, alt } => {
238 if *lon < -180.0 || *lon > 180.0 {
239 return Some("longitude out of range [-180, +180]");
240 }
241 if *lat < -90.0 || *lat > 90.0 {
242 return Some("latitude out of range [-90, +90]");
243 }
244 if lon.is_nan() || lat.is_nan() {
245 return Some("NaN is not allowed in Point coordinates");
246 }
247 if let Some(a) = alt {
248 if a.is_nan() {
249 return Some("NaN is not allowed in Point altitude");
250 }
251 }
252 }
253 Value::Date { offset_min, .. } => {
254 if *offset_min < -1440 || *offset_min > 1440 {
255 return Some("DATE offset_min outside range [-1440, +1440]");
256 }
257 }
258 Value::Time { time_us, offset_min } => {
259 if *time_us < 0 || *time_us > 86_399_999_999 {
260 return Some("TIME time_us outside range [0, 86399999999]");
261 }
262 if *offset_min < -1440 || *offset_min > 1440 {
263 return Some("TIME offset_min outside range [-1440, +1440]");
264 }
265 }
266 Value::Datetime { offset_min, .. } => {
267 if *offset_min < -1440 || *offset_min > 1440 {
268 return Some("DATETIME offset_min outside range [-1440, +1440]");
269 }
270 }
271 Value::Embedding {
272 sub_type,
273 dims,
274 data,
275 } => {
276 let expected = sub_type.bytes_for_dims(*dims);
277 if data.len() != expected {
278 return Some("embedding data length doesn't match dims");
279 }
280 if *sub_type == EmbeddingSubType::Float32 {
282 for chunk in data.chunks_exact(4) {
283 let f = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
284 if f.is_nan() {
285 return Some("NaN is not allowed in float32 embedding");
286 }
287 }
288 }
289 }
290 _ => {}
291 }
292 None
293 }
294}
295
296#[derive(Debug, Clone, PartialEq)]
298pub struct PropertyValue<'a> {
299 pub property: Id,
301 pub value: Value<'a>,
303}
304
305#[derive(Debug, Clone, PartialEq, Eq)]
307pub struct Property {
308 pub id: Id,
310 pub data_type: DataType,
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn test_embedding_bytes_for_dims() {
320 assert_eq!(EmbeddingSubType::Float32.bytes_for_dims(10), 40);
321 assert_eq!(EmbeddingSubType::Int8.bytes_for_dims(10), 10);
322 assert_eq!(EmbeddingSubType::Binary.bytes_for_dims(10), 2);
323 assert_eq!(EmbeddingSubType::Binary.bytes_for_dims(8), 1);
324 assert_eq!(EmbeddingSubType::Binary.bytes_for_dims(9), 2);
325 }
326
327 #[test]
328 fn test_value_validation_nan() {
329 assert!(Value::Float64 { value: f64::NAN, unit: None }.validate().is_some());
330 assert!(Value::Float64 { value: f64::INFINITY, unit: None }.validate().is_none());
331 assert!(Value::Float64 { value: -f64::INFINITY, unit: None }.validate().is_none());
332 assert!(Value::Float64 { value: 42.0, unit: None }.validate().is_none());
333 }
334
335 #[test]
336 fn test_value_validation_point() {
337 assert!(Value::Point { lon: 0.0, lat: 91.0, alt: None }.validate().is_some());
338 assert!(Value::Point { lon: 0.0, lat: -91.0, alt: None }.validate().is_some());
339 assert!(Value::Point { lon: 181.0, lat: 0.0, alt: None }.validate().is_some());
340 assert!(Value::Point { lon: -181.0, lat: 0.0, alt: None }.validate().is_some());
341 assert!(Value::Point { lon: 180.0, lat: 90.0, alt: None }.validate().is_none());
342 assert!(Value::Point { lon: -180.0, lat: -90.0, alt: None }.validate().is_none());
343 assert!(Value::Point { lon: 0.0, lat: 0.0, alt: Some(1000.0) }.validate().is_none());
345 assert!(Value::Point { lon: 0.0, lat: 0.0, alt: Some(f64::NAN) }.validate().is_some());
346 }
347
348 #[test]
349 fn test_decimal_normalization() {
350 let zero_bad = Value::Decimal {
352 exponent: 1,
353 mantissa: DecimalMantissa::I64(0),
354 unit: None,
355 };
356 assert!(zero_bad.validate().is_some());
357
358 let trailing = Value::Decimal {
360 exponent: 0,
361 mantissa: DecimalMantissa::I64(1230),
362 unit: None,
363 };
364 assert!(trailing.validate().is_some());
365
366 let valid = Value::Decimal {
368 exponent: -2,
369 mantissa: DecimalMantissa::I64(1234),
370 unit: None,
371 };
372 assert!(valid.validate().is_none());
373 }
374}