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::Integer(i) | FilterValue::Int(i) => (*i).into(),
199 FilterValue::Float(f) => (*f).into(),
200 FilterValue::Boolean(b) | FilterValue::Bool(b) => (*b).into(),
201 FilterValue::Null => Value::Int(None),
202 FilterValue::Array(_) => Value::String(None),
206 FilterValue::List(_) | FilterValue::Range(_, _) => Value::String(None),
207 FilterValue::FieldRef(f) => {
208 Value::String(Some(Box::new(f.field.clone())))
212 }
213 FilterValue::Expression(expr) => {
214 Value::String(Some(Box::new(expr.to_sql())))
218 }
219 FilterValue::OuterRef(outer) => {
220 Value::String(Some(Box::new(outer.field.clone())))
224 }
225 }
226}
227
228fn annotation_value_to_safe_expr(
233 val: &reinhardt_db::orm::annotation::AnnotationValue,
234) -> SimpleExpr {
235 use reinhardt_db::orm::annotation::AnnotationValue;
236
237 match val {
238 AnnotationValue::Value(v) => {
239 use reinhardt_db::orm::annotation::Value as AnnotValue;
240 match v {
241 AnnotValue::String(s) => Expr::val(s.as_str()).into(),
242 AnnotValue::Int(i) => Expr::val(*i).into(),
243 AnnotValue::Float(f) => Expr::val(*f).into(),
244 AnnotValue::Bool(b) => Expr::val(*b).into(),
245 AnnotValue::Null => Expr::val(Option::<String>::None).into(),
246 }
247 }
248 AnnotationValue::Field(f) => Expr::col(Alias::new(&f.field)).into(),
249 AnnotationValue::Expression(e) => annotation_expr_to_safe_expr(e),
250 AnnotationValue::Aggregate(a) => aggregate_to_safe_expr(a),
251 AnnotationValue::Subquery(_)
255 | AnnotationValue::ArrayAgg(_)
256 | AnnotationValue::StringAgg(_)
257 | AnnotationValue::JsonbAgg(_)
258 | AnnotationValue::JsonbBuildObject(_)
259 | AnnotationValue::TsRank(_) => Expr::cust(val.to_sql()).into(),
260 }
261}
262
263fn aggregate_to_safe_expr(agg: &reinhardt_db::orm::aggregation::Aggregate) -> SimpleExpr {
269 use reinhardt_db::orm::aggregation::AggregateFunc;
270
271 let func_name = match agg.func {
272 AggregateFunc::Count | AggregateFunc::CountDistinct => "COUNT",
273 AggregateFunc::Sum => "SUM",
274 AggregateFunc::Avg => "AVG",
275 AggregateFunc::Max => "MAX",
276 AggregateFunc::Min => "MIN",
277 };
278
279 if let Some(field) = &agg.field {
280 let col_expr: SimpleExpr = Expr::col(Alias::new(field)).into();
281 let is_distinct = agg.distinct || matches!(agg.func, AggregateFunc::CountDistinct);
282 if is_distinct {
283 Expr::cust_with_values(format!("{func_name}(DISTINCT ?)"), [col_expr]).into()
284 } else {
285 Expr::cust_with_values(format!("{func_name}(?)"), [col_expr]).into()
286 }
287 } else {
288 Expr::cust(format!("{func_name}(*)")).into()
290 }
291}
292
293fn annotation_expr_to_safe_expr(expr: &reinhardt_db::orm::annotation::Expression) -> SimpleExpr {
299 use reinhardt_db::orm::annotation::Expression as AnnotExpr;
300
301 match expr {
302 AnnotExpr::Add(left, right) => {
303 let left_expr = annotation_value_to_safe_expr(left);
304 let right_expr = annotation_value_to_safe_expr(right);
305 Expr::cust_with_values("(? + ?)", [left_expr, right_expr]).into()
306 }
307 AnnotExpr::Subtract(left, right) => {
308 let left_expr = annotation_value_to_safe_expr(left);
309 let right_expr = annotation_value_to_safe_expr(right);
310 Expr::cust_with_values("(? - ?)", [left_expr, right_expr]).into()
311 }
312 AnnotExpr::Multiply(left, right) => {
313 let left_expr = annotation_value_to_safe_expr(left);
314 let right_expr = annotation_value_to_safe_expr(right);
315 Expr::cust_with_values("(? * ?)", [left_expr, right_expr]).into()
316 }
317 AnnotExpr::Divide(left, right) => {
318 let left_expr = annotation_value_to_safe_expr(left);
319 let right_expr = annotation_value_to_safe_expr(right);
320 Expr::cust_with_values("(? / ?)", [left_expr, right_expr]).into()
321 }
322 AnnotExpr::Case { whens, default } => {
323 let mut case = CaseStatement::new();
324 for when in whens {
325 let cond_expr: SimpleExpr = Expr::cust(when.condition.to_sql()).into();
329 let then_expr = annotation_value_to_safe_expr(&when.then);
330 case = case.when(cond_expr, then_expr);
331 }
332 if let Some(default_val) = default {
333 case = case.else_result(annotation_value_to_safe_expr(default_val));
334 }
335 SimpleExpr::from(case)
336 }
337 AnnotExpr::Coalesce(values) => {
338 let exprs: Vec<SimpleExpr> = values.iter().map(annotation_value_to_safe_expr).collect();
339 if exprs.is_empty() {
340 Expr::val(Option::<String>::None).into()
341 } else {
342 let placeholders = vec!["?"; exprs.len()].join(", ");
343 Expr::cust_with_values(format!("COALESCE({placeholders})"), exprs).into()
344 }
345 }
346 }
347}
348
349fn escape_like_pattern(input: &str) -> String {
351 input
352 .replace('\\', "\\\\")
353 .replace('%', "\\%")
354 .replace('_', "\\_")
355}
356
357#[doc(hidden)]
359pub fn build_single_filter_expr(filter: &Filter) -> Option<SimpleExpr> {
360 let col = filter.lhs_expr();
361 let lhs_sql = filter.lhs_sql();
362
363 let expr = match (&filter.operator, &filter.value) {
364 (FilterOperator::Eq, FilterValue::Null) => col.is_null(),
366 (FilterOperator::Ne, FilterValue::Null) => col.is_not_null(),
367 (FilterOperator::IExact, FilterValue::String(s)) => {
368 col.binary(BinOper::ILike, SimpleExpr::from(s.clone()))
369 }
370 (FilterOperator::IExact, v) => col.eq(filter_value_to_sea_value(v)),
371
372 (FilterOperator::Eq, FilterValue::FieldRef(f)) => col.eq(Expr::col(Alias::new(&f.field))),
374 (FilterOperator::Ne, FilterValue::FieldRef(f)) => col.ne(Expr::col(Alias::new(&f.field))),
375 (FilterOperator::Gt, FilterValue::FieldRef(f)) => col.gt(Expr::col(Alias::new(&f.field))),
376 (FilterOperator::Gte, FilterValue::FieldRef(f)) => col.gte(Expr::col(Alias::new(&f.field))),
377 (FilterOperator::Lt, FilterValue::FieldRef(f)) => col.lt(Expr::col(Alias::new(&f.field))),
378 (FilterOperator::Lte, FilterValue::FieldRef(f)) => col.lte(Expr::col(Alias::new(&f.field))),
379
380 (FilterOperator::Eq, FilterValue::OuterRef(outer)) => {
382 col.eq(Expr::col(Alias::new(&outer.field)))
383 }
384 (FilterOperator::Ne, FilterValue::OuterRef(outer)) => {
385 col.ne(Expr::col(Alias::new(&outer.field)))
386 }
387 (FilterOperator::Gt, FilterValue::OuterRef(outer)) => {
388 col.gt(Expr::col(Alias::new(&outer.field)))
389 }
390 (FilterOperator::Gte, FilterValue::OuterRef(outer)) => {
391 col.gte(Expr::col(Alias::new(&outer.field)))
392 }
393 (FilterOperator::Lt, FilterValue::OuterRef(outer)) => {
394 col.lt(Expr::col(Alias::new(&outer.field)))
395 }
396 (FilterOperator::Lte, FilterValue::OuterRef(outer)) => {
397 col.lte(Expr::col(Alias::new(&outer.field)))
398 }
399
400 (FilterOperator::Eq, FilterValue::Expression(expr)) => {
402 col.eq(annotation_expr_to_safe_expr(expr))
403 }
404 (FilterOperator::Ne, FilterValue::Expression(expr)) => {
405 col.ne(annotation_expr_to_safe_expr(expr))
406 }
407 (FilterOperator::Gt, FilterValue::Expression(expr)) => {
408 col.gt(annotation_expr_to_safe_expr(expr))
409 }
410 (FilterOperator::Gte, FilterValue::Expression(expr)) => {
411 col.gte(annotation_expr_to_safe_expr(expr))
412 }
413 (FilterOperator::Lt, FilterValue::Expression(expr)) => {
414 col.lt(annotation_expr_to_safe_expr(expr))
415 }
416 (FilterOperator::Lte, FilterValue::Expression(expr)) => {
417 col.lte(annotation_expr_to_safe_expr(expr))
418 }
419
420 (FilterOperator::Eq, v) => col.eq(filter_value_to_sea_value(v)),
422 (FilterOperator::Ne, v) => col.ne(filter_value_to_sea_value(v)),
423 (FilterOperator::Gt, v) => col.gt(filter_value_to_sea_value(v)),
424 (FilterOperator::Gte, v) => col.gte(filter_value_to_sea_value(v)),
425 (FilterOperator::Lt, v) => col.lt(filter_value_to_sea_value(v)),
426 (FilterOperator::Lte, v) => col.lte(filter_value_to_sea_value(v)),
427
428 (FilterOperator::Contains, FilterValue::String(s)) => {
430 col.like(format!("%{}%", escape_like_pattern(s)))
431 }
432 (FilterOperator::IContains, FilterValue::String(s)) => col.binary(
433 BinOper::ILike,
434 SimpleExpr::from(format!("%{}%", escape_like_pattern(s))),
435 ),
436 (FilterOperator::StartsWith, FilterValue::String(s)) => {
437 col.like(format!("{}%", escape_like_pattern(s)))
438 }
439 (FilterOperator::IStartsWith, FilterValue::String(s)) => col.binary(
440 BinOper::ILike,
441 SimpleExpr::from(format!("{}%", escape_like_pattern(s))),
442 ),
443 (FilterOperator::EndsWith, FilterValue::String(s)) => {
444 col.like(format!("%{}", escape_like_pattern(s)))
445 }
446 (FilterOperator::IEndsWith, FilterValue::String(s)) => col.binary(
447 BinOper::ILike,
448 SimpleExpr::from(format!("%{}", escape_like_pattern(s))),
449 ),
450 (FilterOperator::Regex, FilterValue::String(pattern)) => {
451 Expr::cust_with_values(format!("{} ~ ?", lhs_sql), [pattern.clone()]).into()
452 }
453 (FilterOperator::IRegex, FilterValue::String(pattern)) => {
454 Expr::cust_with_values(format!("{} ~* ?", lhs_sql), [pattern.clone()]).into()
455 }
456 (FilterOperator::Range, FilterValue::Range(start, end)) => Expr::cust_with_values(
457 format!("{} BETWEEN ? AND ?", lhs_sql),
458 [
459 filter_value_to_sea_value(start),
460 filter_value_to_sea_value(end),
461 ],
462 )
463 .into(),
464 (FilterOperator::In, FilterValue::Array(arr)) => {
466 if arr.is_empty() {
467 return None;
468 }
469 let values: Vec<Value> = arr.iter().map(|v| v.as_str().into_value()).collect();
470 col.is_in(values)
471 }
472 (FilterOperator::NotIn, FilterValue::Array(arr)) => {
473 if arr.is_empty() {
474 return None;
475 }
476 let values: Vec<Value> = arr.iter().map(|v| v.as_str().into_value()).collect();
477 col.is_not_in(values)
478 }
479 (FilterOperator::In, FilterValue::List(values)) => {
480 if values.is_empty() {
481 return None;
482 }
483 col.is_in(
484 values
485 .iter()
486 .map(filter_value_to_sea_value)
487 .collect::<Vec<_>>(),
488 )
489 }
490 (FilterOperator::NotIn, FilterValue::List(values)) => {
491 if values.is_empty() {
492 return None;
493 }
494 col.is_not_in(
495 values
496 .iter()
497 .map(filter_value_to_sea_value)
498 .collect::<Vec<_>>(),
499 )
500 }
501
502 (FilterOperator::In, FilterValue::String(s)) => {
503 let values: Vec<Value> = s.split(',').map(|v| v.trim().into_value()).collect();
504 col.is_in(values)
505 }
506 (FilterOperator::NotIn, FilterValue::String(s)) => {
507 let values: Vec<Value> = s.split(',').map(|v| v.trim().into_value()).collect();
508 col.is_not_in(values)
509 }
510
511 _ => return None,
513 };
514
515 Some(expr)
516}
517
518#[doc(hidden)]
520pub fn build_filter_condition(filters: &[Filter]) -> Option<Condition> {
521 if filters.is_empty() {
522 return None;
523 }
524
525 let mut condition = Condition::all();
526 let mut added = false;
527
528 for filter in filters {
529 if let Some(expr) = build_single_filter_expr(filter) {
530 condition = condition.add(expr);
531 added = true;
532 }
533 }
534
535 if added { Some(condition) } else { None }
536}
537
538#[doc(hidden)]
540pub const MAX_FILTER_DEPTH: usize = 100;
541
542#[doc(hidden)]
553pub fn build_composite_filter_condition(
554 filter_condition: &FilterCondition,
555) -> AdminResult<Option<Condition>> {
556 build_composite_filter_condition_with_depth(filter_condition, 0)
557}
558
559#[doc(hidden)]
561pub fn build_composite_filter_condition_with_depth(
562 filter_condition: &FilterCondition,
563 depth: usize,
564) -> AdminResult<Option<Condition>> {
565 if depth >= MAX_FILTER_DEPTH {
567 return Err(AdminError::ValidationError(format!(
568 "Filter condition exceeded maximum depth of {} levels",
569 MAX_FILTER_DEPTH
570 )));
571 }
572
573 match filter_condition {
574 FilterCondition::Single(filter) => {
575 Ok(build_single_filter_expr(filter).map(|expr| Condition::all().add(expr)))
576 }
577 FilterCondition::And(conditions) => {
578 if conditions.is_empty() {
579 return Ok(None);
580 }
581 let mut and_condition = Condition::all();
582 let mut added = false;
583 for cond in conditions {
584 if let Some(sub_cond) =
585 build_composite_filter_condition_with_depth(cond, depth + 1)?
586 {
587 and_condition = and_condition.add(sub_cond);
588 added = true;
589 }
590 }
591 if added {
594 Ok(Some(and_condition))
595 } else {
596 Ok(None)
597 }
598 }
599 FilterCondition::Or(conditions) => {
600 if conditions.is_empty() {
601 return Ok(None);
602 }
603 let mut or_condition = Condition::any();
604 let mut added = false;
605 for cond in conditions {
606 if let Some(sub_cond) =
607 build_composite_filter_condition_with_depth(cond, depth + 1)?
608 {
609 or_condition = or_condition.add(sub_cond);
610 added = true;
611 }
612 }
613 if added {
616 Ok(Some(or_condition))
617 } else {
618 Ok(None)
619 }
620 }
621 FilterCondition::Not(inner) => Ok(build_composite_filter_condition_with_depth(
622 inner,
623 depth + 1,
624 )?
625 .map(|inner_cond| inner_cond.not())),
626 }
627}
628
629fn build_combined_filter_condition(
630 filter_condition: Option<&FilterCondition>,
631 additional_filters: &[Filter],
632) -> AdminResult<(Condition, bool)> {
633 let mut combined = Condition::all();
634
635 if let Some(fc) = filter_condition
636 && let Some(cond) = build_composite_filter_condition(fc)?
637 {
638 combined = combined.add(cond);
639 }
640
641 if let Some(simple_cond) = build_filter_condition(additional_filters) {
642 combined = combined.add(simple_cond);
643 }
644
645 Ok((
646 combined,
647 !additional_filters.is_empty() || filter_condition.is_some(),
648 ))
649}
650
651fn extract_admin_list_total_count(
652 map: &serde_json::Map<String, serde_json::Value>,
653) -> AdminResult<u64> {
654 let count_value = map.get(ADMIN_LIST_TOTAL_COUNT_ALIAS).ok_or_else(|| {
655 AdminError::DatabaseError(format!(
656 "Admin list query result missing '{}' key",
657 ADMIN_LIST_TOTAL_COUNT_ALIAS
658 ))
659 })?;
660
661 if let Some(count) = count_value.as_u64() {
662 return Ok(count);
663 }
664
665 count_value
666 .as_i64()
667 .and_then(|count| if count >= 0 { Some(count as u64) } else { None })
668 .ok_or_else(|| {
669 AdminError::DatabaseError(format!(
670 "Admin list query returned invalid total count: {}",
671 count_value
672 ))
673 })
674}
675
676#[injectable(scope = Singleton, prebuilt = true)]
696#[derive(Clone)]
697pub struct AdminDatabase {
698 connection: Arc<DatabaseConnection>,
699}
700
701#[reinhardt_di::injectable_key]
703pub struct AdminDatabaseKey;
704
705impl AdminDatabase {
706 pub fn new(connection: DatabaseConnection) -> Self {
711 Self {
712 connection: Arc::new(connection),
713 }
714 }
715
716 pub fn from_arc(connection: Arc<DatabaseConnection>) -> Self {
721 Self { connection }
722 }
723
724 pub fn connection(&self) -> &DatabaseConnection {
726 &self.connection
727 }
728
729 pub fn connection_arc(&self) -> Arc<DatabaseConnection> {
733 Arc::clone(&self.connection)
734 }
735
736 pub async fn list<M: Model>(
757 &self,
758 table_name: &str,
759 filters: Vec<Filter>,
760 offset: u64,
761 limit: u64,
762 ) -> AdminResult<Vec<HashMap<String, serde_json::Value>>> {
763 let mut query = Query::select()
768 .from(Alias::new(table_name))
769 .column(ColumnRef::Asterisk)
770 .to_owned();
771
772 if let Some(condition) = build_filter_condition(&filters) {
774 query.cond_where(condition);
775 }
776
777 query.limit(limit).offset(offset);
779
780 let (sql, values) = query.build(PostgresQueryBuilder);
782 let params = convert_values(values);
783 let rows = self
784 .connection
785 .query(&sql, params)
786 .await
787 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
788
789 Ok(rows
791 .into_iter()
792 .filter_map(|row| {
793 if let serde_json::Value::Object(map) = row.data {
795 Some(
796 map.into_iter()
797 .collect::<HashMap<String, serde_json::Value>>(),
798 )
799 } else {
800 None
801 }
802 })
803 .collect())
804 }
805
806 pub async fn list_with_condition<M: Model>(
820 &self,
821 table_name: &str,
822 filter_condition: Option<&FilterCondition>,
823 additional_filters: Vec<Filter>,
824 sort_by: Option<&str>,
825 offset: u64,
826 limit: u64,
827 ) -> AdminResult<Vec<HashMap<String, serde_json::Value>>> {
828 let mut query = Query::select()
833 .from(Alias::new(table_name))
834 .column(ColumnRef::Asterisk)
835 .to_owned();
836
837 let (combined, has_filter) =
838 build_combined_filter_condition(filter_condition, &additional_filters)?;
839
840 if has_filter {
841 query.cond_where(combined);
842 }
843
844 if let Some(sort_str) = sort_by {
846 let (field, is_desc) = if let Some(stripped) = sort_str.strip_prefix('-') {
847 (stripped, true)
848 } else {
849 (sort_str, false)
850 };
851
852 let col = Alias::new(field);
853 if is_desc {
854 query.order_by(col, Order::Desc);
855 } else {
856 query.order_by(col, Order::Asc);
857 }
858 }
859
860 query.limit(limit).offset(offset);
862
863 let (sql, values) = query.build(PostgresQueryBuilder);
865 let params = convert_values(values);
866 let rows = self
867 .connection
868 .query(&sql, params)
869 .await
870 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
871
872 Ok(rows
874 .into_iter()
875 .filter_map(|row| {
876 if let serde_json::Value::Object(map) = row.data {
877 Some(
878 map.into_iter()
879 .filter(|(key, _)| !SENSITIVE_FIELDS.contains(&key.as_str()))
880 .collect::<HashMap<String, serde_json::Value>>(),
881 )
882 } else {
883 None
884 }
885 })
886 .collect())
887 }
888
889 pub async fn list_with_condition_and_count<M: Model>(
895 &self,
896 table_name: &str,
897 filter_condition: Option<&FilterCondition>,
898 additional_filters: Vec<Filter>,
899 sort_by: Option<&str>,
900 offset: u64,
901 limit: u64,
902 ) -> AdminResult<(Vec<HashMap<String, serde_json::Value>>, u64)> {
903 let mut query = Query::select()
907 .from(Alias::new(table_name))
908 .column(ColumnRef::Asterisk)
909 .expr_as(
910 Expr::cust("COUNT(*) OVER()"),
911 Alias::new(ADMIN_LIST_TOTAL_COUNT_ALIAS),
912 )
913 .to_owned();
914
915 let (combined, has_filter) =
916 build_combined_filter_condition(filter_condition, &additional_filters)?;
917
918 if has_filter {
919 query.cond_where(combined);
920 }
921
922 if let Some(sort_str) = sort_by {
923 let (field, is_desc) = if let Some(stripped) = sort_str.strip_prefix('-') {
924 (stripped, true)
925 } else {
926 (sort_str, false)
927 };
928
929 let col = Alias::new(field);
930 if is_desc {
931 query.order_by(col, Order::Desc);
932 } else {
933 query.order_by(col, Order::Asc);
934 }
935 }
936
937 query.limit(limit).offset(offset);
938
939 let (sql, values) = query.build(PostgresQueryBuilder);
940 let params = convert_values(values);
941 let rows = self
942 .connection
943 .query(&sql, params)
944 .await
945 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
946
947 if rows.is_empty() {
948 if offset == 0 && limit > 0 {
949 return Ok((Vec::new(), 0));
950 }
951
952 let count = self
953 .count_with_condition::<M>(table_name, filter_condition, additional_filters)
954 .await?;
955 return Ok((Vec::new(), count));
956 }
957
958 let mut total_count = None;
959 let results = rows
960 .into_iter()
961 .filter_map(|row| {
962 if let serde_json::Value::Object(mut map) = row.data {
963 if total_count.is_none() {
964 total_count = Some(extract_admin_list_total_count(&map));
965 }
966
967 map.remove(ADMIN_LIST_TOTAL_COUNT_ALIAS);
968
969 Some(
970 map.into_iter()
971 .filter(|(key, _)| !SENSITIVE_FIELDS.contains(&key.as_str()))
972 .collect::<HashMap<String, serde_json::Value>>(),
973 )
974 } else {
975 None
976 }
977 })
978 .collect::<Vec<_>>();
979
980 let total_count = total_count.unwrap_or_else(|| {
981 Err(AdminError::DatabaseError(
982 "Admin list query returned no object rows".to_string(),
983 ))
984 })?;
985
986 Ok((results, total_count))
987 }
988
989 pub async fn count_with_condition<M: Model>(
997 &self,
998 table_name: &str,
999 filter_condition: Option<&FilterCondition>,
1000 additional_filters: Vec<Filter>,
1001 ) -> AdminResult<u64> {
1002 let mut query = Query::select()
1003 .from(Alias::new(table_name))
1004 .expr(Expr::cust("COUNT(*) AS count"))
1005 .to_owned();
1006
1007 let (combined, has_filter) =
1008 build_combined_filter_condition(filter_condition, &additional_filters)?;
1009
1010 if has_filter {
1011 query.cond_where(combined);
1012 }
1013
1014 let (sql, values) = query.build(PostgresQueryBuilder);
1015 let params = convert_values(values);
1016 let row = self
1017 .connection
1018 .query_one(&sql, params)
1019 .await
1020 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1021
1022 let count = extract_count_from_row(&row.data)?;
1024
1025 Ok(count)
1026 }
1027
1028 pub async fn get<M: Model>(
1045 &self,
1046 table_name: &str,
1047 pk_field: &str,
1048 id: &str,
1049 ) -> AdminResult<Option<HashMap<String, serde_json::Value>>> {
1050 let pk_value = parse_pk_value(table_name, pk_field, id);
1051
1052 let query = Query::select()
1056 .from(Alias::new(table_name))
1057 .column(ColumnRef::Asterisk)
1058 .and_where(Expr::col(Alias::new(pk_field)).eq(pk_value))
1059 .to_owned();
1060
1061 let (sql, values) = query.build(PostgresQueryBuilder);
1062 let params = convert_values(values);
1063 let row = self
1064 .connection
1065 .query_optional(&sql, params)
1066 .await
1067 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1068
1069 Ok(row.and_then(|r| {
1070 if let serde_json::Value::Object(map) = r.data {
1072 Some(
1073 map.into_iter()
1074 .collect::<HashMap<String, serde_json::Value>>(),
1075 )
1076 } else {
1077 None
1078 }
1079 }))
1080 }
1081
1082 pub async fn create<M: Model>(
1104 &self,
1105 table_name: &str,
1106 pk_field: Option<&str>,
1107 data: HashMap<String, serde_json::Value>,
1108 ) -> AdminResult<u64> {
1109 let pk_field = pk_field.unwrap_or("id");
1110 let mut query = Query::insert()
1111 .into_table(Alias::new(table_name))
1112 .to_owned();
1113
1114 let mut sorted_keys: Vec<String> = data.keys().cloned().collect();
1118 sorted_keys.sort();
1119
1120 let mut columns = Vec::new();
1122 let mut values = Vec::new();
1123
1124 for key in sorted_keys {
1125 let value = data.get(&key).cloned().unwrap_or(serde_json::Value::Null);
1126 columns.push(Alias::new(&key));
1127
1128 let sea_value = json_to_sea_value(value);
1129 values.push(sea_value);
1130 }
1131
1132 query.columns(columns).values(values).map_err(|e| {
1134 AdminError::DatabaseError(format!("column/value count mismatch: {}", e))
1135 })?;
1136
1137 query.returning([Alias::new(pk_field)]);
1139
1140 let (sql, values) = query.build(PostgresQueryBuilder);
1141 let params = convert_values(values);
1142 let row = self
1143 .connection
1144 .query_one(&sql, params)
1145 .await
1146 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1147
1148 match row.data.get(pk_field) {
1150 Some(serde_json::Value::Number(n)) => n.as_u64().ok_or_else(|| {
1151 AdminError::DatabaseError(format!(
1152 "RETURNING clause for '{}' returned non-unsigned-integer: {}",
1153 pk_field, n
1154 ))
1155 }),
1156 Some(serde_json::Value::String(_)) => {
1157 Ok(1)
1160 }
1161 _ => Err(AdminError::DatabaseError(format!(
1162 "RETURNING clause did not return expected primary key field '{}'",
1163 pk_field
1164 ))),
1165 }
1166 }
1167
1168 pub async fn update<M: Model>(
1189 &self,
1190 table_name: &str,
1191 pk_field: &str,
1192 id: &str,
1193 data: HashMap<String, serde_json::Value>,
1194 ) -> AdminResult<u64> {
1195 let mut query = Query::update().table(Alias::new(table_name)).to_owned();
1196
1197 let mut sorted_keys: Vec<String> = data.keys().cloned().collect();
1199 sorted_keys.sort();
1200
1201 for key in sorted_keys {
1203 let value = data.get(&key).cloned().unwrap_or(serde_json::Value::Null);
1204 let sea_value = json_to_sea_value(value);
1205 query.value(Alias::new(&key), sea_value);
1206 }
1207
1208 let pk_value = parse_pk_value(table_name, pk_field, id);
1209 query.and_where(Expr::col(Alias::new(pk_field)).eq(pk_value));
1210
1211 let (sql, values) = query.build(PostgresQueryBuilder);
1212 let params = convert_values(values);
1213 let affected = self
1214 .connection
1215 .execute(&sql, params)
1216 .await
1217 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1218
1219 Ok(affected)
1220 }
1221
1222 pub async fn delete<M: Model>(
1239 &self,
1240 table_name: &str,
1241 pk_field: &str,
1242 id: &str,
1243 ) -> AdminResult<u64> {
1244 let pk_value = parse_pk_value(table_name, pk_field, id);
1245
1246 let query = Query::delete()
1247 .from_table(Alias::new(table_name))
1248 .and_where(Expr::col(Alias::new(pk_field)).eq(pk_value))
1249 .to_owned();
1250
1251 let (sql, values) = query.build(PostgresQueryBuilder);
1252 let params = convert_values(values);
1253 let affected = self
1254 .connection
1255 .execute(&sql, params)
1256 .await
1257 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1258
1259 Ok(affected)
1260 }
1261
1262 pub async fn bulk_delete<M: Model>(
1280 &self,
1281 table_name: &str,
1282 pk_field: &str,
1283 ids: Vec<String>,
1284 ) -> AdminResult<u64> {
1285 self.bulk_delete_by_table(table_name, pk_field, ids).await
1286 }
1287
1288 pub async fn bulk_delete_by_table(
1310 &self,
1311 table_name: &str,
1312 pk_field: &str,
1313 ids: Vec<String>,
1314 ) -> AdminResult<u64> {
1315 if ids.is_empty() {
1316 return Ok(0);
1317 }
1318
1319 let pk_values = parse_pk_values(table_name, pk_field, &ids);
1320
1321 let query = Query::delete()
1322 .from_table(Alias::new(table_name))
1323 .and_where(Expr::col(Alias::new(pk_field)).is_in(pk_values))
1324 .to_owned();
1325
1326 let (sql, values) = query.build(PostgresQueryBuilder);
1327 let params = convert_values(values);
1328 let affected = self
1329 .connection
1330 .execute(&sql, params)
1331 .await
1332 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1333
1334 Ok(affected)
1335 }
1336
1337 pub async fn count<M: Model>(
1358 &self,
1359 table_name: &str,
1360 filters: Vec<Filter>,
1361 ) -> AdminResult<u64> {
1362 let mut query = Query::select()
1363 .from(Alias::new(table_name))
1364 .expr(Expr::cust("COUNT(*) AS count"))
1365 .to_owned();
1366
1367 if let Some(condition) = build_filter_condition(&filters) {
1369 query.cond_where(condition);
1370 }
1371
1372 let (sql, values) = query.build(PostgresQueryBuilder);
1373 let params = convert_values(values);
1374 let row = self
1375 .connection
1376 .query_one(&sql, params)
1377 .await
1378 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1379
1380 let count = extract_count_from_row(&row.data)?;
1382
1383 Ok(count)
1384 }
1385}
1386
1387#[doc(hidden)]
1397pub fn extract_count_from_row(data: &serde_json::Value) -> AdminResult<u64> {
1398 if let Some(count_value) = data.get("count") {
1399 return count_value.as_i64().map(|v| v as u64).ok_or_else(|| {
1400 AdminError::DatabaseError(format!(
1401 "COUNT query returned non-integer value: {}",
1402 count_value
1403 ))
1404 });
1405 }
1406
1407 if let Some(obj) = data.as_object() {
1410 let available_keys: Vec<&String> = obj.keys().collect();
1411 return Err(AdminError::DatabaseError(format!(
1412 "COUNT query result missing 'count' key, available keys: {:?}",
1413 available_keys
1414 )));
1415 }
1416
1417 Err(AdminError::DatabaseError(format!(
1418 "COUNT query returned unexpected data format: {}",
1419 data
1420 )))
1421}
1422
1423#[async_trait]
1435impl Injectable for AdminDatabase {
1436 async fn inject(ctx: &InjectionContext) -> DiResult<Self> {
1437 if let Some(db) = ctx.get_singleton::<Self>() {
1439 return Ok((*db).clone());
1440 }
1441
1442 let conn = ctx.get_singleton::<DatabaseConnection>().ok_or_else(|| {
1444 reinhardt_di::DiError::NotRegistered {
1445 type_name: "AdminDatabase".into(),
1446 hint: "DatabaseConnection must be registered as a singleton. \
1447 Use InjectionContextBuilder::singleton(db_connection) during setup."
1448 .into(),
1449 }
1450 })?;
1451
1452 let db = AdminDatabase::from_arc(conn);
1453 ctx.set_singleton(db.clone());
1455 Ok(db)
1456 }
1457}
1458
1459#[reinhardt_di::injectable(scope = "singleton")]
1460async fn admin_database_provider(
1461 #[inject] db: AdminDatabase,
1462) -> FactoryOutput<AdminDatabaseKey, AdminDatabase> {
1463 FactoryOutput::new(db)
1464}
1465
1466fn __register_admin_database(registry: &reinhardt_di::DependencyRegistry) {
1470 registry.register::<AdminDatabase>(
1471 reinhardt_di::DependencyScope::Singleton,
1472 reinhardt_di::InjectableFactory::<AdminDatabase>::new(),
1473 );
1474}
1475
1476reinhardt_di::inventory::submit! {
1477 reinhardt_di::InjectableRegistration::new(
1478 __register_admin_database
1479 )
1480}
1481
1482#[cfg(all(test, server))]
1483mod tests {
1484 use super::*;
1485 use reinhardt_db::orm::annotation::Expression;
1486 use reinhardt_db::orm::expressions::{F, OuterRef};
1487 use rstest::rstest;
1488
1489 #[rstest]
1492 fn test_escape_like_pattern_percent() {
1493 let input = "100%";
1495
1496 let result = escape_like_pattern(input);
1498
1499 assert_eq!(result, "100\\%");
1501 }
1502
1503 #[rstest]
1504 fn test_escape_like_pattern_underscore() {
1505 let input = "user_name";
1507
1508 let result = escape_like_pattern(input);
1510
1511 assert_eq!(result, "user\\_name");
1513 }
1514
1515 #[rstest]
1516 fn test_escape_like_pattern_backslash() {
1517 let input = "path\\to";
1519
1520 let result = escape_like_pattern(input);
1522
1523 assert_eq!(result, "path\\\\to");
1525 }
1526
1527 #[rstest]
1528 fn test_escape_like_pattern_combined() {
1529 let input = "100%_done";
1531
1532 let result = escape_like_pattern(input);
1534
1535 assert_eq!(result, "100\\%\\_done");
1537 }
1538
1539 #[rstest]
1540 fn test_escape_like_pattern_no_special_chars() {
1541 let input = "normal text";
1543
1544 let result = escape_like_pattern(input);
1546
1547 assert_eq!(result, "normal text");
1549 }
1550
1551 #[rstest]
1557 #[case("%wildcard%", "\\%wildcard\\%")]
1558 #[case("under_score", "under\\_score")]
1559 #[case("back\\slash", "back\\\\slash")]
1560 #[case("%_%", "\\%\\_\\%")]
1561 fn test_escape_like_pattern_sanitizes_special_chars(
1562 #[case] input: &str,
1563 #[case] expected: &str,
1564 ) {
1565 let escaped = escape_like_pattern(input);
1568 assert_eq!(
1570 escaped, expected,
1571 "input={input:?} was not correctly escaped"
1572 );
1573 }
1574
1575 #[test]
1578 fn test_build_composite_single_condition() {
1579 let filter = Filter::new(
1580 "name".to_string(),
1581 FilterOperator::Eq,
1582 FilterValue::String("Alice".to_string()),
1583 );
1584 let condition = FilterCondition::Single(filter);
1585
1586 let result = build_composite_filter_condition(&condition);
1587
1588 assert!(result.is_ok());
1589 let result = result.unwrap();
1590 assert!(result.is_some());
1591 let cond = result.unwrap();
1593 let query = Query::select()
1594 .from(Alias::new("users"))
1595 .column(ColumnRef::Asterisk)
1596 .cond_where(cond)
1597 .to_string(PostgresQueryBuilder);
1598 assert!(query.contains("\"name\""));
1599 assert!(query.contains("'Alice'"));
1600 }
1601
1602 #[test]
1603 fn test_build_composite_or_condition() {
1604 let filter1 = Filter::new(
1605 "name".to_string(),
1606 FilterOperator::Contains,
1607 FilterValue::String("Alice".to_string()),
1608 );
1609 let filter2 = Filter::new(
1610 "email".to_string(),
1611 FilterOperator::Contains,
1612 FilterValue::String("alice".to_string()),
1613 );
1614
1615 let condition = FilterCondition::Or(vec![
1616 FilterCondition::Single(filter1),
1617 FilterCondition::Single(filter2),
1618 ]);
1619
1620 let result = build_composite_filter_condition(&condition);
1621
1622 assert!(result.is_ok());
1623 let result = result.unwrap();
1624 assert!(result.is_some());
1625 let cond = result.unwrap();
1626 let query = Query::select()
1627 .from(Alias::new("users"))
1628 .column(ColumnRef::Asterisk)
1629 .cond_where(cond)
1630 .to_string(PostgresQueryBuilder);
1631 assert!(query.contains("\"name\""));
1633 assert!(query.contains("\"email\""));
1634 assert!(query.contains("OR"));
1635 }
1636
1637 #[test]
1638 fn test_build_composite_and_condition() {
1639 let filter1 = Filter::new(
1640 "is_active".to_string(),
1641 FilterOperator::Eq,
1642 FilterValue::Boolean(true),
1643 );
1644 let filter2 = Filter::new(
1645 "is_staff".to_string(),
1646 FilterOperator::Eq,
1647 FilterValue::Boolean(true),
1648 );
1649
1650 let condition = FilterCondition::And(vec![
1651 FilterCondition::Single(filter1),
1652 FilterCondition::Single(filter2),
1653 ]);
1654
1655 let result = build_composite_filter_condition(&condition);
1656
1657 assert!(result.is_ok());
1658 let result = result.unwrap();
1659 assert!(result.is_some());
1660 let cond = result.unwrap();
1661 let query = Query::select()
1662 .from(Alias::new("users"))
1663 .column(ColumnRef::Asterisk)
1664 .cond_where(cond)
1665 .to_string(PostgresQueryBuilder);
1666 assert!(query.contains("\"is_active\""));
1668 assert!(query.contains("\"is_staff\""));
1669 assert!(query.contains("AND"));
1670 }
1671
1672 #[test]
1673 fn test_build_composite_nested_condition() {
1674 let filter_name = Filter::new(
1676 "name".to_string(),
1677 FilterOperator::Contains,
1678 FilterValue::String("Alice".to_string()),
1679 );
1680 let filter_email = Filter::new(
1681 "email".to_string(),
1682 FilterOperator::Contains,
1683 FilterValue::String("alice".to_string()),
1684 );
1685 let filter_active = Filter::new(
1686 "is_active".to_string(),
1687 FilterOperator::Eq,
1688 FilterValue::Boolean(true),
1689 );
1690
1691 let or_condition = FilterCondition::Or(vec![
1692 FilterCondition::Single(filter_name),
1693 FilterCondition::Single(filter_email),
1694 ]);
1695
1696 let and_condition =
1697 FilterCondition::And(vec![or_condition, FilterCondition::Single(filter_active)]);
1698
1699 let result = build_composite_filter_condition(&and_condition);
1700
1701 assert!(result.is_ok());
1702 let result = result.unwrap();
1703 assert!(result.is_some());
1704 let cond = result.unwrap();
1705 let query = Query::select()
1706 .from(Alias::new("users"))
1707 .column(ColumnRef::Asterisk)
1708 .cond_where(cond)
1709 .to_string(PostgresQueryBuilder);
1710 assert!(query.contains("\"name\""));
1712 assert!(query.contains("\"email\""));
1713 assert!(query.contains("\"is_active\""));
1714 assert!(query.contains("OR"));
1715 assert!(query.contains("AND"));
1716 }
1717
1718 #[test]
1719 fn test_build_composite_empty_or() {
1720 let condition = FilterCondition::Or(vec![]);
1721
1722 let result = build_composite_filter_condition(&condition);
1723
1724 assert!(result.is_ok());
1726 assert!(result.unwrap().is_none());
1727 }
1728
1729 #[test]
1730 fn test_build_composite_empty_and() {
1731 let condition = FilterCondition::And(vec![]);
1732
1733 let result = build_composite_filter_condition(&condition);
1734
1735 assert!(result.is_ok());
1737 assert!(result.unwrap().is_none());
1738 }
1739
1740 #[test]
1741 fn test_build_composite_depth_overflow_returns_error() {
1742 let base_filter = Filter::new(
1744 "name".to_string(),
1745 FilterOperator::Eq,
1746 FilterValue::String("Alice".to_string()),
1747 );
1748 let mut condition = FilterCondition::Single(base_filter);
1749 for _ in 0..=MAX_FILTER_DEPTH {
1751 condition = FilterCondition::And(vec![condition]);
1752 }
1753
1754 let result = build_composite_filter_condition(&condition);
1755
1756 assert!(result.is_err());
1757 let err = result.unwrap_err();
1758 assert!(matches!(err, AdminError::ValidationError(_)));
1759 let err_msg = err.to_string();
1760 assert!(
1761 err_msg.contains("exceeded maximum depth"),
1762 "Error message should mention exceeded depth, got: {}",
1763 err_msg
1764 );
1765 }
1766
1767 #[test]
1770 fn test_build_single_filter_expr_field_ref_eq() {
1771 let filter = Filter::new(
1772 "price".to_string(),
1773 FilterOperator::Eq,
1774 FilterValue::FieldRef(F::new("discount_price")),
1775 );
1776 let result = build_single_filter_expr(&filter);
1777 assert!(result.is_some());
1778
1779 let query = Query::select()
1780 .from(Alias::new("products"))
1781 .column(ColumnRef::Asterisk)
1782 .cond_where(Condition::all().add(result.unwrap()))
1783 .to_string(PostgresQueryBuilder);
1784 assert!(query.contains("\"price\""));
1785 assert!(query.contains("\"discount_price\""));
1786 }
1787
1788 #[test]
1789 fn test_build_single_filter_expr_field_ref_gt() {
1790 let filter = Filter::new(
1791 "price".to_string(),
1792 FilterOperator::Gt,
1793 FilterValue::FieldRef(F::new("cost")),
1794 );
1795 let result = build_single_filter_expr(&filter);
1796 assert!(result.is_some());
1797 }
1798
1799 #[test]
1800 fn test_build_single_filter_expr_field_ref_all_operators() {
1801 let operators = [
1802 FilterOperator::Eq,
1803 FilterOperator::Ne,
1804 FilterOperator::Gt,
1805 FilterOperator::Gte,
1806 FilterOperator::Lt,
1807 FilterOperator::Lte,
1808 ];
1809
1810 for op in operators {
1811 let filter = Filter::new(
1812 "field_a".to_string(),
1813 op.clone(),
1814 FilterValue::FieldRef(F::new("field_b")),
1815 );
1816 let result = build_single_filter_expr(&filter);
1817 assert!(
1818 result.is_some(),
1819 "FieldRef with {:?} should produce Some",
1820 op
1821 );
1822 }
1823 }
1824
1825 #[test]
1826 fn test_build_single_filter_expr_outer_ref() {
1827 let filter = Filter::new(
1828 "author_id".to_string(),
1829 FilterOperator::Eq,
1830 FilterValue::OuterRef(OuterRef::new("authors.id")),
1831 );
1832 let result = build_single_filter_expr(&filter);
1833 assert!(result.is_some());
1834
1835 let query = Query::select()
1836 .from(Alias::new("books"))
1837 .column(ColumnRef::Asterisk)
1838 .cond_where(Condition::all().add(result.unwrap()))
1839 .to_string(PostgresQueryBuilder);
1840 assert!(query.contains("author_id"));
1841 assert!(query.contains("authors.id"));
1842 }
1843
1844 #[test]
1845 fn test_build_single_filter_expr_outer_ref_all_operators() {
1846 let operators = [
1847 FilterOperator::Eq,
1848 FilterOperator::Ne,
1849 FilterOperator::Gt,
1850 FilterOperator::Gte,
1851 FilterOperator::Lt,
1852 FilterOperator::Lte,
1853 ];
1854
1855 for op in operators {
1856 let filter = Filter::new(
1857 "child_id".to_string(),
1858 op.clone(),
1859 FilterValue::OuterRef(OuterRef::new("parent.id")),
1860 );
1861 let result = build_single_filter_expr(&filter);
1862 assert!(
1863 result.is_some(),
1864 "OuterRef with {:?} should produce Some",
1865 op
1866 );
1867 }
1868 }
1869
1870 #[test]
1871 fn test_build_single_filter_expr_expression() {
1872 use reinhardt_db::orm::annotation::{AnnotationValue, Value};
1873
1874 let expr = Expression::Multiply(
1876 Box::new(AnnotationValue::Field(F::new("cost"))),
1877 Box::new(AnnotationValue::Value(Value::Int(2))),
1878 );
1879 let filter = Filter::new(
1880 "price".to_string(),
1881 FilterOperator::Gt,
1882 FilterValue::Expression(expr),
1883 );
1884 let result = build_single_filter_expr(&filter);
1885 assert!(result.is_some());
1886 }
1887
1888 #[test]
1889 fn test_build_single_filter_expr_expression_all_operators() {
1890 use reinhardt_db::orm::annotation::{AnnotationValue, Value as OrmValue};
1891
1892 let operators = [
1893 FilterOperator::Eq,
1894 FilterOperator::Ne,
1895 FilterOperator::Gt,
1896 FilterOperator::Gte,
1897 FilterOperator::Lt,
1898 FilterOperator::Lte,
1899 ];
1900
1901 for op in operators {
1902 let expr = Expression::Add(
1903 Box::new(AnnotationValue::Field(F::new("base"))),
1904 Box::new(AnnotationValue::Value(OrmValue::Int(10))),
1905 );
1906 let filter = Filter::new(
1907 "total".to_string(),
1908 op.clone(),
1909 FilterValue::Expression(expr),
1910 );
1911 let result = build_single_filter_expr(&filter);
1912 assert!(
1913 result.is_some(),
1914 "Expression with {:?} should produce Some",
1915 op
1916 );
1917 }
1918 }
1919
1920 #[test]
1921 fn test_build_single_filter_expr_uses_transformed_filter_lhs() {
1922 let filter = reinhardt_db::orm::expressions::FieldRef::<(), i64>::new("created_at")
1924 .year()
1925 .range(2024, 2026);
1926
1927 let result = build_single_filter_expr(&filter);
1929
1930 assert!(result.is_some());
1932 let query = Query::select()
1933 .from(Alias::new("users"))
1934 .column(ColumnRef::Asterisk)
1935 .cond_where(Condition::all().add(result.unwrap()))
1936 .to_string(PostgresQueryBuilder);
1937 assert_eq!(
1938 query,
1939 r#"SELECT * FROM "users" WHERE EXTRACT(YEAR FROM "created_at") BETWEEN 2024 AND 2026"#
1940 );
1941 }
1942
1943 #[test]
1944 fn test_filter_value_to_sea_value_field_ref_fallback() {
1945 let value = FilterValue::FieldRef(F::new("test_field"));
1946 let sea_value = filter_value_to_sea_value(&value);
1947
1948 match sea_value {
1950 Value::String(Some(s)) => assert_eq!(s.as_str(), "test_field"),
1951 _ => panic!("Expected String value"),
1952 }
1953 }
1954
1955 #[test]
1956 fn test_filter_value_to_sea_value_outer_ref_fallback() {
1957 let value = FilterValue::OuterRef(OuterRef::new("outer.field"));
1958 let sea_value = filter_value_to_sea_value(&value);
1959
1960 match sea_value {
1962 Value::String(Some(s)) => assert_eq!(s.as_str(), "outer.field"),
1963 _ => panic!("Expected String value"),
1964 }
1965 }
1966
1967 #[test]
1968 fn test_filter_value_to_sea_value_expression_fallback() {
1969 use reinhardt_db::orm::annotation::{AnnotationValue, Value as OrmValue};
1970
1971 let expr = Expression::Add(
1972 Box::new(AnnotationValue::Field(F::new("a"))),
1973 Box::new(AnnotationValue::Value(OrmValue::Int(1))),
1974 );
1975 let value = FilterValue::Expression(expr);
1976 let sea_value = filter_value_to_sea_value(&value);
1977
1978 match sea_value {
1980 Value::String(Some(s)) => {
1981 assert!(s.contains("a"), "SQL should contain field name 'a'");
1982 assert!(s.contains("1"), "SQL should contain value '1'");
1983 }
1984 _ => panic!("Expected String value"),
1985 }
1986 }
1987
1988 #[rstest]
1991 fn test_insert_values_mismatch_returns_error_not_panic() {
1992 let mut query = Query::insert()
1996 .into_table(Alias::new("test_table"))
1997 .to_owned();
1998
1999 let columns = vec![Alias::new("col1"), Alias::new("col2"), Alias::new("col3")];
2000 let values = vec![Value::String(Some(Box::new("val1".to_string())))]; let result = query.columns(columns).values(values);
2004
2005 assert!(result.is_err());
2007 }
2008
2009 #[rstest]
2010 fn test_insert_values_matching_count_succeeds() {
2011 let mut query = Query::insert()
2013 .into_table(Alias::new("test_table"))
2014 .to_owned();
2015
2016 let columns = vec![Alias::new("col1"), Alias::new("col2")];
2017 let values = vec![
2018 Value::String(Some(Box::new("val1".to_string()))),
2019 Value::String(Some(Box::new("val2".to_string()))),
2020 ];
2021
2022 let result = query.columns(columns).values(values);
2024
2025 assert!(result.is_ok());
2027 }
2028
2029 #[test]
2032 fn test_outer_ref_filter_uses_safe_column_api() {
2033 let filter = Filter::new(
2035 "author_id".to_string(),
2036 FilterOperator::Eq,
2037 FilterValue::OuterRef(OuterRef::new("users.id")),
2038 );
2039
2040 let result = build_single_filter_expr(&filter);
2042
2043 assert!(result.is_some());
2045 let expr = result.unwrap();
2046 let query = Query::select()
2047 .from(Alias::new("books"))
2048 .column(ColumnRef::Asterisk)
2049 .cond_where(Condition::all().add(expr))
2050 .to_string(PostgresQueryBuilder);
2051 assert!(
2053 query.contains("\"author_id\""),
2054 "Column should be properly quoted: {}",
2055 query
2056 );
2057 }
2058
2059 #[test]
2060 fn test_outer_ref_injection_attempt_is_safely_quoted() {
2061 let filter = Filter::new(
2063 "id".to_string(),
2064 FilterOperator::Eq,
2065 FilterValue::OuterRef(OuterRef::new("id; DROP TABLE users; --")),
2066 );
2067
2068 let result = build_single_filter_expr(&filter);
2070
2071 assert!(result.is_some());
2073 let expr = result.unwrap();
2074 let query = Query::select()
2075 .from(Alias::new("items"))
2076 .column(ColumnRef::Asterisk)
2077 .cond_where(Condition::all().add(expr))
2078 .to_string(PostgresQueryBuilder);
2079 assert!(
2084 query.contains("\"id; DROP TABLE users; --\""),
2085 "Injection payload should be enclosed in double quotes as identifier: {}",
2086 query
2087 );
2088 let unquoted_parts: Vec<&str> = query.split('"').enumerate()
2091 .filter(|(i, _)| i % 2 == 0) .map(|(_, s)| s)
2093 .collect();
2094 let unquoted_sql = unquoted_parts.join("");
2095 assert!(
2096 !unquoted_sql.contains(';'),
2097 "No semicolons should appear outside quoted identifiers: {}",
2098 query
2099 );
2100 }
2101
2102 #[test]
2103 fn test_expression_filter_uses_safe_api() {
2104 use reinhardt_db::orm::annotation::AnnotationValue;
2105
2106 let expr = Expression::Multiply(
2108 Box::new(AnnotationValue::Field(F::new("unit_price"))),
2109 Box::new(AnnotationValue::Field(F::new("quantity"))),
2110 );
2111 let filter = Filter::new(
2112 "total".to_string(),
2113 FilterOperator::Eq,
2114 FilterValue::Expression(expr),
2115 );
2116
2117 let result = build_single_filter_expr(&filter);
2119
2120 assert!(result.is_some());
2122 let sea_expr = result.unwrap();
2123 let query = Query::select()
2124 .from(Alias::new("orders"))
2125 .column(ColumnRef::Asterisk)
2126 .cond_where(Condition::all().add(sea_expr))
2127 .to_string(PostgresQueryBuilder);
2128 assert!(
2129 query.contains("\"total\""),
2130 "Left side should be quoted: {}",
2131 query
2132 );
2133 }
2134
2135 #[test]
2136 fn test_expression_filter_with_literal_value() {
2137 use reinhardt_db::orm::annotation::{AnnotationValue, Value as OrmValue};
2138
2139 let expr = Expression::Add(
2141 Box::new(AnnotationValue::Field(F::new("price"))),
2142 Box::new(AnnotationValue::Value(OrmValue::Int(100))),
2143 );
2144 let filter = Filter::new(
2145 "adjusted_price".to_string(),
2146 FilterOperator::Gt,
2147 FilterValue::Expression(expr),
2148 );
2149
2150 let result = build_single_filter_expr(&filter);
2152
2153 assert!(result.is_some());
2155 }
2156
2157 #[test]
2158 fn test_outer_ref_all_operators_use_safe_api() {
2159 let operators = vec![
2161 FilterOperator::Eq,
2162 FilterOperator::Ne,
2163 FilterOperator::Gt,
2164 FilterOperator::Gte,
2165 FilterOperator::Lt,
2166 FilterOperator::Lte,
2167 ];
2168
2169 for op in operators {
2170 let filter = Filter::new(
2171 "field_a".to_string(),
2172 op.clone(),
2173 FilterValue::OuterRef(OuterRef::new("field_b")),
2174 );
2175 let result = build_single_filter_expr(&filter);
2176 assert!(
2177 result.is_some(),
2178 "OuterRef with {:?} should produce Some",
2179 op
2180 );
2181 }
2182 }
2183
2184 #[test]
2187 fn test_coalesce_expression_uses_safe_parameterized_api() {
2188 use reinhardt_db::orm::annotation::{AnnotationValue, Value as OrmValue};
2189
2190 let expr = Expression::Coalesce(vec![
2192 AnnotationValue::Field(F::new("field_a")),
2193 AnnotationValue::Value(OrmValue::Int(0)),
2194 ]);
2195 let filter = Filter::new(
2196 "result".to_string(),
2197 FilterOperator::Gt,
2198 FilterValue::Expression(expr),
2199 );
2200
2201 let result = build_single_filter_expr(&filter);
2203
2204 assert!(result.is_some());
2206 let sea_expr = result.unwrap();
2207 let query = Query::select()
2208 .from(Alias::new("items"))
2209 .column(ColumnRef::Asterisk)
2210 .cond_where(Condition::all().add(sea_expr))
2211 .to_string(PostgresQueryBuilder);
2212 assert!(
2213 query.contains("COALESCE"),
2214 "Should contain COALESCE function: {}",
2215 query
2216 );
2217 assert!(
2218 query.contains("\"result\""),
2219 "Left side should be quoted: {}",
2220 query
2221 );
2222 }
2223
2224 #[test]
2225 fn test_case_expression_uses_safe_api() {
2226 use reinhardt_db::orm::annotation::{
2227 AnnotationValue, Value as OrmValue, When as AnnotWhen,
2228 };
2229 use reinhardt_db::orm::expressions::Q;
2230
2231 let expr = Expression::Case {
2233 whens: vec![AnnotWhen::new(
2234 Q::new("status", "=", "'active'"),
2235 AnnotationValue::Value(OrmValue::Int(1)),
2236 )],
2237 default: Some(Box::new(AnnotationValue::Value(OrmValue::Int(0)))),
2238 };
2239 let filter = Filter::new(
2240 "priority".to_string(),
2241 FilterOperator::Eq,
2242 FilterValue::Expression(expr),
2243 );
2244
2245 let result = build_single_filter_expr(&filter);
2247
2248 assert!(result.is_some());
2250 let sea_expr = result.unwrap();
2251 let query = Query::select()
2252 .from(Alias::new("tasks"))
2253 .column(ColumnRef::Asterisk)
2254 .cond_where(Condition::all().add(sea_expr))
2255 .to_string(PostgresQueryBuilder);
2256 assert!(
2257 query.contains("CASE"),
2258 "Should contain CASE keyword: {}",
2259 query
2260 );
2261 assert!(
2262 query.contains("WHEN"),
2263 "Should contain WHEN keyword: {}",
2264 query
2265 );
2266 assert!(
2267 query.contains("ELSE"),
2268 "Should contain ELSE keyword: {}",
2269 query
2270 );
2271 }
2272
2273 #[test]
2274 fn test_empty_coalesce_returns_null() {
2275 let expr = Expression::Coalesce(vec![]);
2277
2278 let result = annotation_expr_to_safe_expr(&expr);
2280
2281 let query = Query::select()
2283 .from(Alias::new("test"))
2284 .column(ColumnRef::Asterisk)
2285 .cond_where(Condition::all().add(result))
2286 .to_string(PostgresQueryBuilder);
2287 assert!(
2288 query.contains("NULL"),
2289 "Empty COALESCE should produce NULL: {}",
2290 query
2291 );
2292 }
2293
2294 #[test]
2297 fn test_aggregate_count_uses_safe_api() {
2298 use reinhardt_db::orm::aggregation::{Aggregate, AggregateFunc};
2299
2300 let agg = Aggregate {
2302 func: AggregateFunc::Count,
2303 field: None,
2304 alias: None,
2305 distinct: false,
2306 };
2307
2308 let result = aggregate_to_safe_expr(&agg);
2310
2311 let query = Query::select()
2313 .from(Alias::new("items"))
2314 .expr(result)
2315 .to_string(PostgresQueryBuilder);
2316 assert!(
2317 query.contains("COUNT(*)"),
2318 "Should contain COUNT(*): {}",
2319 query
2320 );
2321 }
2322
2323 #[test]
2324 fn test_aggregate_sum_field_uses_quoted_identifier() {
2325 use reinhardt_db::orm::aggregation::{Aggregate, AggregateFunc};
2326
2327 let agg = Aggregate {
2329 func: AggregateFunc::Sum,
2330 field: Some("price".to_string()),
2331 alias: None,
2332 distinct: false,
2333 };
2334
2335 let result = aggregate_to_safe_expr(&agg);
2337
2338 let query = Query::select()
2340 .from(Alias::new("orders"))
2341 .expr(result)
2342 .to_string(PostgresQueryBuilder);
2343 assert!(
2344 query.contains("SUM("),
2345 "Should contain SUM function: {}",
2346 query
2347 );
2348 assert!(
2349 query.contains("\"price\""),
2350 "Field name should be quoted: {}",
2351 query
2352 );
2353 }
2354
2355 #[test]
2356 fn test_aggregate_count_distinct_uses_distinct_keyword() {
2357 use reinhardt_db::orm::aggregation::{Aggregate, AggregateFunc};
2358
2359 let agg = Aggregate {
2361 func: AggregateFunc::CountDistinct,
2362 field: Some("category".to_string()),
2363 alias: None,
2364 distinct: false, };
2366
2367 let result = aggregate_to_safe_expr(&agg);
2369
2370 let query = Query::select()
2372 .from(Alias::new("products"))
2373 .expr(result)
2374 .to_string(PostgresQueryBuilder);
2375 assert!(
2376 query.contains("COUNT(DISTINCT"),
2377 "Should contain COUNT(DISTINCT: {}",
2378 query
2379 );
2380 assert!(
2381 query.contains("\"category\""),
2382 "Field name should be quoted: {}",
2383 query
2384 );
2385 }
2386
2387 #[test]
2388 fn test_aggregate_injection_attempt_is_quoted() {
2389 use reinhardt_db::orm::aggregation::{Aggregate, AggregateFunc};
2390
2391 let agg = Aggregate {
2393 func: AggregateFunc::Sum,
2394 field: Some("price); DROP TABLE users; --".to_string()),
2395 alias: None,
2396 distinct: false,
2397 };
2398
2399 let result = aggregate_to_safe_expr(&agg);
2401
2402 let query = Query::select()
2404 .from(Alias::new("orders"))
2405 .expr(result)
2406 .to_string(PostgresQueryBuilder);
2407 assert!(
2408 query.contains("\"price); DROP TABLE users; --\""),
2409 "Injection payload should be enclosed in double quotes: {}",
2410 query
2411 );
2412 }
2413
2414 #[rstest]
2417 fn test_build_composite_and_all_unsupported_returns_none() {
2418 let filter1 = Filter::new(
2421 "field1".to_string(),
2422 FilterOperator::Contains,
2423 FilterValue::Boolean(true),
2424 );
2425 let filter2 = Filter::new(
2426 "field2".to_string(),
2427 FilterOperator::StartsWith,
2428 FilterValue::Integer(5),
2429 );
2430 let condition = FilterCondition::And(vec![
2431 FilterCondition::Single(filter1),
2432 FilterCondition::Single(filter2),
2433 ]);
2434
2435 let result = build_composite_filter_condition(&condition);
2437
2438 assert!(result.is_ok());
2440 assert!(
2441 result.unwrap().is_none(),
2442 "And with all unsupported filters should return None"
2443 );
2444 }
2445
2446 #[rstest]
2447 fn test_build_composite_or_all_unsupported_returns_none() {
2448 let filter1 = Filter::new(
2450 "field1".to_string(),
2451 FilterOperator::Contains,
2452 FilterValue::Boolean(true),
2453 );
2454 let filter2 = Filter::new(
2455 "field2".to_string(),
2456 FilterOperator::StartsWith,
2457 FilterValue::Integer(5),
2458 );
2459 let condition = FilterCondition::Or(vec![
2460 FilterCondition::Single(filter1),
2461 FilterCondition::Single(filter2),
2462 ]);
2463
2464 let result = build_composite_filter_condition(&condition);
2466
2467 assert!(result.is_ok());
2469 assert!(
2470 result.unwrap().is_none(),
2471 "Or with all unsupported filters should return None"
2472 );
2473 }
2474
2475 #[rstest]
2476 fn test_build_composite_and_mixed_valid_and_unsupported() {
2477 let valid_filter = Filter::new(
2479 "name".to_string(),
2480 FilterOperator::Eq,
2481 FilterValue::String("Alice".to_string()),
2482 );
2483 let unsupported_filter = Filter::new(
2484 "field2".to_string(),
2485 FilterOperator::Contains,
2486 FilterValue::Boolean(true),
2487 );
2488 let condition = FilterCondition::And(vec![
2489 FilterCondition::Single(valid_filter),
2490 FilterCondition::Single(unsupported_filter),
2491 ]);
2492
2493 let result = build_composite_filter_condition(&condition);
2495
2496 assert!(result.is_ok());
2498 let cond = result.unwrap();
2499 assert!(
2500 cond.is_some(),
2501 "And with at least one valid filter should return Some"
2502 );
2503 let query = Query::select()
2504 .from(Alias::new("t"))
2505 .column(ColumnRef::Asterisk)
2506 .cond_where(cond.unwrap())
2507 .to_string(PostgresQueryBuilder);
2508 assert!(
2509 query.contains("\"name\""),
2510 "SQL should contain the valid filter field, got: {}",
2511 query
2512 );
2513 assert!(
2514 query.contains("'Alice'"),
2515 "SQL should contain the valid filter value, got: {}",
2516 query
2517 );
2518 }
2519
2520 #[rstest]
2521 fn test_build_composite_or_mixed_valid_and_unsupported() {
2522 let valid_filter = Filter::new(
2524 "email".to_string(),
2525 FilterOperator::Eq,
2526 FilterValue::String("test@example.com".to_string()),
2527 );
2528 let unsupported_filter = Filter::new(
2529 "field2".to_string(),
2530 FilterOperator::StartsWith,
2531 FilterValue::Integer(5),
2532 );
2533 let condition = FilterCondition::Or(vec![
2534 FilterCondition::Single(valid_filter),
2535 FilterCondition::Single(unsupported_filter),
2536 ]);
2537
2538 let result = build_composite_filter_condition(&condition);
2540
2541 assert!(result.is_ok());
2543 let cond = result.unwrap();
2544 assert!(
2545 cond.is_some(),
2546 "Or with at least one valid filter should return Some"
2547 );
2548 let query = Query::select()
2549 .from(Alias::new("t"))
2550 .column(ColumnRef::Asterisk)
2551 .cond_where(cond.unwrap())
2552 .to_string(PostgresQueryBuilder);
2553 assert!(
2554 query.contains("\"email\""),
2555 "SQL should contain the valid filter field, got: {}",
2556 query
2557 );
2558 assert!(
2559 query.contains("'test@example.com'"),
2560 "SQL should contain the valid filter value, got: {}",
2561 query
2562 );
2563 }
2564
2565 #[rstest]
2566 fn test_build_filter_condition_all_unsupported_returns_none() {
2567 let filters = vec![
2569 Filter::new(
2570 "field1".to_string(),
2571 FilterOperator::Contains,
2572 FilterValue::Boolean(true),
2573 ),
2574 Filter::new(
2575 "field2".to_string(),
2576 FilterOperator::StartsWith,
2577 FilterValue::Integer(5),
2578 ),
2579 ];
2580
2581 let result = build_filter_condition(&filters);
2583
2584 assert!(
2586 result.is_none(),
2587 "build_filter_condition with all unsupported filters should return None"
2588 );
2589 }
2590
2591 #[rstest]
2594 fn test_extract_count_from_row_with_count_key() {
2595 let data = serde_json::json!({"count": 42});
2597
2598 let result = extract_count_from_row(&data);
2600
2601 assert_eq!(result.unwrap(), 42);
2603 }
2604
2605 #[rstest]
2606 fn test_extract_count_from_row_without_count_key() {
2607 let data = serde_json::json!({"total": 10});
2609
2610 let result = extract_count_from_row(&data);
2612
2613 let err = result.unwrap_err();
2615 assert!(
2616 err.to_string().contains("missing 'count' key"),
2617 "Error should mention missing 'count' key, got: {}",
2618 err
2619 );
2620 }
2621
2622 #[rstest]
2623 fn test_extract_count_from_row_empty_object() {
2624 let data = serde_json::json!({});
2626
2627 let result = extract_count_from_row(&data);
2629
2630 let err = result.unwrap_err();
2632 assert!(
2633 err.to_string().contains("missing 'count' key"),
2634 "Error should mention missing 'count' key, got: {}",
2635 err
2636 );
2637 }
2638
2639 #[rstest]
2640 fn test_extract_count_from_row_non_integer() {
2641 let data = serde_json::json!({"count": "abc"});
2643
2644 let result = extract_count_from_row(&data);
2646
2647 let err = result.unwrap_err();
2649 assert!(
2650 err.to_string().contains("non-integer"),
2651 "Error should mention non-integer value, got: {}",
2652 err
2653 );
2654 }
2655
2656 #[rstest]
2657 fn test_extract_count_from_row_null_data() {
2658 let data = serde_json::Value::Null;
2660
2661 let result = extract_count_from_row(&data);
2663
2664 let err = result.unwrap_err();
2666 assert!(
2667 err.to_string().contains("unexpected data format"),
2668 "Error should mention unexpected data format, got: {}",
2669 err
2670 );
2671 }
2672
2673 #[rstest]
2674 fn test_extract_count_from_row_zero() {
2675 let data = serde_json::json!({"count": 0});
2677
2678 let result = extract_count_from_row(&data);
2680
2681 assert_eq!(result.unwrap(), 0);
2683 }
2684
2685 #[rstest]
2688 #[tokio::test]
2689 async fn test_admin_database_inject_error_hint_mentions_connection() {
2690 let singleton = Arc::new(reinhardt_di::SingletonScope::new());
2692 let ctx = reinhardt_di::InjectionContext::builder(singleton).build();
2693
2694 let result = AdminDatabase::inject(&ctx).await;
2696
2697 assert!(result.is_err());
2699 let err = result.err().unwrap();
2700 assert!(
2701 err.to_string().contains("DatabaseConnection"),
2702 "Error hint should mention DatabaseConnection, got: {}",
2703 err
2704 );
2705 }
2706
2707 #[rstest]
2708 #[tokio::test]
2709 async fn test_admin_database_inject_returns_prebuilt_from_singleton() {
2710 let singleton = Arc::new(reinhardt_di::SingletonScope::new());
2712 let ctx = reinhardt_di::InjectionContext::builder(singleton).build();
2719
2720 let result = AdminDatabase::inject(&ctx).await;
2722
2723 assert!(result.is_err());
2725 let err = result.err().unwrap();
2726 match err {
2727 reinhardt_di::DiError::NotRegistered { type_name, hint } => {
2728 assert_eq!(type_name, "AdminDatabase");
2729 assert_eq!(
2730 hint,
2731 "DatabaseConnection must be registered as a singleton. \
2732 Use InjectionContextBuilder::singleton(db_connection) during setup."
2733 );
2734 }
2735 other => panic!("Expected NotRegistered error, got: {other:?}"),
2736 }
2737 }
2738
2739 #[rstest]
2740 #[tokio::test]
2741 async fn test_admin_database_keyed_provider_reports_missing_connection() {
2742 let singleton = Arc::new(reinhardt_di::SingletonScope::new());
2743 let ctx = reinhardt_di::InjectionContext::builder(singleton).build();
2744
2745 let result =
2746 reinhardt_di::Depends::<AdminDatabaseKey, AdminDatabase>::resolve_from_registry(
2747 &ctx, true,
2748 )
2749 .await;
2750
2751 assert!(result.is_err());
2752 let err = result.err().unwrap();
2753 match err {
2754 reinhardt_di::DiError::NotRegistered { type_name, hint } => {
2755 assert_eq!(type_name, "AdminDatabase");
2756 assert_eq!(
2757 hint,
2758 "DatabaseConnection must be registered as a singleton. \
2759 Use InjectionContextBuilder::singleton(db_connection) during setup."
2760 );
2761 }
2762 other => panic!("Expected NotRegistered error, got: {other:?}"),
2763 }
2764 }
2765
2766 #[rstest]
2769 fn test_build_single_filter_expr_array_in() {
2770 let filter = Filter::new(
2772 "status".to_string(),
2773 FilterOperator::In,
2774 FilterValue::Array(vec!["a".to_string(), "b".to_string(), "c".to_string()]),
2775 );
2776
2777 let result = build_single_filter_expr(&filter);
2779
2780 assert!(
2782 result.is_some(),
2783 "Array In with non-empty values should return Some"
2784 );
2785 let query = Query::select()
2786 .from(Alias::new("table"))
2787 .column(ColumnRef::Asterisk)
2788 .cond_where(Condition::all().add(result.unwrap()))
2789 .to_string(PostgresQueryBuilder);
2790 assert!(query.contains("IN"), "SQL should contain IN operator");
2791 assert!(query.contains("'a'"), "SQL should contain value 'a'");
2792 assert!(query.contains("'b'"), "SQL should contain value 'b'");
2793 assert!(query.contains("'c'"), "SQL should contain value 'c'");
2794 }
2795
2796 #[rstest]
2797 fn test_build_single_filter_expr_array_not_in() {
2798 let filter = Filter::new(
2800 "status".to_string(),
2801 FilterOperator::NotIn,
2802 FilterValue::Array(vec!["x".to_string(), "y".to_string()]),
2803 );
2804
2805 let result = build_single_filter_expr(&filter);
2807
2808 assert!(
2810 result.is_some(),
2811 "Array NotIn with non-empty values should return Some"
2812 );
2813 let query = Query::select()
2814 .from(Alias::new("table"))
2815 .column(ColumnRef::Asterisk)
2816 .cond_where(Condition::all().add(result.unwrap()))
2817 .to_string(PostgresQueryBuilder);
2818 assert!(
2819 query.contains("NOT IN"),
2820 "SQL should contain NOT IN operator"
2821 );
2822 assert!(query.contains("'x'"), "SQL should contain value 'x'");
2823 assert!(query.contains("'y'"), "SQL should contain value 'y'");
2824 }
2825
2826 #[rstest]
2827 fn test_build_single_filter_expr_array_in_empty() {
2828 let filter = Filter::new(
2830 "status".to_string(),
2831 FilterOperator::In,
2832 FilterValue::Array(vec![]),
2833 );
2834
2835 let result = build_single_filter_expr(&filter);
2837
2838 assert!(
2840 result.is_none(),
2841 "Array In with empty values should return None"
2842 );
2843 }
2844
2845 #[rstest]
2846 fn test_build_single_filter_expr_array_in_single_element() {
2847 let filter = Filter::new(
2849 "category".to_string(),
2850 FilterOperator::In,
2851 FilterValue::Array(vec!["solo".to_string()]),
2852 );
2853
2854 let result = build_single_filter_expr(&filter);
2856
2857 assert!(
2859 result.is_some(),
2860 "Array In with single element should return Some"
2861 );
2862 let query = Query::select()
2863 .from(Alias::new("table"))
2864 .column(ColumnRef::Asterisk)
2865 .cond_where(Condition::all().add(result.unwrap()))
2866 .to_string(PostgresQueryBuilder);
2867 assert!(query.contains("IN"), "SQL should contain IN operator");
2868 assert!(query.contains("'solo'"), "SQL should contain value 'solo'");
2869 }
2870
2871 #[rstest]
2872 fn test_build_single_filter_expr_array_in_special_chars() {
2873 let filter = Filter::new(
2875 "name".to_string(),
2876 FilterOperator::In,
2877 FilterValue::Array(vec!["O'Brien".to_string(), "a;DROP TABLE".to_string()]),
2878 );
2879
2880 let result = build_single_filter_expr(&filter);
2882
2883 assert!(
2885 result.is_some(),
2886 "Array In with special chars should return Some"
2887 );
2888 let query = Query::select()
2889 .from(Alias::new("table"))
2890 .column(ColumnRef::Asterisk)
2891 .cond_where(Condition::all().add(result.unwrap()))
2892 .to_string(PostgresQueryBuilder);
2893 assert!(query.contains("IN"), "SQL should contain IN operator");
2894 assert!(
2896 query.contains("O''Brien"),
2897 "Single quote in value should be escaped, got: {}",
2898 query
2899 );
2900 assert!(
2902 query.contains("'a;DROP TABLE'"),
2903 "SQL injection attempt should be safely quoted as a string literal, got: {}",
2904 query
2905 );
2906 }
2907
2908 #[rstest]
2911 fn test_and_with_all_unsupported_returns_none() {
2912 let unsupported1 = FilterCondition::Single(Filter::new(
2914 "name",
2915 FilterOperator::Contains,
2916 FilterValue::Integer(42),
2917 ));
2918 let unsupported2 = FilterCondition::Single(Filter::new(
2919 "email",
2920 FilterOperator::StartsWith,
2921 FilterValue::Integer(99),
2922 ));
2923 let condition = FilterCondition::And(vec![unsupported1, unsupported2]);
2924
2925 let result = build_composite_filter_condition(&condition);
2927
2928 assert!(result.is_ok());
2931 let cond = result.unwrap();
2932 assert!(
2933 cond.is_none(),
2934 "And with all unsupported sub-conditions should return None"
2935 );
2936 }
2937
2938 #[rstest]
2939 fn test_or_with_all_unsupported_returns_none() {
2940 let unsupported1 = FilterCondition::Single(Filter::new(
2942 "name",
2943 FilterOperator::Contains,
2944 FilterValue::Integer(42),
2945 ));
2946 let unsupported2 = FilterCondition::Single(Filter::new(
2947 "email",
2948 FilterOperator::StartsWith,
2949 FilterValue::Integer(99),
2950 ));
2951 let condition = FilterCondition::Or(vec![unsupported1, unsupported2]);
2952
2953 let result = build_composite_filter_condition(&condition);
2955
2956 assert!(result.is_ok());
2959 let cond = result.unwrap();
2960 assert!(
2961 cond.is_none(),
2962 "Or with all unsupported sub-conditions should return None"
2963 );
2964 }
2965
2966 #[rstest]
2967 fn test_and_with_mix_supported_unsupported_keeps_supported() {
2968 let supported = FilterCondition::Single(Filter::new(
2970 "name",
2971 FilterOperator::Eq,
2972 FilterValue::String("Alice".to_string()),
2973 ));
2974 let unsupported = FilterCondition::Single(Filter::new(
2975 "email",
2976 FilterOperator::Contains,
2977 FilterValue::Integer(42),
2978 ));
2979 let condition = FilterCondition::And(vec![supported, unsupported]);
2980
2981 let result = build_composite_filter_condition(&condition);
2983
2984 assert!(result.is_ok());
2986 let cond = result.unwrap();
2987 assert!(
2988 cond.is_some(),
2989 "And with mix of supported/unsupported should return Some with supported filters"
2990 );
2991 let query = Query::select()
2993 .from(Alias::new("test"))
2994 .column(ColumnRef::Asterisk)
2995 .cond_where(cond.unwrap())
2996 .to_string(PostgresQueryBuilder);
2997 assert!(
2998 query.contains("\"name\""),
2999 "SQL should contain the supported filter field 'name': {}",
3000 query
3001 );
3002 }
3003
3004 #[rstest]
3005 fn test_or_with_one_supported_one_unsupported() {
3006 let supported = FilterCondition::Single(Filter::new(
3008 "status",
3009 FilterOperator::Eq,
3010 FilterValue::String("active".to_string()),
3011 ));
3012 let unsupported = FilterCondition::Single(Filter::new(
3013 "count",
3014 FilterOperator::Contains,
3015 FilterValue::Integer(42),
3016 ));
3017 let condition = FilterCondition::Or(vec![supported, unsupported]);
3018
3019 let result = build_composite_filter_condition(&condition);
3021
3022 assert!(result.is_ok());
3024 let cond = result.unwrap();
3025 assert!(
3026 cond.is_some(),
3027 "Or with one supported condition should return Some"
3028 );
3029 let query = Query::select()
3030 .from(Alias::new("test"))
3031 .column(ColumnRef::Asterisk)
3032 .cond_where(cond.unwrap())
3033 .to_string(PostgresQueryBuilder);
3034 assert!(
3035 query.contains("\"status\""),
3036 "SQL should contain the supported filter field 'status': {}",
3037 query
3038 );
3039 }
3040
3041 #[rstest]
3044 fn test_extract_count_with_count_key() {
3045 let data = serde_json::json!({"count": 42});
3047
3048 let result = extract_count_from_row(&data);
3050
3051 assert!(result.is_ok());
3053 assert_eq!(result.unwrap(), 42);
3054 }
3055
3056 #[rstest]
3057 fn test_extract_count_without_count_key_returns_error() {
3058 let data = serde_json::json!({"total": 42});
3060
3061 let result = extract_count_from_row(&data);
3063
3064 assert!(result.is_err());
3067 let err = result.unwrap_err();
3068 assert!(
3069 err.to_string().contains("missing 'count' key"),
3070 "Error should mention missing 'count' key, got: {}",
3071 err
3072 );
3073 }
3074
3075 #[rstest]
3076 fn test_extract_count_with_multiple_keys_no_count_returns_error() {
3077 let data = serde_json::json!({"total": 42, "other": 99});
3079
3080 let result = extract_count_from_row(&data);
3082
3083 assert!(result.is_err());
3086 let err = result.unwrap_err();
3087 assert!(
3088 err.to_string().contains("available keys"),
3089 "Error should list available keys, got: {}",
3090 err
3091 );
3092 }
3093
3094 #[rstest]
3095 fn test_extract_count_non_integer_returns_error() {
3096 let data = serde_json::json!({"count": "not_a_number"});
3098
3099 let result = extract_count_from_row(&data);
3101
3102 assert!(result.is_err());
3104 let err = result.unwrap_err();
3105 assert!(matches!(err, AdminError::DatabaseError(_)));
3106 }
3107
3108 #[rstest]
3109 fn test_extract_count_null_returns_error() {
3110 let data = serde_json::json!({"count": null});
3112
3113 let result = extract_count_from_row(&data);
3115
3116 assert!(result.is_err());
3118 }
3119
3120 #[rstest]
3121 fn test_extract_count_empty_object_returns_error() {
3122 let data = serde_json::json!({});
3124
3125 let result = extract_count_from_row(&data);
3127
3128 assert!(result.is_err());
3130 }
3131
3132 #[rstest]
3133 fn test_extract_count_non_object_returns_error() {
3134 let data = serde_json::json!([1, 2, 3]);
3136
3137 let result = extract_count_from_row(&data);
3139
3140 assert!(result.is_err());
3142 }
3143
3144 #[rstest]
3147 fn test_parse_pk_value_integer_falls_back_to_bigint() {
3148 let val = parse_pk_value("nonexistent_table", "id", "42");
3152
3153 assert_eq!(val, Value::BigInt(Some(42)));
3155 }
3156
3157 #[rstest]
3158 fn test_parse_pk_value_uuid_string_without_registry_falls_back_to_string() {
3159 let val = parse_pk_value(
3163 "nonexistent_table",
3164 "id",
3165 "c1a363b1-cc42-4dea-81f0-9dc1cedf0083",
3166 );
3167
3168 assert!(matches!(val, Value::String(Some(_))));
3170 }
3171
3172 #[rstest]
3173 fn test_parse_pk_value_non_numeric_string_falls_back_to_string() {
3174 let val = parse_pk_value("nonexistent_table", "id", "hello-world");
3178
3179 assert!(matches!(val, Value::String(Some(_))));
3181 }
3182
3183 #[rstest]
3184 fn test_parse_pk_value_negative_integer() {
3185 let val = parse_pk_value("nonexistent_table", "id", "-1");
3189
3190 assert_eq!(val, Value::BigInt(Some(-1)));
3192 }
3193
3194 #[rstest]
3195 fn test_parse_pk_value_zero() {
3196 let val = parse_pk_value("nonexistent_table", "id", "0");
3200
3201 assert_eq!(val, Value::BigInt(Some(0)));
3203 }
3204}