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