Skip to main content

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