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
544pub(crate) fn build_object_scope_condition(filters: &[Filter]) -> AdminResult<Option<Condition>> {
545 if filters.is_empty() {
546 return Ok(None);
547 }
548
549 let mut condition = Condition::all();
550 for filter in filters {
551 let expression = build_single_filter_expr(filter).ok_or_else(|| {
552 AdminError::ValidationError("Unsupported object permission filter".to_string())
553 })?;
554 condition = condition.add(expression);
555 }
556 Ok(Some(condition))
557}
558
559#[doc(hidden)]
561pub const MAX_FILTER_DEPTH: usize = 100;
562
563#[doc(hidden)]
574pub fn build_composite_filter_condition(
575 filter_condition: &FilterCondition,
576) -> AdminResult<Option<Condition>> {
577 build_composite_filter_condition_with_depth(filter_condition, 0)
578}
579
580#[doc(hidden)]
582pub fn build_composite_filter_condition_with_depth(
583 filter_condition: &FilterCondition,
584 depth: usize,
585) -> AdminResult<Option<Condition>> {
586 if depth >= MAX_FILTER_DEPTH {
588 return Err(AdminError::ValidationError(format!(
589 "Filter condition exceeded maximum depth of {} levels",
590 MAX_FILTER_DEPTH
591 )));
592 }
593
594 match filter_condition {
595 FilterCondition::Single(filter) => {
596 Ok(build_single_filter_expr(filter).map(|expr| Condition::all().add(expr)))
597 }
598 FilterCondition::And(conditions) => {
599 if conditions.is_empty() {
600 return Ok(None);
601 }
602 let mut and_condition = Condition::all();
603 let mut added = false;
604 for cond in conditions {
605 if let Some(sub_cond) =
606 build_composite_filter_condition_with_depth(cond, depth + 1)?
607 {
608 and_condition = and_condition.add(sub_cond);
609 added = true;
610 }
611 }
612 if added {
615 Ok(Some(and_condition))
616 } else {
617 Ok(None)
618 }
619 }
620 FilterCondition::Or(conditions) => {
621 if conditions.is_empty() {
622 return Ok(None);
623 }
624 let mut or_condition = Condition::any();
625 let mut added = false;
626 for cond in conditions {
627 if let Some(sub_cond) =
628 build_composite_filter_condition_with_depth(cond, depth + 1)?
629 {
630 or_condition = or_condition.add(sub_cond);
631 added = true;
632 }
633 }
634 if added {
637 Ok(Some(or_condition))
638 } else {
639 Ok(None)
640 }
641 }
642 FilterCondition::Not(inner) => Ok(build_composite_filter_condition_with_depth(
643 inner,
644 depth + 1,
645 )?
646 .map(|inner_cond| inner_cond.not())),
647 }
648}
649
650fn build_combined_filter_condition(
651 filter_condition: Option<&FilterCondition>,
652 additional_filters: &[Filter],
653) -> AdminResult<(Condition, bool)> {
654 let mut combined = Condition::all();
655
656 if let Some(fc) = filter_condition
657 && let Some(cond) = build_composite_filter_condition(fc)?
658 {
659 combined = combined.add(cond);
660 }
661
662 if let Some(simple_cond) = build_filter_condition(additional_filters) {
663 combined = combined.add(simple_cond);
664 }
665
666 Ok((
667 combined,
668 !additional_filters.is_empty() || filter_condition.is_some(),
669 ))
670}
671
672fn extract_admin_list_total_count(
673 map: &serde_json::Map<String, serde_json::Value>,
674) -> AdminResult<u64> {
675 let count_value = map.get(ADMIN_LIST_TOTAL_COUNT_ALIAS).ok_or_else(|| {
676 AdminError::DatabaseError(format!(
677 "Admin list query result missing '{}' key",
678 ADMIN_LIST_TOTAL_COUNT_ALIAS
679 ))
680 })?;
681
682 if let Some(count) = count_value.as_u64() {
683 return Ok(count);
684 }
685
686 count_value
687 .as_i64()
688 .and_then(|count| if count >= 0 { Some(count as u64) } else { None })
689 .ok_or_else(|| {
690 AdminError::DatabaseError(format!(
691 "Admin list query returned invalid total count: {}",
692 count_value
693 ))
694 })
695}
696
697#[injectable(scope = Singleton, prebuilt = true)]
717#[derive(Clone)]
718pub struct AdminDatabase {
719 connection: Arc<DatabaseConnection>,
720}
721
722#[reinhardt_di::injectable_key]
724pub struct AdminDatabaseKey;
725
726impl AdminDatabase {
727 pub fn new(connection: DatabaseConnection) -> Self {
732 Self {
733 connection: Arc::new(connection),
734 }
735 }
736
737 pub fn from_arc(connection: Arc<DatabaseConnection>) -> Self {
742 Self { connection }
743 }
744
745 pub fn connection(&self) -> &DatabaseConnection {
747 &self.connection
748 }
749
750 pub fn connection_arc(&self) -> Arc<DatabaseConnection> {
754 Arc::clone(&self.connection)
755 }
756
757 pub async fn list<M: Model>(
778 &self,
779 table_name: &str,
780 filters: Vec<Filter>,
781 offset: u64,
782 limit: u64,
783 ) -> AdminResult<Vec<HashMap<String, serde_json::Value>>> {
784 let mut query = Query::select()
789 .from(Alias::new(table_name))
790 .column(ColumnRef::Asterisk)
791 .to_owned();
792
793 if let Some(condition) = build_filter_condition(&filters) {
795 query.cond_where(condition);
796 }
797
798 query.limit(limit).offset(offset);
800
801 let (sql, values) = query.build(PostgresQueryBuilder);
803 let params = convert_values(values);
804 let rows = self
805 .connection
806 .query(&sql, params)
807 .await
808 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
809
810 Ok(rows
812 .into_iter()
813 .filter_map(|row| {
814 if let serde_json::Value::Object(map) = row.data {
816 Some(
817 map.into_iter()
818 .collect::<HashMap<String, serde_json::Value>>(),
819 )
820 } else {
821 None
822 }
823 })
824 .collect())
825 }
826
827 pub async fn list_with_condition<M: Model>(
841 &self,
842 table_name: &str,
843 filter_condition: Option<&FilterCondition>,
844 additional_filters: Vec<Filter>,
845 sort_by: Option<&str>,
846 offset: u64,
847 limit: u64,
848 ) -> AdminResult<Vec<HashMap<String, serde_json::Value>>> {
849 let mut query = Query::select()
854 .from(Alias::new(table_name))
855 .column(ColumnRef::Asterisk)
856 .to_owned();
857
858 let (combined, has_filter) =
859 build_combined_filter_condition(filter_condition, &additional_filters)?;
860
861 if has_filter {
862 query.cond_where(combined);
863 }
864
865 if let Some(sort_str) = sort_by {
867 let (field, is_desc) = if let Some(stripped) = sort_str.strip_prefix('-') {
868 (stripped, true)
869 } else {
870 (sort_str, false)
871 };
872
873 let col = Alias::new(field);
874 if is_desc {
875 query.order_by(col, Order::Desc);
876 } else {
877 query.order_by(col, Order::Asc);
878 }
879 }
880
881 query.limit(limit).offset(offset);
883
884 let (sql, values) = query.build(PostgresQueryBuilder);
886 let params = convert_values(values);
887 let rows = self
888 .connection
889 .query(&sql, params)
890 .await
891 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
892
893 Ok(rows
895 .into_iter()
896 .filter_map(|row| {
897 if let serde_json::Value::Object(map) = row.data {
898 Some(
899 map.into_iter()
900 .filter(|(key, _)| !SENSITIVE_FIELDS.contains(&key.as_str()))
901 .collect::<HashMap<String, serde_json::Value>>(),
902 )
903 } else {
904 None
905 }
906 })
907 .collect())
908 }
909
910 pub async fn list_with_condition_and_count<M: Model>(
916 &self,
917 table_name: &str,
918 filter_condition: Option<&FilterCondition>,
919 additional_filters: Vec<Filter>,
920 sort_by: Option<&str>,
921 offset: u64,
922 limit: u64,
923 ) -> AdminResult<(Vec<HashMap<String, serde_json::Value>>, u64)> {
924 let mut query = Query::select()
928 .from(Alias::new(table_name))
929 .column(ColumnRef::Asterisk)
930 .expr_as(
931 Expr::cust("COUNT(*) OVER()"),
932 Alias::new(ADMIN_LIST_TOTAL_COUNT_ALIAS),
933 )
934 .to_owned();
935
936 let (combined, has_filter) =
937 build_combined_filter_condition(filter_condition, &additional_filters)?;
938
939 if has_filter {
940 query.cond_where(combined);
941 }
942
943 if let Some(sort_str) = sort_by {
944 let (field, is_desc) = if let Some(stripped) = sort_str.strip_prefix('-') {
945 (stripped, true)
946 } else {
947 (sort_str, false)
948 };
949
950 let col = Alias::new(field);
951 if is_desc {
952 query.order_by(col, Order::Desc);
953 } else {
954 query.order_by(col, Order::Asc);
955 }
956 }
957
958 query.limit(limit).offset(offset);
959
960 let (sql, values) = query.build(PostgresQueryBuilder);
961 let params = convert_values(values);
962 let rows = self
963 .connection
964 .query(&sql, params)
965 .await
966 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
967
968 if rows.is_empty() {
969 if offset == 0 && limit > 0 {
970 return Ok((Vec::new(), 0));
971 }
972
973 let count = self
974 .count_with_condition::<M>(table_name, filter_condition, additional_filters)
975 .await?;
976 return Ok((Vec::new(), count));
977 }
978
979 let mut total_count = None;
980 let results = rows
981 .into_iter()
982 .filter_map(|row| {
983 if let serde_json::Value::Object(mut map) = row.data {
984 if total_count.is_none() {
985 total_count = Some(extract_admin_list_total_count(&map));
986 }
987
988 map.remove(ADMIN_LIST_TOTAL_COUNT_ALIAS);
989
990 Some(
991 map.into_iter()
992 .filter(|(key, _)| !SENSITIVE_FIELDS.contains(&key.as_str()))
993 .collect::<HashMap<String, serde_json::Value>>(),
994 )
995 } else {
996 None
997 }
998 })
999 .collect::<Vec<_>>();
1000
1001 let total_count = total_count.unwrap_or_else(|| {
1002 Err(AdminError::DatabaseError(
1003 "Admin list query returned no object rows".to_string(),
1004 ))
1005 })?;
1006
1007 Ok((results, total_count))
1008 }
1009
1010 pub async fn count_with_condition<M: Model>(
1018 &self,
1019 table_name: &str,
1020 filter_condition: Option<&FilterCondition>,
1021 additional_filters: Vec<Filter>,
1022 ) -> AdminResult<u64> {
1023 let mut query = Query::select()
1024 .from(Alias::new(table_name))
1025 .expr(Expr::cust("COUNT(*) AS count"))
1026 .to_owned();
1027
1028 let (combined, has_filter) =
1029 build_combined_filter_condition(filter_condition, &additional_filters)?;
1030
1031 if has_filter {
1032 query.cond_where(combined);
1033 }
1034
1035 let (sql, values) = query.build(PostgresQueryBuilder);
1036 let params = convert_values(values);
1037 let row = self
1038 .connection
1039 .query_one(&sql, params)
1040 .await
1041 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1042
1043 let count = extract_count_from_row(&row.data)?;
1045
1046 Ok(count)
1047 }
1048
1049 pub async fn get<M: Model>(
1066 &self,
1067 table_name: &str,
1068 pk_field: &str,
1069 id: &str,
1070 ) -> AdminResult<Option<HashMap<String, serde_json::Value>>> {
1071 self.get_with_filters::<M>(table_name, pk_field, id, Vec::new())
1072 .await
1073 }
1074
1075 pub async fn get_with_filters<M: Model>(
1077 &self,
1078 table_name: &str,
1079 pk_field: &str,
1080 id: &str,
1081 filters: Vec<Filter>,
1082 ) -> AdminResult<Option<HashMap<String, serde_json::Value>>> {
1083 let pk_value = parse_pk_value(table_name, pk_field, id);
1084
1085 let mut query = Query::select()
1089 .from(Alias::new(table_name))
1090 .column(ColumnRef::Asterisk)
1091 .and_where(Expr::col(Alias::new(pk_field)).eq(pk_value))
1092 .to_owned();
1093 if let Some(condition) = build_object_scope_condition(&filters)? {
1094 query.cond_where(condition);
1095 }
1096
1097 let (sql, values) = query.build(PostgresQueryBuilder);
1098 let params = convert_values(values);
1099 let row = self
1100 .connection
1101 .query_optional(&sql, params)
1102 .await
1103 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1104
1105 Ok(row.and_then(|r| {
1106 if let serde_json::Value::Object(map) = r.data {
1108 Some(
1109 map.into_iter()
1110 .collect::<HashMap<String, serde_json::Value>>(),
1111 )
1112 } else {
1113 None
1114 }
1115 }))
1116 }
1117
1118 pub async fn create<M: Model>(
1140 &self,
1141 table_name: &str,
1142 pk_field: Option<&str>,
1143 data: HashMap<String, serde_json::Value>,
1144 ) -> AdminResult<u64> {
1145 let pk_field = pk_field.unwrap_or("id");
1146 let mut query = Query::insert()
1147 .into_table(Alias::new(table_name))
1148 .to_owned();
1149
1150 let mut sorted_keys: Vec<String> = data.keys().cloned().collect();
1154 sorted_keys.sort();
1155
1156 let mut columns = Vec::new();
1158 let mut values = Vec::new();
1159
1160 for key in sorted_keys {
1161 let value = data.get(&key).cloned().unwrap_or(serde_json::Value::Null);
1162 columns.push(Alias::new(&key));
1163
1164 let sea_value = json_to_sea_value(value);
1165 values.push(sea_value);
1166 }
1167
1168 query.columns(columns).values(values).map_err(|e| {
1170 AdminError::DatabaseError(format!("column/value count mismatch: {}", e))
1171 })?;
1172
1173 query.returning([Alias::new(pk_field)]);
1175
1176 let (sql, values) = query.build(PostgresQueryBuilder);
1177 let params = convert_values(values);
1178 let row = self
1179 .connection
1180 .query_one(&sql, params)
1181 .await
1182 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1183
1184 match row.data.get(pk_field) {
1186 Some(serde_json::Value::Number(n)) => n.as_u64().ok_or_else(|| {
1187 AdminError::DatabaseError(format!(
1188 "RETURNING clause for '{}' returned non-unsigned-integer: {}",
1189 pk_field, n
1190 ))
1191 }),
1192 Some(serde_json::Value::String(_)) => {
1193 Ok(1)
1196 }
1197 _ => Err(AdminError::DatabaseError(format!(
1198 "RETURNING clause did not return expected primary key field '{}'",
1199 pk_field
1200 ))),
1201 }
1202 }
1203
1204 pub async fn update<M: Model>(
1225 &self,
1226 table_name: &str,
1227 pk_field: &str,
1228 id: &str,
1229 data: HashMap<String, serde_json::Value>,
1230 ) -> AdminResult<u64> {
1231 self.update_with_filters::<M>(table_name, pk_field, id, data, Vec::new())
1232 .await
1233 }
1234
1235 pub async fn update_with_filters<M: Model>(
1237 &self,
1238 table_name: &str,
1239 pk_field: &str,
1240 id: &str,
1241 data: HashMap<String, serde_json::Value>,
1242 filters: Vec<Filter>,
1243 ) -> AdminResult<u64> {
1244 let mut query = Query::update().table(Alias::new(table_name)).to_owned();
1245
1246 let mut sorted_keys: Vec<String> = data.keys().cloned().collect();
1248 sorted_keys.sort();
1249
1250 for key in sorted_keys {
1252 let value = data.get(&key).cloned().unwrap_or(serde_json::Value::Null);
1253 let sea_value = json_to_sea_value(value);
1254 query.value(Alias::new(&key), sea_value);
1255 }
1256
1257 let pk_value = parse_pk_value(table_name, pk_field, id);
1258 query.and_where(Expr::col(Alias::new(pk_field)).eq(pk_value));
1259 if let Some(condition) = build_object_scope_condition(&filters)? {
1260 query.cond_where(condition);
1261 }
1262
1263 let (sql, values) = query.build(PostgresQueryBuilder);
1264 let params = convert_values(values);
1265 let affected = self
1266 .connection
1267 .execute(&sql, params)
1268 .await
1269 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1270
1271 Ok(affected)
1272 }
1273
1274 pub async fn delete<M: Model>(
1291 &self,
1292 table_name: &str,
1293 pk_field: &str,
1294 id: &str,
1295 ) -> AdminResult<u64> {
1296 self.delete_with_filters::<M>(table_name, pk_field, id, Vec::new())
1297 .await
1298 }
1299
1300 pub async fn delete_with_filters<M: Model>(
1302 &self,
1303 table_name: &str,
1304 pk_field: &str,
1305 id: &str,
1306 filters: Vec<Filter>,
1307 ) -> AdminResult<u64> {
1308 let pk_value = parse_pk_value(table_name, pk_field, id);
1309
1310 let mut query = Query::delete()
1311 .from_table(Alias::new(table_name))
1312 .and_where(Expr::col(Alias::new(pk_field)).eq(pk_value))
1313 .to_owned();
1314 if let Some(condition) = build_object_scope_condition(&filters)? {
1315 query.cond_where(condition);
1316 }
1317
1318 let (sql, values) = query.build(PostgresQueryBuilder);
1319 let params = convert_values(values);
1320 let affected = self
1321 .connection
1322 .execute(&sql, params)
1323 .await
1324 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1325
1326 Ok(affected)
1327 }
1328
1329 pub async fn bulk_delete<M: Model>(
1347 &self,
1348 table_name: &str,
1349 pk_field: &str,
1350 ids: Vec<String>,
1351 ) -> AdminResult<u64> {
1352 self.bulk_delete_by_table(table_name, pk_field, ids).await
1353 }
1354
1355 pub async fn bulk_delete_with_filters<M: Model>(
1357 &self,
1358 table_name: &str,
1359 pk_field: &str,
1360 ids: Vec<String>,
1361 filters: Vec<Filter>,
1362 ) -> AdminResult<u64> {
1363 if ids.is_empty() {
1364 return Ok(0);
1365 }
1366 let pk_values = parse_pk_values(table_name, pk_field, &ids);
1367 let mut query = Query::delete()
1368 .from_table(Alias::new(table_name))
1369 .and_where(Expr::col(Alias::new(pk_field)).is_in(pk_values))
1370 .to_owned();
1371 if let Some(condition) = build_object_scope_condition(&filters)? {
1372 query.cond_where(condition);
1373 }
1374 let (sql, values) = query.build(PostgresQueryBuilder);
1375 let params = convert_values(values);
1376 self.connection
1377 .execute(&sql, params)
1378 .await
1379 .map_err(|error| AdminError::DatabaseError(error.to_string()))
1380 }
1381
1382 pub async fn bulk_delete_by_table(
1404 &self,
1405 table_name: &str,
1406 pk_field: &str,
1407 ids: Vec<String>,
1408 ) -> AdminResult<u64> {
1409 if ids.is_empty() {
1410 return Ok(0);
1411 }
1412
1413 let pk_values = parse_pk_values(table_name, pk_field, &ids);
1414
1415 let query = Query::delete()
1416 .from_table(Alias::new(table_name))
1417 .and_where(Expr::col(Alias::new(pk_field)).is_in(pk_values))
1418 .to_owned();
1419
1420 let (sql, values) = query.build(PostgresQueryBuilder);
1421 let params = convert_values(values);
1422 let affected = self
1423 .connection
1424 .execute(&sql, params)
1425 .await
1426 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1427
1428 Ok(affected)
1429 }
1430
1431 pub async fn count<M: Model>(
1452 &self,
1453 table_name: &str,
1454 filters: Vec<Filter>,
1455 ) -> AdminResult<u64> {
1456 let mut query = Query::select()
1457 .from(Alias::new(table_name))
1458 .expr(Expr::cust("COUNT(*) AS count"))
1459 .to_owned();
1460
1461 if let Some(condition) = build_filter_condition(&filters) {
1463 query.cond_where(condition);
1464 }
1465
1466 let (sql, values) = query.build(PostgresQueryBuilder);
1467 let params = convert_values(values);
1468 let row = self
1469 .connection
1470 .query_one(&sql, params)
1471 .await
1472 .map_err(|e| AdminError::DatabaseError(e.to_string()))?;
1473
1474 let count = extract_count_from_row(&row.data)?;
1476
1477 Ok(count)
1478 }
1479}
1480
1481#[doc(hidden)]
1491pub fn extract_count_from_row(data: &serde_json::Value) -> AdminResult<u64> {
1492 if let Some(count_value) = data.get("count") {
1493 return count_value.as_i64().map(|v| v as u64).ok_or_else(|| {
1494 AdminError::DatabaseError(format!(
1495 "COUNT query returned non-integer value: {}",
1496 count_value
1497 ))
1498 });
1499 }
1500
1501 if let Some(obj) = data.as_object() {
1504 let available_keys: Vec<&String> = obj.keys().collect();
1505 return Err(AdminError::DatabaseError(format!(
1506 "COUNT query result missing 'count' key, available keys: {:?}",
1507 available_keys
1508 )));
1509 }
1510
1511 Err(AdminError::DatabaseError(format!(
1512 "COUNT query returned unexpected data format: {}",
1513 data
1514 )))
1515}
1516
1517#[async_trait]
1529impl Injectable for AdminDatabase {
1530 async fn inject(ctx: &InjectionContext) -> DiResult<Self> {
1531 if let Some(db) = ctx.get_singleton::<Self>() {
1533 return Ok((*db).clone());
1534 }
1535
1536 let conn = ctx.get_singleton::<DatabaseConnection>().ok_or_else(|| {
1538 reinhardt_di::DiError::NotRegistered {
1539 type_name: "AdminDatabase".into(),
1540 hint: "DatabaseConnection must be registered as a singleton. \
1541 Use InjectionContextBuilder::singleton(db_connection) during setup."
1542 .into(),
1543 }
1544 })?;
1545
1546 let db = AdminDatabase::from_arc(conn);
1547 ctx.set_singleton(db.clone());
1549 Ok(db)
1550 }
1551}
1552
1553#[reinhardt_di::injectable(scope = "singleton")]
1554async fn admin_database_provider(
1555 #[inject] db: AdminDatabase,
1556) -> FactoryOutput<AdminDatabaseKey, AdminDatabase> {
1557 FactoryOutput::new(db)
1558}
1559
1560fn __register_admin_database(registry: &reinhardt_di::DependencyRegistry) {
1564 registry.register::<AdminDatabase>(
1565 reinhardt_di::DependencyScope::Singleton,
1566 reinhardt_di::InjectableFactory::<AdminDatabase>::new(),
1567 );
1568}
1569
1570reinhardt_di::inventory::submit! {
1571 reinhardt_di::InjectableRegistration::new(
1572 __register_admin_database
1573 )
1574}
1575
1576#[cfg(all(test, server))]
1577mod tests {
1578 use super::*;
1579 use reinhardt_db::orm::annotation::Expression;
1580 use reinhardt_db::orm::expressions::{F, OuterRef};
1581 use rstest::rstest;
1582
1583 #[rstest]
1586 fn test_escape_like_pattern_percent() {
1587 let input = "100%";
1589
1590 let result = escape_like_pattern(input);
1592
1593 assert_eq!(result, "100\\%");
1595 }
1596
1597 #[rstest]
1598 fn test_escape_like_pattern_underscore() {
1599 let input = "user_name";
1601
1602 let result = escape_like_pattern(input);
1604
1605 assert_eq!(result, "user\\_name");
1607 }
1608
1609 #[rstest]
1610 fn test_escape_like_pattern_backslash() {
1611 let input = "path\\to";
1613
1614 let result = escape_like_pattern(input);
1616
1617 assert_eq!(result, "path\\\\to");
1619 }
1620
1621 #[rstest]
1622 fn test_escape_like_pattern_combined() {
1623 let input = "100%_done";
1625
1626 let result = escape_like_pattern(input);
1628
1629 assert_eq!(result, "100\\%\\_done");
1631 }
1632
1633 #[rstest]
1634 fn test_escape_like_pattern_no_special_chars() {
1635 let input = "normal text";
1637
1638 let result = escape_like_pattern(input);
1640
1641 assert_eq!(result, "normal text");
1643 }
1644
1645 #[rstest]
1651 #[case("%wildcard%", "\\%wildcard\\%")]
1652 #[case("under_score", "under\\_score")]
1653 #[case("back\\slash", "back\\\\slash")]
1654 #[case("%_%", "\\%\\_\\%")]
1655 fn test_escape_like_pattern_sanitizes_special_chars(
1656 #[case] input: &str,
1657 #[case] expected: &str,
1658 ) {
1659 let escaped = escape_like_pattern(input);
1662 assert_eq!(
1664 escaped, expected,
1665 "input={input:?} was not correctly escaped"
1666 );
1667 }
1668
1669 #[test]
1672 fn test_build_composite_single_condition() {
1673 let filter = Filter::new(
1674 "name".to_string(),
1675 FilterOperator::Eq,
1676 FilterValue::String("Alice".to_string()),
1677 );
1678 let condition = FilterCondition::Single(filter);
1679
1680 let result = build_composite_filter_condition(&condition);
1681
1682 assert!(result.is_ok());
1683 let result = result.unwrap();
1684 assert!(result.is_some());
1685 let cond = result.unwrap();
1687 let query = Query::select()
1688 .from(Alias::new("users"))
1689 .column(ColumnRef::Asterisk)
1690 .cond_where(cond)
1691 .to_string(PostgresQueryBuilder);
1692 assert!(query.contains("\"name\""));
1693 assert!(query.contains("'Alice'"));
1694 }
1695
1696 #[test]
1697 fn test_build_composite_or_condition() {
1698 let filter1 = Filter::new(
1699 "name".to_string(),
1700 FilterOperator::Contains,
1701 FilterValue::String("Alice".to_string()),
1702 );
1703 let filter2 = Filter::new(
1704 "email".to_string(),
1705 FilterOperator::Contains,
1706 FilterValue::String("alice".to_string()),
1707 );
1708
1709 let condition = FilterCondition::Or(vec![
1710 FilterCondition::Single(filter1),
1711 FilterCondition::Single(filter2),
1712 ]);
1713
1714 let result = build_composite_filter_condition(&condition);
1715
1716 assert!(result.is_ok());
1717 let result = result.unwrap();
1718 assert!(result.is_some());
1719 let cond = result.unwrap();
1720 let query = Query::select()
1721 .from(Alias::new("users"))
1722 .column(ColumnRef::Asterisk)
1723 .cond_where(cond)
1724 .to_string(PostgresQueryBuilder);
1725 assert!(query.contains("\"name\""));
1727 assert!(query.contains("\"email\""));
1728 assert!(query.contains("OR"));
1729 }
1730
1731 #[test]
1732 fn test_build_composite_and_condition() {
1733 let filter1 = Filter::new(
1734 "is_active".to_string(),
1735 FilterOperator::Eq,
1736 FilterValue::Boolean(true),
1737 );
1738 let filter2 = Filter::new(
1739 "is_staff".to_string(),
1740 FilterOperator::Eq,
1741 FilterValue::Boolean(true),
1742 );
1743
1744 let condition = FilterCondition::And(vec![
1745 FilterCondition::Single(filter1),
1746 FilterCondition::Single(filter2),
1747 ]);
1748
1749 let result = build_composite_filter_condition(&condition);
1750
1751 assert!(result.is_ok());
1752 let result = result.unwrap();
1753 assert!(result.is_some());
1754 let cond = result.unwrap();
1755 let query = Query::select()
1756 .from(Alias::new("users"))
1757 .column(ColumnRef::Asterisk)
1758 .cond_where(cond)
1759 .to_string(PostgresQueryBuilder);
1760 assert!(query.contains("\"is_active\""));
1762 assert!(query.contains("\"is_staff\""));
1763 assert!(query.contains("AND"));
1764 }
1765
1766 #[test]
1767 fn test_build_composite_nested_condition() {
1768 let filter_name = Filter::new(
1770 "name".to_string(),
1771 FilterOperator::Contains,
1772 FilterValue::String("Alice".to_string()),
1773 );
1774 let filter_email = Filter::new(
1775 "email".to_string(),
1776 FilterOperator::Contains,
1777 FilterValue::String("alice".to_string()),
1778 );
1779 let filter_active = Filter::new(
1780 "is_active".to_string(),
1781 FilterOperator::Eq,
1782 FilterValue::Boolean(true),
1783 );
1784
1785 let or_condition = FilterCondition::Or(vec![
1786 FilterCondition::Single(filter_name),
1787 FilterCondition::Single(filter_email),
1788 ]);
1789
1790 let and_condition =
1791 FilterCondition::And(vec![or_condition, FilterCondition::Single(filter_active)]);
1792
1793 let result = build_composite_filter_condition(&and_condition);
1794
1795 assert!(result.is_ok());
1796 let result = result.unwrap();
1797 assert!(result.is_some());
1798 let cond = result.unwrap();
1799 let query = Query::select()
1800 .from(Alias::new("users"))
1801 .column(ColumnRef::Asterisk)
1802 .cond_where(cond)
1803 .to_string(PostgresQueryBuilder);
1804 assert!(query.contains("\"name\""));
1806 assert!(query.contains("\"email\""));
1807 assert!(query.contains("\"is_active\""));
1808 assert!(query.contains("OR"));
1809 assert!(query.contains("AND"));
1810 }
1811
1812 #[test]
1813 fn test_build_composite_empty_or() {
1814 let condition = FilterCondition::Or(vec![]);
1815
1816 let result = build_composite_filter_condition(&condition);
1817
1818 assert!(result.is_ok());
1820 assert!(result.unwrap().is_none());
1821 }
1822
1823 #[test]
1824 fn test_build_composite_empty_and() {
1825 let condition = FilterCondition::And(vec![]);
1826
1827 let result = build_composite_filter_condition(&condition);
1828
1829 assert!(result.is_ok());
1831 assert!(result.unwrap().is_none());
1832 }
1833
1834 #[test]
1835 fn test_build_composite_depth_overflow_returns_error() {
1836 let base_filter = Filter::new(
1838 "name".to_string(),
1839 FilterOperator::Eq,
1840 FilterValue::String("Alice".to_string()),
1841 );
1842 let mut condition = FilterCondition::Single(base_filter);
1843 for _ in 0..=MAX_FILTER_DEPTH {
1845 condition = FilterCondition::And(vec![condition]);
1846 }
1847
1848 let result = build_composite_filter_condition(&condition);
1849
1850 assert!(result.is_err());
1851 let err = result.unwrap_err();
1852 assert!(matches!(err, AdminError::ValidationError(_)));
1853 let err_msg = err.to_string();
1854 assert!(
1855 err_msg.contains("exceeded maximum depth"),
1856 "Error message should mention exceeded depth, got: {}",
1857 err_msg
1858 );
1859 }
1860
1861 #[test]
1864 fn test_build_single_filter_expr_field_ref_eq() {
1865 let filter = Filter::new(
1866 "price".to_string(),
1867 FilterOperator::Eq,
1868 FilterValue::FieldRef(F::new("discount_price")),
1869 );
1870 let result = build_single_filter_expr(&filter);
1871 assert!(result.is_some());
1872
1873 let query = Query::select()
1874 .from(Alias::new("products"))
1875 .column(ColumnRef::Asterisk)
1876 .cond_where(Condition::all().add(result.unwrap()))
1877 .to_string(PostgresQueryBuilder);
1878 assert!(query.contains("\"price\""));
1879 assert!(query.contains("\"discount_price\""));
1880 }
1881
1882 #[test]
1883 fn test_build_single_filter_expr_field_ref_gt() {
1884 let filter = Filter::new(
1885 "price".to_string(),
1886 FilterOperator::Gt,
1887 FilterValue::FieldRef(F::new("cost")),
1888 );
1889 let result = build_single_filter_expr(&filter);
1890 assert!(result.is_some());
1891 }
1892
1893 #[test]
1894 fn test_build_single_filter_expr_field_ref_all_operators() {
1895 let operators = [
1896 FilterOperator::Eq,
1897 FilterOperator::Ne,
1898 FilterOperator::Gt,
1899 FilterOperator::Gte,
1900 FilterOperator::Lt,
1901 FilterOperator::Lte,
1902 ];
1903
1904 for op in operators {
1905 let filter = Filter::new(
1906 "field_a".to_string(),
1907 op.clone(),
1908 FilterValue::FieldRef(F::new("field_b")),
1909 );
1910 let result = build_single_filter_expr(&filter);
1911 assert!(
1912 result.is_some(),
1913 "FieldRef with {:?} should produce Some",
1914 op
1915 );
1916 }
1917 }
1918
1919 #[test]
1920 fn test_build_single_filter_expr_outer_ref() {
1921 let filter = Filter::new(
1922 "author_id".to_string(),
1923 FilterOperator::Eq,
1924 FilterValue::OuterRef(OuterRef::new("authors.id")),
1925 );
1926 let result = build_single_filter_expr(&filter);
1927 assert!(result.is_some());
1928
1929 let query = Query::select()
1930 .from(Alias::new("books"))
1931 .column(ColumnRef::Asterisk)
1932 .cond_where(Condition::all().add(result.unwrap()))
1933 .to_string(PostgresQueryBuilder);
1934 assert!(query.contains("author_id"));
1935 assert!(query.contains("authors.id"));
1936 }
1937
1938 #[test]
1939 fn test_build_single_filter_expr_outer_ref_all_operators() {
1940 let operators = [
1941 FilterOperator::Eq,
1942 FilterOperator::Ne,
1943 FilterOperator::Gt,
1944 FilterOperator::Gte,
1945 FilterOperator::Lt,
1946 FilterOperator::Lte,
1947 ];
1948
1949 for op in operators {
1950 let filter = Filter::new(
1951 "child_id".to_string(),
1952 op.clone(),
1953 FilterValue::OuterRef(OuterRef::new("parent.id")),
1954 );
1955 let result = build_single_filter_expr(&filter);
1956 assert!(
1957 result.is_some(),
1958 "OuterRef with {:?} should produce Some",
1959 op
1960 );
1961 }
1962 }
1963
1964 #[test]
1965 fn test_build_single_filter_expr_expression() {
1966 use reinhardt_db::orm::annotation::{AnnotationValue, Value};
1967
1968 let expr = Expression::Multiply(
1970 Box::new(AnnotationValue::Field(F::new("cost"))),
1971 Box::new(AnnotationValue::Value(Value::Int(2))),
1972 );
1973 let filter = Filter::new(
1974 "price".to_string(),
1975 FilterOperator::Gt,
1976 FilterValue::Expression(expr),
1977 );
1978 let result = build_single_filter_expr(&filter);
1979 assert!(result.is_some());
1980 }
1981
1982 #[test]
1983 fn test_build_single_filter_expr_expression_all_operators() {
1984 use reinhardt_db::orm::annotation::{AnnotationValue, Value as OrmValue};
1985
1986 let operators = [
1987 FilterOperator::Eq,
1988 FilterOperator::Ne,
1989 FilterOperator::Gt,
1990 FilterOperator::Gte,
1991 FilterOperator::Lt,
1992 FilterOperator::Lte,
1993 ];
1994
1995 for op in operators {
1996 let expr = Expression::Add(
1997 Box::new(AnnotationValue::Field(F::new("base"))),
1998 Box::new(AnnotationValue::Value(OrmValue::Int(10))),
1999 );
2000 let filter = Filter::new(
2001 "total".to_string(),
2002 op.clone(),
2003 FilterValue::Expression(expr),
2004 );
2005 let result = build_single_filter_expr(&filter);
2006 assert!(
2007 result.is_some(),
2008 "Expression with {:?} should produce Some",
2009 op
2010 );
2011 }
2012 }
2013
2014 #[test]
2015 fn test_build_single_filter_expr_uses_transformed_filter_lhs() {
2016 let filter = reinhardt_db::orm::expressions::FieldRef::<(), i64>::new("created_at")
2018 .year()
2019 .range(2024, 2026);
2020
2021 let result = build_single_filter_expr(&filter);
2023
2024 assert!(result.is_some());
2026 let query = Query::select()
2027 .from(Alias::new("users"))
2028 .column(ColumnRef::Asterisk)
2029 .cond_where(Condition::all().add(result.unwrap()))
2030 .to_string(PostgresQueryBuilder);
2031 assert_eq!(
2032 query,
2033 r#"SELECT * FROM "users" WHERE EXTRACT(YEAR FROM "created_at") BETWEEN 2024 AND 2026"#
2034 );
2035 }
2036
2037 #[rstest]
2038 fn test_filter_value_to_sea_value_preserves_timestamp_binding() {
2039 let timestamp = chrono::DateTime::parse_from_rfc3339("2026-07-26T00:00:00Z")
2041 .expect("timestamp should be valid")
2042 .with_timezone(&chrono::Utc);
2043 let value = FilterValue::Timestamp(timestamp);
2044
2045 let sea_value = filter_value_to_sea_value(&value);
2047
2048 assert_eq!(
2050 sea_value,
2051 Value::ChronoDateTimeUtc(Some(Box::new(timestamp)))
2052 );
2053 }
2054
2055 #[rstest]
2056 fn test_filter_value_to_sea_value_preserves_uuid_binding() {
2057 let uuid =
2059 uuid::Uuid::parse_str("123e4567-e89b-12d3-a456-426614174000").expect("UUID is valid");
2060 let value = FilterValue::Uuid(uuid);
2061
2062 let sea_value = filter_value_to_sea_value(&value);
2064
2065 assert_eq!(sea_value, Value::Uuid(Some(Box::new(uuid))));
2067 }
2068
2069 #[test]
2070 fn test_filter_value_to_sea_value_field_ref_fallback() {
2071 let value = FilterValue::FieldRef(F::new("test_field"));
2072 let sea_value = filter_value_to_sea_value(&value);
2073
2074 match sea_value {
2076 Value::String(Some(s)) => assert_eq!(s.as_str(), "test_field"),
2077 _ => panic!("Expected String value"),
2078 }
2079 }
2080
2081 #[test]
2082 fn test_filter_value_to_sea_value_outer_ref_fallback() {
2083 let value = FilterValue::OuterRef(OuterRef::new("outer.field"));
2084 let sea_value = filter_value_to_sea_value(&value);
2085
2086 match sea_value {
2088 Value::String(Some(s)) => assert_eq!(s.as_str(), "outer.field"),
2089 _ => panic!("Expected String value"),
2090 }
2091 }
2092
2093 #[test]
2094 fn test_filter_value_to_sea_value_expression_fallback() {
2095 use reinhardt_db::orm::annotation::{AnnotationValue, Value as OrmValue};
2096
2097 let expr = Expression::Add(
2098 Box::new(AnnotationValue::Field(F::new("a"))),
2099 Box::new(AnnotationValue::Value(OrmValue::Int(1))),
2100 );
2101 let value = FilterValue::Expression(expr);
2102 let sea_value = filter_value_to_sea_value(&value);
2103
2104 match sea_value {
2106 Value::String(Some(s)) => {
2107 assert!(s.contains("a"), "SQL should contain field name 'a'");
2108 assert!(s.contains("1"), "SQL should contain value '1'");
2109 }
2110 _ => panic!("Expected String value"),
2111 }
2112 }
2113
2114 #[test]
2115 fn test_filter_value_to_sea_value_preserves_timestamp_and_uuid_types() {
2116 let timestamp = chrono::DateTime::parse_from_rfc3339("2026-07-27T00:00:00Z")
2117 .expect("timestamp fixture should parse")
2118 .with_timezone(&chrono::Utc);
2119 let uuid = uuid::Uuid::parse_str("123e4567-e89b-12d3-a456-426614174000")
2120 .expect("UUID fixture should parse");
2121
2122 assert!(matches!(
2123 filter_value_to_sea_value(&FilterValue::Timestamp(timestamp)),
2124 Value::ChronoDateTimeUtc(Some(_))
2125 ));
2126 assert!(matches!(
2127 filter_value_to_sea_value(&FilterValue::Uuid(uuid)),
2128 Value::Uuid(Some(_))
2129 ));
2130 }
2131
2132 #[test]
2133 fn test_filter_value_to_sea_value_preserves_date_time_and_naive_datetime() {
2134 let date = chrono::NaiveDate::from_ymd_opt(2026, 8, 20).expect("valid date");
2135 let time = chrono::NaiveTime::from_hms_opt(23, 51, 53).expect("valid time");
2136 let naive_datetime = chrono::NaiveDateTime::new(date, time);
2137
2138 assert_eq!(
2139 filter_value_to_sea_value(&FilterValue::Date(date)),
2140 Value::ChronoDate(Some(Box::new(date)))
2141 );
2142 assert_eq!(
2143 filter_value_to_sea_value(&FilterValue::Time(time)),
2144 Value::ChronoTime(Some(Box::new(time)))
2145 );
2146 assert_eq!(
2147 filter_value_to_sea_value(&FilterValue::NaiveDateTime(naive_datetime)),
2148 Value::ChronoDateTime(Some(Box::new(naive_datetime)))
2149 );
2150 }
2151
2152 #[rstest]
2155 fn test_insert_values_mismatch_returns_error_not_panic() {
2156 let mut query = Query::insert()
2160 .into_table(Alias::new("test_table"))
2161 .to_owned();
2162
2163 let columns = vec![Alias::new("col1"), Alias::new("col2"), Alias::new("col3")];
2164 let values = vec![Value::String(Some(Box::new("val1".to_string())))]; let result = query.columns(columns).values(values);
2168
2169 assert!(result.is_err());
2171 }
2172
2173 #[rstest]
2174 fn test_insert_values_matching_count_succeeds() {
2175 let mut query = Query::insert()
2177 .into_table(Alias::new("test_table"))
2178 .to_owned();
2179
2180 let columns = vec![Alias::new("col1"), Alias::new("col2")];
2181 let values = vec![
2182 Value::String(Some(Box::new("val1".to_string()))),
2183 Value::String(Some(Box::new("val2".to_string()))),
2184 ];
2185
2186 let result = query.columns(columns).values(values);
2188
2189 assert!(result.is_ok());
2191 }
2192
2193 #[test]
2196 fn test_outer_ref_filter_uses_safe_column_api() {
2197 let filter = Filter::new(
2199 "author_id".to_string(),
2200 FilterOperator::Eq,
2201 FilterValue::OuterRef(OuterRef::new("users.id")),
2202 );
2203
2204 let result = build_single_filter_expr(&filter);
2206
2207 assert!(result.is_some());
2209 let expr = result.unwrap();
2210 let query = Query::select()
2211 .from(Alias::new("books"))
2212 .column(ColumnRef::Asterisk)
2213 .cond_where(Condition::all().add(expr))
2214 .to_string(PostgresQueryBuilder);
2215 assert!(
2217 query.contains("\"author_id\""),
2218 "Column should be properly quoted: {}",
2219 query
2220 );
2221 }
2222
2223 #[test]
2224 fn test_outer_ref_injection_attempt_is_safely_quoted() {
2225 let filter = Filter::new(
2227 "id".to_string(),
2228 FilterOperator::Eq,
2229 FilterValue::OuterRef(OuterRef::new("id; DROP TABLE users; --")),
2230 );
2231
2232 let result = build_single_filter_expr(&filter);
2234
2235 assert!(result.is_some());
2237 let expr = result.unwrap();
2238 let query = Query::select()
2239 .from(Alias::new("items"))
2240 .column(ColumnRef::Asterisk)
2241 .cond_where(Condition::all().add(expr))
2242 .to_string(PostgresQueryBuilder);
2243 assert!(
2248 query.contains("\"id; DROP TABLE users; --\""),
2249 "Injection payload should be enclosed in double quotes as identifier: {}",
2250 query
2251 );
2252 let unquoted_parts: Vec<&str> = query.split('"').enumerate()
2255 .filter(|(i, _)| i % 2 == 0) .map(|(_, s)| s)
2257 .collect();
2258 let unquoted_sql = unquoted_parts.join("");
2259 assert!(
2260 !unquoted_sql.contains(';'),
2261 "No semicolons should appear outside quoted identifiers: {}",
2262 query
2263 );
2264 }
2265
2266 #[test]
2267 fn test_expression_filter_uses_safe_api() {
2268 use reinhardt_db::orm::annotation::AnnotationValue;
2269
2270 let expr = Expression::Multiply(
2272 Box::new(AnnotationValue::Field(F::new("unit_price"))),
2273 Box::new(AnnotationValue::Field(F::new("quantity"))),
2274 );
2275 let filter = Filter::new(
2276 "total".to_string(),
2277 FilterOperator::Eq,
2278 FilterValue::Expression(expr),
2279 );
2280
2281 let result = build_single_filter_expr(&filter);
2283
2284 assert!(result.is_some());
2286 let sea_expr = result.unwrap();
2287 let query = Query::select()
2288 .from(Alias::new("orders"))
2289 .column(ColumnRef::Asterisk)
2290 .cond_where(Condition::all().add(sea_expr))
2291 .to_string(PostgresQueryBuilder);
2292 assert!(
2293 query.contains("\"total\""),
2294 "Left side should be quoted: {}",
2295 query
2296 );
2297 }
2298
2299 #[test]
2300 fn test_expression_filter_with_literal_value() {
2301 use reinhardt_db::orm::annotation::{AnnotationValue, Value as OrmValue};
2302
2303 let expr = Expression::Add(
2305 Box::new(AnnotationValue::Field(F::new("price"))),
2306 Box::new(AnnotationValue::Value(OrmValue::Int(100))),
2307 );
2308 let filter = Filter::new(
2309 "adjusted_price".to_string(),
2310 FilterOperator::Gt,
2311 FilterValue::Expression(expr),
2312 );
2313
2314 let result = build_single_filter_expr(&filter);
2316
2317 assert!(result.is_some());
2319 }
2320
2321 #[test]
2322 fn test_outer_ref_all_operators_use_safe_api() {
2323 let operators = vec![
2325 FilterOperator::Eq,
2326 FilterOperator::Ne,
2327 FilterOperator::Gt,
2328 FilterOperator::Gte,
2329 FilterOperator::Lt,
2330 FilterOperator::Lte,
2331 ];
2332
2333 for op in operators {
2334 let filter = Filter::new(
2335 "field_a".to_string(),
2336 op.clone(),
2337 FilterValue::OuterRef(OuterRef::new("field_b")),
2338 );
2339 let result = build_single_filter_expr(&filter);
2340 assert!(
2341 result.is_some(),
2342 "OuterRef with {:?} should produce Some",
2343 op
2344 );
2345 }
2346 }
2347
2348 #[test]
2351 fn test_coalesce_expression_uses_safe_parameterized_api() {
2352 use reinhardt_db::orm::annotation::{AnnotationValue, Value as OrmValue};
2353
2354 let expr = Expression::Coalesce(vec![
2356 AnnotationValue::Field(F::new("field_a")),
2357 AnnotationValue::Value(OrmValue::Int(0)),
2358 ]);
2359 let filter = Filter::new(
2360 "result".to_string(),
2361 FilterOperator::Gt,
2362 FilterValue::Expression(expr),
2363 );
2364
2365 let result = build_single_filter_expr(&filter);
2367
2368 assert!(result.is_some());
2370 let sea_expr = result.unwrap();
2371 let query = Query::select()
2372 .from(Alias::new("items"))
2373 .column(ColumnRef::Asterisk)
2374 .cond_where(Condition::all().add(sea_expr))
2375 .to_string(PostgresQueryBuilder);
2376 assert!(
2377 query.contains("COALESCE"),
2378 "Should contain COALESCE function: {}",
2379 query
2380 );
2381 assert!(
2382 query.contains("\"result\""),
2383 "Left side should be quoted: {}",
2384 query
2385 );
2386 }
2387
2388 #[test]
2389 fn test_case_expression_uses_safe_api() {
2390 use reinhardt_db::orm::annotation::{
2391 AnnotationValue, Value as OrmValue, When as AnnotWhen,
2392 };
2393 use reinhardt_db::orm::expressions::Q;
2394
2395 let expr = Expression::Case {
2397 whens: vec![AnnotWhen::new(
2398 Q::new("status", "=", "'active'"),
2399 AnnotationValue::Value(OrmValue::Int(1)),
2400 )],
2401 default: Some(Box::new(AnnotationValue::Value(OrmValue::Int(0)))),
2402 };
2403 let filter = Filter::new(
2404 "priority".to_string(),
2405 FilterOperator::Eq,
2406 FilterValue::Expression(expr),
2407 );
2408
2409 let result = build_single_filter_expr(&filter);
2411
2412 assert!(result.is_some());
2414 let sea_expr = result.unwrap();
2415 let query = Query::select()
2416 .from(Alias::new("tasks"))
2417 .column(ColumnRef::Asterisk)
2418 .cond_where(Condition::all().add(sea_expr))
2419 .to_string(PostgresQueryBuilder);
2420 assert!(
2421 query.contains("CASE"),
2422 "Should contain CASE keyword: {}",
2423 query
2424 );
2425 assert!(
2426 query.contains("WHEN"),
2427 "Should contain WHEN keyword: {}",
2428 query
2429 );
2430 assert!(
2431 query.contains("ELSE"),
2432 "Should contain ELSE keyword: {}",
2433 query
2434 );
2435 }
2436
2437 #[test]
2438 fn test_empty_coalesce_returns_null() {
2439 let expr = Expression::Coalesce(vec![]);
2441
2442 let result = annotation_expr_to_safe_expr(&expr);
2444
2445 let query = Query::select()
2447 .from(Alias::new("test"))
2448 .column(ColumnRef::Asterisk)
2449 .cond_where(Condition::all().add(result))
2450 .to_string(PostgresQueryBuilder);
2451 assert!(
2452 query.contains("NULL"),
2453 "Empty COALESCE should produce NULL: {}",
2454 query
2455 );
2456 }
2457
2458 #[test]
2461 fn test_aggregate_count_uses_safe_api() {
2462 use reinhardt_db::orm::aggregation::{Aggregate, AggregateFunc};
2463
2464 let agg = Aggregate {
2466 func: AggregateFunc::Count,
2467 field: None,
2468 alias: None,
2469 distinct: false,
2470 };
2471
2472 let result = aggregate_to_safe_expr(&agg);
2474
2475 let query = Query::select()
2477 .from(Alias::new("items"))
2478 .expr(result)
2479 .to_string(PostgresQueryBuilder);
2480 assert!(
2481 query.contains("COUNT(*)"),
2482 "Should contain COUNT(*): {}",
2483 query
2484 );
2485 }
2486
2487 #[test]
2488 fn test_aggregate_sum_field_uses_quoted_identifier() {
2489 use reinhardt_db::orm::aggregation::{Aggregate, AggregateFunc};
2490
2491 let agg = Aggregate {
2493 func: AggregateFunc::Sum,
2494 field: Some("price".to_string()),
2495 alias: None,
2496 distinct: false,
2497 };
2498
2499 let result = aggregate_to_safe_expr(&agg);
2501
2502 let query = Query::select()
2504 .from(Alias::new("orders"))
2505 .expr(result)
2506 .to_string(PostgresQueryBuilder);
2507 assert!(
2508 query.contains("SUM("),
2509 "Should contain SUM function: {}",
2510 query
2511 );
2512 assert!(
2513 query.contains("\"price\""),
2514 "Field name should be quoted: {}",
2515 query
2516 );
2517 }
2518
2519 #[test]
2520 fn test_aggregate_count_distinct_uses_distinct_keyword() {
2521 use reinhardt_db::orm::aggregation::{Aggregate, AggregateFunc};
2522
2523 let agg = Aggregate {
2525 func: AggregateFunc::CountDistinct,
2526 field: Some("category".to_string()),
2527 alias: None,
2528 distinct: false, };
2530
2531 let result = aggregate_to_safe_expr(&agg);
2533
2534 let query = Query::select()
2536 .from(Alias::new("products"))
2537 .expr(result)
2538 .to_string(PostgresQueryBuilder);
2539 assert!(
2540 query.contains("COUNT(DISTINCT"),
2541 "Should contain COUNT(DISTINCT: {}",
2542 query
2543 );
2544 assert!(
2545 query.contains("\"category\""),
2546 "Field name should be quoted: {}",
2547 query
2548 );
2549 }
2550
2551 #[test]
2552 fn test_aggregate_injection_attempt_is_quoted() {
2553 use reinhardt_db::orm::aggregation::{Aggregate, AggregateFunc};
2554
2555 let agg = Aggregate {
2557 func: AggregateFunc::Sum,
2558 field: Some("price); DROP TABLE users; --".to_string()),
2559 alias: None,
2560 distinct: false,
2561 };
2562
2563 let result = aggregate_to_safe_expr(&agg);
2565
2566 let query = Query::select()
2568 .from(Alias::new("orders"))
2569 .expr(result)
2570 .to_string(PostgresQueryBuilder);
2571 assert!(
2572 query.contains("\"price); DROP TABLE users; --\""),
2573 "Injection payload should be enclosed in double quotes: {}",
2574 query
2575 );
2576 }
2577
2578 #[rstest]
2581 fn test_build_composite_and_all_unsupported_returns_none() {
2582 let filter1 = Filter::new(
2585 "field1".to_string(),
2586 FilterOperator::Contains,
2587 FilterValue::Boolean(true),
2588 );
2589 let filter2 = Filter::new(
2590 "field2".to_string(),
2591 FilterOperator::StartsWith,
2592 FilterValue::Integer(5),
2593 );
2594 let condition = FilterCondition::And(vec![
2595 FilterCondition::Single(filter1),
2596 FilterCondition::Single(filter2),
2597 ]);
2598
2599 let result = build_composite_filter_condition(&condition);
2601
2602 assert!(result.is_ok());
2604 assert!(
2605 result.unwrap().is_none(),
2606 "And with all unsupported filters should return None"
2607 );
2608 }
2609
2610 #[rstest]
2611 fn test_build_composite_or_all_unsupported_returns_none() {
2612 let filter1 = Filter::new(
2614 "field1".to_string(),
2615 FilterOperator::Contains,
2616 FilterValue::Boolean(true),
2617 );
2618 let filter2 = Filter::new(
2619 "field2".to_string(),
2620 FilterOperator::StartsWith,
2621 FilterValue::Integer(5),
2622 );
2623 let condition = FilterCondition::Or(vec![
2624 FilterCondition::Single(filter1),
2625 FilterCondition::Single(filter2),
2626 ]);
2627
2628 let result = build_composite_filter_condition(&condition);
2630
2631 assert!(result.is_ok());
2633 assert!(
2634 result.unwrap().is_none(),
2635 "Or with all unsupported filters should return None"
2636 );
2637 }
2638
2639 #[rstest]
2640 fn test_build_composite_and_mixed_valid_and_unsupported() {
2641 let valid_filter = Filter::new(
2643 "name".to_string(),
2644 FilterOperator::Eq,
2645 FilterValue::String("Alice".to_string()),
2646 );
2647 let unsupported_filter = Filter::new(
2648 "field2".to_string(),
2649 FilterOperator::Contains,
2650 FilterValue::Boolean(true),
2651 );
2652 let condition = FilterCondition::And(vec![
2653 FilterCondition::Single(valid_filter),
2654 FilterCondition::Single(unsupported_filter),
2655 ]);
2656
2657 let result = build_composite_filter_condition(&condition);
2659
2660 assert!(result.is_ok());
2662 let cond = result.unwrap();
2663 assert!(
2664 cond.is_some(),
2665 "And with at least one valid filter should return Some"
2666 );
2667 let query = Query::select()
2668 .from(Alias::new("t"))
2669 .column(ColumnRef::Asterisk)
2670 .cond_where(cond.unwrap())
2671 .to_string(PostgresQueryBuilder);
2672 assert!(
2673 query.contains("\"name\""),
2674 "SQL should contain the valid filter field, got: {}",
2675 query
2676 );
2677 assert!(
2678 query.contains("'Alice'"),
2679 "SQL should contain the valid filter value, got: {}",
2680 query
2681 );
2682 }
2683
2684 #[rstest]
2685 fn test_build_composite_or_mixed_valid_and_unsupported() {
2686 let valid_filter = Filter::new(
2688 "email".to_string(),
2689 FilterOperator::Eq,
2690 FilterValue::String("test@example.com".to_string()),
2691 );
2692 let unsupported_filter = Filter::new(
2693 "field2".to_string(),
2694 FilterOperator::StartsWith,
2695 FilterValue::Integer(5),
2696 );
2697 let condition = FilterCondition::Or(vec![
2698 FilterCondition::Single(valid_filter),
2699 FilterCondition::Single(unsupported_filter),
2700 ]);
2701
2702 let result = build_composite_filter_condition(&condition);
2704
2705 assert!(result.is_ok());
2707 let cond = result.unwrap();
2708 assert!(
2709 cond.is_some(),
2710 "Or with at least one valid filter should return Some"
2711 );
2712 let query = Query::select()
2713 .from(Alias::new("t"))
2714 .column(ColumnRef::Asterisk)
2715 .cond_where(cond.unwrap())
2716 .to_string(PostgresQueryBuilder);
2717 assert!(
2718 query.contains("\"email\""),
2719 "SQL should contain the valid filter field, got: {}",
2720 query
2721 );
2722 assert!(
2723 query.contains("'test@example.com'"),
2724 "SQL should contain the valid filter value, got: {}",
2725 query
2726 );
2727 }
2728
2729 #[rstest]
2730 fn test_build_filter_condition_all_unsupported_returns_none() {
2731 let filters = vec![
2733 Filter::new(
2734 "field1".to_string(),
2735 FilterOperator::Contains,
2736 FilterValue::Boolean(true),
2737 ),
2738 Filter::new(
2739 "field2".to_string(),
2740 FilterOperator::StartsWith,
2741 FilterValue::Integer(5),
2742 ),
2743 ];
2744
2745 let result = build_filter_condition(&filters);
2747
2748 assert!(
2750 result.is_none(),
2751 "build_filter_condition with all unsupported filters should return None"
2752 );
2753 }
2754
2755 #[rstest]
2756 fn object_scope_rejects_filters_that_cannot_be_enforced() {
2757 let unsupported = Filter::new(
2758 "tenant_id".to_string(),
2759 FilterOperator::Contains,
2760 FilterValue::Boolean(true),
2761 );
2762 let empty_scope = Filter::new(
2763 "tenant_id".to_string(),
2764 FilterOperator::In,
2765 FilterValue::List(Vec::new()),
2766 );
2767
2768 assert!(build_object_scope_condition(&[unsupported]).is_err());
2769 assert!(build_object_scope_condition(&[empty_scope]).is_err());
2770 assert!(build_object_scope_condition(&[]).unwrap().is_none());
2771 }
2772
2773 #[rstest]
2776 fn test_extract_count_from_row_with_count_key() {
2777 let data = serde_json::json!({"count": 42});
2779
2780 let result = extract_count_from_row(&data);
2782
2783 assert_eq!(result.unwrap(), 42);
2785 }
2786
2787 #[rstest]
2788 fn test_extract_count_from_row_without_count_key() {
2789 let data = serde_json::json!({"total": 10});
2791
2792 let result = extract_count_from_row(&data);
2794
2795 let err = result.unwrap_err();
2797 assert!(
2798 err.to_string().contains("missing 'count' key"),
2799 "Error should mention missing 'count' key, got: {}",
2800 err
2801 );
2802 }
2803
2804 #[rstest]
2805 fn test_extract_count_from_row_empty_object() {
2806 let data = serde_json::json!({});
2808
2809 let result = extract_count_from_row(&data);
2811
2812 let err = result.unwrap_err();
2814 assert!(
2815 err.to_string().contains("missing 'count' key"),
2816 "Error should mention missing 'count' key, got: {}",
2817 err
2818 );
2819 }
2820
2821 #[rstest]
2822 fn test_extract_count_from_row_non_integer() {
2823 let data = serde_json::json!({"count": "abc"});
2825
2826 let result = extract_count_from_row(&data);
2828
2829 let err = result.unwrap_err();
2831 assert!(
2832 err.to_string().contains("non-integer"),
2833 "Error should mention non-integer value, got: {}",
2834 err
2835 );
2836 }
2837
2838 #[rstest]
2839 fn test_extract_count_from_row_null_data() {
2840 let data = serde_json::Value::Null;
2842
2843 let result = extract_count_from_row(&data);
2845
2846 let err = result.unwrap_err();
2848 assert!(
2849 err.to_string().contains("unexpected data format"),
2850 "Error should mention unexpected data format, got: {}",
2851 err
2852 );
2853 }
2854
2855 #[rstest]
2856 fn test_extract_count_from_row_zero() {
2857 let data = serde_json::json!({"count": 0});
2859
2860 let result = extract_count_from_row(&data);
2862
2863 assert_eq!(result.unwrap(), 0);
2865 }
2866
2867 #[rstest]
2870 #[tokio::test]
2871 async fn test_admin_database_inject_error_hint_mentions_connection() {
2872 let singleton = Arc::new(reinhardt_di::SingletonScope::new());
2874 let ctx = reinhardt_di::InjectionContext::builder(singleton).build();
2875
2876 let result = AdminDatabase::inject(&ctx).await;
2878
2879 assert!(result.is_err());
2881 let err = result.err().unwrap();
2882 assert!(
2883 err.to_string().contains("DatabaseConnection"),
2884 "Error hint should mention DatabaseConnection, got: {}",
2885 err
2886 );
2887 }
2888
2889 #[rstest]
2890 #[tokio::test]
2891 async fn test_admin_database_inject_returns_prebuilt_from_singleton() {
2892 let singleton = Arc::new(reinhardt_di::SingletonScope::new());
2894 let ctx = reinhardt_di::InjectionContext::builder(singleton).build();
2901
2902 let result = AdminDatabase::inject(&ctx).await;
2904
2905 assert!(result.is_err());
2907 let err = result.err().unwrap();
2908 match err {
2909 reinhardt_di::DiError::NotRegistered { type_name, hint } => {
2910 assert_eq!(type_name, "AdminDatabase");
2911 assert_eq!(
2912 hint,
2913 "DatabaseConnection must be registered as a singleton. \
2914 Use InjectionContextBuilder::singleton(db_connection) during setup."
2915 );
2916 }
2917 other => panic!("Expected NotRegistered error, got: {other:?}"),
2918 }
2919 }
2920
2921 #[rstest]
2922 #[tokio::test]
2923 async fn test_admin_database_keyed_provider_reports_missing_connection() {
2924 let singleton = Arc::new(reinhardt_di::SingletonScope::new());
2925 let ctx = reinhardt_di::InjectionContext::builder(singleton).build();
2926
2927 let result =
2928 reinhardt_di::Depends::<AdminDatabaseKey, AdminDatabase>::resolve_from_registry(
2929 &ctx, true,
2930 )
2931 .await;
2932
2933 assert!(result.is_err());
2934 let err = result.err().unwrap();
2935 match err {
2936 reinhardt_di::DiError::NotRegistered { type_name, hint } => {
2937 assert_eq!(type_name, "AdminDatabase");
2938 assert_eq!(
2939 hint,
2940 "DatabaseConnection must be registered as a singleton. \
2941 Use InjectionContextBuilder::singleton(db_connection) during setup."
2942 );
2943 }
2944 other => panic!("Expected NotRegistered error, got: {other:?}"),
2945 }
2946 }
2947
2948 #[rstest]
2951 fn test_build_single_filter_expr_array_in() {
2952 let filter = Filter::new(
2954 "status".to_string(),
2955 FilterOperator::In,
2956 FilterValue::Array(vec!["a".to_string(), "b".to_string(), "c".to_string()]),
2957 );
2958
2959 let result = build_single_filter_expr(&filter);
2961
2962 assert!(
2964 result.is_some(),
2965 "Array In with non-empty values should return Some"
2966 );
2967 let query = Query::select()
2968 .from(Alias::new("table"))
2969 .column(ColumnRef::Asterisk)
2970 .cond_where(Condition::all().add(result.unwrap()))
2971 .to_string(PostgresQueryBuilder);
2972 assert!(query.contains("IN"), "SQL should contain IN operator");
2973 assert!(query.contains("'a'"), "SQL should contain value 'a'");
2974 assert!(query.contains("'b'"), "SQL should contain value 'b'");
2975 assert!(query.contains("'c'"), "SQL should contain value 'c'");
2976 }
2977
2978 #[rstest]
2979 fn test_build_single_filter_expr_array_not_in() {
2980 let filter = Filter::new(
2982 "status".to_string(),
2983 FilterOperator::NotIn,
2984 FilterValue::Array(vec!["x".to_string(), "y".to_string()]),
2985 );
2986
2987 let result = build_single_filter_expr(&filter);
2989
2990 assert!(
2992 result.is_some(),
2993 "Array NotIn with non-empty values should return Some"
2994 );
2995 let query = Query::select()
2996 .from(Alias::new("table"))
2997 .column(ColumnRef::Asterisk)
2998 .cond_where(Condition::all().add(result.unwrap()))
2999 .to_string(PostgresQueryBuilder);
3000 assert!(
3001 query.contains("NOT IN"),
3002 "SQL should contain NOT IN operator"
3003 );
3004 assert!(query.contains("'x'"), "SQL should contain value 'x'");
3005 assert!(query.contains("'y'"), "SQL should contain value 'y'");
3006 }
3007
3008 #[rstest]
3009 fn test_build_single_filter_expr_array_in_empty() {
3010 let filter = Filter::new(
3012 "status".to_string(),
3013 FilterOperator::In,
3014 FilterValue::Array(vec![]),
3015 );
3016
3017 let result = build_single_filter_expr(&filter);
3019
3020 assert!(
3022 result.is_none(),
3023 "Array In with empty values should return None"
3024 );
3025 }
3026
3027 #[rstest]
3028 fn test_build_single_filter_expr_array_in_single_element() {
3029 let filter = Filter::new(
3031 "category".to_string(),
3032 FilterOperator::In,
3033 FilterValue::Array(vec!["solo".to_string()]),
3034 );
3035
3036 let result = build_single_filter_expr(&filter);
3038
3039 assert!(
3041 result.is_some(),
3042 "Array In with single element should return Some"
3043 );
3044 let query = Query::select()
3045 .from(Alias::new("table"))
3046 .column(ColumnRef::Asterisk)
3047 .cond_where(Condition::all().add(result.unwrap()))
3048 .to_string(PostgresQueryBuilder);
3049 assert!(query.contains("IN"), "SQL should contain IN operator");
3050 assert!(query.contains("'solo'"), "SQL should contain value 'solo'");
3051 }
3052
3053 #[rstest]
3054 fn test_build_single_filter_expr_array_in_special_chars() {
3055 let filter = Filter::new(
3057 "name".to_string(),
3058 FilterOperator::In,
3059 FilterValue::Array(vec!["O'Brien".to_string(), "a;DROP TABLE".to_string()]),
3060 );
3061
3062 let result = build_single_filter_expr(&filter);
3064
3065 assert!(
3067 result.is_some(),
3068 "Array In with special chars should return Some"
3069 );
3070 let query = Query::select()
3071 .from(Alias::new("table"))
3072 .column(ColumnRef::Asterisk)
3073 .cond_where(Condition::all().add(result.unwrap()))
3074 .to_string(PostgresQueryBuilder);
3075 assert!(query.contains("IN"), "SQL should contain IN operator");
3076 assert!(
3078 query.contains("O''Brien"),
3079 "Single quote in value should be escaped, got: {}",
3080 query
3081 );
3082 assert!(
3084 query.contains("'a;DROP TABLE'"),
3085 "SQL injection attempt should be safely quoted as a string literal, got: {}",
3086 query
3087 );
3088 }
3089
3090 #[rstest]
3093 fn test_and_with_all_unsupported_returns_none() {
3094 let unsupported1 = FilterCondition::Single(Filter::new(
3096 "name",
3097 FilterOperator::Contains,
3098 FilterValue::Integer(42),
3099 ));
3100 let unsupported2 = FilterCondition::Single(Filter::new(
3101 "email",
3102 FilterOperator::StartsWith,
3103 FilterValue::Integer(99),
3104 ));
3105 let condition = FilterCondition::And(vec![unsupported1, unsupported2]);
3106
3107 let result = build_composite_filter_condition(&condition);
3109
3110 assert!(result.is_ok());
3113 let cond = result.unwrap();
3114 assert!(
3115 cond.is_none(),
3116 "And with all unsupported sub-conditions should return None"
3117 );
3118 }
3119
3120 #[rstest]
3121 fn test_or_with_all_unsupported_returns_none() {
3122 let unsupported1 = FilterCondition::Single(Filter::new(
3124 "name",
3125 FilterOperator::Contains,
3126 FilterValue::Integer(42),
3127 ));
3128 let unsupported2 = FilterCondition::Single(Filter::new(
3129 "email",
3130 FilterOperator::StartsWith,
3131 FilterValue::Integer(99),
3132 ));
3133 let condition = FilterCondition::Or(vec![unsupported1, unsupported2]);
3134
3135 let result = build_composite_filter_condition(&condition);
3137
3138 assert!(result.is_ok());
3141 let cond = result.unwrap();
3142 assert!(
3143 cond.is_none(),
3144 "Or with all unsupported sub-conditions should return None"
3145 );
3146 }
3147
3148 #[rstest]
3149 fn test_and_with_mix_supported_unsupported_keeps_supported() {
3150 let supported = FilterCondition::Single(Filter::new(
3152 "name",
3153 FilterOperator::Eq,
3154 FilterValue::String("Alice".to_string()),
3155 ));
3156 let unsupported = FilterCondition::Single(Filter::new(
3157 "email",
3158 FilterOperator::Contains,
3159 FilterValue::Integer(42),
3160 ));
3161 let condition = FilterCondition::And(vec![supported, unsupported]);
3162
3163 let result = build_composite_filter_condition(&condition);
3165
3166 assert!(result.is_ok());
3168 let cond = result.unwrap();
3169 assert!(
3170 cond.is_some(),
3171 "And with mix of supported/unsupported should return Some with supported filters"
3172 );
3173 let query = Query::select()
3175 .from(Alias::new("test"))
3176 .column(ColumnRef::Asterisk)
3177 .cond_where(cond.unwrap())
3178 .to_string(PostgresQueryBuilder);
3179 assert!(
3180 query.contains("\"name\""),
3181 "SQL should contain the supported filter field 'name': {}",
3182 query
3183 );
3184 }
3185
3186 #[rstest]
3187 fn test_or_with_one_supported_one_unsupported() {
3188 let supported = FilterCondition::Single(Filter::new(
3190 "status",
3191 FilterOperator::Eq,
3192 FilterValue::String("active".to_string()),
3193 ));
3194 let unsupported = FilterCondition::Single(Filter::new(
3195 "count",
3196 FilterOperator::Contains,
3197 FilterValue::Integer(42),
3198 ));
3199 let condition = FilterCondition::Or(vec![supported, unsupported]);
3200
3201 let result = build_composite_filter_condition(&condition);
3203
3204 assert!(result.is_ok());
3206 let cond = result.unwrap();
3207 assert!(
3208 cond.is_some(),
3209 "Or with one supported condition should return Some"
3210 );
3211 let query = Query::select()
3212 .from(Alias::new("test"))
3213 .column(ColumnRef::Asterisk)
3214 .cond_where(cond.unwrap())
3215 .to_string(PostgresQueryBuilder);
3216 assert!(
3217 query.contains("\"status\""),
3218 "SQL should contain the supported filter field 'status': {}",
3219 query
3220 );
3221 }
3222
3223 #[rstest]
3226 fn test_extract_count_with_count_key() {
3227 let data = serde_json::json!({"count": 42});
3229
3230 let result = extract_count_from_row(&data);
3232
3233 assert!(result.is_ok());
3235 assert_eq!(result.unwrap(), 42);
3236 }
3237
3238 #[rstest]
3239 fn test_extract_count_without_count_key_returns_error() {
3240 let data = serde_json::json!({"total": 42});
3242
3243 let result = extract_count_from_row(&data);
3245
3246 assert!(result.is_err());
3249 let err = result.unwrap_err();
3250 assert!(
3251 err.to_string().contains("missing 'count' key"),
3252 "Error should mention missing 'count' key, got: {}",
3253 err
3254 );
3255 }
3256
3257 #[rstest]
3258 fn test_extract_count_with_multiple_keys_no_count_returns_error() {
3259 let data = serde_json::json!({"total": 42, "other": 99});
3261
3262 let result = extract_count_from_row(&data);
3264
3265 assert!(result.is_err());
3268 let err = result.unwrap_err();
3269 assert!(
3270 err.to_string().contains("available keys"),
3271 "Error should list available keys, got: {}",
3272 err
3273 );
3274 }
3275
3276 #[rstest]
3277 fn test_extract_count_non_integer_returns_error() {
3278 let data = serde_json::json!({"count": "not_a_number"});
3280
3281 let result = extract_count_from_row(&data);
3283
3284 assert!(result.is_err());
3286 let err = result.unwrap_err();
3287 assert!(matches!(err, AdminError::DatabaseError(_)));
3288 }
3289
3290 #[rstest]
3291 fn test_extract_count_null_returns_error() {
3292 let data = serde_json::json!({"count": null});
3294
3295 let result = extract_count_from_row(&data);
3297
3298 assert!(result.is_err());
3300 }
3301
3302 #[rstest]
3303 fn test_extract_count_empty_object_returns_error() {
3304 let data = serde_json::json!({});
3306
3307 let result = extract_count_from_row(&data);
3309
3310 assert!(result.is_err());
3312 }
3313
3314 #[rstest]
3315 fn test_extract_count_non_object_returns_error() {
3316 let data = serde_json::json!([1, 2, 3]);
3318
3319 let result = extract_count_from_row(&data);
3321
3322 assert!(result.is_err());
3324 }
3325
3326 #[rstest]
3329 fn test_parse_pk_value_integer_falls_back_to_bigint() {
3330 let val = parse_pk_value("nonexistent_table", "id", "42");
3334
3335 assert_eq!(val, Value::BigInt(Some(42)));
3337 }
3338
3339 #[rstest]
3340 fn test_parse_pk_value_uuid_string_without_registry_falls_back_to_string() {
3341 let val = parse_pk_value(
3345 "nonexistent_table",
3346 "id",
3347 "c1a363b1-cc42-4dea-81f0-9dc1cedf0083",
3348 );
3349
3350 assert!(matches!(val, Value::String(Some(_))));
3352 }
3353
3354 #[rstest]
3355 fn test_parse_pk_value_non_numeric_string_falls_back_to_string() {
3356 let val = parse_pk_value("nonexistent_table", "id", "hello-world");
3360
3361 assert!(matches!(val, Value::String(Some(_))));
3363 }
3364
3365 #[rstest]
3366 fn test_parse_pk_value_negative_integer() {
3367 let val = parse_pk_value("nonexistent_table", "id", "-1");
3371
3372 assert_eq!(val, Value::BigInt(Some(-1)));
3374 }
3375
3376 #[rstest]
3377 fn test_parse_pk_value_zero() {
3378 let val = parse_pk_value("nonexistent_table", "id", "0");
3382
3383 assert_eq!(val, Value::BigInt(Some(0)));
3385 }
3386}