Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_datime/src/database/storage.rs

23.9 KiB, 97 runs

created by r1870400018:8434, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Database storage and retrieval for the datetime types.
2//!
3//! Several storage formats are offered because their strengths differ: binary
4//! for time-ordered queries, ISO 8601 for readability, components for partial
5//! queries, and Unix timestamps for existing systems.
6//!
7//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
8//! Anthropic Claude
9
10use crate::{
11 time::{CalClock, CalClockZone},
12 clock::ClockTime,
13 calendar::CalendarDate,
14};
15
16use oxedyne_fe2o3_core::prelude::*;
17use oxedyne_fe2o3_jdat::{prelude::*, tup2dat};
18
19use std::collections::{HashMap, BTreeMap};
20
21#[derive(Clone, Debug, PartialEq)]
22pub enum StorageFormat {
23 // Each variant pairs its value with the timezone id. Binary counts
24 // nanoseconds: since the epoch for a CalClock, within the day for a
25 // ClockTime, and as a Julian day number for a CalendarDate.
26 Binary,
27 Iso8601, // "2024-06-15T14:30:00.123456789Z"
28 Component, // year, month, day, hour, minute, second, nanosecond apart
29 UnixTimestamp, // milliseconds
30}
31
32#[derive(Clone, Debug, PartialEq)]
33pub struct DatabaseRecord {
34 pub primary_data: Dat, // shape set by storage_format
35 pub storage_format: StorageFormat,
36 pub indexes: HashMap<String, Dat>, // secondary
37 pub metadata: HashMap<String, String>,
38}
39
40impl DatabaseRecord {
41 pub fn new(primary_data: Dat, storage_format: StorageFormat) -> Self {
42 Self {
43 primary_data,
44 storage_format,
45 indexes: HashMap::new(),
46 metadata: HashMap::new(),
47 }
48 }
49
50 pub fn add_index(&mut self, key: &str, value: Dat) {
51 self.indexes.insert(key.to_string(), value);
52 }
53
54 pub fn add_metadata(&mut self, key: &str, value: &str) {
55 self.metadata.insert(key.to_string(), value.to_string());
56 }
57
58 pub fn get_index(&self, key: &str) -> Option<&Dat> {
59 self.indexes.get(key)
60 }
61
62 pub fn get_metadata(&self, key: &str) -> Option<&String> {
63 self.metadata.get(key)
64 }
65}
66
67pub trait DatabaseStorable {
68 fn to_database_record(&self, format: StorageFormat) -> Outcome<DatabaseRecord>;
69
70 fn from_database_record(record: &DatabaseRecord) -> Outcome<Self>
71 where
72 Self: Sized;
73
74 fn recommended_storage_format() -> StorageFormat;
75
76 fn create_indexes(&self) -> HashMap<String, Dat>;
77}
78
79impl DatabaseStorable for CalClock {
80 fn to_database_record(&self, format: StorageFormat) -> Outcome<DatabaseRecord> {
81 let primary_data = match format {
82 StorageFormat::Binary => {
83 let nanos = res!(self.to_nanos_since_epoch());
84 let zone_id = self.zone().id().to_string();
85 tup2dat!(nanos, zone_id)
86 },
87
88 StorageFormat::Iso8601 => {
89 let iso_string = res!(self.to_iso8601());
90 Dat::Str(iso_string)
91 },
92
93 StorageFormat::Component => {
94 let mut components = BTreeMap::new();
95 components.insert(Dat::Str("year".to_string()), Dat::I32(self.year()));
96 components.insert(Dat::Str("month".to_string()), Dat::U8(self.month()));
97 components.insert(Dat::Str("day".to_string()), Dat::U8(self.day()));
98 components.insert(Dat::Str("hour".to_string()), Dat::U8(self.hour()));
99 components.insert(Dat::Str("minute".to_string()), Dat::U8(self.minute()));
100 components.insert(Dat::Str("second".to_string()), Dat::U8(self.second()));
101 components.insert(Dat::Str("nanosecond".to_string()), Dat::U32(self.nanosecond()));
102 components.insert(Dat::Str("timezone".to_string()), Dat::Str(self.zone().id().to_string()));
103 Dat::Map(components)
104 },
105
106 StorageFormat::UnixTimestamp => {
107 let millis = res!(self.to_millis());
108 let zone_id = self.zone().id().to_string();
109 tup2dat!(millis, zone_id)
110 },
111 };
112
113 let mut record = DatabaseRecord::new(primary_data, format);
114
115 // Add query indexes
116 let indexes = self.create_indexes();
117 for (key, value) in indexes {
118 record.add_index(&key, value);
119 }
120
121 // Add metadata
122 record.add_metadata("type", "CalClock");
123 record.add_metadata("timezone", self.zone().id());
124 record.add_metadata("leap_second", &self.is_leap_second().to_string());
125
126 Ok(record)
127 }
128
129 fn from_database_record(record: &DatabaseRecord) -> Outcome<Self> {
130 match record.storage_format {
131 StorageFormat::Binary => {
132 let (nanos, zone_id) = if let Dat::Tup2(arr) = &record.primary_data {
133 let first = res!(arr[0].get_i64().ok_or_else(|| err!("Invalid nanos in binary format"; Invalid, Input)));
134 let second = res!(arr[1].get_string().ok_or_else(|| err!("Invalid zone_id in binary format"; Invalid, Input)));
135 (first, second)
136 } else {
137 return Err(err!("Expected Tup2 for binary format"; Invalid, Input));
138 };
139 let zone = res!(CalClockZone::new(&zone_id));
140 Self::from_nanos_since_epoch(nanos, zone)
141 },
142
143 StorageFormat::Iso8601 => {
144 let iso_string = res!(record.primary_data.get_string().ok_or_else(|| err!("Expected string for ISO format"; Invalid, Input)));
145 Self::parse_iso(&iso_string)
146 },
147
148 StorageFormat::Component => {
149 let components = res!(record.primary_data.get_map().ok_or_else(|| err!("Expected map for component format"; Invalid, Input)));
150 let year = res!(ok!(components.get(&Dat::Str("year".to_string()))
151 .ok_or_else(|| err!("Missing year component"; Invalid, Input)))
152 .get_i32().ok_or_else(|| err!("Invalid year type"; Invalid, Input)));
153 let month = res!(ok!(components.get(&Dat::Str("month".to_string()))
154 .ok_or_else(|| err!("Missing month component"; Invalid, Input)))
155 .get_u8().ok_or_else(|| err!("Invalid month type"; Invalid, Input)));
156 let day = res!(ok!(components.get(&Dat::Str("day".to_string()))
157 .ok_or_else(|| err!("Missing day component"; Invalid, Input)))
158 .get_u8().ok_or_else(|| err!("Invalid day type"; Invalid, Input)));
159 let hour = res!(ok!(components.get(&Dat::Str("hour".to_string()))
160 .ok_or_else(|| err!("Missing hour component"; Invalid, Input)))
161 .get_u8().ok_or_else(|| err!("Invalid hour type"; Invalid, Input)));
162 let minute = res!(ok!(components.get(&Dat::Str("minute".to_string()))
163 .ok_or_else(|| err!("Missing minute component"; Invalid, Input)))
164 .get_u8().ok_or_else(|| err!("Invalid minute type"; Invalid, Input)));
165 let second = res!(ok!(components.get(&Dat::Str("second".to_string()))
166 .ok_or_else(|| err!("Missing second component"; Invalid, Input)))
167 .get_u8().ok_or_else(|| err!("Invalid second type"; Invalid, Input)));
168 let nanosecond = res!(ok!(components.get(&Dat::Str("nanosecond".to_string()))
169 .ok_or_else(|| err!("Missing nanosecond component"; Invalid, Input)))
170 .get_u32().ok_or_else(|| err!("Invalid nanosecond type"; Invalid, Input)));
171 let zone_id = res!(ok!(components.get(&Dat::Str("timezone".to_string()))
172 .ok_or_else(|| err!("Missing timezone component"; Invalid, Input)))
173 .get_string().ok_or_else(|| err!("Invalid timezone type"; Invalid, Input)));
174 let zone = res!(CalClockZone::new(&zone_id));
175
176 Self::new(year, month, day, hour, minute, second, nanosecond, zone)
177 },
178
179 StorageFormat::UnixTimestamp => {
180 let (millis, zone_id) = if let Dat::Tup2(arr) = &record.primary_data {
181 let first = res!(arr[0].get_i64().ok_or_else(|| err!("Invalid millis in timestamp format"; Invalid, Input)));
182 let second = res!(arr[1].get_string().ok_or_else(|| err!("Invalid zone_id in timestamp format"; Invalid, Input)));
183 (first, second)
184 } else {
185 return Err(err!("Expected Tup2 for timestamp format"; Invalid, Input));
186 };
187 let zone = res!(CalClockZone::new(&zone_id));
188 Self::from_millis(millis, zone)
189 },
190 }
191 }
192
193 fn recommended_storage_format() -> StorageFormat {
194 StorageFormat::Binary
195 }
196
197 fn create_indexes(&self) -> HashMap<String, Dat> {
198 let mut indexes = HashMap::new();
199
200 // Time-based indexes
201 indexes.insert("year".to_string(), Dat::I32(self.year()));
202 indexes.insert("month".to_string(), Dat::U8(self.month()));
203 indexes.insert("day".to_string(), Dat::U8(self.day()));
204 indexes.insert("hour".to_string(), Dat::U8(self.hour()));
205 indexes.insert("day_of_week".to_string(), Dat::U8(self.day_of_week().of()));
206 if let Ok(day_of_year) = self.day_of_year() {
207 indexes.insert("day_of_year".to_string(), Dat::U16(day_of_year));
208 }
209
210 // Timestamp indexes for range queries
211 if let Ok(nanos) = self.to_nanos_since_epoch() {
212 indexes.insert("nanos_since_epoch".to_string(), Dat::I64(nanos));
213 }
214 if let Ok(millis) = self.to_millis() {
215 indexes.insert("millis_since_epoch".to_string(), Dat::I64(millis));
216 }
217
218 // Calendar indexes
219 indexes.insert("year_month".to_string(), Dat::Str(fmt!("{}-{:02}", self.year(), self.month())));
220 indexes.insert("date".to_string(), Dat::Str(fmt!("{}-{:02}-{:02}", self.year(), self.month(), self.day())));
221
222 // Special flags
223 indexes.insert("is_leap_second".to_string(), Dat::Bool(self.is_leap_second()));
224 indexes.insert("timezone".to_string(), Dat::Str(self.zone().id().to_string()));
225
226 indexes
227 }
228}
229
230impl DatabaseStorable for ClockTime {
231 fn to_database_record(&self, format: StorageFormat) -> Outcome<DatabaseRecord> {
232 let primary_data = match format {
233 StorageFormat::Binary => {
234 let nanos = self.to_nanos_of_day();
235 let zone_id = self.zone().id().to_string();
236 tup2dat!(nanos, zone_id)
237 },
238
239 StorageFormat::Iso8601 => {
240 Dat::Str(self.to_iso_string())
241 },
242
243 StorageFormat::Component => {
244 let mut components = BTreeMap::new();
245 components.insert(Dat::Str("hour".to_string()), Dat::U8(self.hour().of()));
246 components.insert(Dat::Str("minute".to_string()), Dat::U8(self.minute().of()));
247 components.insert(Dat::Str("second".to_string()), Dat::U8(self.second().of()));
248 components.insert(Dat::Str("nanosecond".to_string()), Dat::U32(self.nanosecond().of()));
249 components.insert(Dat::Str("timezone".to_string()), Dat::Str(self.zone().id().to_string()));
250 Dat::Map(components)
251 },
252
253 StorageFormat::UnixTimestamp => {
254 // For time-only, we store milliseconds of day
255 let millis = self.millis_of_day();
256 let zone_id = self.zone().id().to_string();
257 tup2dat!(millis, zone_id)
258 },
259 };
260
261 let mut record = DatabaseRecord::new(primary_data, format);
262
263 // Add query indexes
264 let indexes = self.create_indexes();
265 for (key, value) in indexes {
266 record.add_index(&key, value);
267 }
268
269 // Add metadata
270 record.add_metadata("type", "ClockTime");
271 record.add_metadata("timezone", self.zone().id());
272 record.add_metadata("is_leap_second", &self.is_leap_second().to_string());
273 record.add_metadata("is_end_of_day", &self.is_end_of_day().to_string());
274
275 Ok(record)
276 }
277
278 fn from_database_record(record: &DatabaseRecord) -> Outcome<Self> {
279 match record.storage_format {
280 StorageFormat::Binary => {
281 let (nanos, zone_id) = if let Dat::Tup2(arr) = &record.primary_data {
282 let first = res!(arr[0].get_u64().ok_or_else(|| err!("Invalid nanos in binary format"; Invalid, Input)));
283 let second = res!(arr[1].get_string().ok_or_else(|| err!("Invalid zone_id in binary format"; Invalid, Input)));
284 (first, second)
285 } else {
286 return Err(err!("Expected Tup2 for binary format"; Invalid, Input));
287 };
288 let zone = res!(CalClockZone::new(&zone_id));
289 Self::from_nanos_of_day(nanos, zone)
290 },
291
292 StorageFormat::Iso8601 => {
293 let _iso_string = res!(record.primary_data.get_string().ok_or_else(|| err!("Invalid component type"; Invalid, Input)));
294 // Parse ISO time string - this would need a proper parser
295 // For now, return an error as ClockTime doesn't have direct ISO parsing
296 Err(err!("ClockTime ISO parsing not implemented"; Unimplemented))
297 },
298
299 StorageFormat::Component => {
300 let components = res!(record.primary_data.get_map().ok_or_else(|| err!("Expected map"; Invalid, Input)));
301 let hour = res!(ok!(components.get(&Dat::Str("hour".to_string()))
302 .ok_or_else(|| err!("Missing hour component"; Invalid, Input)))
303 .get_u8().ok_or_else(|| err!("Invalid component type"; Invalid, Input)));
304 let minute = res!(ok!(components.get(&Dat::Str("minute".to_string()))
305 .ok_or_else(|| err!("Missing minute component"; Invalid, Input)))
306 .get_u8().ok_or_else(|| err!("Invalid component type"; Invalid, Input)));
307 let second = res!(ok!(components.get(&Dat::Str("second".to_string()))
308 .ok_or_else(|| err!("Missing second component"; Invalid, Input)))
309 .get_u8().ok_or_else(|| err!("Invalid component type"; Invalid, Input)));
310 let nanosecond = res!(ok!(components.get(&Dat::Str("nanosecond".to_string()))
311 .ok_or_else(|| err!("Missing nanosecond component"; Invalid, Input)))
312 .get_u32().ok_or_else(|| err!("Invalid component type"; Invalid, Input)));
313 let zone_id = res!(ok!(components.get(&Dat::Str("timezone".to_string()))
314 .ok_or_else(|| err!("Missing timezone component"; Invalid, Input)))
315 .get_string().ok_or_else(|| err!("Invalid component type"; Invalid, Input)));
316 let zone = res!(CalClockZone::new(&zone_id));
317
318 Self::new(hour, minute, second, nanosecond, zone)
319 },
320
321 StorageFormat::UnixTimestamp => {
322 let (millis, zone_id) = if let Dat::Tup2(arr) = &record.primary_data {
323 let first = res!(arr[0].get_u32().ok_or_else(|| err!("Invalid millis in timestamp format"; Invalid, Input)));
324 let second = res!(arr[1].get_string().ok_or_else(|| err!("Invalid zone_id in timestamp format"; Invalid, Input)));
325 (first, second)
326 } else {
327 return Err(err!("Expected Tup2 for timestamp format"; Invalid, Input));
328 };
329 let zone = res!(CalClockZone::new(&zone_id));
330 Self::from_millis_of_day(millis, zone)
331 },
332 }
333 }
334
335 fn recommended_storage_format() -> StorageFormat {
336 StorageFormat::Binary
337 }
338
339 fn create_indexes(&self) -> HashMap<String, Dat> {
340 let mut indexes = HashMap::new();
341
342 // Time component indexes
343 indexes.insert("hour".to_string(), Dat::U8(self.hour().of()));
344 indexes.insert("minute".to_string(), Dat::U8(self.minute().of()));
345 indexes.insert("second".to_string(), Dat::U8(self.second().of()));
346 indexes.insert("hour_minute".to_string(), Dat::Str(fmt!("{:02}:{:02}", self.hour().of(), self.minute().of())));
347
348 // Nanosecond-based indexes for precise queries
349 indexes.insert("nanos_of_day".to_string(), Dat::U64(self.to_nanos_of_day()));
350 indexes.insert("millis_of_day".to_string(), Dat::U32(self.millis_of_day()));
351
352 // Special time flags
353 indexes.insert("is_leap_second".to_string(), Dat::Bool(self.is_leap_second()));
354 indexes.insert("is_end_of_day".to_string(), Dat::Bool(self.is_end_of_day()));
355 indexes.insert("is_potential_leap_second".to_string(), Dat::Bool(self.is_potential_leap_second()));
356
357 // Time periods for grouping
358 let hour = self.hour().of();
359 indexes.insert("time_period".to_string(), Dat::Str(
360 if hour < 6 { "night" }
361 else if hour < 12 { "morning" }
362 else if hour < 18 { "afternoon" }
363 else { "evening" }
364 .to_string()
365 ));
366
367 indexes.insert("timezone".to_string(), Dat::Str(self.zone().id().to_string()));
368
369 indexes
370 }
371}
372
373impl DatabaseStorable for CalendarDate {
374 fn to_database_record(&self, format: StorageFormat) -> Outcome<DatabaseRecord> {
375 let primary_data = match format {
376 StorageFormat::Binary => {
377 let julian_day = self.to_julian_day_number();
378 let zone_id = self.zone().id().to_string();
379 tup2dat!(julian_day, zone_id)
380 },
381
382 StorageFormat::Iso8601 => {
383 Dat::Str(self.to_string())
384 },
385
386 StorageFormat::Component => {
387 let mut components = BTreeMap::new();
388 components.insert(Dat::Str("year".to_string()), Dat::I32(self.year()));
389 components.insert(Dat::Str("month".to_string()), Dat::U8(self.month()));
390 components.insert(Dat::Str("day".to_string()), Dat::U8(self.day()));
391 components.insert(Dat::Str("timezone".to_string()), Dat::Str(self.zone().id().to_string()));
392 Dat::Map(components)
393 },
394
395 StorageFormat::UnixTimestamp => {
396 // For dates, we use days since epoch
397 let days = res!(self.days_since_epoch());
398 let zone_id = self.zone().id().to_string();
399 tup2dat!(days, zone_id)
400 },
401 };
402
403 let mut record = DatabaseRecord::new(primary_data, format);
404
405 // Add query indexes
406 let indexes = self.create_indexes();
407 for (key, value) in indexes {
408 record.add_index(&key, value);
409 }
410
411 // Add metadata
412 record.add_metadata("type", "CalendarDate");
413 record.add_metadata("timezone", self.zone().id());
414 record.add_metadata("is_leap_year", &self.is_leap_year().to_string());
415
416 Ok(record)
417 }
418
419 fn from_database_record(record: &DatabaseRecord) -> Outcome<Self> {
420 match record.storage_format {
421 StorageFormat::Binary => {
422 let (julian_day, zone_id) = if let Dat::Tup2(arr) = &record.primary_data {
423 let first = res!(arr[0].get_i64().ok_or_else(|| err!("Invalid julian_day in binary format"; Invalid, Input)));
424 let second = res!(arr[1].get_string().ok_or_else(|| err!("Invalid zone_id in binary format"; Invalid, Input)));
425 (first, second)
426 } else {
427 return Err(err!("Expected Tup2 for binary format"; Invalid, Input));
428 };
429 let zone = res!(CalClockZone::new(&zone_id));
430 Self::from_julian_day_number(julian_day, zone)
431 },
432
433 StorageFormat::Iso8601 => {
434 let iso_string = res!(record.primary_data.get_string().ok_or_else(|| err!("Invalid component type"; Invalid, Input)));
435 // Parse using the default zone (UTC) and then convert if needed
436 Self::parse(&iso_string, CalClockZone::utc())
437 },
438
439 StorageFormat::Component => {
440 let components = res!(record.primary_data.get_map().ok_or_else(|| err!("Expected map"; Invalid, Input)));
441 let year = res!(ok!(components.get(&Dat::Str("year".to_string()))
442 .ok_or_else(|| err!("Missing year component"; Invalid, Input)))
443 .get_i32().ok_or_else(|| err!("Invalid component type"; Invalid, Input)));
444 let month = res!(ok!(components.get(&Dat::Str("month".to_string()))
445 .ok_or_else(|| err!("Missing month component"; Invalid, Input)))
446 .get_u8().ok_or_else(|| err!("Invalid component type"; Invalid, Input)));
447 let day = res!(ok!(components.get(&Dat::Str("day".to_string()))
448 .ok_or_else(|| err!("Missing day component"; Invalid, Input)))
449 .get_u8().ok_or_else(|| err!("Invalid component type"; Invalid, Input)));
450 let zone_id = res!(ok!(components.get(&Dat::Str("timezone".to_string()))
451 .ok_or_else(|| err!("Missing timezone component"; Invalid, Input)))
452 .get_string().ok_or_else(|| err!("Invalid component type"; Invalid, Input)));
453 let zone = res!(CalClockZone::new(&zone_id));
454
455 Self::new(year, month, day, zone)
456 },
457
458 StorageFormat::UnixTimestamp => {
459 let (days, zone_id) = if let Dat::Tup2(arr) = &record.primary_data {
460 let first = res!(arr[0].get_i32().ok_or_else(|| err!("Invalid days in timestamp format"; Invalid, Input)));
461 let second = res!(arr[1].get_string().ok_or_else(|| err!("Invalid zone_id in timestamp format"; Invalid, Input)));
462 (first, second)
463 } else {
464 return Err(err!("Expected Tup2 for timestamp format"; Invalid, Input));
465 };
466 let zone = res!(CalClockZone::new(&zone_id));
467 Self::from_days_since_epoch(days as i64, zone)
468 },
469 }
470 }
471
472 fn recommended_storage_format() -> StorageFormat {
473 StorageFormat::Binary
474 }
475
476 fn create_indexes(&self) -> HashMap<String, Dat> {
477 let mut indexes = HashMap::new();
478
479 // Date component indexes
480 indexes.insert("year".to_string(), Dat::I32(self.year()));
481 indexes.insert("month".to_string(), Dat::U8(self.month()));
482 indexes.insert("day".to_string(), Dat::U8(self.day()));
483 indexes.insert("day_of_week".to_string(), Dat::U8(self.day_of_week().of() as u8));
484 if let Ok(day_of_year) = self.day_of_year() {
485 indexes.insert("day_of_year".to_string(), Dat::U16(day_of_year));
486 }
487
488 // Julian day for efficient date calculations
489 indexes.insert("julian_day".to_string(), Dat::I64(self.to_julian_day_number()));
490 if let Ok(days) = self.days_since_epoch() {
491 indexes.insert("days_since_epoch".to_string(), Dat::I64(days));
492 }
493
494 // Calendar groupings
495 indexes.insert("year_month".to_string(), Dat::Str(fmt!("{}-{:02}", self.year(), self.month())));
496 indexes.insert("quarter".to_string(), Dat::U8((self.month() - 1) / 3 + 1));
497 if let Ok(week) = self.week_of_year() {
498 indexes.insert("week_of_year".to_string(), Dat::U8(week));
499 }
500
501 // Special date flags
502 indexes.insert("is_leap_year".to_string(), Dat::Bool(self.is_leap_year()));
503 indexes.insert("is_weekend".to_string(), Dat::Bool(self.is_weekend()));
504
505 indexes.insert("timezone".to_string(), Dat::Str(self.zone().id().to_string()));
506
507 indexes
508 }
509}