Skip to main content

datapress_client/
models.rs

1//! Request and response types for the structured query API.
2//!
3//! These mirror the server-side `QueryRequest` shape but are
4//! **serialize-first** (the server's copy is deserialize-only) and carry
5//! no engine dependencies, so this crate stays lightweight and
6//! publishable on its own.
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value as JsonValue;
10
11/// A single filter predicate.
12///
13/// `op` is one of `eq | neq | gt | gte | lt | lte | like | ilike | in |
14/// is_null | is_not_null`. `val` is omitted for the null checks and is an
15/// array for `in`.
16#[derive(Clone, Debug, Serialize, Deserialize)]
17pub struct Predicate {
18    pub col: String,
19    pub op: String,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub val: Option<JsonValue>,
22}
23
24impl Predicate {
25    /// Binary/`like` predicate: `col op val`.
26    pub fn new(col: impl Into<String>, op: impl Into<String>, val: impl Into<JsonValue>) -> Self {
27        Self {
28            col: col.into(),
29            op: op.into(),
30            val: Some(val.into()),
31        }
32    }
33
34    /// A value-less predicate (`is_null` / `is_not_null`).
35    pub fn unary(col: impl Into<String>, op: impl Into<String>) -> Self {
36        Self {
37            col: col.into(),
38            op: op.into(),
39            val: None,
40        }
41    }
42}
43
44/// One `ORDER BY` entry. `dir` is `"asc"` (default) or `"desc"`.
45#[derive(Clone, Debug, Serialize, Deserialize)]
46pub struct OrderBy {
47    pub col: String,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub dir: Option<String>,
50}
51
52impl OrderBy {
53    pub fn asc(col: impl Into<String>) -> Self {
54        Self {
55            col: col.into(),
56            dir: Some("asc".into()),
57        }
58    }
59    pub fn desc(col: impl Into<String>) -> Self {
60        Self {
61            col: col.into(),
62            dir: Some("desc".into()),
63        }
64    }
65}
66
67/// One aggregation in a `group_by` query.
68///
69/// `op` is `count | sum | avg | min | max`. `col` is required for every op
70/// except `count`. `alias` is the output key; defaults server-side to
71/// `count` for `COUNT(*)` and `{op}_{col}` otherwise.
72#[derive(Clone, Debug, Serialize, Deserialize)]
73pub struct Aggregation {
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub col: Option<String>,
76    pub op: String,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub alias: Option<String>,
79}
80
81impl Aggregation {
82    /// `COUNT(*)` with an optional alias.
83    pub fn count(alias: Option<&str>) -> Self {
84        Self {
85            col: None,
86            op: "count".into(),
87            alias: alias.map(str::to_owned),
88        }
89    }
90
91    /// An aggregation over a named column (`sum`, `avg`, `min`, `max`,
92    /// or `count`).
93    pub fn over(op: impl Into<String>, col: impl Into<String>, alias: Option<&str>) -> Self {
94        Self {
95            col: Some(col.into()),
96            op: op.into(),
97            alias: alias.map(str::to_owned),
98        }
99    }
100}
101
102/// A structured query, sent as the body of `POST /datasets/{name}/query`.
103///
104/// Build one with [`QueryRequest::builder`]. Fields left at their defaults
105/// are omitted from the wire payload so the server applies its own
106/// defaults (page size, etc.).
107#[derive(Clone, Debug, Default, Serialize, Deserialize)]
108pub struct QueryRequest {
109    #[serde(default, skip_serializing_if = "Vec::is_empty")]
110    pub columns: Vec<String>,
111    #[serde(default, skip_serializing_if = "Vec::is_empty")]
112    pub predicates: Vec<Predicate>,
113    #[serde(default, skip_serializing_if = "Vec::is_empty")]
114    pub group_by: Vec<String>,
115    #[serde(default, skip_serializing_if = "Vec::is_empty")]
116    pub aggregations: Vec<Aggregation>,
117    #[serde(default, skip_serializing_if = "Vec::is_empty")]
118    pub having: Vec<Predicate>,
119    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
120    pub distinct: bool,
121    #[serde(default, skip_serializing_if = "Vec::is_empty")]
122    pub order_by: Vec<OrderBy>,
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub limit: Option<u64>,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub page: Option<u64>,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub page_size: Option<u64>,
129}
130
131impl QueryRequest {
132    /// Start building a query.
133    pub fn builder() -> QueryRequestBuilder {
134        QueryRequestBuilder::default()
135    }
136}
137
138/// Fluent builder for [`QueryRequest`].
139#[derive(Clone, Debug, Default)]
140pub struct QueryRequestBuilder {
141    inner: QueryRequest,
142}
143
144impl QueryRequestBuilder {
145    /// Restrict the projection to these columns. Empty = all columns.
146    pub fn columns<I, S>(mut self, cols: I) -> Self
147    where
148        I: IntoIterator<Item = S>,
149        S: Into<String>,
150    {
151        self.inner.columns = cols.into_iter().map(Into::into).collect();
152        self
153    }
154
155    /// Add a filter predicate (ANDed with the others).
156    pub fn predicate(mut self, p: Predicate) -> Self {
157        self.inner.predicates.push(p);
158        self
159    }
160
161    /// Group by these columns.
162    pub fn group_by<I, S>(mut self, cols: I) -> Self
163    where
164        I: IntoIterator<Item = S>,
165        S: Into<String>,
166    {
167        self.inner.group_by = cols.into_iter().map(Into::into).collect();
168        self
169    }
170
171    /// Add an aggregation.
172    pub fn aggregation(mut self, a: Aggregation) -> Self {
173        self.inner.aggregations.push(a);
174        self
175    }
176
177    /// Add a post-aggregation (`HAVING`) predicate.
178    pub fn having(mut self, p: Predicate) -> Self {
179        self.inner.having.push(p);
180        self
181    }
182
183    /// Return only distinct rows over the projected columns.
184    pub fn distinct(mut self, yes: bool) -> Self {
185        self.inner.distinct = yes;
186        self
187    }
188
189    /// Add a sort key.
190    pub fn order_by(mut self, o: OrderBy) -> Self {
191        self.inner.order_by.push(o);
192        self
193    }
194
195    /// Cap the total number of rows returned.
196    pub fn limit(mut self, n: u64) -> Self {
197        self.inner.limit = Some(n);
198        self
199    }
200
201    /// Set the (1-based) page number.
202    pub fn page(mut self, n: u64) -> Self {
203        self.inner.page = Some(n);
204        self
205    }
206
207    /// Set the page size.
208    pub fn page_size(mut self, n: u64) -> Self {
209        self.inner.page_size = Some(n);
210        self
211    }
212
213    /// Finish building.
214    pub fn build(self) -> QueryRequest {
215        self.inner
216    }
217}
218
219/// JSON envelope returned by `POST /datasets/{name}/query`.
220#[derive(Clone, Debug, Serialize, Deserialize)]
221pub struct QueryResponse {
222    /// One object per row.
223    pub data: Vec<JsonValue>,
224    /// Echoed page number.
225    #[serde(default)]
226    pub page: Option<u64>,
227    /// Echoed page size.
228    #[serde(default)]
229    pub page_size: Option<u64>,
230}
231
232/// JSON envelope returned by `POST /sql`.
233#[derive(Clone, Debug, Serialize, Deserialize)]
234pub struct SqlResponse {
235    /// One object per row.
236    pub data: Vec<JsonValue>,
237    /// Effective row cap applied by the server.
238    #[serde(default)]
239    pub max_rows: Option<u64>,
240}
241
242/// Raw-SQL request body (`POST /sql`).
243#[derive(Clone, Debug, Serialize)]
244pub struct SqlRequest {
245    pub sql: String,
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub max_rows: Option<u64>,
248}