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