1mod 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
24pub type QueryResult<T> = Result<T, QueryError>;
26
27#[derive(Debug, Error, Serialize, Deserialize)]
29#[cfg_attr(feature = "candid", derive(candid::CandidType))]
30pub enum QueryError {
31 #[error("Primary key conflict: record with the same primary key already exists")]
33 PrimaryKeyConflict,
34
35 #[error("Unique constraint violation on field '{field}'")]
37 UniqueConstraintViolation { field: String },
38
39 #[error("Broken foreign key reference to table '{table}' with key '{key:?}'")]
41 BrokenForeignKeyReference { table: String, key: Value },
42
43 #[error("Foreign key constraint violation on table '{referencing_table}' for field '{field}'")]
45 ForeignKeyConstraintViolation {
46 referencing_table: String,
47 field: String,
48 },
49
50 #[error("Unknown column: {0}")]
52 UnknownColumn(String),
53
54 #[error("Missing non-nullable field: {0}")]
56 MissingNonNullableField(String),
57
58 #[error("transaction not found")]
60 TransactionNotFound,
61
62 #[error("Invalid query: {0}")]
64 InvalidQuery(String),
65
66 #[error("Join cannot be used on type select")]
68 JoinInsideTypedSelect,
69
70 #[error("GROUP BY / HAVING require aggregate(); use Database::aggregate")]
73 AggregateClauseInSelect,
74
75 #[error("Constraint violation: {0}")]
77 ConstraintViolation(String),
78
79 #[error("Memory error: {0}")]
81 MemoryError(MemoryError),
82
83 #[error("Table not found: {0}")]
85 TableNotFound(String),
86
87 #[error("Record not found")]
89 RecordNotFound,
90
91 #[error("Serialization error: {0}")]
93 SerializationError(String),
94
95 #[error("Internal error: {0}")]
97 Internal(String),
98}
99
100#[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#[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#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
119pub struct Query {
120 columns: Select,
122 pub distinct_by: Vec<String>,
124 pub eager_relations: Vec<String>,
126 pub filter: Option<Filter>,
128 pub group_by: Vec<String>,
130 pub having: Option<Filter>,
132 pub joins: Vec<Join>,
134 pub limit: Option<usize>,
136 pub offset: Option<usize>,
138 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 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 pub fn builder() -> QueryBuilder {
189 QueryBuilder::default()
190 }
191
192 pub fn all_selected(&self) -> bool {
194 matches!(self.columns, Select::All)
195 }
196 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 pub fn has_joins(&self) -> bool {
212 !self.joins.is_empty()
213 }
214
215 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}