mod builder;
mod delete;
mod filter;
mod join;
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub use self::builder::QueryBuilder;
pub use self::delete::DeleteBehavior;
pub use self::filter::{Filter, JsonCmp, JsonFilter};
pub use self::join::{Join, JoinType};
use crate::dbms::table::TableSchema;
use crate::dbms::value::Value;
use crate::memory::MemoryError;
pub type QueryResult<T> = Result<T, QueryError>;
#[derive(Debug, Error, Serialize, Deserialize)]
#[cfg_attr(feature = "candid", derive(candid::CandidType))]
pub enum QueryError {
#[error("Primary key conflict: record with the same primary key already exists")]
PrimaryKeyConflict,
#[error("Unique constraint violation on field '{field}'")]
UniqueConstraintViolation { field: String },
#[error("Broken foreign key reference to table '{table}' with key '{key:?}'")]
BrokenForeignKeyReference { table: String, key: Value },
#[error("Foreign key constraint violation on table '{referencing_table}' for field '{field}'")]
ForeignKeyConstraintViolation {
referencing_table: String,
field: String,
},
#[error("Unknown column: {0}")]
UnknownColumn(String),
#[error("Missing non-nullable field: {0}")]
MissingNonNullableField(String),
#[error("transaction not found")]
TransactionNotFound,
#[error("Invalid query: {0}")]
InvalidQuery(String),
#[error("Join cannot be used on type select")]
JoinInsideTypedSelect,
#[error("Constraint violation: {0}")]
ConstraintViolation(String),
#[error("Memory error: {0}")]
MemoryError(MemoryError),
#[error("Table not found: {0}")]
TableNotFound(String),
#[error("Record not found")]
RecordNotFound,
#[error("Serialization error: {0}")]
SerializationError(String),
#[error("Internal error: {0}")]
Internal(String),
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "candid", derive(candid::CandidType))]
pub enum Select {
#[default]
All,
Columns(Vec<String>),
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "candid", derive(candid::CandidType))]
pub enum OrderDirection {
Ascending,
Descending,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Query {
columns: Select,
pub eager_relations: Vec<String>,
pub joins: Vec<Join>,
pub filter: Option<Filter>,
pub order_by: Vec<(String, OrderDirection)>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
#[cfg(feature = "candid")]
impl candid::CandidType for Query {
fn _ty() -> candid::types::Type {
use candid::types::TypeInner;
let mut fields = vec![
candid::field! { columns: Select::_ty() },
candid::field! { eager_relations: <Vec<String>>::_ty() },
candid::field! { joins: <Vec<Join>>::_ty() },
candid::field! { filter: <Option<Filter>>::_ty() },
candid::field! { order_by: <Vec<(String, OrderDirection)>>::_ty() },
candid::field! { limit: <Option<usize>>::_ty() },
candid::field! { offset: <Option<usize>>::_ty() },
];
fields.sort_by_key(|f| f.id.clone());
TypeInner::Record(fields).into()
}
fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
where
S: candid::types::Serializer,
{
use candid::types::Compound;
let mut record_serializer = serializer.serialize_struct()?;
record_serializer.serialize_element(&self.eager_relations)?;
record_serializer.serialize_element(&self.joins)?;
record_serializer.serialize_element(&self.offset)?;
record_serializer.serialize_element(&self.limit)?;
record_serializer.serialize_element(&self.filter)?;
record_serializer.serialize_element(&self.order_by)?;
record_serializer.serialize_element(&self.columns)?;
Ok(())
}
}
impl Query {
pub fn builder() -> QueryBuilder {
QueryBuilder::default()
}
pub fn all_selected(&self) -> bool {
matches!(self.columns, Select::All)
}
pub fn columns<T>(&self) -> Vec<String>
where
T: TableSchema,
{
match &self.columns {
Select::All => T::columns()
.iter()
.map(|col| col.name.to_string())
.collect(),
Select::Columns(cols) => cols.clone(),
}
}
pub fn has_joins(&self) -> bool {
!self.joins.is_empty()
}
pub fn raw_columns(&self) -> &[String] {
match &self.columns {
Select::All => &[],
Select::Columns(cols) => cols,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::User;
#[test]
fn test_should_build_default_query() {
let query = Query::default();
assert!(matches!(query.columns, Select::All));
assert!(query.eager_relations.is_empty());
assert!(query.filter.is_none());
assert!(query.order_by.is_empty());
assert!(query.limit.is_none());
assert!(query.offset.is_none());
}
#[test]
fn test_should_get_columns() {
let query = Query::default();
let columns = query.columns::<User>();
assert_eq!(columns, vec!["id", "name",]);
let query = Query {
columns: Select::Columns(vec!["id".to_string()]),
..Default::default()
};
let columns = query.columns::<User>();
assert_eq!(columns, vec!["id"]);
}
#[test]
fn test_should_check_all_selected() {
let query = Query::default();
assert!(query.all_selected());
}
#[cfg(feature = "candid")]
#[test]
fn test_should_encode_decode_query_candid() {
let query = Query::builder()
.field("id")
.with("posts")
.and_where(Filter::eq("name", Value::Text("Alice".into())))
.order_by_asc("id")
.limit(10)
.offset(5)
.build();
let encoded = candid::encode_one(&query).unwrap();
let decoded: Query = candid::decode_one(&encoded).unwrap();
assert_eq!(query, decoded);
}
#[test]
fn test_should_build_query_with_joins() {
let query = Query::builder()
.all()
.inner_join("posts", "id", "user")
.build();
assert_eq!(query.joins.len(), 1);
assert_eq!(query.joins[0].table, "posts");
}
#[cfg(feature = "candid")]
#[test]
fn test_should_encode_decode_query_with_joins_candid() {
let query = Query::builder()
.all()
.inner_join("posts", "id", "user")
.left_join("comments", "posts.id", "post_id")
.and_where(Filter::eq("users.name", Value::Text("Alice".into())))
.build();
let encoded = candid::encode_one(&query).unwrap();
let decoded: Query = candid::decode_one(&encoded).unwrap();
assert_eq!(query, decoded);
}
#[test]
fn test_default_query_has_empty_joins() {
let query = Query::default();
assert!(query.joins.is_empty());
assert!(!query.has_joins());
}
#[test]
fn test_has_joins() {
let query = Query::builder()
.all()
.inner_join("posts", "id", "user")
.build();
assert!(query.has_joins());
}
#[test]
fn test_raw_columns_returns_empty_for_all() {
let query = Query::builder().all().build();
assert!(query.raw_columns().is_empty());
}
#[test]
fn test_raw_columns_returns_specified_columns() {
let query = Query::builder().field("id").field("name").build();
assert_eq!(query.raw_columns(), &["id", "name"]);
}
}