ic_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;
6
7use std::marker::PhantomData;
8
9use candid::CandidType;
10use candid::types::{Compound, Type, TypeInner};
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14pub use self::builder::QueryBuilder;
15pub use self::delete::DeleteBehavior;
16pub use self::filter::Filter;
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, CandidType, Serialize, Deserialize)]
26pub enum QueryError {
27    /// The specified primary key value already exists in the table.
28    #[error("Primary key conflict: record with the same primary key already exists")]
29    PrimaryKeyConflict,
30
31    /// A foreign key references a non-existent record in another table.
32    #[error("Broken foreign key reference to table '{table}' with key '{key:?}'")]
33    BrokenForeignKeyReference { table: String, key: Value },
34
35    /// Tried to delete or update a record that is referenced by another table's foreign key.
36    #[error("Foreign key constraint violation on table '{referencing_table}' for field '{field}'")]
37    ForeignKeyConstraintViolation {
38        referencing_table: String,
39        field: String,
40    },
41
42    /// Tried to reference a column that does not exist in the table schema.
43    #[error("Unknown column: {0}")]
44    UnknownColumn(String),
45
46    /// Tried to insert a record missing non-nullable fields.
47    #[error("Missing non-nullable field: {0}")]
48    MissingNonNullableField(String),
49
50    /// The specified transaction was not found or has expired.
51    #[error("transaction not found")]
52    TransactionNotFound,
53
54    /// Query contains syntactically or semantically invalid conditions.
55    #[error("Invalid query: {0}")]
56    InvalidQuery(String),
57
58    /// Generic constraint violation (e.g., UNIQUE, CHECK, etc.)
59    #[error("Constraint violation: {0}")]
60    ConstraintViolation(String),
61
62    /// The memory allocator or memory manager failed to allocate or access stable memory.
63    #[error("Memory error: {0}")]
64    MemoryError(MemoryError),
65
66    /// The table or schema was not found.
67    #[error("Table not found: {0}")]
68    TableNotFound(String),
69
70    /// The record identified by the given key or filter does not exist.
71    #[error("Record not found")]
72    RecordNotFound,
73
74    /// Any low-level IO or serialization/deserialization issue.
75    #[error("Serialization error: {0}")]
76    SerializationError(String),
77
78    /// Generic catch-all error (for internal, unexpected conditions).
79    #[error("Internal error: {0}")]
80    Internal(String),
81}
82
83/// An enum representing the fields to select in a query.
84#[derive(Debug, Default, Clone, PartialEq, Eq, CandidType, Serialize, Deserialize)]
85pub enum Select {
86    #[default]
87    All,
88    Columns(Vec<String>),
89}
90
91/// An enum representing the direction of ordering in a query.
92#[derive(Debug, Clone, PartialEq, Eq, CandidType, Serialize, Deserialize)]
93pub enum OrderDirection {
94    Ascending,
95    Descending,
96}
97
98/// A struct representing a query in the DBMS.
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub struct Query<T>
101where
102    T: TableSchema,
103{
104    /// Fields to select in the query.
105    columns: Select,
106    /// Relations to eagerly load with the main records.
107    pub eager_relations: Vec<String>,
108    /// [`Filter`] to apply to the query.
109    pub filter: Option<Filter>,
110    /// Order by clauses for sorting the results.
111    pub order_by: Vec<(String, OrderDirection)>,
112    /// Limit on the number of records to return.
113    pub limit: Option<usize>,
114    /// Offset for pagination.
115    pub offset: Option<usize>,
116    /// Marker for the table schema type.
117    #[serde(skip)]
118    _marker: PhantomData<T>,
119}
120
121impl<T: TableSchema> CandidType for Query<T> {
122    fn _ty() -> Type {
123        let mut fields = vec![
124            candid::field! { columns: Select::_ty() },
125            candid::field! { eager_relations: <Vec<String>>::_ty() },
126            candid::field! { filter: <Option<Filter>>::_ty() },
127            candid::field! { order_by: <Vec<(String, OrderDirection)>>::_ty() },
128            candid::field! { limit: <Option<usize>>::_ty() },
129            candid::field! { offset: <Option<usize>>::_ty() },
130            // We do not include _marker in the Candid type representation
131        ];
132
133        fields.sort_by_key(|f| f.id.clone());
134        TypeInner::Record(fields).into()
135    }
136
137    fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
138    where
139        S: candid::types::Serializer,
140    {
141        // so apparently the order here is important, and for some reason is different from the one above.
142        // if you need to change see the order, or ask chatgpt.
143        let mut record_serializer = serializer.serialize_struct()?;
144        record_serializer.serialize_element(&self.eager_relations)?;
145        record_serializer.serialize_element(&self.offset)?;
146        record_serializer.serialize_element(&self.limit)?;
147        record_serializer.serialize_element(&self.filter)?;
148        record_serializer.serialize_element(&self.order_by)?;
149        record_serializer.serialize_element(&self.columns)?;
150
151        Ok(())
152    }
153}
154
155impl<T> Default for Query<T>
156where
157    T: TableSchema,
158{
159    fn default() -> Self {
160        Self {
161            columns: Select::All,
162            eager_relations: Vec::new(),
163            filter: None,
164            order_by: Vec::new(),
165            limit: None,
166            offset: None,
167            _marker: PhantomData,
168        }
169    }
170}
171
172impl<T> Query<T>
173where
174    T: TableSchema,
175{
176    /// Creates a new [`QueryBuilder`] for building a query.
177    pub fn builder() -> QueryBuilder<T> {
178        QueryBuilder::default()
179    }
180
181    /// Returns whether all columns are selected in the query.
182    pub fn all_selected(&self) -> bool {
183        matches!(self.columns, Select::All)
184    }
185
186    /// Returns the list of columns to be selected in the query.
187    pub fn columns(&self) -> Vec<String> {
188        match &self.columns {
189            Select::All => T::columns()
190                .iter()
191                .map(|col| col.name.to_string())
192                .collect(),
193            Select::Columns(cols) => cols.clone(),
194        }
195    }
196}
197
198#[cfg(test)]
199mod tests {
200
201    use super::*;
202    use crate::tests::User;
203
204    #[test]
205    fn test_should_build_default_query() {
206        let query: Query<User> = Query::default();
207        assert!(matches!(query.columns, Select::All));
208        assert!(query.eager_relations.is_empty());
209        assert!(query.filter.is_none());
210        assert!(query.order_by.is_empty());
211        assert!(query.limit.is_none());
212        assert!(query.offset.is_none());
213    }
214
215    #[test]
216    fn test_should_get_columns() {
217        let query = Query::<User>::default();
218        let columns = query.columns();
219        assert_eq!(columns, vec!["id", "name",]);
220
221        let query = Query::<User> {
222            columns: Select::Columns(vec!["id".to_string()]),
223            ..Default::default()
224        };
225
226        let columns = query.columns();
227        assert_eq!(columns, vec!["id"]);
228    }
229
230    #[test]
231    fn test_should_check_all_selected() {
232        let query = Query::<User>::default();
233        assert!(query.all_selected());
234    }
235
236    #[test]
237    fn test_should_encode_decode_query_candid() {
238        let query: Query<User> = Query::builder()
239            .field("id")
240            .with("posts")
241            .and_where(Filter::eq("name", Value::Text("Alice".into())))
242            .order_by_asc("id")
243            .limit(10)
244            .offset(5)
245            .build();
246        let encoded = candid::encode_one(&query).unwrap();
247        let decoded: Query<User> = candid::decode_one(&encoded).unwrap();
248        assert_eq!(query, decoded);
249    }
250}