1use crate::types::{AdminError, AdminResult};
7use async_trait::async_trait;
8use reinhardt_core::macros::injectable;
9use reinhardt_db::migrations::FieldType as DbFieldType;
10use reinhardt_db::orm::execution::convert_values;
11use reinhardt_db::orm::{
12 DatabaseConnection, Filter, FilterCondition, FilterOperator, FilterValue, Model,
13};
14use reinhardt_di::{DiResult, FactoryOutput, Injectable, InjectionContext};
15use reinhardt_query::prelude::{
16 Alias, BinOper, CaseStatement, ColumnRef, Condition, Expr, ExprTrait, IntoValue, Order,
17 PostgresQueryBuilder, Query, QueryStatementBuilder, SimpleExpr, Value,
18};
19use serde::{Deserialize, Serialize};
20use std::collections::HashMap;
21
22const ADMIN_LIST_TOTAL_COUNT_ALIAS: &str = "__reinhardt_total_count";
23const SENSITIVE_FIELDS: &[&str] = &["password_hash", "password_salt"];
24
25fn json_to_sea_value(value: serde_json::Value) -> Value {
31 match value {
32 serde_json::Value::String(s) => {
33 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&s) {
35 Value::ChronoDateTimeUtc(Some(Box::new(dt.with_timezone(&chrono::Utc))))
36 } else if let Ok(dt) =
38 chrono::NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S%.fZ")
39 {
40 Value::ChronoDateTimeUtc(Some(Box::new(dt.and_utc())))
41 } else if s.len() == 10 {
43 if let Ok(d) = chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d") {
44 return Value::ChronoDate(Some(Box::new(d)));
45 }
46 Value::String(Some(Box::new(s)))
47 } else if s.len() == 8 && s.chars().filter(|c| *c == ':').count() == 2 {
49 if let Ok(t) = chrono::NaiveTime::parse_from_str(&s, "%H:%M:%S") {
50 return Value::ChronoTime(Some(Box::new(t)));
51 }
52 Value::String(Some(Box::new(s)))
53 } else if s.len() == 36
55 && s.chars().enumerate().all(|(i, c)| {
56 matches!(i, 8 | 13 | 18 | 23) && c == '-' || c.is_ascii_hexdigit()
57 }) {
58 if let Ok(uuid) = uuid::Uuid::parse_str(&s) {
59 return Value::Uuid(Some(Box::new(uuid)));
60 }
61 Value::String(Some(Box::new(s)))
62 } else {
63 Value::String(Some(Box::new(s)))
64 }
65 }
66 serde_json::Value::Number(n) => {
67 if let Some(i) = n.as_i64() {
68 Value::BigInt(Some(i))
69 } else if let Some(f) = n.as_f64() {
70 Value::Double(Some(f))
71 } else {
72 Value::String(Some(Box::new(n.to_string())))
73 }
74 }
75 serde_json::Value::Bool(b) => Value::Bool(Some(b)),
76 serde_json::Value::Null => Value::Int(None),
77 _ => Value::String(Some(Box::new(value.to_string()))),
78 }
79}
80use std::sync::Arc;
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct AdminRecord {
89 pub id: Option<i64>,
91}
92
93#[derive(Debug, Clone)]
95pub struct AdminRecordFields {
96 pub id: reinhardt_db::orm::query_fields::Field<AdminRecord, Option<i64>>,
98}
99
100impl Default for AdminRecordFields {
101 fn default() -> Self {
102 Self::new()
103 }
104}
105
106impl AdminRecordFields {
107 pub fn new() -> Self {
109 Self {
110 id: reinhardt_db::orm::query_fields::Field::new(vec!["id".to_string()]),
111 }
112 }
113}
114
115impl reinhardt_db::orm::FieldSelector for AdminRecordFields {
116 fn with_alias(mut self, alias: &str) -> Self {
117 self.id = self.id.with_alias(alias);
118 self
119 }
120}
121
122impl Model for AdminRecord {
123 type PrimaryKey = i64;
124 type Fields = AdminRecordFields;
125 type Objects = reinhardt_db::orm::Manager<Self>;
126
127 fn table_name() -> &'static str {
128 "admin_records"
129 }
130
131 fn new_fields() -> Self::Fields {
132 AdminRecordFields::new()
133 }
134
135 fn primary_key(&self) -> Option<Self::PrimaryKey> {
136 self.id
137 }
138
139 fn set_primary_key(&mut self, pk: Self::PrimaryKey) {
140 self.id = Some(pk);
141 }
142}
143
144fn parse_pk_value(table_name: &str, pk_field: &str, id: &str) -> Value {
152 if let Some(field_meta) =
153 crate::server::type_inference::get_field_metadata(table_name, pk_field)
154 {
155 match field_meta.field_type {
156 DbFieldType::Uuid => {
157 if let Ok(uuid) = uuid::Uuid::parse_str(id) {
158 return Value::Uuid(Some(Box::new(uuid)));
159 }
160 }
161 DbFieldType::BigInteger => {
162 if let Ok(num) = id.parse::<i64>() {
163 return Value::BigInt(Some(num));
164 }
165 }
166 DbFieldType::Integer
167 | DbFieldType::SmallInteger
168 | DbFieldType::TinyInt
169 | DbFieldType::MediumInt => {
170 if let Ok(num) = id.parse::<i32>() {
171 return Value::Int(Some(num));
172 }
173 }
174 _ => {}
175 }
176 }
177
178 if let Ok(num_id) = id.parse::<i64>() {
180 Value::BigInt(Some(num_id))
181 } else {
182 Value::String(Some(Box::new(id.to_string())))
183 }
184}
185
186fn parse_pk_values(table_name: &str, pk_field: &str, ids: &[String]) -> Vec<Value> {
188 ids.iter()
189 .map(|id| parse_pk_value(table_name, pk_field, id))
190 .collect()
191}
192
193#[doc(hidden)]
195pub fn filter_value_to_sea_value(v: &FilterValue) -> Value {
196 match v {
197 FilterValue::String(s) => s.clone().into(),
198 FilterValue::Timestamp(value) => (*value).into(),
199 FilterValue::Date(value) => (*value).into(),
200 FilterValue::Time(value) => (*value).into(),
201 FilterValue::NaiveDateTime(value) => (*value).into(),
202 FilterValue::Decimal(value) => (*value).into(),
203 FilterValue::Uuid(value) => (*value).into(),
204 FilterValue::Integer(i) | FilterValue::Int(i) => (*i).into(),
205 FilterValue::Float(f) => (*f).into(),
206 FilterValue::Boolean(b) | FilterValue::Bool(b) => (*b).into(),
207 FilterValue::Null => Value::Int(None),
208 FilterValue::Array(_) => Value::String(None),
212 FilterValue::List(_) | FilterValue::Range(_, _) => Value::String(None),
213 FilterValue::FieldRef(f) => {
214 Value::String(Some(Box::new(f.field.clone())))
218 }
219 FilterValue::Expression(expr) => {
220 Value::String(Some(Box::new(expr.to_sql())))
224 }
225 FilterValue::OuterRef(outer) => {
226 Value::String(Some(Box::new(outer.field.clone())))
230 }
231 }
232}
233
234fn annotation_value_to_safe_expr(
239 val: &reinhardt_db::orm::annotation::AnnotationValue,
240) -> SimpleExpr {
241 use reinhardt_db::orm::annotation::AnnotationValue;
242
243 match val {
244 AnnotationValue::Value(v) => {
245 use reinhardt_db::orm::annotation::Value as AnnotValue;
246 match v {
247 AnnotValue::String(s) => Expr::val(s.as_str()).into(),
248 AnnotValue::Int(i) => Expr::val(*i).into(),
249 AnnotValue::Float(f) => Expr::val(*f).into(),
250 AnnotValue::Bool(b) => Expr::val(*b).into(),
251 AnnotValue::Null => Expr::val(Option::<String>::None).into(),
252 }
253 }
254 AnnotationValue::Field(f) => Expr::col(Alias::new(&f.field)).into(),
255 AnnotationValue::Expression(e) => annotation_expr_to_safe_expr(e),
256 AnnotationValue::Aggregate(a) => aggregate_to_safe_expr(a),
257 AnnotationValue::Subquery(_)
261 | AnnotationValue::ArrayAgg(_)
262 | AnnotationValue::StringAgg(_)
263 | AnnotationValue::JsonbAgg(_)
264 | AnnotationValue::JsonbBuildObject(_)
265 | AnnotationValue::TsRank(_) => Expr::cust(val.to_sql()).into(),
266 }
267}
268
269fn aggregate_to_safe_expr(agg: &reinhardt_db::orm::aggregation::Aggregate) -> SimpleExpr {
275 use reinhardt_db::orm::aggregation::AggregateFunc;
276
277 let func_name = match agg.func {
278 AggregateFunc::Count | AggregateFunc::CountDistinct => "COUNT",
279 AggregateFunc::Sum => "SUM",
280 AggregateFunc::Avg => "AVG",
281 AggregateFunc::Max => "MAX",
282 AggregateFunc::Min => "MIN",
283 };
284
285 if let Some(field) = &agg.field {
286 let col_expr: SimpleExpr = Expr::col(Alias::new(field)).into();
287 let is_distinct = agg.distinct || matches!(agg.func, AggregateFunc::CountDistinct);
288 if is_distinct {
289 Expr::cust_with_values(format!("{func_name}(DISTINCT ?)"), [col_expr]).into()
290 } else {
291 Expr::cust_with_values(format!("{func_name}(?)"), [col_expr]).into()
292 }
293 } else {
294 Expr::cust(format!("{func_name}(*)")).into()
296 }
297}
298
299fn annotation_expr_to_safe_expr(expr: &reinhardt_db::orm::annotation::Expression) -> SimpleExpr {
305 use reinhardt_db::orm::annotation::Expression as AnnotExpr;
306
307 match expr {
308 AnnotExpr::Add(left, right) => {
309 let left_expr = annotation_value_to_safe_expr(left);
310 let right_expr = annotation_value_to_safe_expr(right);
311 Expr::cust_with_values("(? + ?)", [left_expr, right_expr]).into()
312 }
313 AnnotExpr::Subtract(left, right) => {
314 let left_expr = annotation_value_to_safe_expr(left);
315 let right_expr = annotation_value_to_safe_expr(right);
316 Expr::cust_with_values("(? - ?)", [left_expr, right_expr]).into()
317 }
318 AnnotExpr::Multiply(left, right) => {
319 let left_expr = annotation_value_to_safe_expr(left);
320 let right_expr = annotation_value_to_safe_expr(right);
321 Expr::cust_with_values("(? * ?)", [left_expr, right_expr]).into()
322 }
323 AnnotExpr::Divide(left, right) => {
324 let left_expr = annotation_value_to_safe_expr(left);
325 let right_expr = annotation_value_to_safe_expr(right);
326 Expr::cust_with_values("(? / ?)", [left_expr, right_expr]).into()
327 }
328 AnnotExpr::Case { whens, default } => {
329 let mut case = CaseStatement::new();
330 for when in whens {
331 let cond_expr: SimpleExpr = Expr::cust(when.condition.to_sql()).into();
335 let then_expr = annotation_value_to_safe_expr(&when.then);
336 case = case.when(cond_expr, then_expr);
337 }
338 if let Some(default_val) = default {
339 case = case.else_result(annotation_value_to_safe_expr(default_val));
340 }
341 SimpleExpr::from(case)
342 }
343 AnnotExpr::Coalesce(values) => {
344 let exprs: Vec<SimpleExpr> = values.iter().map(annotation_value_to_safe_expr).collect();
345 if exprs.is_empty() {
346 Expr::val(Option::<String>::None).into()
347 } else {
348 let placeholders = vec!["?"; exprs.len()].join(", ");
349 Expr::cust_with_values(format!("COALESCE({placeholders})"), exprs).into()
350 }
351 }
352 }
353}
354
355fn escape_like_pattern(input: &str) -> String {
357 input
358 .replace('\\', "\\\\")
359 .replace('%', "\\%")
360 .replace('_', "\\_")
361}
362
363#[doc(hidden)]
365pub fn build_single_filter_expr(filter: &Filter) -> Option<SimpleExpr> {
366 let col = filter.lhs_expr();
367 let lhs_sql = filter.lhs_sql();
368
369 let expr = match (&filter.operator, &filter.value) {
370 (FilterOperator::Eq, FilterValue::Null) => col.is_null(),
372 (FilterOperator::Ne, FilterValue::Null) => col.is_not_null(),
373 (FilterOperator::IExact, FilterValue::String(s)) => {
374 col.binary(BinOper::ILike, SimpleExpr::from(s.clone()))
375 }
376 (FilterOperator::IExact, v) => col.eq(filter_value_to_sea_value(v)),
377
378 (FilterOperator::Eq, FilterValue::FieldRef(f)) => col.eq(Expr::col(Alias::new(&f.field))),
380 (FilterOperator::Ne, FilterValue::FieldRef(f)) => col.ne(Expr::col(Alias::new(&f.field))),
381 (FilterOperator::Gt, FilterValue::FieldRef(f)) => col.gt(Expr::col(Alias::new(&f.field))),
382 (FilterOperator::Gte, FilterValue::FieldRef(f)) => col.gte(Expr::col(Alias::new(&f.field))),
383 (FilterOperator::Lt, FilterValue::FieldRef(f)) => col.lt(Expr::col(Alias::new(&f.field))),
384 (FilterOperator::Lte, FilterValue::FieldRef(f)) => col.lte(Expr::col(Alias::new(&f.field))),
385
386 (FilterOperator::Eq, FilterValue::OuterRef(outer)) => {
388 col.eq(Expr::col(Alias::new(&outer.field)))
389 }
390 (FilterOperator::Ne, FilterValue::OuterRef(outer)) => {
391 col.ne(Expr::col(Alias::new(&outer.field)))
392 }
393 (FilterOperator::Gt, FilterValue::OuterRef(outer)) => {
394 col.gt(Expr::col(Alias::new(&outer.field)))
395 }
396 (FilterOperator::Gte, FilterValue::OuterRef(outer)) => {
397 col.gte(Expr::col(Alias::new(&outer.field)))
398 }
399 (FilterOperator::Lt, FilterValue::OuterRef(outer)) => {
400 col.lt(Expr::col(Alias::new(&outer.field)))
401 }
402 (FilterOperator::Lte, FilterValue::OuterRef(outer)) => {
403 col.lte(Expr::col(Alias::new(&outer.field)))
404 }
405
406 (FilterOperator::Eq, FilterValue::Expression(expr)) => {
408 col.eq(annotation_expr_to_safe_expr(expr))
409 }
410 (FilterOperator::Ne, FilterValue::Expression(expr)) => {
411 col.ne(annotation_expr_to_safe_expr(expr))
412 }
413 (FilterOperator::Gt, FilterValue::Expression(expr)) => {
414 col.gt(annotation_expr_to_safe_expr(expr))
415 }
416 (FilterOperator::Gte, FilterValue::Expression(expr)) => {
417 col.gte(annotation_expr_to_safe_expr(expr))
418 }
419 (FilterOperator::Lt, FilterValue::Expression(expr)) => {
420 col.lt(annotation_expr_to_safe_expr(expr))
421 }
422 (FilterOperator::Lte, FilterValue::Expression(expr)) => {
423 col.lte(annotation_expr_to_safe_expr(expr))
424 }
425
426 (FilterOperator::Eq, v) => col.eq(filter_value_to_sea_value(v)),
428 (FilterOperator::Ne, v) => col.ne(filter_value_to_sea_value(v)),
429 (FilterOperator::Gt, v) => col.gt(filter_value_to_sea_value(v)),
430 (FilterOperator::Gte, v) => col.gte(filter_value_to_sea_value(v)),
431 (FilterOperator::Lt, v) => col.lt(filter_value_to_sea_value(v)),
432 (FilterOperator::Lte, v) => col.lte(filter_value_to_sea_value(v)),
433
434 (FilterOperator::Contains, FilterValue::String(s)) => {
436 col.like(format!("%{}%", escape_like_pattern(s)))
437 }
438 (FilterOperator::IContains, FilterValue::String(s)) => col.binary(
439 BinOper::ILike,
440 SimpleExpr::from(format!("%{}%", escape_like_pattern(s))),
441 ),
442 (FilterOperator::StartsWith, FilterValue::String(s)) => {
443 col.like(format!("{}%", escape_like_pattern(s)))
444 }
445 (FilterOperator::IStartsWith, FilterValue::String(s)) => col.binary(
446 BinOper::ILike,
447 SimpleExpr::from(format!("{}%", escape_like_pattern(s))),
448 ),
449 (FilterOperator::EndsWith, FilterValue::String(s)) => {
450 col.like(format!("%{}", escape_like_pattern(s)))
451 }
452 (FilterOperator::IEndsWith, FilterValue::String(s)) => col.binary(
453 BinOper::ILike,
454 SimpleExpr::from(format!("%{}", escape_like_pattern(s))),
455 ),
456 (FilterOperator::Regex, FilterValue::String(pattern)) => {
457 Expr::cust_with_values(format!("{} ~ ?", lhs_sql), [pattern.clone()]).into()
458 }
459 (FilterOperator::IRegex, FilterValue::String(pattern)) => {
460 Expr::cust_with_values(format!("{} ~* ?", lhs_sql), [pattern.clone()]).into()
461 }
462 (FilterOperator::Range, FilterValue::Range(start, end)) => Expr::cust_with_values(
463 format!("{} BETWEEN ? AND ?", lhs_sql),
464 [
465 filter_value_to_sea_value(start),
466 filter_value_to_sea_value(end),
467 ],
468 )
469 .into(),
470 (FilterOperator::In, FilterValue::Array(arr)) => {
472 if arr.is_empty() {
473 return None;
474 }
475 let values: Vec<Value> = arr.iter().map(|v| v.as_str().into_value()).collect();
476 col.is_in(values)
477 }
478 (FilterOperator::NotIn, FilterValue::Array(arr)) => {
479 if arr.is_empty() {
480 return None;
481 }
482 let values: Vec<Value> = arr.iter().map(|v| v.as_str().into_value()).collect();
483 col.is_not_in(values)
484 }
485 (FilterOperator::In, FilterValue::List(values)) => {
486 if values.is_empty() {
487 return None;
488 }
489 col.is_in(
490 values
491 .iter()
492 .map(filter_value_to_sea_value)
493 .collect::<Vec<_>>(),
494 )
495 }
496 (FilterOperator::NotIn, FilterValue::List(values)) => {
497 if values.is_empty() {
498 return None;
499 }
500 col.is_not_in(
501 values
502 .iter()
503 .map(filter_value_to_sea_value)
504 .collect::<Vec<_>>(),
505 )
506 }
507
508 (FilterOperator::In, FilterValue::String(s)) => {
509 let values: Vec<Value> = s.split(',').map(|v| v.trim().into_value()).collect();
510 col.is_in(values)
511 }
512 (FilterOperator::NotIn, FilterValue::String(s)) => {
513 let values: Vec<Value> = s.split(',').map(|v| v.trim().into_value()).collect();
514 col.is_not_in(values)
515 }
516
517 _ => return None,
519 };
520
521 Some(expr)
522}
523
524#[doc(hidden)]
526pub fn build_filter_condition(filters: &[Filter]) -> Option<Condition> {
527 if filters.is_empty() {
528 return None;
529 }
530
531 let mut condition = Condition::all();
532 let mut added = false;
533
534 for filter in filters {
535 if let Some(expr) = build_single_filter_expr(filter) {
536 condition = condition.add(expr);
537 added = true;
538 }
539 }
540
541 if added { Some(condition) } else { None }
542}
543
544#[doc(hidden)]
546pub const MAX_FILTER_DEPTH: usize = 100;
547
548#[doc(hidden)]
559pub fn build_composite_filter_condition(
560 filter_condition: &FilterCondition,
561) -> AdminResult<Option<Condition>> {
562 build_composite_filter_condition_with_depth(filter_condition, 0)
563}
564
565#[doc(hidden)]
567pub fn build_composite_filter_condition_with_depth(
568 filter_condition: &FilterCondition,
569 depth: usize,
570) -> AdminResult<Option<Condition>> {
571 if depth >= MAX_FILTER_DEPTH {
573 return Err(AdminError::ValidationError(format!(
574 "Filter condition exceeded maximum depth of {} levels",
575 MAX_FILTER_DEPTH
576 )));
577 }
578
579 match filter_condition {
580 FilterCondition::Single(filter) => {
581 Ok(build_single_filter_expr(filter).map(|expr| Condition::all().add(expr)))
582 }
583 FilterCondition::And(conditions) => {
584 if conditions.is_empty() {
585 return Ok(None);
586 }
587 let mut and_condition = Condition::all();
588 let mut added = false;
589 for cond in conditions {
590 if let Some(sub_cond) =
591 build_composite_filter_condition_with_depth(cond, depth + 1)?
592 {
593 and_condition = and_condition.add(sub_cond);
594 added = true;
595 }
596 }
597 if added {
600 Ok(Some(and_condition))
601 } else {
602 Ok(None)
603 }
604 }
605 FilterCondition::Or(conditions) => {
606 if conditions.is_empty() {
607 return Ok(None);
608 }
609 let mut or_condition = Condition::any();
610 let mut added = false;
611 for cond in conditions {
612 if let Some(sub_cond) =
613 build_composite_filter_condition_with_depth(cond, depth + 1)?
614 {
615 or_condition = or_condition.add(sub_cond);
616 added = true;
617 }
618 }
619 if added {
622 Ok(Some(or_condition))
623 } else {
624 Ok(None)
625 }
626 }
627 FilterCondition::Not(inner) => Ok(build_composite_filter_condition_with_depth(
628 inner,
629 depth + 1,
630 )?
631 .map(|inner_cond| inner_cond.not())),
632 }
633}
634
635fn build_combined_filter_condition(
636 filter_condition: Option<&FilterCondition>,
637 additional_filters: &[Filter],
638) -> AdminResult<(Condition, bool)> {
639 let mut combined = Condition::all();
640
641 if let Some(fc) = filter_condition
642 && let Some(cond) = build_composite_filter_condition(fc)?
643 {
644 combined = combined.add(cond);
645 }
646
647 if let Some(simple_cond) = build_filter_condition(additional_filters) {
648 combined = combined.add(simple_cond);
649 }
650
651 Ok((
652 combined,
653 !additional_filters.is_empty() || filter_condition.is_some(),
654 ))
655}
656
657fn extract_admin_list_total_count(
658 map: &serde_json::Map<String, serde_json::Value>,
659) -> AdminResult<u64> {
660 let count_value = map.get(ADMIN_LIST_TOTAL_COUNT_ALIAS).ok_or_else(|| {
661 AdminError::DatabaseError(format!(
662 "Admin list query result missing '{}' key",
663 ADMIN_LIST_TOTAL_COUNT_ALIAS
664 ))
665 })?;
666
667 if let Some(count) = count_value.as_u64() {
668 return Ok(count);
669 }
670
671 count_value
672 .as_i64()
673 .and_then(|count| if count >= 0 { Some(count as u64) } else { None })
674 .ok_or_else(|| {
675 AdminError::DatabaseError(format!(
676 "Admin list query returned invalid total count: {}",
677 count_value
678 ))
679 })
680}
681
682#[injectable(scope = Singleton, prebuilt = true)]
702#[derive(Clone)]
703pub struct AdminDatabase {
704 connection: Arc<DatabaseConnection>,
705}
706
707#[reinhardt_di::injectable_key]
709pub struct AdminDatabaseKey;
710
711impl AdminDatabase {
712 pub fn new(connection: DatabaseConnection) -> Self {
717 Self {
718 connection: Arc::new(connection),
719 }
720 }
721
722 pub fn from_arc(connection: Arc<DatabaseConnection>) -> Self {
727 Self { connection }
728 }
729
730 pub fn connection(&self) -> &DatabaseConnection {
732 &self.connection
733 }
734
735 pub fn connection_arc(&self) -> Arc<DatabaseConnection> {
739 Arc::clone(&self.connection)
740 }
741
742 pub async fn list<M: Model>(
763 &self,
764 table_name: &str,
765 filters: Vec<Filter>,
766 offset: u64,
767 limit: u64,
768 ) -> AdminResult<Vec<HashMap<String, serde_json::Value>>> {
769 let mut query = Query::select()
774 .from(Alias::new(table_name))
775 .column(ColumnRef::Asterisk)
776 .to_owned();
777
778 if let Some(condition) = build_filter_condition(&filters) {
780 query.cond_where(condition);
781 }
782
783 query.limit(limit).offset(offset);
785
786 let (sql, values) = query.build(PostgresQueryBuilder);
788 let params = convert_values(values);
789 let rows = self
790 .connection
791 .query(&sql, params)
792 .await
793 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
794
795 Ok(rows
797 .into_iter()
798 .filter_map(|row| {
799 if let serde_json::Value::Object(map) = row.data {
801 Some(
802 map.into_iter()
803 .collect::<HashMap<String, serde_json::Value>>(),
804 )
805 } else {
806 None
807 }
808 })
809 .collect())
810 }
811
812 pub async fn list_with_condition<M: Model>(
826 &self,
827 table_name: &str,
828 filter_condition: Option<&FilterCondition>,
829 additional_filters: Vec<Filter>,
830 sort_by: Option<&str>,
831 offset: u64,
832 limit: u64,
833 ) -> AdminResult<Vec<HashMap<String, serde_json::Value>>> {
834 let mut query = Query::select()
839 .from(Alias::new(table_name))
840 .column(ColumnRef::Asterisk)
841 .to_owned();
842
843 let (combined, has_filter) =
844 build_combined_filter_condition(filter_condition, &additional_filters)?;
845
846 if has_filter {
847 query.cond_where(combined);
848 }
849
850 if let Some(sort_str) = sort_by {
852 let (field, is_desc) = if let Some(stripped) = sort_str.strip_prefix('-') {
853 (stripped, true)
854 } else {
855 (sort_str, false)
856 };
857
858 let col = Alias::new(field);
859 if is_desc {
860 query.order_by(col, Order::Desc);
861 } else {
862 query.order_by(col, Order::Asc);
863 }
864 }
865
866 query.limit(limit).offset(offset);
868
869 let (sql, values) = query.build(PostgresQueryBuilder);
871 let params = convert_values(values);
872 let rows = self
873 .connection
874 .query(&sql, params)
875 .await
876 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
877
878 Ok(rows
880 .into_iter()
881 .filter_map(|row| {
882 if let serde_json::Value::Object(map) = row.data {
883 Some(
884 map.into_iter()
885 .filter(|(key, _)| !SENSITIVE_FIELDS.contains(&key.as_str()))
886 .collect::<HashMap<String, serde_json::Value>>(),
887 )
888 } else {
889 None
890 }
891 })
892 .collect())
893 }
894
895 pub async fn list_with_condition_and_count<M: Model>(
901 &self,
902 table_name: &str,
903 filter_condition: Option<&FilterCondition>,
904 additional_filters: Vec<Filter>,
905 sort_by: Option<&str>,
906 offset: u64,
907 limit: u64,
908 ) -> AdminResult<(Vec<HashMap<String, serde_json::Value>>, u64)> {
909 let mut query = Query::select()
913 .from(Alias::new(table_name))
914 .column(ColumnRef::Asterisk)
915 .expr_as(
916 Expr::cust("COUNT(*) OVER()"),
917 Alias::new(ADMIN_LIST_TOTAL_COUNT_ALIAS),
918 )
919 .to_owned();
920
921 let (combined, has_filter) =
922 build_combined_filter_condition(filter_condition, &additional_filters)?;
923
924 if has_filter {
925 query.cond_where(combined);
926 }
927
928 if let Some(sort_str) = sort_by {
929 let (field, is_desc) = if let Some(stripped) = sort_str.strip_prefix('-') {
930 (stripped, true)
931 } else {
932 (sort_str, false)
933 };
934
935 let col = Alias::new(field);
936 if is_desc {
937 query.order_by(col, Order::Desc);
938 } else {
939 query.order_by(col, Order::Asc);
940 }
941 }
942
943 query.limit(limit).offset(offset);
944
945 let (sql, values) = query.build(PostgresQueryBuilder);
946 let params = convert_values(values);
947 let rows = self
948 .connection
949 .query(&sql, params)
950 .await
951 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
952
953 if rows.is_empty() {
954 if offset == 0 && limit > 0 {
955 return Ok((Vec::new(), 0));
956 }
957
958 let count = self
959 .count_with_condition::<M>(table_name, filter_condition, additional_filters)
960 .await?;
961 return Ok((Vec::new(), count));
962 }
963
964 let mut total_count = None;
965 let results = rows
966 .into_iter()
967 .filter_map(|row| {
968 if let serde_json::Value::Object(mut map) = row.data {
969 if total_count.is_none() {
970 total_count = Some(extract_admin_list_total_count(&map));
971 }
972
973 map.remove(ADMIN_LIST_TOTAL_COUNT_ALIAS);
974
975 Some(
976 map.into_iter()
977 .filter(|(key, _)| !SENSITIVE_FIELDS.contains(&key.as_str()))
978 .collect::<HashMap<String, serde_json::Value>>(),
979 )
980 } else {
981 None
982 }
983 })
984 .collect::<Vec<_>>();
985
986 let total_count = total_count.unwrap_or_else(|| {
987 Err(AdminError::DatabaseError(
988 "Admin list query returned no object rows".to_string(),
989 ))
990 })?;
991
992 Ok((results, total_count))
993 }
994
995 pub async fn count_with_condition<M: Model>(
1003 &self,
1004 table_name: &str,
1005 filter_condition: Option<&FilterCondition>,
1006 additional_filters: Vec<Filter>,
1007 ) -> AdminResult<u64> {
1008 let mut query = Query::select()
1009 .from(Alias::new(table_name))
1010 .expr(Expr::cust("COUNT(*) AS count"))
1011 .to_owned();
1012
1013 let (combined, has_filter) =
1014 build_combined_filter_condition(filter_condition, &additional_filters)?;
1015
1016 if has_filter {
1017 query.cond_where(combined);
1018 }
1019
1020 let (sql, values) = query.build(PostgresQueryBuilder);
1021 let params = convert_values(values);
1022 let row = self
1023 .connection
1024 .query_one(&sql, params)
1025 .await
1026 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1027
1028 let count = extract_count_from_row(&row.data)?;
1030
1031 Ok(count)
1032 }
1033
1034 pub async fn get<M: Model>(
1051 &self,
1052 table_name: &str,
1053 pk_field: &str,
1054 id: &str,
1055 ) -> AdminResult<Option<HashMap<String, serde_json::Value>>> {
1056 let pk_value = parse_pk_value(table_name, pk_field, id);
1057
1058 let query = Query::select()
1062 .from(Alias::new(table_name))
1063 .column(ColumnRef::Asterisk)
1064 .and_where(Expr::col(Alias::new(pk_field)).eq(pk_value))
1065 .to_owned();
1066
1067 let (sql, values) = query.build(PostgresQueryBuilder);
1068 let params = convert_values(values);
1069 let row = self
1070 .connection
1071 .query_optional(&sql, params)
1072 .await
1073 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1074
1075 Ok(row.and_then(|r| {
1076 if let serde_json::Value::Object(map) = r.data {
1078 Some(
1079 map.into_iter()
1080 .collect::<HashMap<String, serde_json::Value>>(),
1081 )
1082 } else {
1083 None
1084 }
1085 }))
1086 }
1087
1088 pub async fn create<M: Model>(
1110 &self,
1111 table_name: &str,
1112 pk_field: Option<&str>,
1113 data: HashMap<String, serde_json::Value>,
1114 ) -> AdminResult<u64> {
1115 let pk_field = pk_field.unwrap_or("id");
1116 let mut query = Query::insert()
1117 .into_table(Alias::new(table_name))
1118 .to_owned();
1119
1120 let mut sorted_keys: Vec<String> = data.keys().cloned().collect();
1124 sorted_keys.sort();
1125
1126 let mut columns = Vec::new();
1128 let mut values = Vec::new();
1129
1130 for key in sorted_keys {
1131 let value = data.get(&key).cloned().unwrap_or(serde_json::Value::Null);
1132 columns.push(Alias::new(&key));
1133
1134 let sea_value = json_to_sea_value(value);
1135 values.push(sea_value);
1136 }
1137
1138 query.columns(columns).values(values).map_err(|e| {
1140 AdminError::DatabaseError(format!("column/value count mismatch: {}", e))
1141 })?;
1142
1143 query.returning([Alias::new(pk_field)]);
1145
1146 let (sql, values) = query.build(PostgresQueryBuilder);
1147 let params = convert_values(values);
1148 let row = self
1149 .connection
1150 .query_one(&sql, params)
1151 .await
1152 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1153
1154 match row.data.get(pk_field) {
1156 Some(serde_json::Value::Number(n)) => n.as_u64().ok_or_else(|| {
1157 AdminError::DatabaseError(format!(
1158 "RETURNING clause for '{}' returned non-unsigned-integer: {}",
1159 pk_field, n
1160 ))
1161 }),
1162 Some(serde_json::Value::String(_)) => {
1163 Ok(1)
1166 }
1167 _ => Err(AdminError::DatabaseError(format!(
1168 "RETURNING clause did not return expected primary key field '{}'",
1169 pk_field
1170 ))),
1171 }
1172 }
1173
1174 pub async fn update<M: Model>(
1195 &self,
1196 table_name: &str,
1197 pk_field: &str,
1198 id: &str,
1199 data: HashMap<String, serde_json::Value>,
1200 ) -> AdminResult<u64> {
1201 let mut query = Query::update().table(Alias::new(table_name)).to_owned();
1202
1203 let mut sorted_keys: Vec<String> = data.keys().cloned().collect();
1205 sorted_keys.sort();
1206
1207 for key in sorted_keys {
1209 let value = data.get(&key).cloned().unwrap_or(serde_json::Value::Null);
1210 let sea_value = json_to_sea_value(value);
1211 query.value(Alias::new(&key), sea_value);
1212 }
1213
1214 let pk_value = parse_pk_value(table_name, pk_field, id);
1215 query.and_where(Expr::col(Alias::new(pk_field)).eq(pk_value));
1216
1217 let (sql, values) = query.build(PostgresQueryBuilder);
1218 let params = convert_values(values);
1219 let affected = self
1220 .connection
1221 .execute(&sql, params)
1222 .await
1223 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1224
1225 Ok(affected)
1226 }
1227
1228 pub async fn delete<M: Model>(
1245 &self,
1246 table_name: &str,
1247 pk_field: &str,
1248 id: &str,
1249 ) -> AdminResult<u64> {
1250 let pk_value = parse_pk_value(table_name, pk_field, id);
1251
1252 let query = Query::delete()
1253 .from_table(Alias::new(table_name))
1254 .and_where(Expr::col(Alias::new(pk_field)).eq(pk_value))
1255 .to_owned();
1256
1257 let (sql, values) = query.build(PostgresQueryBuilder);
1258 let params = convert_values(values);
1259 let affected = self
1260 .connection
1261 .execute(&sql, params)
1262 .await
1263 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1264
1265 Ok(affected)
1266 }
1267
1268 pub async fn bulk_delete<M: Model>(
1286 &self,
1287 table_name: &str,
1288 pk_field: &str,
1289 ids: Vec<String>,
1290 ) -> AdminResult<u64> {
1291 self.bulk_delete_by_table(table_name, pk_field, ids).await
1292 }
1293
1294 pub async fn bulk_delete_by_table(
1316 &self,
1317 table_name: &str,
1318 pk_field: &str,
1319 ids: Vec<String>,
1320 ) -> AdminResult<u64> {
1321 if ids.is_empty() {
1322 return Ok(0);
1323 }
1324
1325 let pk_values = parse_pk_values(table_name, pk_field, &ids);
1326
1327 let query = Query::delete()
1328 .from_table(Alias::new(table_name))
1329 .and_where(Expr::col(Alias::new(pk_field)).is_in(pk_values))
1330 .to_owned();
1331
1332 let (sql, values) = query.build(PostgresQueryBuilder);
1333 let params = convert_values(values);
1334 let affected = self
1335 .connection
1336 .execute(&sql, params)
1337 .await
1338 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1339
1340 Ok(affected)
1341 }
1342
1343 pub async fn count<M: Model>(
1364 &self,
1365 table_name: &str,
1366 filters: Vec<Filter>,
1367 ) -> AdminResult<u64> {
1368 let mut query = Query::select()
1369 .from(Alias::new(table_name))
1370 .expr(Expr::cust("COUNT(*) AS count"))
1371 .to_owned();
1372
1373 if let Some(condition) = build_filter_condition(&filters) {
1375 query.cond_where(condition);
1376 }
1377
1378 let (sql, values) = query.build(PostgresQueryBuilder);
1379 let params = convert_values(values);
1380 let row = self
1381 .connection
1382 .query_one(&sql, params)
1383 .await
1384 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1385
1386 let count = extract_count_from_row(&row.data)?;
1388
1389 Ok(count)
1390 }
1391}
1392
1393#[doc(hidden)]
1403pub fn extract_count_from_row(data: &serde_json::Value) -> AdminResult<u64> {
1404 if let Some(count_value) = data.get("count") {
1405 return count_value.as_i64().map(|v| v as u64).ok_or_else(|| {
1406 AdminError::DatabaseError(format!(
1407 "COUNT query returned non-integer value: {}",
1408 count_value
1409 ))
1410 });
1411 }
1412
1413 if let Some(obj) = data.as_object() {
1416 let available_keys: Vec<&String> = obj.keys().collect();
1417 return Err(AdminError::DatabaseError(format!(
1418 "COUNT query result missing 'count' key, available keys: {:?}",
1419 available_keys
1420 )));
1421 }
1422
1423 Err(AdminError::DatabaseError(format!(
1424 "COUNT query returned unexpected data format: {}",
1425 data
1426 )))
1427}
1428
1429#[async_trait]
1441impl Injectable for AdminDatabase {
1442 async fn inject(ctx: &InjectionContext) -> DiResult<Self> {
1443 if let Some(db) = ctx.get_singleton::<Self>() {
1445 return Ok((*db).clone());
1446 }
1447
1448 let conn = ctx.get_singleton::<DatabaseConnection>().ok_or_else(|| {
1450 reinhardt_di::DiError::NotRegistered {
1451 type_name: "AdminDatabase".into(),
1452 hint: "DatabaseConnection must be registered as a singleton. \
1453 Use InjectionContextBuilder::singleton(db_connection) during setup."
1454 .into(),
1455 }
1456 })?;
1457
1458 let db = AdminDatabase::from_arc(conn);
1459 ctx.set_singleton(db.clone());
1461 Ok(db)
1462 }
1463}
1464
1465#[reinhardt_di::injectable(scope = "singleton")]
1466async fn admin_database_provider(
1467 #[inject] db: AdminDatabase,
1468) -> FactoryOutput<AdminDatabaseKey, AdminDatabase> {
1469 FactoryOutput::new(db)
1470}
1471
1472fn __register_admin_database(registry: &reinhardt_di::DependencyRegistry) {
1476 registry.register::<AdminDatabase>(
1477 reinhardt_di::DependencyScope::Singleton,
1478 reinhardt_di::InjectableFactory::<AdminDatabase>::new(),
1479 );
1480}
1481
1482reinhardt_di::inventory::submit! {
1483 reinhardt_di::InjectableRegistration::new(
1484 __register_admin_database
1485 )
1486}
1487
1488#[cfg(all(test, server))]
1489mod tests {
1490 use super::*;
1491 use reinhardt_db::orm::annotation::Expression;
1492 use reinhardt_db::orm::expressions::{F, OuterRef};
1493 use rstest::rstest;
1494
1495 #[rstest]
1498 fn test_escape_like_pattern_percent() {
1499 let input = "100%";
1501
1502 let result = escape_like_pattern(input);
1504
1505 assert_eq!(result, "100\\%");
1507 }
1508
1509 #[rstest]
1510 fn test_escape_like_pattern_underscore() {
1511 let input = "user_name";
1513
1514 let result = escape_like_pattern(input);
1516
1517 assert_eq!(result, "user\\_name");
1519 }
1520
1521 #[rstest]
1522 fn test_escape_like_pattern_backslash() {
1523 let input = "path\\to";
1525
1526 let result = escape_like_pattern(input);
1528
1529 assert_eq!(result, "path\\\\to");
1531 }
1532
1533 #[rstest]
1534 fn test_escape_like_pattern_combined() {
1535 let input = "100%_done";
1537
1538 let result = escape_like_pattern(input);
1540
1541 assert_eq!(result, "100\\%\\_done");
1543 }
1544
1545 #[rstest]
1546 fn test_escape_like_pattern_no_special_chars() {
1547 let input = "normal text";
1549
1550 let result = escape_like_pattern(input);
1552
1553 assert_eq!(result, "normal text");
1555 }
1556
1557 #[rstest]
1563 #[case("%wildcard%", "\\%wildcard\\%")]
1564 #[case("under_score", "under\\_score")]
1565 #[case("back\\slash", "back\\\\slash")]
1566 #[case("%_%", "\\%\\_\\%")]
1567 fn test_escape_like_pattern_sanitizes_special_chars(
1568 #[case] input: &str,
1569 #[case] expected: &str,
1570 ) {
1571 let escaped = escape_like_pattern(input);
1574 assert_eq!(
1576 escaped, expected,
1577 "input={input:?} was not correctly escaped"
1578 );
1579 }
1580
1581 #[test]
1584 fn test_build_composite_single_condition() {
1585 let filter = Filter::new(
1586 "name".to_string(),
1587 FilterOperator::Eq,
1588 FilterValue::String("Alice".to_string()),
1589 );
1590 let condition = FilterCondition::Single(filter);
1591
1592 let result = build_composite_filter_condition(&condition);
1593
1594 assert!(result.is_ok());
1595 let result = result.unwrap();
1596 assert!(result.is_some());
1597 let cond = result.unwrap();
1599 let query = Query::select()
1600 .from(Alias::new("users"))
1601 .column(ColumnRef::Asterisk)
1602 .cond_where(cond)
1603 .to_string(PostgresQueryBuilder);
1604 assert!(query.contains("\"name\""));
1605 assert!(query.contains("'Alice'"));
1606 }
1607
1608 #[test]
1609 fn test_build_composite_or_condition() {
1610 let filter1 = Filter::new(
1611 "name".to_string(),
1612 FilterOperator::Contains,
1613 FilterValue::String("Alice".to_string()),
1614 );
1615 let filter2 = Filter::new(
1616 "email".to_string(),
1617 FilterOperator::Contains,
1618 FilterValue::String("alice".to_string()),
1619 );
1620
1621 let condition = FilterCondition::Or(vec![
1622 FilterCondition::Single(filter1),
1623 FilterCondition::Single(filter2),
1624 ]);
1625
1626 let result = build_composite_filter_condition(&condition);
1627
1628 assert!(result.is_ok());
1629 let result = result.unwrap();
1630 assert!(result.is_some());
1631 let cond = result.unwrap();
1632 let query = Query::select()
1633 .from(Alias::new("users"))
1634 .column(ColumnRef::Asterisk)
1635 .cond_where(cond)
1636 .to_string(PostgresQueryBuilder);
1637 assert!(query.contains("\"name\""));
1639 assert!(query.contains("\"email\""));
1640 assert!(query.contains("OR"));
1641 }
1642
1643 #[test]
1644 fn test_build_composite_and_condition() {
1645 let filter1 = Filter::new(
1646 "is_active".to_string(),
1647 FilterOperator::Eq,
1648 FilterValue::Boolean(true),
1649 );
1650 let filter2 = Filter::new(
1651 "is_staff".to_string(),
1652 FilterOperator::Eq,
1653 FilterValue::Boolean(true),
1654 );
1655
1656 let condition = FilterCondition::And(vec![
1657 FilterCondition::Single(filter1),
1658 FilterCondition::Single(filter2),
1659 ]);
1660
1661 let result = build_composite_filter_condition(&condition);
1662
1663 assert!(result.is_ok());
1664 let result = result.unwrap();
1665 assert!(result.is_some());
1666 let cond = result.unwrap();
1667 let query = Query::select()
1668 .from(Alias::new("users"))
1669 .column(ColumnRef::Asterisk)
1670 .cond_where(cond)
1671 .to_string(PostgresQueryBuilder);
1672 assert!(query.contains("\"is_active\""));
1674 assert!(query.contains("\"is_staff\""));
1675 assert!(query.contains("AND"));
1676 }
1677
1678 #[test]
1679 fn test_build_composite_nested_condition() {
1680 let filter_name = Filter::new(
1682 "name".to_string(),
1683 FilterOperator::Contains,
1684 FilterValue::String("Alice".to_string()),
1685 );
1686 let filter_email = Filter::new(
1687 "email".to_string(),
1688 FilterOperator::Contains,
1689 FilterValue::String("alice".to_string()),
1690 );
1691 let filter_active = Filter::new(
1692 "is_active".to_string(),
1693 FilterOperator::Eq,
1694 FilterValue::Boolean(true),
1695 );
1696
1697 let or_condition = FilterCondition::Or(vec![
1698 FilterCondition::Single(filter_name),
1699 FilterCondition::Single(filter_email),
1700 ]);
1701
1702 let and_condition =
1703 FilterCondition::And(vec![or_condition, FilterCondition::Single(filter_active)]);
1704
1705 let result = build_composite_filter_condition(&and_condition);
1706
1707 assert!(result.is_ok());
1708 let result = result.unwrap();
1709 assert!(result.is_some());
1710 let cond = result.unwrap();
1711 let query = Query::select()
1712 .from(Alias::new("users"))
1713 .column(ColumnRef::Asterisk)
1714 .cond_where(cond)
1715 .to_string(PostgresQueryBuilder);
1716 assert!(query.contains("\"name\""));
1718 assert!(query.contains("\"email\""));
1719 assert!(query.contains("\"is_active\""));
1720 assert!(query.contains("OR"));
1721 assert!(query.contains("AND"));
1722 }
1723
1724 #[test]
1725 fn test_build_composite_empty_or() {
1726 let condition = FilterCondition::Or(vec![]);
1727
1728 let result = build_composite_filter_condition(&condition);
1729
1730 assert!(result.is_ok());
1732 assert!(result.unwrap().is_none());
1733 }
1734
1735 #[test]
1736 fn test_build_composite_empty_and() {
1737 let condition = FilterCondition::And(vec![]);
1738
1739 let result = build_composite_filter_condition(&condition);
1740
1741 assert!(result.is_ok());
1743 assert!(result.unwrap().is_none());
1744 }
1745
1746 #[test]
1747 fn test_build_composite_depth_overflow_returns_error() {
1748 let base_filter = Filter::new(
1750 "name".to_string(),
1751 FilterOperator::Eq,
1752 FilterValue::String("Alice".to_string()),
1753 );
1754 let mut condition = FilterCondition::Single(base_filter);
1755 for _ in 0..=MAX_FILTER_DEPTH {
1757 condition = FilterCondition::And(vec![condition]);
1758 }
1759
1760 let result = build_composite_filter_condition(&condition);
1761
1762 assert!(result.is_err());
1763 let err = result.unwrap_err();
1764 assert!(matches!(err, AdminError::ValidationError(_)));
1765 let err_msg = err.to_string();
1766 assert!(
1767 err_msg.contains("exceeded maximum depth"),
1768 "Error message should mention exceeded depth, got: {}",
1769 err_msg
1770 );
1771 }
1772
1773 #[test]
1776 fn test_build_single_filter_expr_field_ref_eq() {
1777 let filter = Filter::new(
1778 "price".to_string(),
1779 FilterOperator::Eq,
1780 FilterValue::FieldRef(F::new("discount_price")),
1781 );
1782 let result = build_single_filter_expr(&filter);
1783 assert!(result.is_some());
1784
1785 let query = Query::select()
1786 .from(Alias::new("products"))
1787 .column(ColumnRef::Asterisk)
1788 .cond_where(Condition::all().add(result.unwrap()))
1789 .to_string(PostgresQueryBuilder);
1790 assert!(query.contains("\"price\""));
1791 assert!(query.contains("\"discount_price\""));
1792 }
1793
1794 #[test]
1795 fn test_build_single_filter_expr_field_ref_gt() {
1796 let filter = Filter::new(
1797 "price".to_string(),
1798 FilterOperator::Gt,
1799 FilterValue::FieldRef(F::new("cost")),
1800 );
1801 let result = build_single_filter_expr(&filter);
1802 assert!(result.is_some());
1803 }
1804
1805 #[test]
1806 fn test_build_single_filter_expr_field_ref_all_operators() {
1807 let operators = [
1808 FilterOperator::Eq,
1809 FilterOperator::Ne,
1810 FilterOperator::Gt,
1811 FilterOperator::Gte,
1812 FilterOperator::Lt,
1813 FilterOperator::Lte,
1814 ];
1815
1816 for op in operators {
1817 let filter = Filter::new(
1818 "field_a".to_string(),
1819 op.clone(),
1820 FilterValue::FieldRef(F::new("field_b")),
1821 );
1822 let result = build_single_filter_expr(&filter);
1823 assert!(
1824 result.is_some(),
1825 "FieldRef with {:?} should produce Some",
1826 op
1827 );
1828 }
1829 }
1830
1831 #[test]
1832 fn test_build_single_filter_expr_outer_ref() {
1833 let filter = Filter::new(
1834 "author_id".to_string(),
1835 FilterOperator::Eq,
1836 FilterValue::OuterRef(OuterRef::new("authors.id")),
1837 );
1838 let result = build_single_filter_expr(&filter);
1839 assert!(result.is_some());
1840
1841 let query = Query::select()
1842 .from(Alias::new("books"))
1843 .column(ColumnRef::Asterisk)
1844 .cond_where(Condition::all().add(result.unwrap()))
1845 .to_string(PostgresQueryBuilder);
1846 assert!(query.contains("author_id"));
1847 assert!(query.contains("authors.id"));
1848 }
1849
1850 #[test]
1851 fn test_build_single_filter_expr_outer_ref_all_operators() {
1852 let operators = [
1853 FilterOperator::Eq,
1854 FilterOperator::Ne,
1855 FilterOperator::Gt,
1856 FilterOperator::Gte,
1857 FilterOperator::Lt,
1858 FilterOperator::Lte,
1859 ];
1860
1861 for op in operators {
1862 let filter = Filter::new(
1863 "child_id".to_string(),
1864 op.clone(),
1865 FilterValue::OuterRef(OuterRef::new("parent.id")),
1866 );
1867 let result = build_single_filter_expr(&filter);
1868 assert!(
1869 result.is_some(),
1870 "OuterRef with {:?} should produce Some",
1871 op
1872 );
1873 }
1874 }
1875
1876 #[test]
1877 fn test_build_single_filter_expr_expression() {
1878 use reinhardt_db::orm::annotation::{AnnotationValue, Value};
1879
1880 let expr = Expression::Multiply(
1882 Box::new(AnnotationValue::Field(F::new("cost"))),
1883 Box::new(AnnotationValue::Value(Value::Int(2))),
1884 );
1885 let filter = Filter::new(
1886 "price".to_string(),
1887 FilterOperator::Gt,
1888 FilterValue::Expression(expr),
1889 );
1890 let result = build_single_filter_expr(&filter);
1891 assert!(result.is_some());
1892 }
1893
1894 #[test]
1895 fn test_build_single_filter_expr_expression_all_operators() {
1896 use reinhardt_db::orm::annotation::{AnnotationValue, Value as OrmValue};
1897
1898 let operators = [
1899 FilterOperator::Eq,
1900 FilterOperator::Ne,
1901 FilterOperator::Gt,
1902 FilterOperator::Gte,
1903 FilterOperator::Lt,
1904 FilterOperator::Lte,
1905 ];
1906
1907 for op in operators {
1908 let expr = Expression::Add(
1909 Box::new(AnnotationValue::Field(F::new("base"))),
1910 Box::new(AnnotationValue::Value(OrmValue::Int(10))),
1911 );
1912 let filter = Filter::new(
1913 "total".to_string(),
1914 op.clone(),
1915 FilterValue::Expression(expr),
1916 );
1917 let result = build_single_filter_expr(&filter);
1918 assert!(
1919 result.is_some(),
1920 "Expression with {:?} should produce Some",
1921 op
1922 );
1923 }
1924 }
1925
1926 #[test]
1927 fn test_build_single_filter_expr_uses_transformed_filter_lhs() {
1928 let filter = reinhardt_db::orm::expressions::FieldRef::<(), i64>::new("created_at")
1930 .year()
1931 .range(2024, 2026);
1932
1933 let result = build_single_filter_expr(&filter);
1935
1936 assert!(result.is_some());
1938 let query = Query::select()
1939 .from(Alias::new("users"))
1940 .column(ColumnRef::Asterisk)
1941 .cond_where(Condition::all().add(result.unwrap()))
1942 .to_string(PostgresQueryBuilder);
1943 assert_eq!(
1944 query,
1945 r#"SELECT * FROM "users" WHERE EXTRACT(YEAR FROM "created_at") BETWEEN 2024 AND 2026"#
1946 );
1947 }
1948
1949 #[rstest]
1950 fn test_filter_value_to_sea_value_preserves_timestamp_binding() {
1951 let timestamp = chrono::DateTime::parse_from_rfc3339("2026-07-26T00:00:00Z")
1953 .expect("timestamp should be valid")
1954 .with_timezone(&chrono::Utc);
1955 let value = FilterValue::Timestamp(timestamp);
1956
1957 let sea_value = filter_value_to_sea_value(&value);
1959
1960 assert_eq!(
1962 sea_value,
1963 Value::ChronoDateTimeUtc(Some(Box::new(timestamp)))
1964 );
1965 }
1966
1967 #[rstest]
1968 fn test_filter_value_to_sea_value_preserves_uuid_binding() {
1969 let uuid =
1971 uuid::Uuid::parse_str("123e4567-e89b-12d3-a456-426614174000").expect("UUID is valid");
1972 let value = FilterValue::Uuid(uuid);
1973
1974 let sea_value = filter_value_to_sea_value(&value);
1976
1977 assert_eq!(sea_value, Value::Uuid(Some(Box::new(uuid))));
1979 }
1980
1981 #[test]
1982 fn test_filter_value_to_sea_value_field_ref_fallback() {
1983 let value = FilterValue::FieldRef(F::new("test_field"));
1984 let sea_value = filter_value_to_sea_value(&value);
1985
1986 match sea_value {
1988 Value::String(Some(s)) => assert_eq!(s.as_str(), "test_field"),
1989 _ => panic!("Expected String value"),
1990 }
1991 }
1992
1993 #[test]
1994 fn test_filter_value_to_sea_value_outer_ref_fallback() {
1995 let value = FilterValue::OuterRef(OuterRef::new("outer.field"));
1996 let sea_value = filter_value_to_sea_value(&value);
1997
1998 match sea_value {
2000 Value::String(Some(s)) => assert_eq!(s.as_str(), "outer.field"),
2001 _ => panic!("Expected String value"),
2002 }
2003 }
2004
2005 #[test]
2006 fn test_filter_value_to_sea_value_expression_fallback() {
2007 use reinhardt_db::orm::annotation::{AnnotationValue, Value as OrmValue};
2008
2009 let expr = Expression::Add(
2010 Box::new(AnnotationValue::Field(F::new("a"))),
2011 Box::new(AnnotationValue::Value(OrmValue::Int(1))),
2012 );
2013 let value = FilterValue::Expression(expr);
2014 let sea_value = filter_value_to_sea_value(&value);
2015
2016 match sea_value {
2018 Value::String(Some(s)) => {
2019 assert!(s.contains("a"), "SQL should contain field name 'a'");
2020 assert!(s.contains("1"), "SQL should contain value '1'");
2021 }
2022 _ => panic!("Expected String value"),
2023 }
2024 }
2025
2026 #[test]
2027 fn test_filter_value_to_sea_value_preserves_timestamp_and_uuid_types() {
2028 let timestamp = chrono::DateTime::parse_from_rfc3339("2026-07-27T00:00:00Z")
2029 .expect("timestamp fixture should parse")
2030 .with_timezone(&chrono::Utc);
2031 let uuid = uuid::Uuid::parse_str("123e4567-e89b-12d3-a456-426614174000")
2032 .expect("UUID fixture should parse");
2033
2034 assert!(matches!(
2035 filter_value_to_sea_value(&FilterValue::Timestamp(timestamp)),
2036 Value::ChronoDateTimeUtc(Some(_))
2037 ));
2038 assert!(matches!(
2039 filter_value_to_sea_value(&FilterValue::Uuid(uuid)),
2040 Value::Uuid(Some(_))
2041 ));
2042 }
2043
2044 #[test]
2045 fn test_filter_value_to_sea_value_preserves_date_time_and_naive_datetime() {
2046 let date = chrono::NaiveDate::from_ymd_opt(2026, 8, 20).expect("valid date");
2047 let time = chrono::NaiveTime::from_hms_opt(23, 51, 53).expect("valid time");
2048 let naive_datetime = chrono::NaiveDateTime::new(date, time);
2049
2050 assert_eq!(
2051 filter_value_to_sea_value(&FilterValue::Date(date)),
2052 Value::ChronoDate(Some(Box::new(date)))
2053 );
2054 assert_eq!(
2055 filter_value_to_sea_value(&FilterValue::Time(time)),
2056 Value::ChronoTime(Some(Box::new(time)))
2057 );
2058 assert_eq!(
2059 filter_value_to_sea_value(&FilterValue::NaiveDateTime(naive_datetime)),
2060 Value::ChronoDateTime(Some(Box::new(naive_datetime)))
2061 );
2062 }
2063
2064 #[rstest]
2067 fn test_insert_values_mismatch_returns_error_not_panic() {
2068 let mut query = Query::insert()
2072 .into_table(Alias::new("test_table"))
2073 .to_owned();
2074
2075 let columns = vec![Alias::new("col1"), Alias::new("col2"), Alias::new("col3")];
2076 let values = vec![Value::String(Some(Box::new("val1".to_string())))]; let result = query.columns(columns).values(values);
2080
2081 assert!(result.is_err());
2083 }
2084
2085 #[rstest]
2086 fn test_insert_values_matching_count_succeeds() {
2087 let mut query = Query::insert()
2089 .into_table(Alias::new("test_table"))
2090 .to_owned();
2091
2092 let columns = vec![Alias::new("col1"), Alias::new("col2")];
2093 let values = vec![
2094 Value::String(Some(Box::new("val1".to_string()))),
2095 Value::String(Some(Box::new("val2".to_string()))),
2096 ];
2097
2098 let result = query.columns(columns).values(values);
2100
2101 assert!(result.is_ok());
2103 }
2104
2105 #[test]
2108 fn test_outer_ref_filter_uses_safe_column_api() {
2109 let filter = Filter::new(
2111 "author_id".to_string(),
2112 FilterOperator::Eq,
2113 FilterValue::OuterRef(OuterRef::new("users.id")),
2114 );
2115
2116 let result = build_single_filter_expr(&filter);
2118
2119 assert!(result.is_some());
2121 let expr = result.unwrap();
2122 let query = Query::select()
2123 .from(Alias::new("books"))
2124 .column(ColumnRef::Asterisk)
2125 .cond_where(Condition::all().add(expr))
2126 .to_string(PostgresQueryBuilder);
2127 assert!(
2129 query.contains("\"author_id\""),
2130 "Column should be properly quoted: {}",
2131 query
2132 );
2133 }
2134
2135 #[test]
2136 fn test_outer_ref_injection_attempt_is_safely_quoted() {
2137 let filter = Filter::new(
2139 "id".to_string(),
2140 FilterOperator::Eq,
2141 FilterValue::OuterRef(OuterRef::new("id; DROP TABLE users; --")),
2142 );
2143
2144 let result = build_single_filter_expr(&filter);
2146
2147 assert!(result.is_some());
2149 let expr = result.unwrap();
2150 let query = Query::select()
2151 .from(Alias::new("items"))
2152 .column(ColumnRef::Asterisk)
2153 .cond_where(Condition::all().add(expr))
2154 .to_string(PostgresQueryBuilder);
2155 assert!(
2160 query.contains("\"id; DROP TABLE users; --\""),
2161 "Injection payload should be enclosed in double quotes as identifier: {}",
2162 query
2163 );
2164 let unquoted_parts: Vec<&str> = query.split('"').enumerate()
2167 .filter(|(i, _)| i % 2 == 0) .map(|(_, s)| s)
2169 .collect();
2170 let unquoted_sql = unquoted_parts.join("");
2171 assert!(
2172 !unquoted_sql.contains(';'),
2173 "No semicolons should appear outside quoted identifiers: {}",
2174 query
2175 );
2176 }
2177
2178 #[test]
2179 fn test_expression_filter_uses_safe_api() {
2180 use reinhardt_db::orm::annotation::AnnotationValue;
2181
2182 let expr = Expression::Multiply(
2184 Box::new(AnnotationValue::Field(F::new("unit_price"))),
2185 Box::new(AnnotationValue::Field(F::new("quantity"))),
2186 );
2187 let filter = Filter::new(
2188 "total".to_string(),
2189 FilterOperator::Eq,
2190 FilterValue::Expression(expr),
2191 );
2192
2193 let result = build_single_filter_expr(&filter);
2195
2196 assert!(result.is_some());
2198 let sea_expr = result.unwrap();
2199 let query = Query::select()
2200 .from(Alias::new("orders"))
2201 .column(ColumnRef::Asterisk)
2202 .cond_where(Condition::all().add(sea_expr))
2203 .to_string(PostgresQueryBuilder);
2204 assert!(
2205 query.contains("\"total\""),
2206 "Left side should be quoted: {}",
2207 query
2208 );
2209 }
2210
2211 #[test]
2212 fn test_expression_filter_with_literal_value() {
2213 use reinhardt_db::orm::annotation::{AnnotationValue, Value as OrmValue};
2214
2215 let expr = Expression::Add(
2217 Box::new(AnnotationValue::Field(F::new("price"))),
2218 Box::new(AnnotationValue::Value(OrmValue::Int(100))),
2219 );
2220 let filter = Filter::new(
2221 "adjusted_price".to_string(),
2222 FilterOperator::Gt,
2223 FilterValue::Expression(expr),
2224 );
2225
2226 let result = build_single_filter_expr(&filter);
2228
2229 assert!(result.is_some());
2231 }
2232
2233 #[test]
2234 fn test_outer_ref_all_operators_use_safe_api() {
2235 let operators = vec![
2237 FilterOperator::Eq,
2238 FilterOperator::Ne,
2239 FilterOperator::Gt,
2240 FilterOperator::Gte,
2241 FilterOperator::Lt,
2242 FilterOperator::Lte,
2243 ];
2244
2245 for op in operators {
2246 let filter = Filter::new(
2247 "field_a".to_string(),
2248 op.clone(),
2249 FilterValue::OuterRef(OuterRef::new("field_b")),
2250 );
2251 let result = build_single_filter_expr(&filter);
2252 assert!(
2253 result.is_some(),
2254 "OuterRef with {:?} should produce Some",
2255 op
2256 );
2257 }
2258 }
2259
2260 #[test]
2263 fn test_coalesce_expression_uses_safe_parameterized_api() {
2264 use reinhardt_db::orm::annotation::{AnnotationValue, Value as OrmValue};
2265
2266 let expr = Expression::Coalesce(vec![
2268 AnnotationValue::Field(F::new("field_a")),
2269 AnnotationValue::Value(OrmValue::Int(0)),
2270 ]);
2271 let filter = Filter::new(
2272 "result".to_string(),
2273 FilterOperator::Gt,
2274 FilterValue::Expression(expr),
2275 );
2276
2277 let result = build_single_filter_expr(&filter);
2279
2280 assert!(result.is_some());
2282 let sea_expr = result.unwrap();
2283 let query = Query::select()
2284 .from(Alias::new("items"))
2285 .column(ColumnRef::Asterisk)
2286 .cond_where(Condition::all().add(sea_expr))
2287 .to_string(PostgresQueryBuilder);
2288 assert!(
2289 query.contains("COALESCE"),
2290 "Should contain COALESCE function: {}",
2291 query
2292 );
2293 assert!(
2294 query.contains("\"result\""),
2295 "Left side should be quoted: {}",
2296 query
2297 );
2298 }
2299
2300 #[test]
2301 fn test_case_expression_uses_safe_api() {
2302 use reinhardt_db::orm::annotation::{
2303 AnnotationValue, Value as OrmValue, When as AnnotWhen,
2304 };
2305 use reinhardt_db::orm::expressions::Q;
2306
2307 let expr = Expression::Case {
2309 whens: vec![AnnotWhen::new(
2310 Q::new("status", "=", "'active'"),
2311 AnnotationValue::Value(OrmValue::Int(1)),
2312 )],
2313 default: Some(Box::new(AnnotationValue::Value(OrmValue::Int(0)))),
2314 };
2315 let filter = Filter::new(
2316 "priority".to_string(),
2317 FilterOperator::Eq,
2318 FilterValue::Expression(expr),
2319 );
2320
2321 let result = build_single_filter_expr(&filter);
2323
2324 assert!(result.is_some());
2326 let sea_expr = result.unwrap();
2327 let query = Query::select()
2328 .from(Alias::new("tasks"))
2329 .column(ColumnRef::Asterisk)
2330 .cond_where(Condition::all().add(sea_expr))
2331 .to_string(PostgresQueryBuilder);
2332 assert!(
2333 query.contains("CASE"),
2334 "Should contain CASE keyword: {}",
2335 query
2336 );
2337 assert!(
2338 query.contains("WHEN"),
2339 "Should contain WHEN keyword: {}",
2340 query
2341 );
2342 assert!(
2343 query.contains("ELSE"),
2344 "Should contain ELSE keyword: {}",
2345 query
2346 );
2347 }
2348
2349 #[test]
2350 fn test_empty_coalesce_returns_null() {
2351 let expr = Expression::Coalesce(vec![]);
2353
2354 let result = annotation_expr_to_safe_expr(&expr);
2356
2357 let query = Query::select()
2359 .from(Alias::new("test"))
2360 .column(ColumnRef::Asterisk)
2361 .cond_where(Condition::all().add(result))
2362 .to_string(PostgresQueryBuilder);
2363 assert!(
2364 query.contains("NULL"),
2365 "Empty COALESCE should produce NULL: {}",
2366 query
2367 );
2368 }
2369
2370 #[test]
2373 fn test_aggregate_count_uses_safe_api() {
2374 use reinhardt_db::orm::aggregation::{Aggregate, AggregateFunc};
2375
2376 let agg = Aggregate {
2378 func: AggregateFunc::Count,
2379 field: None,
2380 alias: None,
2381 distinct: false,
2382 };
2383
2384 let result = aggregate_to_safe_expr(&agg);
2386
2387 let query = Query::select()
2389 .from(Alias::new("items"))
2390 .expr(result)
2391 .to_string(PostgresQueryBuilder);
2392 assert!(
2393 query.contains("COUNT(*)"),
2394 "Should contain COUNT(*): {}",
2395 query
2396 );
2397 }
2398
2399 #[test]
2400 fn test_aggregate_sum_field_uses_quoted_identifier() {
2401 use reinhardt_db::orm::aggregation::{Aggregate, AggregateFunc};
2402
2403 let agg = Aggregate {
2405 func: AggregateFunc::Sum,
2406 field: Some("price".to_string()),
2407 alias: None,
2408 distinct: false,
2409 };
2410
2411 let result = aggregate_to_safe_expr(&agg);
2413
2414 let query = Query::select()
2416 .from(Alias::new("orders"))
2417 .expr(result)
2418 .to_string(PostgresQueryBuilder);
2419 assert!(
2420 query.contains("SUM("),
2421 "Should contain SUM function: {}",
2422 query
2423 );
2424 assert!(
2425 query.contains("\"price\""),
2426 "Field name should be quoted: {}",
2427 query
2428 );
2429 }
2430
2431 #[test]
2432 fn test_aggregate_count_distinct_uses_distinct_keyword() {
2433 use reinhardt_db::orm::aggregation::{Aggregate, AggregateFunc};
2434
2435 let agg = Aggregate {
2437 func: AggregateFunc::CountDistinct,
2438 field: Some("category".to_string()),
2439 alias: None,
2440 distinct: false, };
2442
2443 let result = aggregate_to_safe_expr(&agg);
2445
2446 let query = Query::select()
2448 .from(Alias::new("products"))
2449 .expr(result)
2450 .to_string(PostgresQueryBuilder);
2451 assert!(
2452 query.contains("COUNT(DISTINCT"),
2453 "Should contain COUNT(DISTINCT: {}",
2454 query
2455 );
2456 assert!(
2457 query.contains("\"category\""),
2458 "Field name should be quoted: {}",
2459 query
2460 );
2461 }
2462
2463 #[test]
2464 fn test_aggregate_injection_attempt_is_quoted() {
2465 use reinhardt_db::orm::aggregation::{Aggregate, AggregateFunc};
2466
2467 let agg = Aggregate {
2469 func: AggregateFunc::Sum,
2470 field: Some("price); DROP TABLE users; --".to_string()),
2471 alias: None,
2472 distinct: false,
2473 };
2474
2475 let result = aggregate_to_safe_expr(&agg);
2477
2478 let query = Query::select()
2480 .from(Alias::new("orders"))
2481 .expr(result)
2482 .to_string(PostgresQueryBuilder);
2483 assert!(
2484 query.contains("\"price); DROP TABLE users; --\""),
2485 "Injection payload should be enclosed in double quotes: {}",
2486 query
2487 );
2488 }
2489
2490 #[rstest]
2493 fn test_build_composite_and_all_unsupported_returns_none() {
2494 let filter1 = Filter::new(
2497 "field1".to_string(),
2498 FilterOperator::Contains,
2499 FilterValue::Boolean(true),
2500 );
2501 let filter2 = Filter::new(
2502 "field2".to_string(),
2503 FilterOperator::StartsWith,
2504 FilterValue::Integer(5),
2505 );
2506 let condition = FilterCondition::And(vec![
2507 FilterCondition::Single(filter1),
2508 FilterCondition::Single(filter2),
2509 ]);
2510
2511 let result = build_composite_filter_condition(&condition);
2513
2514 assert!(result.is_ok());
2516 assert!(
2517 result.unwrap().is_none(),
2518 "And with all unsupported filters should return None"
2519 );
2520 }
2521
2522 #[rstest]
2523 fn test_build_composite_or_all_unsupported_returns_none() {
2524 let filter1 = Filter::new(
2526 "field1".to_string(),
2527 FilterOperator::Contains,
2528 FilterValue::Boolean(true),
2529 );
2530 let filter2 = Filter::new(
2531 "field2".to_string(),
2532 FilterOperator::StartsWith,
2533 FilterValue::Integer(5),
2534 );
2535 let condition = FilterCondition::Or(vec![
2536 FilterCondition::Single(filter1),
2537 FilterCondition::Single(filter2),
2538 ]);
2539
2540 let result = build_composite_filter_condition(&condition);
2542
2543 assert!(result.is_ok());
2545 assert!(
2546 result.unwrap().is_none(),
2547 "Or with all unsupported filters should return None"
2548 );
2549 }
2550
2551 #[rstest]
2552 fn test_build_composite_and_mixed_valid_and_unsupported() {
2553 let valid_filter = Filter::new(
2555 "name".to_string(),
2556 FilterOperator::Eq,
2557 FilterValue::String("Alice".to_string()),
2558 );
2559 let unsupported_filter = Filter::new(
2560 "field2".to_string(),
2561 FilterOperator::Contains,
2562 FilterValue::Boolean(true),
2563 );
2564 let condition = FilterCondition::And(vec![
2565 FilterCondition::Single(valid_filter),
2566 FilterCondition::Single(unsupported_filter),
2567 ]);
2568
2569 let result = build_composite_filter_condition(&condition);
2571
2572 assert!(result.is_ok());
2574 let cond = result.unwrap();
2575 assert!(
2576 cond.is_some(),
2577 "And with at least one valid filter should return Some"
2578 );
2579 let query = Query::select()
2580 .from(Alias::new("t"))
2581 .column(ColumnRef::Asterisk)
2582 .cond_where(cond.unwrap())
2583 .to_string(PostgresQueryBuilder);
2584 assert!(
2585 query.contains("\"name\""),
2586 "SQL should contain the valid filter field, got: {}",
2587 query
2588 );
2589 assert!(
2590 query.contains("'Alice'"),
2591 "SQL should contain the valid filter value, got: {}",
2592 query
2593 );
2594 }
2595
2596 #[rstest]
2597 fn test_build_composite_or_mixed_valid_and_unsupported() {
2598 let valid_filter = Filter::new(
2600 "email".to_string(),
2601 FilterOperator::Eq,
2602 FilterValue::String("test@example.com".to_string()),
2603 );
2604 let unsupported_filter = Filter::new(
2605 "field2".to_string(),
2606 FilterOperator::StartsWith,
2607 FilterValue::Integer(5),
2608 );
2609 let condition = FilterCondition::Or(vec![
2610 FilterCondition::Single(valid_filter),
2611 FilterCondition::Single(unsupported_filter),
2612 ]);
2613
2614 let result = build_composite_filter_condition(&condition);
2616
2617 assert!(result.is_ok());
2619 let cond = result.unwrap();
2620 assert!(
2621 cond.is_some(),
2622 "Or with at least one valid filter should return Some"
2623 );
2624 let query = Query::select()
2625 .from(Alias::new("t"))
2626 .column(ColumnRef::Asterisk)
2627 .cond_where(cond.unwrap())
2628 .to_string(PostgresQueryBuilder);
2629 assert!(
2630 query.contains("\"email\""),
2631 "SQL should contain the valid filter field, got: {}",
2632 query
2633 );
2634 assert!(
2635 query.contains("'test@example.com'"),
2636 "SQL should contain the valid filter value, got: {}",
2637 query
2638 );
2639 }
2640
2641 #[rstest]
2642 fn test_build_filter_condition_all_unsupported_returns_none() {
2643 let filters = vec![
2645 Filter::new(
2646 "field1".to_string(),
2647 FilterOperator::Contains,
2648 FilterValue::Boolean(true),
2649 ),
2650 Filter::new(
2651 "field2".to_string(),
2652 FilterOperator::StartsWith,
2653 FilterValue::Integer(5),
2654 ),
2655 ];
2656
2657 let result = build_filter_condition(&filters);
2659
2660 assert!(
2662 result.is_none(),
2663 "build_filter_condition with all unsupported filters should return None"
2664 );
2665 }
2666
2667 #[rstest]
2670 fn test_extract_count_from_row_with_count_key() {
2671 let data = serde_json::json!({"count": 42});
2673
2674 let result = extract_count_from_row(&data);
2676
2677 assert_eq!(result.unwrap(), 42);
2679 }
2680
2681 #[rstest]
2682 fn test_extract_count_from_row_without_count_key() {
2683 let data = serde_json::json!({"total": 10});
2685
2686 let result = extract_count_from_row(&data);
2688
2689 let err = result.unwrap_err();
2691 assert!(
2692 err.to_string().contains("missing 'count' key"),
2693 "Error should mention missing 'count' key, got: {}",
2694 err
2695 );
2696 }
2697
2698 #[rstest]
2699 fn test_extract_count_from_row_empty_object() {
2700 let data = serde_json::json!({});
2702
2703 let result = extract_count_from_row(&data);
2705
2706 let err = result.unwrap_err();
2708 assert!(
2709 err.to_string().contains("missing 'count' key"),
2710 "Error should mention missing 'count' key, got: {}",
2711 err
2712 );
2713 }
2714
2715 #[rstest]
2716 fn test_extract_count_from_row_non_integer() {
2717 let data = serde_json::json!({"count": "abc"});
2719
2720 let result = extract_count_from_row(&data);
2722
2723 let err = result.unwrap_err();
2725 assert!(
2726 err.to_string().contains("non-integer"),
2727 "Error should mention non-integer value, got: {}",
2728 err
2729 );
2730 }
2731
2732 #[rstest]
2733 fn test_extract_count_from_row_null_data() {
2734 let data = serde_json::Value::Null;
2736
2737 let result = extract_count_from_row(&data);
2739
2740 let err = result.unwrap_err();
2742 assert!(
2743 err.to_string().contains("unexpected data format"),
2744 "Error should mention unexpected data format, got: {}",
2745 err
2746 );
2747 }
2748
2749 #[rstest]
2750 fn test_extract_count_from_row_zero() {
2751 let data = serde_json::json!({"count": 0});
2753
2754 let result = extract_count_from_row(&data);
2756
2757 assert_eq!(result.unwrap(), 0);
2759 }
2760
2761 #[rstest]
2764 #[tokio::test]
2765 async fn test_admin_database_inject_error_hint_mentions_connection() {
2766 let singleton = Arc::new(reinhardt_di::SingletonScope::new());
2768 let ctx = reinhardt_di::InjectionContext::builder(singleton).build();
2769
2770 let result = AdminDatabase::inject(&ctx).await;
2772
2773 assert!(result.is_err());
2775 let err = result.err().unwrap();
2776 assert!(
2777 err.to_string().contains("DatabaseConnection"),
2778 "Error hint should mention DatabaseConnection, got: {}",
2779 err
2780 );
2781 }
2782
2783 #[rstest]
2784 #[tokio::test]
2785 async fn test_admin_database_inject_returns_prebuilt_from_singleton() {
2786 let singleton = Arc::new(reinhardt_di::SingletonScope::new());
2788 let ctx = reinhardt_di::InjectionContext::builder(singleton).build();
2795
2796 let result = AdminDatabase::inject(&ctx).await;
2798
2799 assert!(result.is_err());
2801 let err = result.err().unwrap();
2802 match err {
2803 reinhardt_di::DiError::NotRegistered { type_name, hint } => {
2804 assert_eq!(type_name, "AdminDatabase");
2805 assert_eq!(
2806 hint,
2807 "DatabaseConnection must be registered as a singleton. \
2808 Use InjectionContextBuilder::singleton(db_connection) during setup."
2809 );
2810 }
2811 other => panic!("Expected NotRegistered error, got: {other:?}"),
2812 }
2813 }
2814
2815 #[rstest]
2816 #[tokio::test]
2817 async fn test_admin_database_keyed_provider_reports_missing_connection() {
2818 let singleton = Arc::new(reinhardt_di::SingletonScope::new());
2819 let ctx = reinhardt_di::InjectionContext::builder(singleton).build();
2820
2821 let result =
2822 reinhardt_di::Depends::<AdminDatabaseKey, AdminDatabase>::resolve_from_registry(
2823 &ctx, true,
2824 )
2825 .await;
2826
2827 assert!(result.is_err());
2828 let err = result.err().unwrap();
2829 match err {
2830 reinhardt_di::DiError::NotRegistered { type_name, hint } => {
2831 assert_eq!(type_name, "AdminDatabase");
2832 assert_eq!(
2833 hint,
2834 "DatabaseConnection must be registered as a singleton. \
2835 Use InjectionContextBuilder::singleton(db_connection) during setup."
2836 );
2837 }
2838 other => panic!("Expected NotRegistered error, got: {other:?}"),
2839 }
2840 }
2841
2842 #[rstest]
2845 fn test_build_single_filter_expr_array_in() {
2846 let filter = Filter::new(
2848 "status".to_string(),
2849 FilterOperator::In,
2850 FilterValue::Array(vec!["a".to_string(), "b".to_string(), "c".to_string()]),
2851 );
2852
2853 let result = build_single_filter_expr(&filter);
2855
2856 assert!(
2858 result.is_some(),
2859 "Array In with non-empty values should return Some"
2860 );
2861 let query = Query::select()
2862 .from(Alias::new("table"))
2863 .column(ColumnRef::Asterisk)
2864 .cond_where(Condition::all().add(result.unwrap()))
2865 .to_string(PostgresQueryBuilder);
2866 assert!(query.contains("IN"), "SQL should contain IN operator");
2867 assert!(query.contains("'a'"), "SQL should contain value 'a'");
2868 assert!(query.contains("'b'"), "SQL should contain value 'b'");
2869 assert!(query.contains("'c'"), "SQL should contain value 'c'");
2870 }
2871
2872 #[rstest]
2873 fn test_build_single_filter_expr_array_not_in() {
2874 let filter = Filter::new(
2876 "status".to_string(),
2877 FilterOperator::NotIn,
2878 FilterValue::Array(vec!["x".to_string(), "y".to_string()]),
2879 );
2880
2881 let result = build_single_filter_expr(&filter);
2883
2884 assert!(
2886 result.is_some(),
2887 "Array NotIn with non-empty values should return Some"
2888 );
2889 let query = Query::select()
2890 .from(Alias::new("table"))
2891 .column(ColumnRef::Asterisk)
2892 .cond_where(Condition::all().add(result.unwrap()))
2893 .to_string(PostgresQueryBuilder);
2894 assert!(
2895 query.contains("NOT IN"),
2896 "SQL should contain NOT IN operator"
2897 );
2898 assert!(query.contains("'x'"), "SQL should contain value 'x'");
2899 assert!(query.contains("'y'"), "SQL should contain value 'y'");
2900 }
2901
2902 #[rstest]
2903 fn test_build_single_filter_expr_array_in_empty() {
2904 let filter = Filter::new(
2906 "status".to_string(),
2907 FilterOperator::In,
2908 FilterValue::Array(vec![]),
2909 );
2910
2911 let result = build_single_filter_expr(&filter);
2913
2914 assert!(
2916 result.is_none(),
2917 "Array In with empty values should return None"
2918 );
2919 }
2920
2921 #[rstest]
2922 fn test_build_single_filter_expr_array_in_single_element() {
2923 let filter = Filter::new(
2925 "category".to_string(),
2926 FilterOperator::In,
2927 FilterValue::Array(vec!["solo".to_string()]),
2928 );
2929
2930 let result = build_single_filter_expr(&filter);
2932
2933 assert!(
2935 result.is_some(),
2936 "Array In with single element should return Some"
2937 );
2938 let query = Query::select()
2939 .from(Alias::new("table"))
2940 .column(ColumnRef::Asterisk)
2941 .cond_where(Condition::all().add(result.unwrap()))
2942 .to_string(PostgresQueryBuilder);
2943 assert!(query.contains("IN"), "SQL should contain IN operator");
2944 assert!(query.contains("'solo'"), "SQL should contain value 'solo'");
2945 }
2946
2947 #[rstest]
2948 fn test_build_single_filter_expr_array_in_special_chars() {
2949 let filter = Filter::new(
2951 "name".to_string(),
2952 FilterOperator::In,
2953 FilterValue::Array(vec!["O'Brien".to_string(), "a;DROP TABLE".to_string()]),
2954 );
2955
2956 let result = build_single_filter_expr(&filter);
2958
2959 assert!(
2961 result.is_some(),
2962 "Array In with special chars should return Some"
2963 );
2964 let query = Query::select()
2965 .from(Alias::new("table"))
2966 .column(ColumnRef::Asterisk)
2967 .cond_where(Condition::all().add(result.unwrap()))
2968 .to_string(PostgresQueryBuilder);
2969 assert!(query.contains("IN"), "SQL should contain IN operator");
2970 assert!(
2972 query.contains("O''Brien"),
2973 "Single quote in value should be escaped, got: {}",
2974 query
2975 );
2976 assert!(
2978 query.contains("'a;DROP TABLE'"),
2979 "SQL injection attempt should be safely quoted as a string literal, got: {}",
2980 query
2981 );
2982 }
2983
2984 #[rstest]
2987 fn test_and_with_all_unsupported_returns_none() {
2988 let unsupported1 = FilterCondition::Single(Filter::new(
2990 "name",
2991 FilterOperator::Contains,
2992 FilterValue::Integer(42),
2993 ));
2994 let unsupported2 = FilterCondition::Single(Filter::new(
2995 "email",
2996 FilterOperator::StartsWith,
2997 FilterValue::Integer(99),
2998 ));
2999 let condition = FilterCondition::And(vec![unsupported1, unsupported2]);
3000
3001 let result = build_composite_filter_condition(&condition);
3003
3004 assert!(result.is_ok());
3007 let cond = result.unwrap();
3008 assert!(
3009 cond.is_none(),
3010 "And with all unsupported sub-conditions should return None"
3011 );
3012 }
3013
3014 #[rstest]
3015 fn test_or_with_all_unsupported_returns_none() {
3016 let unsupported1 = FilterCondition::Single(Filter::new(
3018 "name",
3019 FilterOperator::Contains,
3020 FilterValue::Integer(42),
3021 ));
3022 let unsupported2 = FilterCondition::Single(Filter::new(
3023 "email",
3024 FilterOperator::StartsWith,
3025 FilterValue::Integer(99),
3026 ));
3027 let condition = FilterCondition::Or(vec![unsupported1, unsupported2]);
3028
3029 let result = build_composite_filter_condition(&condition);
3031
3032 assert!(result.is_ok());
3035 let cond = result.unwrap();
3036 assert!(
3037 cond.is_none(),
3038 "Or with all unsupported sub-conditions should return None"
3039 );
3040 }
3041
3042 #[rstest]
3043 fn test_and_with_mix_supported_unsupported_keeps_supported() {
3044 let supported = FilterCondition::Single(Filter::new(
3046 "name",
3047 FilterOperator::Eq,
3048 FilterValue::String("Alice".to_string()),
3049 ));
3050 let unsupported = FilterCondition::Single(Filter::new(
3051 "email",
3052 FilterOperator::Contains,
3053 FilterValue::Integer(42),
3054 ));
3055 let condition = FilterCondition::And(vec![supported, unsupported]);
3056
3057 let result = build_composite_filter_condition(&condition);
3059
3060 assert!(result.is_ok());
3062 let cond = result.unwrap();
3063 assert!(
3064 cond.is_some(),
3065 "And with mix of supported/unsupported should return Some with supported filters"
3066 );
3067 let query = Query::select()
3069 .from(Alias::new("test"))
3070 .column(ColumnRef::Asterisk)
3071 .cond_where(cond.unwrap())
3072 .to_string(PostgresQueryBuilder);
3073 assert!(
3074 query.contains("\"name\""),
3075 "SQL should contain the supported filter field 'name': {}",
3076 query
3077 );
3078 }
3079
3080 #[rstest]
3081 fn test_or_with_one_supported_one_unsupported() {
3082 let supported = FilterCondition::Single(Filter::new(
3084 "status",
3085 FilterOperator::Eq,
3086 FilterValue::String("active".to_string()),
3087 ));
3088 let unsupported = FilterCondition::Single(Filter::new(
3089 "count",
3090 FilterOperator::Contains,
3091 FilterValue::Integer(42),
3092 ));
3093 let condition = FilterCondition::Or(vec![supported, unsupported]);
3094
3095 let result = build_composite_filter_condition(&condition);
3097
3098 assert!(result.is_ok());
3100 let cond = result.unwrap();
3101 assert!(
3102 cond.is_some(),
3103 "Or with one supported condition should return Some"
3104 );
3105 let query = Query::select()
3106 .from(Alias::new("test"))
3107 .column(ColumnRef::Asterisk)
3108 .cond_where(cond.unwrap())
3109 .to_string(PostgresQueryBuilder);
3110 assert!(
3111 query.contains("\"status\""),
3112 "SQL should contain the supported filter field 'status': {}",
3113 query
3114 );
3115 }
3116
3117 #[rstest]
3120 fn test_extract_count_with_count_key() {
3121 let data = serde_json::json!({"count": 42});
3123
3124 let result = extract_count_from_row(&data);
3126
3127 assert!(result.is_ok());
3129 assert_eq!(result.unwrap(), 42);
3130 }
3131
3132 #[rstest]
3133 fn test_extract_count_without_count_key_returns_error() {
3134 let data = serde_json::json!({"total": 42});
3136
3137 let result = extract_count_from_row(&data);
3139
3140 assert!(result.is_err());
3143 let err = result.unwrap_err();
3144 assert!(
3145 err.to_string().contains("missing 'count' key"),
3146 "Error should mention missing 'count' key, got: {}",
3147 err
3148 );
3149 }
3150
3151 #[rstest]
3152 fn test_extract_count_with_multiple_keys_no_count_returns_error() {
3153 let data = serde_json::json!({"total": 42, "other": 99});
3155
3156 let result = extract_count_from_row(&data);
3158
3159 assert!(result.is_err());
3162 let err = result.unwrap_err();
3163 assert!(
3164 err.to_string().contains("available keys"),
3165 "Error should list available keys, got: {}",
3166 err
3167 );
3168 }
3169
3170 #[rstest]
3171 fn test_extract_count_non_integer_returns_error() {
3172 let data = serde_json::json!({"count": "not_a_number"});
3174
3175 let result = extract_count_from_row(&data);
3177
3178 assert!(result.is_err());
3180 let err = result.unwrap_err();
3181 assert!(matches!(err, AdminError::DatabaseError(_)));
3182 }
3183
3184 #[rstest]
3185 fn test_extract_count_null_returns_error() {
3186 let data = serde_json::json!({"count": null});
3188
3189 let result = extract_count_from_row(&data);
3191
3192 assert!(result.is_err());
3194 }
3195
3196 #[rstest]
3197 fn test_extract_count_empty_object_returns_error() {
3198 let data = serde_json::json!({});
3200
3201 let result = extract_count_from_row(&data);
3203
3204 assert!(result.is_err());
3206 }
3207
3208 #[rstest]
3209 fn test_extract_count_non_object_returns_error() {
3210 let data = serde_json::json!([1, 2, 3]);
3212
3213 let result = extract_count_from_row(&data);
3215
3216 assert!(result.is_err());
3218 }
3219
3220 #[rstest]
3223 fn test_parse_pk_value_integer_falls_back_to_bigint() {
3224 let val = parse_pk_value("nonexistent_table", "id", "42");
3228
3229 assert_eq!(val, Value::BigInt(Some(42)));
3231 }
3232
3233 #[rstest]
3234 fn test_parse_pk_value_uuid_string_without_registry_falls_back_to_string() {
3235 let val = parse_pk_value(
3239 "nonexistent_table",
3240 "id",
3241 "c1a363b1-cc42-4dea-81f0-9dc1cedf0083",
3242 );
3243
3244 assert!(matches!(val, Value::String(Some(_))));
3246 }
3247
3248 #[rstest]
3249 fn test_parse_pk_value_non_numeric_string_falls_back_to_string() {
3250 let val = parse_pk_value("nonexistent_table", "id", "hello-world");
3254
3255 assert!(matches!(val, Value::String(Some(_))));
3257 }
3258
3259 #[rstest]
3260 fn test_parse_pk_value_negative_integer() {
3261 let val = parse_pk_value("nonexistent_table", "id", "-1");
3265
3266 assert_eq!(val, Value::BigInt(Some(-1)));
3268 }
3269
3270 #[rstest]
3271 fn test_parse_pk_value_zero() {
3272 let val = parse_pk_value("nonexistent_table", "id", "0");
3276
3277 assert_eq!(val, Value::BigInt(Some(0)));
3279 }
3280}