1#[derive(Debug, Clone, PartialEq)]
5pub enum ScalarValue {
6 Null,
8 Bool(bool),
10 Int32(i32),
12 Int64(i64),
14 Float64(f64),
16 Str(String),
18 Binary(Vec<u8>),
20 Timestamp(String),
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ComparisonOp {
27 Eq,
29 Ne,
31 Gt,
33 Gte,
35 Lt,
37 Lte,
39}
40
41#[derive(Debug, Clone)]
43pub enum Filter {
44 Compare {
46 field: String,
48 op: ComparisonOp,
50 value: ScalarValue,
52 },
53 InList {
55 field: String,
57 values: Vec<ScalarValue>,
59 },
60 NotInList {
62 field: String,
64 values: Vec<ScalarValue>,
66 },
67 IsNull(String),
69 IsNotNull(String),
71 Contains {
73 field: String,
75 pattern: String,
77 },
78 StartsWith {
80 field: String,
82 pattern: String,
84 },
85 EndsWith {
87 field: String,
89 pattern: String,
91 },
92 And(Vec<Self>),
94 Or(Vec<Self>),
96 Not(Box<Self>),
98}
99
100#[derive(Debug, Clone, Default)]
102pub struct SortField {
103 pub field: String,
105 pub descending: bool,
107}
108
109#[derive(Debug, Clone, Default)]
111pub struct QueryOptions {
112 pub filter: Option<Filter>,
114 pub order_by: Vec<SortField>,
116 pub limit: Option<u32>,
118 pub offset: Option<u32>,
120 pub continuation: Option<String>,
122}
123
124#[derive(Debug, Clone)]
126pub struct Document {
127 pub id: String,
129 pub data: Vec<u8>,
131}
132
133#[derive(Debug, Clone, Default)]
135pub struct QueryResult {
136 pub documents: Vec<Document>,
138 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#[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
271 pub fn is_null(field: &str) -> Self {
272 Self::IsNull(field.to_string())
273 }
274
275 #[must_use]
277 pub fn is_not_null(field: &str) -> Self {
278 Self::IsNotNull(field.to_string())
279 }
280
281 #[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 #[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 #[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 #[must_use]
310 pub fn and(filters: impl IntoIterator<Item = Self>) -> Self {
311 Self::And(filters.into_iter().collect())
312 }
313
314 #[must_use]
316 pub fn or(filters: impl IntoIterator<Item = Self>) -> Self {
317 Self::Or(filters.into_iter().collect())
318 }
319
320 #[must_use]
322 #[allow(clippy::should_implement_trait)] pub fn not(inner: Self) -> Self {
324 Self::Not(Box::new(inner))
325 }
326
327 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}