Skip to main content

wasm_dbms_api/dbms/
query.rs

1//! This module exposes all the types related to queries that can be performed on the DBMS.
2
3mod aggregate;
4mod builder;
5mod delete;
6mod filter;
7mod join;
8
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12pub use self::aggregate::{AggregateFunction, AggregatedRow, AggregatedValue};
13pub use self::builder::QueryBuilder;
14pub use self::delete::DeleteBehavior;
15pub use self::filter::{Filter, JsonCmp, JsonFilter};
16pub use self::join::{Join, JoinType};
17use crate::dbms::table::TableSchema;
18use crate::dbms::value::Value;
19use crate::memory::MemoryError;
20
21/// The result type for query operations.
22pub type QueryResult<T> = Result<T, QueryError>;
23
24/// An enum representing possible errors that can occur during query operations.
25#[derive(Debug, Error, Serialize, Deserialize)]
26#[cfg_attr(feature = "candid", derive(candid::CandidType))]
27pub enum QueryError {
28    /// The specified primary key value already exists in the table.
29    #[error("Primary key conflict: record with the same primary key already exists")]
30    PrimaryKeyConflict,
31
32    /// A unique constraint was violated (e.g., UNIQUE index, CHECK constraint, etc.)
33    #[error("Unique constraint violation on field '{field}'")]
34    UniqueConstraintViolation { field: String },
35
36    /// A foreign key references a non-existent record in another table.
37    #[error("Broken foreign key reference to table '{table}' with key '{key:?}'")]
38    BrokenForeignKeyReference { table: String, key: Value },
39
40    /// Tried to delete or update a record that is referenced by another table's foreign key.
41    #[error("Foreign key constraint violation on table '{referencing_table}' for field '{field}'")]
42    ForeignKeyConstraintViolation {
43        referencing_table: String,
44        field: String,
45    },
46
47    /// Tried to reference a column that does not exist in the table schema.
48    #[error("Unknown column: {0}")]
49    UnknownColumn(String),
50
51    /// Tried to insert a record missing non-nullable fields.
52    #[error("Missing non-nullable field: {0}")]
53    MissingNonNullableField(String),
54
55    /// The specified transaction was not found or has expired.
56    #[error("transaction not found")]
57    TransactionNotFound,
58
59    /// Query contains syntactically or semantically invalid conditions.
60    #[error("Invalid query: {0}")]
61    InvalidQuery(String),
62
63    /// Join inside a typed select operation
64    #[error("Join cannot be used on type select")]
65    JoinInsideTypedSelect,
66
67    /// `GROUP BY` or `HAVING` was set on a non-aggregate query.
68    /// Use `Database::aggregate` instead.
69    #[error("GROUP BY / HAVING require aggregate(); use Database::aggregate")]
70    AggregateClauseInSelect,
71
72    /// Generic constraint violation (e.g., UNIQUE, CHECK, etc.)
73    #[error("Constraint violation: {0}")]
74    ConstraintViolation(String),
75
76    /// The memory allocator or memory manager failed to allocate or access stable memory.
77    #[error("Memory error: {0}")]
78    MemoryError(MemoryError),
79
80    /// The table or schema was not found.
81    #[error("Table not found: {0}")]
82    TableNotFound(String),
83
84    /// The record identified by the given key or filter does not exist.
85    #[error("Record not found")]
86    RecordNotFound,
87
88    /// Any low-level IO or serialization/deserialization issue.
89    #[error("Serialization error: {0}")]
90    SerializationError(String),
91
92    /// Generic catch-all error (for internal, unexpected conditions).
93    #[error("Internal error: {0}")]
94    Internal(String),
95}
96
97/// An enum representing the fields to select in a query.
98#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
99#[cfg_attr(feature = "candid", derive(candid::CandidType))]
100pub enum Select {
101    #[default]
102    All,
103    Columns(Vec<String>),
104}
105
106/// An enum representing the direction of ordering in a query.
107#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
108#[cfg_attr(feature = "candid", derive(candid::CandidType))]
109pub enum OrderDirection {
110    Ascending,
111    Descending,
112}
113
114/// A struct representing a query in the DBMS.
115#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
116pub struct Query {
117    /// Fields to select in the query.
118    columns: Select,
119    /// Distinct records by the specified fields.
120    pub distinct_by: Vec<String>,
121    /// Relations to eagerly load with the main records.
122    pub eager_relations: Vec<String>,
123    /// [`Filter`] to apply to the query.
124    pub filter: Option<Filter>,
125    /// Group by fields for aggregate queries.
126    pub group_by: Vec<String>,
127    /// Having [`Filter`] for aggregate queries.
128    pub having: Option<Filter>,
129    /// Join operations
130    pub joins: Vec<Join>,
131    /// Limit on the number of records to return.
132    pub limit: Option<usize>,
133    /// Offset for pagination.
134    pub offset: Option<usize>,
135    /// Order by clauses for sorting the results.
136    pub order_by: Vec<(String, OrderDirection)>,
137}
138
139#[cfg(feature = "candid")]
140impl candid::CandidType for Query {
141    fn _ty() -> candid::types::Type {
142        use candid::types::TypeInner;
143        let mut fields = vec![
144            candid::field! { columns: Select::_ty() },
145            candid::field! { distinct_by: <Vec<String>>::_ty() },
146            candid::field! { eager_relations: <Vec<String>>::_ty() },
147            candid::field! { filter: <Option<Filter>>::_ty() },
148            candid::field! { group_by: <Vec<String>>::_ty() },
149            candid::field! { having: <Option<Filter>>::_ty() },
150            candid::field! { joins: <Vec<Join>>::_ty() },
151            candid::field! { limit: <Option<usize>>::_ty() },
152            candid::field! { offset: <Option<usize>>::_ty() },
153            candid::field! { order_by: <Vec<(String, OrderDirection)>>::_ty() },
154        ];
155
156        fields.sort_by_key(|f| f.id.clone());
157        TypeInner::Record(fields).into()
158    }
159
160    fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
161    where
162        S: candid::types::Serializer,
163    {
164        use candid::types::Compound;
165        // Fields must be serialized in Candid field hash order. The order
166        // below matches the ascending hash of each field name (idl_hash).
167        let mut record_serializer = serializer.serialize_struct()?;
168        record_serializer.serialize_element(&self.eager_relations)?;
169        record_serializer.serialize_element(&self.distinct_by)?;
170        record_serializer.serialize_element(&self.joins)?;
171        record_serializer.serialize_element(&self.offset)?;
172        record_serializer.serialize_element(&self.limit)?;
173        record_serializer.serialize_element(&self.filter)?;
174        record_serializer.serialize_element(&self.group_by)?;
175        record_serializer.serialize_element(&self.having)?;
176        record_serializer.serialize_element(&self.order_by)?;
177        record_serializer.serialize_element(&self.columns)?;
178
179        Ok(())
180    }
181}
182
183impl Query {
184    /// Creates a new [`QueryBuilder`] for building a query.
185    pub fn builder() -> QueryBuilder {
186        QueryBuilder::default()
187    }
188
189    /// Returns whether all columns are selected in the query.
190    pub fn all_selected(&self) -> bool {
191        matches!(self.columns, Select::All)
192    }
193    /// Returns the list of columns to be selected in the query.
194    pub fn columns<T>(&self) -> Vec<String>
195    where
196        T: TableSchema,
197    {
198        match &self.columns {
199            Select::All => T::columns()
200                .iter()
201                .map(|col| col.name.to_string())
202                .collect(),
203            Select::Columns(cols) => cols.clone(),
204        }
205    }
206
207    /// Returns whether the query has any joins.
208    pub fn has_joins(&self) -> bool {
209        !self.joins.is_empty()
210    }
211
212    /// Returns the raw column names from the Select clause.
213    ///
214    /// Unlike `columns::<T>()`, this does not expand `Select::All`
215    /// using the table schema.
216    pub fn raw_columns(&self) -> &[String] {
217        match &self.columns {
218            Select::All => &[],
219            Select::Columns(cols) => cols,
220        }
221    }
222}
223
224#[cfg(test)]
225mod tests {
226
227    use super::*;
228    use crate::tests::User;
229
230    #[test]
231    fn test_should_build_default_query() {
232        let query = Query::default();
233        assert!(matches!(query.columns, Select::All));
234        assert!(query.eager_relations.is_empty());
235        assert!(query.filter.is_none());
236        assert!(query.order_by.is_empty());
237        assert!(query.limit.is_none());
238        assert!(query.offset.is_none());
239    }
240
241    #[test]
242    fn test_should_get_columns() {
243        let query = Query::default();
244        let columns = query.columns::<User>();
245        assert_eq!(columns, vec!["id", "name",]);
246
247        let query = Query {
248            columns: Select::Columns(vec!["id".to_string()]),
249            ..Default::default()
250        };
251
252        let columns = query.columns::<User>();
253        assert_eq!(columns, vec!["id"]);
254    }
255
256    #[test]
257    fn test_should_check_all_selected() {
258        let query = Query::default();
259        assert!(query.all_selected());
260    }
261
262    #[cfg(feature = "candid")]
263    #[test]
264    fn test_should_encode_decode_query_candid() {
265        let query = Query::builder()
266            .field("id")
267            .with("posts")
268            .and_where(Filter::eq("name", Value::Text("Alice".into())))
269            .order_by_asc("id")
270            .limit(10)
271            .offset(5)
272            .build();
273        let encoded = candid::encode_one(&query).unwrap();
274        let decoded: Query = candid::decode_one(&encoded).unwrap();
275        assert_eq!(query, decoded);
276    }
277
278    #[test]
279    fn test_should_build_query_with_joins() {
280        let query = Query::builder()
281            .all()
282            .inner_join("posts", "id", "user")
283            .build();
284        assert_eq!(query.joins.len(), 1);
285        assert_eq!(query.joins[0].table, "posts");
286    }
287
288    #[cfg(feature = "candid")]
289    #[test]
290    fn test_should_encode_decode_query_with_joins_candid() {
291        let query = Query::builder()
292            .all()
293            .inner_join("posts", "id", "user")
294            .left_join("comments", "posts.id", "post_id")
295            .and_where(Filter::eq("users.name", Value::Text("Alice".into())))
296            .build();
297        let encoded = candid::encode_one(&query).unwrap();
298        let decoded: Query = candid::decode_one(&encoded).unwrap();
299        assert_eq!(query, decoded);
300    }
301
302    #[test]
303    fn test_default_query_has_empty_joins() {
304        let query = Query::default();
305        assert!(query.joins.is_empty());
306        assert!(!query.has_joins());
307    }
308
309    #[test]
310    fn test_has_joins() {
311        let query = Query::builder()
312            .all()
313            .inner_join("posts", "id", "user")
314            .build();
315        assert!(query.has_joins());
316    }
317
318    #[test]
319    fn test_raw_columns_returns_empty_for_all() {
320        let query = Query::builder().all().build();
321        assert!(query.raw_columns().is_empty());
322    }
323
324    #[test]
325    fn test_raw_columns_returns_specified_columns() {
326        let query = Query::builder().field("id").field("name").build();
327        assert_eq!(query.raw_columns(), &["id", "name"]);
328    }
329}