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