Skip to main content

omnia_wasi_jsondb/
document_store.rs

1//! Domain types for JSON document storage and queries (shared by host and guest).
2
3/// Scalar values for filter comparisons.
4#[derive(Debug, Clone, PartialEq)]
5pub enum ScalarValue {
6    /// Null literal.
7    Null,
8    /// Boolean.
9    Bool(bool),
10    /// 32-bit integer.
11    Int32(i32),
12    /// 64-bit integer.
13    Int64(i64),
14    /// Floating point.
15    Float64(f64),
16    /// UTF-8 string.
17    Str(String),
18    /// Opaque bytes.
19    Binary(Vec<u8>),
20    /// ISO-8601 timestamp string for comparisons.
21    Timestamp(String),
22}
23
24/// Comparison operators for filters.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ComparisonOp {
27    /// Equal.
28    Eq,
29    /// Not equal.
30    Ne,
31    /// Greater than.
32    Gt,
33    /// Greater than or equal.
34    Gte,
35    /// Less than.
36    Lt,
37    /// Less than or equal.
38    Lte,
39}
40
41/// A filter expression tree.
42#[derive(Debug, Clone)]
43pub enum Filter {
44    /// Compare `field` to `value` using `op`.
45    Compare {
46        /// Field path.
47        field: String,
48        /// Comparison operator.
49        op: ComparisonOp,
50        /// Right-hand value.
51        value: ScalarValue,
52    },
53    /// Field value is one of the given values.
54    InList {
55        /// Field path.
56        field: String,
57        /// Allowed values.
58        values: Vec<ScalarValue>,
59    },
60    /// Field value is not in the given set.
61    NotInList {
62        /// Field path.
63        field: String,
64        /// Excluded values.
65        values: Vec<ScalarValue>,
66    },
67    /// Field is null or missing.
68    IsNull(String),
69    /// Field exists and is not null.
70    IsNotNull(String),
71    /// String contains pattern (backend-defined semantics).
72    Contains {
73        /// Field path.
74        field: String,
75        /// Substring pattern.
76        pattern: String,
77    },
78    /// String starts with pattern.
79    StartsWith {
80        /// Field path.
81        field: String,
82        /// Prefix pattern.
83        pattern: String,
84    },
85    /// String ends with pattern.
86    EndsWith {
87        /// Field path.
88        field: String,
89        /// Suffix pattern.
90        pattern: String,
91    },
92    /// Logical AND of child filters.
93    And(Vec<Self>),
94    /// Logical OR of child filters.
95    Or(Vec<Self>),
96    /// Logical NOT.
97    Not(Box<Self>),
98}
99
100/// Sort field for queries.
101#[derive(Debug, Clone, Default)]
102pub struct SortField {
103    /// Field path.
104    pub field: String,
105    /// When `true`, sort descending.
106    pub descending: bool,
107}
108
109/// Options for listing or searching documents.
110#[derive(Debug, Clone, Default)]
111pub struct QueryOptions {
112    /// Optional filter tree.
113    pub filter: Option<Filter>,
114    /// Sort order (first key wins, then next, etc.).
115    pub order_by: Vec<SortField>,
116    /// Maximum documents to return.
117    pub limit: Option<u32>,
118    /// Skip this many documents after filter/sort (offset pagination).
119    pub offset: Option<u32>,
120    /// Opaque continuation token from a previous page.
121    pub continuation: Option<String>,
122}
123
124/// Stored document: identifier plus JSON body bytes.
125#[derive(Debug, Clone)]
126pub struct Document {
127    /// Primary key string.
128    pub id: String,
129    /// JSON payload.
130    pub data: Vec<u8>,
131}
132
133/// Result of a query with optional next-page token.
134#[derive(Debug, Clone, Default)]
135pub struct QueryResult {
136    /// Matching documents.
137    pub documents: Vec<Document>,
138    /// Continuation token for the next page, if any.
139    pub continuation: Option<String>,
140}
141
142impl From<&str> for ScalarValue {
143    fn from(s: &str) -> Self {
144        Self::Str(s.to_string())
145    }
146}
147
148impl From<String> for ScalarValue {
149    fn from(s: String) -> Self {
150        Self::Str(s)
151    }
152}
153
154impl From<i32> for ScalarValue {
155    fn from(v: i32) -> Self {
156        Self::Int32(v)
157    }
158}
159
160impl From<i64> for ScalarValue {
161    fn from(v: i64) -> Self {
162        Self::Int64(v)
163    }
164}
165
166impl From<f64> for ScalarValue {
167    fn from(v: f64) -> Self {
168        Self::Float64(v)
169    }
170}
171
172impl From<bool> for ScalarValue {
173    fn from(v: bool) -> Self {
174        Self::Bool(v)
175    }
176}
177
178/// Newtype for timestamp strings so `Filter::gte("ts", Timestamp(...))` is explicit.
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct Timestamp(pub String);
181
182impl From<Timestamp> for ScalarValue {
183    fn from(t: Timestamp) -> Self {
184        Self::Timestamp(t.0)
185    }
186}
187
188impl Filter {
189    /// Equality comparison.
190    #[must_use]
191    pub fn eq(field: &str, val: impl Into<ScalarValue>) -> Self {
192        Self::Compare {
193            field: field.to_string(),
194            op: ComparisonOp::Eq,
195            value: val.into(),
196        }
197    }
198
199    /// Inequality comparison.
200    #[must_use]
201    pub fn ne(field: &str, val: impl Into<ScalarValue>) -> Self {
202        Self::Compare {
203            field: field.to_string(),
204            op: ComparisonOp::Ne,
205            value: val.into(),
206        }
207    }
208
209    /// Greater than.
210    #[must_use]
211    pub fn gt(field: &str, val: impl Into<ScalarValue>) -> Self {
212        Self::Compare {
213            field: field.to_string(),
214            op: ComparisonOp::Gt,
215            value: val.into(),
216        }
217    }
218
219    /// Greater than or equal.
220    #[must_use]
221    pub fn gte(field: &str, val: impl Into<ScalarValue>) -> Self {
222        Self::Compare {
223            field: field.to_string(),
224            op: ComparisonOp::Gte,
225            value: val.into(),
226        }
227    }
228
229    /// Less than.
230    #[must_use]
231    pub fn lt(field: &str, val: impl Into<ScalarValue>) -> Self {
232        Self::Compare {
233            field: field.to_string(),
234            op: ComparisonOp::Lt,
235            value: val.into(),
236        }
237    }
238
239    /// Less than or equal.
240    #[must_use]
241    pub fn lte(field: &str, val: impl Into<ScalarValue>) -> Self {
242        Self::Compare {
243            field: field.to_string(),
244            op: ComparisonOp::Lte,
245            value: val.into(),
246        }
247    }
248
249    /// Field value is in the given set.
250    #[must_use]
251    pub fn in_list(field: &str, vals: impl IntoIterator<Item = impl Into<ScalarValue>>) -> Self {
252        Self::InList {
253            field: field.to_string(),
254            values: vals.into_iter().map(Into::into).collect(),
255        }
256    }
257
258    /// Field value is not in the given set.
259    #[must_use]
260    pub fn not_in_list(
261        field: &str, vals: impl IntoIterator<Item = impl Into<ScalarValue>>,
262    ) -> Self {
263        Self::NotInList {
264            field: field.to_string(),
265            values: vals.into_iter().map(Into::into).collect(),
266        }
267    }
268
269    /// Field is null or missing.
270    #[must_use]
271    pub fn is_null(field: &str) -> Self {
272        Self::IsNull(field.to_string())
273    }
274
275    /// Field exists and is not null.
276    #[must_use]
277    pub fn is_not_null(field: &str) -> Self {
278        Self::IsNotNull(field.to_string())
279    }
280
281    /// String contains pattern.
282    #[must_use]
283    pub fn contains(field: &str, pattern: &str) -> Self {
284        Self::Contains {
285            field: field.to_string(),
286            pattern: pattern.to_string(),
287        }
288    }
289
290    /// String starts with pattern.
291    #[must_use]
292    pub fn starts_with(field: &str, pattern: &str) -> Self {
293        Self::StartsWith {
294            field: field.to_string(),
295            pattern: pattern.to_string(),
296        }
297    }
298
299    /// String ends with pattern.
300    #[must_use]
301    pub fn ends_with(field: &str, pattern: &str) -> Self {
302        Self::EndsWith {
303            field: field.to_string(),
304            pattern: pattern.to_string(),
305        }
306    }
307
308    /// Logical AND.
309    #[must_use]
310    pub fn and(filters: impl IntoIterator<Item = Self>) -> Self {
311        Self::And(filters.into_iter().collect())
312    }
313
314    /// Logical OR.
315    #[must_use]
316    pub fn or(filters: impl IntoIterator<Item = Self>) -> Self {
317        Self::Or(filters.into_iter().collect())
318    }
319
320    /// Logical NOT.
321    #[must_use]
322    #[allow(clippy::should_implement_trait)] // Domain API mirrors `std::ops::Not`, not the trait.
323    pub fn not(inner: Self) -> Self {
324        Self::Not(Box::new(inner))
325    }
326
327    /// Restrict `field` to a calendar date (UTC day) using range on timestamp strings.
328    ///
329    /// # Errors
330    ///
331    /// Returns an error if `iso_date` is not a valid `YYYY-MM-DD` date or is the
332    /// maximum representable date (no next day).
333    pub fn on_date(field: &str, iso_date: &str) -> anyhow::Result<Self> {
334        let next = next_iso_date(iso_date)?;
335        let start = format!("{iso_date}T00:00:00Z");
336        let end = format!("{next}T00:00:00Z");
337        Ok(Self::And(vec![Self::gte(field, Timestamp(start)), Self::lt(field, Timestamp(end))]))
338    }
339}
340
341fn next_iso_date(iso_date: &str) -> anyhow::Result<String> {
342    use chrono::NaiveDate;
343    let date = NaiveDate::parse_from_str(iso_date, "%Y-%m-%d")
344        .map_err(|_e| anyhow::anyhow!("invalid ISO date: {iso_date:?}"))?;
345    let next = date
346        .succ_opt()
347        .ok_or_else(|| anyhow::anyhow!("cannot compute next day for date: {iso_date}"))?;
348    Ok(next.format("%Y-%m-%d").to_string())
349}