use prax_orm::Model;
use prax_query::row::{RowError, RowRef};
use prax_query::traits::Model as QueryModel;
#[derive(Model, Debug, Clone, Default)]
#[prax(table = "posts")]
pub struct Post {
#[prax(id, auto)]
pub id: i32,
pub author_id: i32,
pub views: i32,
pub created_at: String,
}
#[derive(Model, Debug, Clone, Default)]
#[prax(table = "users")]
pub struct User {
#[prax(id, auto)]
pub id: i32,
#[prax(unique)]
pub email: String,
pub first_name: String,
pub last_name: String,
#[prax(generated = "first_name || ' ' || last_name", stored)]
pub full_name: String,
#[prax(generated = "LOWER(email)", virtual)]
pub search_key: String,
#[prax(relation(target = "Post", foreign_key = "author_id"))]
pub posts: Vec<Post>,
#[prax(count(posts))]
pub post_count: i64,
#[prax(sum(posts.views))]
pub total_views: Option<i32>,
}
#[test]
fn user_emits_generated_field_metadata() {
assert_eq!(
User::GENERATED_FIELDS,
&[
("full_name", "first_name || ' ' || last_name", true),
("search_key", "LOWER(email)", false),
][..],
);
}
#[test]
fn user_emits_aggregate_field_metadata() {
assert_eq!(
User::AGGREGATE_FIELDS,
&[
("post_count", "count", "posts", None),
("total_views", "sum", "posts", Some("views")),
][..],
);
}
#[test]
fn post_has_no_computed_metadata() {
assert!(Post::GENERATED_FIELDS.is_empty());
assert!(Post::AGGREGATE_FIELDS.is_empty());
}
#[test]
fn user_full_columns_includes_generated_excludes_aggregate() {
let cols: Vec<&str> = User::COLUMNS.to_vec();
assert!(
cols.contains(&"full_name"),
"COLUMNS missing @generated `full_name`; got: {cols:?}"
);
assert!(
cols.contains(&"search_key"),
"COLUMNS missing @generated `search_key`; got: {cols:?}"
);
assert!(
!cols.contains(&"post_count"),
"COLUMNS must not include @count `post_count`; got: {cols:?}"
);
assert!(
!cols.contains(&"total_views"),
"COLUMNS must not include @sum `total_views`; got: {cols:?}"
);
}
struct UserTestRow {
data: std::collections::HashMap<String, Option<String>>,
}
impl UserTestRow {
fn new() -> Self {
Self {
data: std::collections::HashMap::new(),
}
}
fn set(mut self, col: &str, val: &str) -> Self {
self.data.insert(col.to_string(), Some(val.to_string()));
self
}
}
impl RowRef for UserTestRow {
fn get_i32(&self, column: &str) -> Result<i32, RowError> {
match self.data.get(column) {
Some(Some(v)) => {
v.parse()
.map_err(|e: std::num::ParseIntError| RowError::TypeConversion {
column: column.to_string(),
message: e.to_string(),
})
}
Some(None) => Err(RowError::UnexpectedNull(column.to_string())),
None => Err(RowError::ColumnNotFound(column.to_string())),
}
}
fn get_i32_opt(&self, column: &str) -> Result<Option<i32>, RowError> {
match self.data.get(column) {
Some(Some(v)) => {
v.parse()
.map(Some)
.map_err(|e: std::num::ParseIntError| RowError::TypeConversion {
column: column.to_string(),
message: e.to_string(),
})
}
Some(None) | None => Ok(None),
}
}
fn get_i64(&self, column: &str) -> Result<i64, RowError> {
match self.data.get(column) {
Some(Some(v)) => {
v.parse()
.map_err(|e: std::num::ParseIntError| RowError::TypeConversion {
column: column.to_string(),
message: e.to_string(),
})
}
Some(None) => Err(RowError::UnexpectedNull(column.to_string())),
None => Err(RowError::ColumnNotFound(column.to_string())),
}
}
fn get_i64_opt(&self, column: &str) -> Result<Option<i64>, RowError> {
match self.data.get(column) {
Some(Some(v)) => {
v.parse()
.map(Some)
.map_err(|e: std::num::ParseIntError| RowError::TypeConversion {
column: column.to_string(),
message: e.to_string(),
})
}
Some(None) => Ok(None),
None => Err(RowError::ColumnNotFound(column.to_string())),
}
}
fn get_f64(&self, column: &str) -> Result<f64, RowError> {
match self.data.get(column) {
Some(Some(v)) => {
v.parse()
.map_err(|e: std::num::ParseFloatError| RowError::TypeConversion {
column: column.to_string(),
message: e.to_string(),
})
}
Some(None) => Err(RowError::UnexpectedNull(column.to_string())),
None => Err(RowError::ColumnNotFound(column.to_string())),
}
}
fn get_f64_opt(&self, column: &str) -> Result<Option<f64>, RowError> {
match self.data.get(column) {
Some(Some(v)) => v.parse().map(Some).map_err(|e: std::num::ParseFloatError| {
RowError::TypeConversion {
column: column.to_string(),
message: e.to_string(),
}
}),
Some(None) | None => Ok(None),
}
}
fn get_bool(&self, _column: &str) -> Result<bool, RowError> {
unimplemented!("UserTestRow: bool not needed for User")
}
fn get_bool_opt(&self, _column: &str) -> Result<Option<bool>, RowError> {
unimplemented!("UserTestRow: bool_opt not needed for User")
}
fn get_str(&self, column: &str) -> Result<&str, RowError> {
match self.data.get(column) {
Some(Some(v)) => Ok(v.as_str()),
Some(None) => Err(RowError::UnexpectedNull(column.to_string())),
None => Err(RowError::ColumnNotFound(column.to_string())),
}
}
fn get_str_opt(&self, column: &str) -> Result<Option<&str>, RowError> {
match self.data.get(column) {
Some(Some(v)) => Ok(Some(v.as_str())),
Some(None) | None => Ok(None),
}
}
fn get_bytes(&self, _column: &str) -> Result<&[u8], RowError> {
unimplemented!("UserTestRow: bytes not needed for User")
}
fn get_bytes_opt(&self, _column: &str) -> Result<Option<&[u8]>, RowError> {
unimplemented!("UserTestRow: bytes_opt not needed for User")
}
}
#[test]
fn count_field_defaults_to_zero_when_row_missing() {
use prax_query::row::FromRow;
let row = UserTestRow::new()
.set("id", "1")
.set("email", "a@b.c")
.set("first_name", "Alice")
.set("last_name", "Smith")
.set("full_name", "Alice Smith")
.set("search_key", "a@b.c");
let user = User::from_row(&row).expect("from_row must succeed even without aggregate columns");
assert_eq!(
user.post_count, 0,
"missing @count column must default to 0"
);
assert_eq!(
user.total_views, None,
"missing @sum column must default to None"
);
}
#[test]
fn user_create_input_excludes_computed_fields() {
let _ = user::UserCreateInput {
email: "a@b.com".into(),
first_name: "Ada".into(),
last_name: "Lovelace".into(),
};
}
#[test]
fn user_update_input_excludes_computed_fields() {
let _ = user::UserUpdateInput::default();
}
#[test]
fn user_where_input_has_aggregate_filters() {
let _ = user::UserWhereInput {
post_count: Some(prax_query::inputs::BigIntFilter::equals(7)),
total_views: Some(prax_query::inputs::IntNullableFilter {
equals: Some(42),
..Default::default()
}),
..Default::default()
};
}
#[test]
fn user_where_input_has_generated_filters() {
let _ = user::UserWhereInput {
full_name: Some(prax_query::inputs::StringFilter::equals("Ada Lovelace")),
search_key: Some(prax_query::inputs::StringFilter::equals("ada lovelace")),
..Default::default()
};
}
#[test]
fn user_select_input_has_computed_fields() {
let _ = user::UserSelect {
full_name: Some(true),
search_key: Some(true),
post_count: Some(true),
total_views: Some(true),
..Default::default()
};
}
#[test]
fn user_order_by_includes_computed_variants() {
use prax_query::types::SortOrder;
let _ = user::UserOrderBy::FullName(SortOrder::Asc);
let _ = user::UserOrderBy::SearchKey(SortOrder::Desc);
let _ = user::UserOrderBy::PostCount(SortOrder::Asc);
let _ = user::UserOrderBy::TotalViews(SortOrder::Desc);
}
#[test]
fn aggregate_fields_decode_when_row_has_them() {
use prax_query::row::FromRow;
let row = UserTestRow::new()
.set("id", "2")
.set("email", "b@b.c")
.set("first_name", "Bob")
.set("last_name", "Jones")
.set("full_name", "Bob Jones")
.set("search_key", "b@b.c")
.set("post_count", "7")
.set("total_views", "42");
let user = User::from_row(&row).expect("from_row must succeed with aggregate columns present");
assert_eq!(user.post_count, 7, "@count column must decode to i64 value");
assert_eq!(
user.total_views,
Some(42),
"@sum column must decode to Some(value)"
);
}