Skip to main content

reinhardt_admin/core/
database.rs

1//! Database integration for admin operations
2//!
3//! This module provides database access layer for admin CRUD operations,
4//! integrating with reinhardt-orm's QuerySet API.
5
6use 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
25/// Converts a `serde_json::Value` into a reinhardt-query `Value`.
26///
27/// String values are inspected for ISO 8601 date/time patterns and converted
28/// to the appropriate chrono type so that PostgreSQL accepts them for
29/// `timestamptz`, `date`, and `time` columns without an explicit cast.
30fn json_to_sea_value(value: serde_json::Value) -> Value {
31	match value {
32		serde_json::Value::String(s) => {
33			// ISO 8601 datetime with timezone offset (e.g. "2026-04-02T16:45:50Z")
34			if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&s) {
35				Value::ChronoDateTimeUtc(Some(Box::new(dt.with_timezone(&chrono::Utc))))
36			// ISO 8601 datetime with fractional seconds and Z suffix
37			} 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			// Date only
42			} 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			// Time only
48			} 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			// UUID (8-4-4-4-12 hex pattern)
54			} 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/// Dummy record type for admin panel CRUD operations
83///
84/// This type exists solely to satisfy the `<M: Model>` generic constraint
85/// in `AdminDatabase` methods. The admin panel operates on dynamic data
86/// (serde_json::Value), not statically-typed models.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct AdminRecord {
89	/// The primary key identifier for the admin record.
90	pub id: Option<i64>,
91}
92
93/// Field accessors for `AdminRecord` used in typed query construction.
94#[derive(Debug, Clone)]
95pub struct AdminRecordFields {
96	/// Typed field accessor for the `id` column.
97	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	/// Creates a new set of field accessors with default column names.
108	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
144/// Converts a string primary key value to the appropriate SeaQuery `Value`
145/// based on the field's registered database type.
146///
147/// Looks up the field type from the migration registry. When the registry has
148/// metadata for the given table/field, the conversion is type-aware (e.g.
149/// UUID strings become `Value::Uuid`). Falls back to the i64-then-String
150/// heuristic when metadata is unavailable.
151fn 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	// Fallback: existing heuristic for backward compatibility
179	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
186/// Batch version of `parse_pk_value` for bulk operations.
187fn 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/// Convert FilterValue to Value
194#[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		// Array values are not scalar; they are handled by In/NotIn arms
203		// in build_single_filter_expr(). Return None-string as fallback
204		// for unexpected scalar contexts.
205		FilterValue::Array(_) => Value::String(None),
206		FilterValue::List(_) | FilterValue::Range(_, _) => Value::String(None),
207		FilterValue::FieldRef(f) => {
208			// FieldRef generates column reference, not scalar value.
209			// For Value context, return field name as string.
210			// Proper handling is in build_single_filter_expr().
211			Value::String(Some(Box::new(f.field.clone())))
212		}
213		FilterValue::Expression(expr) => {
214			// Expression generates SQL expression, not scalar value.
215			// For Value context, return SQL string representation.
216			// Proper handling is in build_single_filter_expr().
217			Value::String(Some(Box::new(expr.to_sql())))
218		}
219		FilterValue::OuterRef(outer) => {
220			// OuterRef generates outer query reference, not scalar value.
221			// For Value context, return field name as string.
222			// Proper handling is in build_single_filter_expr().
223			Value::String(Some(Box::new(outer.field.clone())))
224		}
225	}
226}
227
228/// Convert an annotation `AnnotationValue` to a safe SeaQuery `SimpleExpr`.
229///
230/// Uses type-safe SeaQuery API for field references and literal values
231/// instead of raw SQL string interpolation, preventing SQL injection.
232fn 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		// Subquery and PostgreSQL-specific aggregation types produce SQL
252		// from internally constructed ORM queries, not from user HTTP input.
253		// Their SQL output is safe because it's built through type-safe ORM APIs.
254		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
263/// Convert an `Aggregate` to a safe SeaQuery `SimpleExpr`.
264///
265/// Uses parameterized function templates with quoted column identifiers
266/// instead of raw SQL string interpolation, preventing SQL injection
267/// through field name manipulation.
268fn 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		// COUNT(*) case - static SQL template, no user input
289		Expr::cust(format!("{func_name}(*)")).into()
290	}
291}
292
293/// Convert an annotation `Expression` to a safe SeaQuery `SimpleExpr`.
294///
295/// Recursively converts all expression types using type-safe SeaQuery API
296/// for field references and values, preventing SQL injection through
297/// value manipulation in expression trees.
298fn 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				// Q conditions are constructed internally by the ORM's query builder,
326				// not from user HTTP input. The THEN values are safely converted
327				// through annotation_value_to_safe_expr.
328				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
349/// Escape SQL LIKE wildcard characters in user input
350fn escape_like_pattern(input: &str) -> String {
351	input
352		.replace('\\', "\\\\")
353		.replace('%', "\\%")
354		.replace('_', "\\_")
355}
356
357/// Build a SimpleExpr from a single Filter
358#[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		// Null handling (must come before generic patterns)
365		(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		// FieldRef: Column-to-column comparisons
373		(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		// OuterRef: Correlated subquery references (use type-safe column API)
381		(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		// Expression: Arithmetic expressions (validate field names before building SQL)
401		(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		// Generic scalar value patterns
421		(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		// String-specific operators
429		(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		// Array-based In/NotIn: convert each element to a Value
465		(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		// Skip unsupported combinations
512		_ => return None,
513	};
514
515	Some(expr)
516}
517
518/// Build Condition from filters (AND logic only)
519#[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/// Maximum recursion depth for filter conditions to prevent stack overflow
539#[doc(hidden)]
540pub const MAX_FILTER_DEPTH: usize = 100;
541
542/// Build Condition from FilterCondition (supports AND/OR logic)
543///
544/// This function recursively processes FilterCondition to build complex
545/// query conditions with nested AND/OR logic.
546///
547/// # Stack Overflow Protection
548///
549/// To prevent stack overflow with deeply nested filter conditions, this function
550/// limits recursion depth to `MAX_FILTER_DEPTH` (100 levels). If the depth limit
551/// is exceeded, the function returns an error.
552#[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/// Internal helper for building composite filter conditions with depth tracking
560#[doc(hidden)]
561pub fn build_composite_filter_condition_with_depth(
562	filter_condition: &FilterCondition,
563	depth: usize,
564) -> AdminResult<Option<Condition>> {
565	// Prevent stack overflow by limiting recursion depth
566	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			// Return None if all sub-conditions were unsupported,
592			// preventing an empty Condition::all() that produces WHERE TRUE
593			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			// Return None if all sub-conditions were unsupported,
614			// preventing an empty Condition::any() that produces WHERE FALSE
615			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/// Admin database interface
677///
678/// Provides CRUD operations for admin panel, leveraging reinhardt-orm.
679///
680/// # Examples
681///
682/// ```
683/// use reinhardt_admin::core::{AdminDatabase, AdminRecord};
684/// use reinhardt_db::orm::DatabaseConnection;
685///
686/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
687/// let conn = DatabaseConnection::connect("postgres://localhost/test").await?;
688/// let db = AdminDatabase::new(conn);
689///
690/// // List items with filters
691/// let items = db.list::<AdminRecord>("admin_records", vec![], 0, 50).await?;
692/// # Ok(())
693/// # }
694/// ```
695#[injectable(scope = Singleton, prebuilt = true)]
696#[derive(Clone)]
697pub struct AdminDatabase {
698	connection: Arc<DatabaseConnection>,
699}
700
701/// Provider key for the admin database dependency.
702#[reinhardt_di::injectable_key]
703pub struct AdminDatabaseKey;
704
705impl AdminDatabase {
706	/// Create a new admin database interface
707	///
708	/// This method accepts a DatabaseConnection directly without requiring `Arc` wrapping.
709	/// The `Arc` wrapping is handled internally for you.
710	pub fn new(connection: DatabaseConnection) -> Self {
711		Self {
712			connection: Arc::new(connection),
713		}
714	}
715
716	/// Create a new admin database interface from an Arc-wrapped connection
717	///
718	/// This is provided for cases where you already have an `Arc<DatabaseConnection>`.
719	/// In most cases, you should use `new()` instead.
720	pub fn from_arc(connection: Arc<DatabaseConnection>) -> Self {
721		Self { connection }
722	}
723
724	/// Get a reference to the underlying database connection
725	pub fn connection(&self) -> &DatabaseConnection {
726		&self.connection
727	}
728
729	/// Get a cloned Arc of the connection (for cases where you need ownership)
730	///
731	/// In most cases, you should use `connection()` instead to get a reference.
732	pub fn connection_arc(&self) -> Arc<DatabaseConnection> {
733		Arc::clone(&self.connection)
734	}
735
736	/// List items with filters, ordering, and pagination
737	///
738	/// # Examples
739	///
740	/// ```
741	/// use reinhardt_admin::core::{AdminDatabase, AdminRecord};
742	/// use reinhardt_db::orm::{DatabaseConnection, Filter, FilterOperator, FilterValue};
743	///
744	/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
745	/// let conn = DatabaseConnection::connect("postgres://localhost/test").await?;
746	/// let db = AdminDatabase::new(conn);
747	///
748	/// let filters = vec![
749	///     Filter::new("is_active".to_string(), FilterOperator::Eq, FilterValue::Boolean(true))
750	/// ];
751	///
752	/// let items = db.list::<AdminRecord>("admin_records", filters, 0, 50).await?;
753	/// # Ok(())
754	/// # }
755	/// ```
756	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		// SELECT * is intentional: admin panel operates on dynamic schemas where
764		// the column set is not known at compile time. Each ModelAdmin defines
765		// list_display fields, and column filtering is applied at the application
766		// layer after fetching all columns.
767		let mut query = Query::select()
768			.from(Alias::new(table_name))
769			.column(ColumnRef::Asterisk)
770			.to_owned();
771
772		// Apply filters using build_filter_condition helper
773		if let Some(condition) = build_filter_condition(&filters) {
774			query.cond_where(condition);
775		}
776
777		// Apply pagination
778		query.limit(limit).offset(offset);
779
780		// Execute query
781		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		// Convert QueryRow to HashMap
790		Ok(rows
791			.into_iter()
792			.filter_map(|row| {
793				// row.data is already a serde_json::Value, typically an Object
794				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	/// List items with composite filter conditions (supports AND/OR logic)
807	///
808	/// This method supports complex filter conditions using FilterCondition,
809	/// which allows building nested AND/OR queries.
810	///
811	/// # Arguments
812	///
813	/// * `table_name` - The name of the table to query
814	/// * `filter_condition` - Optional composite filter condition (AND/OR logic)
815	/// * `additional_filters` - Additional simple filters to AND with the condition
816	/// * `sort_by` - Optional sort field (prefix with "-" for descending, e.g., "created_at" or "-created_at")
817	/// * `offset` - Number of items to skip for pagination
818	/// * `limit` - Maximum number of items to return
819	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		// SELECT * is intentional: admin panel operates on dynamic schemas where
829		// the column set is not known at compile time. Each ModelAdmin defines
830		// list_display fields, and column filtering is applied at the application
831		// layer after fetching all columns.
832		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		// Apply sorting (if specified)
845		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		// Apply pagination
861		query.limit(limit).offset(offset);
862
863		// Execute query
864		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		// Convert QueryRow to HashMap
873		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	/// List items and return the filtered total count with one query for non-empty pages.
890	///
891	/// This uses a windowed `COUNT(*) OVER()` expression so the admin list endpoint
892	/// can fetch page rows and pagination metadata without issuing a separate count
893	/// query on the common path.
894	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		// SELECT * is intentional: admin panel operates on dynamic schemas where
904		// the column set is not known at compile time. The synthetic total-count
905		// column is removed before returning API rows.
906		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	/// Count items with composite filter conditions (supports AND/OR logic)
990	///
991	/// # Arguments
992	///
993	/// * `table_name` - The name of the table to query
994	/// * `filter_condition` - Optional composite filter condition (AND/OR logic)
995	/// * `additional_filters` - Additional simple filters to AND with the condition
996	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		// Extract count from result, propagating errors for unexpected formats
1023		let count = extract_count_from_row(&row.data)?;
1024
1025		Ok(count)
1026	}
1027
1028	/// Get a single item by ID
1029	///
1030	/// # Examples
1031	///
1032	/// ```
1033	/// use reinhardt_admin::core::{AdminDatabase, AdminRecord};
1034	/// use reinhardt_db::orm::DatabaseConnection;
1035	///
1036	/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1037	/// let conn = DatabaseConnection::connect("postgres://localhost/test").await?;
1038	/// let db = AdminDatabase::new(conn);
1039	///
1040	/// let item = db.get::<AdminRecord>("admin_records", "id", "1").await?;
1041	/// # Ok(())
1042	/// # }
1043	/// ```
1044	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		// SELECT * is intentional: admin detail view displays all fields from the
1053		// model. The admin panel operates on dynamic schemas where the column set
1054		// is determined by the ModelAdmin configuration at runtime.
1055		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			// r.data is already a serde_json::Value, typically an Object
1071			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	/// Create a new item
1083	///
1084	/// # Examples
1085	///
1086	/// ```
1087	/// use reinhardt_admin::core::{AdminDatabase, AdminRecord};
1088	/// use reinhardt_db::orm::DatabaseConnection;
1089	/// use std::collections::HashMap;
1090	///
1091	/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1092	/// let conn = DatabaseConnection::connect("postgres://localhost/test").await?;
1093	/// let db = AdminDatabase::new(conn);
1094	///
1095	/// let mut data = HashMap::new();
1096	/// data.insert("name".to_string(), serde_json::json!("Alice"));
1097	/// data.insert("email".to_string(), serde_json::json!("alice@example.com"));
1098	///
1099	/// db.create::<AdminRecord>("admin_records", Some("id"), data).await?;
1100	/// # Ok(())
1101	/// # }
1102	/// ```
1103	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		// Sort keys for deterministic column ordering in generated SQL.
1115		// HashMap iteration order is non-deterministic, which causes
1116		// flaky tests and non-reproducible query plans.
1117		let mut sorted_keys: Vec<String> = data.keys().cloned().collect();
1118		sorted_keys.sort();
1119
1120		// Build column and value lists in sorted order
1121		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		// Pass values directly for reinhardt-query
1133		query.columns(columns).values(values).map_err(|e| {
1134			AdminError::DatabaseError(format!("column/value count mismatch: {}", e))
1135		})?;
1136
1137		// Add RETURNING clause using the actual primary key field
1138		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		// Extract the ID from the returned row using the primary key field
1149		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				// UUID and other string-based PKs: return 1 as affected count
1158				// (the actual PK value is a string, not representable as u64)
1159				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	/// Update an existing item
1169	///
1170	/// # Examples
1171	///
1172	/// ```
1173	/// use reinhardt_admin::core::{AdminDatabase, AdminRecord};
1174	/// use reinhardt_db::orm::DatabaseConnection;
1175	/// use std::collections::HashMap;
1176	///
1177	/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1178	/// let conn = DatabaseConnection::connect("postgres://localhost/test").await?;
1179	/// let db = AdminDatabase::new(conn);
1180	///
1181	/// let mut data = HashMap::new();
1182	/// data.insert("name".to_string(), serde_json::json!("Alice Updated"));
1183	///
1184	/// db.update::<AdminRecord>("admin_records", "id", "1", data).await?;
1185	/// # Ok(())
1186	/// # }
1187	/// ```
1188	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		// Sort keys for deterministic SET clause ordering in generated SQL
1198		let mut sorted_keys: Vec<String> = data.keys().cloned().collect();
1199		sorted_keys.sort();
1200
1201		// Build SET clauses in sorted order
1202		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	/// Delete an item by ID
1223	///
1224	/// # Examples
1225	///
1226	/// ```
1227	/// use reinhardt_admin::core::{AdminDatabase, AdminRecord};
1228	/// use reinhardt_db::orm::DatabaseConnection;
1229	///
1230	/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1231	/// let conn = DatabaseConnection::connect("postgres://localhost/test").await?;
1232	/// let db = AdminDatabase::new(conn);
1233	///
1234	/// db.delete::<AdminRecord>("admin_records", "id", "1").await?;
1235	/// # Ok(())
1236	/// # }
1237	/// ```
1238	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	/// Delete multiple items by IDs (bulk delete)
1263	///
1264	/// # Examples
1265	///
1266	/// ```
1267	/// use reinhardt_admin::core::{AdminDatabase, AdminRecord};
1268	/// use reinhardt_db::orm::DatabaseConnection;
1269	///
1270	/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1271	/// let conn = DatabaseConnection::connect("postgres://localhost/test").await?;
1272	/// let db = AdminDatabase::new(conn);
1273	///
1274	/// let ids = vec!["1".to_string(), "2".to_string(), "3".to_string()];
1275	/// db.bulk_delete::<AdminRecord>("admin_records", "id", ids).await?;
1276	/// # Ok(())
1277	/// # }
1278	/// ```
1279	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	/// Delete multiple items by IDs without requiring Model type parameter
1289	///
1290	/// This method provides a type-safe way to perform bulk deletions without
1291	/// requiring a Model type parameter. It's particularly useful for admin actions
1292	/// where the model type may not be known at compile time.
1293	///
1294	/// # Examples
1295	///
1296	/// ```
1297	/// use reinhardt_admin::core::AdminDatabase;
1298	/// use reinhardt_db::orm::DatabaseConnection;
1299	///
1300	/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1301	/// let conn = DatabaseConnection::connect("postgres://localhost/test").await?;
1302	/// let db = AdminDatabase::new(conn);
1303	///
1304	/// let ids = vec!["1".to_string(), "2".to_string(), "3".to_string()];
1305	/// db.bulk_delete_by_table("users", "id", ids).await?;
1306	/// # Ok(())
1307	/// # }
1308	/// ```
1309	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	/// Count total items with optional filters
1338	///
1339	/// # Examples
1340	///
1341	/// ```
1342	/// use reinhardt_admin::core::{AdminDatabase, AdminRecord};
1343	/// use reinhardt_db::orm::{DatabaseConnection, Filter, FilterOperator, FilterValue};
1344	///
1345	/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1346	/// let conn = DatabaseConnection::connect("postgres://localhost/test").await?;
1347	/// let db = AdminDatabase::new(conn);
1348	///
1349	/// let filters = vec![
1350	///     Filter::new("is_active".to_string(), FilterOperator::Eq, FilterValue::Boolean(true))
1351	/// ];
1352	///
1353	/// let count = db.count::<AdminRecord>("admin_records", filters).await?;
1354	/// # Ok(())
1355	/// # }
1356	/// ```
1357	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		// Apply filters using build_filter_condition helper
1368		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		// Extract count from result, propagating errors for unexpected formats
1381		let count = extract_count_from_row(&row.data)?;
1382
1383		Ok(count)
1384	}
1385}
1386
1387/// Extract count value from a query result row
1388///
1389/// Attempts to extract an integer count from the query result by looking for
1390/// a "count" key in the JSON object.
1391///
1392/// Returns an error if:
1393/// - The "count" key is missing (lists available keys for debugging)
1394/// - The "count" value is not an integer
1395/// - The data format is not a JSON object
1396#[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	// Report available keys for diagnostics instead of using non-deterministic
1408	// HashMap iteration order to pick the first value
1409	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/// Injectable trait implementation for AdminDatabase
1424///
1425/// Auto-constructs from [`DatabaseConnection`] in the singleton scope when
1426/// no pre-built `AdminDatabase` exists. This enables admin DI dependencies
1427/// to be resolved at request time without requiring async initialization
1428/// in the synchronous `routes()` function.
1429///
1430/// Resolution order:
1431/// 1. Check singleton cache for pre-built `AdminDatabase` (backward compat)
1432/// 2. If not found, construct from `DatabaseConnection` in singleton scope
1433/// 3. Cache the constructed instance for subsequent requests
1434#[async_trait]
1435impl Injectable for AdminDatabase {
1436	async fn inject(ctx: &InjectionContext) -> DiResult<Self> {
1437		// Check if pre-built AdminDatabase exists (backward compat with configure_di)
1438		if let Some(db) = ctx.get_singleton::<Self>() {
1439			return Ok((*db).clone());
1440		}
1441
1442		// Auto-construct from DatabaseConnection in singleton scope
1443		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		// Cache for subsequent requests
1454		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
1466// Register AdminDatabase in the global dependency registry so direct
1467// `#[inject] AdminDatabase` parameters can resolve it via ctx.resolve().
1468// Delegates to Injectable::inject() for lazy construction from DatabaseConnection.
1469fn __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	// ==================== escape_like_pattern tests ====================
1490
1491	#[rstest]
1492	fn test_escape_like_pattern_percent() {
1493		// Arrange
1494		let input = "100%";
1495
1496		// Act
1497		let result = escape_like_pattern(input);
1498
1499		// Assert
1500		assert_eq!(result, "100\\%");
1501	}
1502
1503	#[rstest]
1504	fn test_escape_like_pattern_underscore() {
1505		// Arrange
1506		let input = "user_name";
1507
1508		// Act
1509		let result = escape_like_pattern(input);
1510
1511		// Assert
1512		assert_eq!(result, "user\\_name");
1513	}
1514
1515	#[rstest]
1516	fn test_escape_like_pattern_backslash() {
1517		// Arrange
1518		let input = "path\\to";
1519
1520		// Act
1521		let result = escape_like_pattern(input);
1522
1523		// Assert
1524		assert_eq!(result, "path\\\\to");
1525	}
1526
1527	#[rstest]
1528	fn test_escape_like_pattern_combined() {
1529		// Arrange
1530		let input = "100%_done";
1531
1532		// Act
1533		let result = escape_like_pattern(input);
1534
1535		// Assert
1536		assert_eq!(result, "100\\%\\_done");
1537	}
1538
1539	#[rstest]
1540	fn test_escape_like_pattern_no_special_chars() {
1541		// Arrange
1542		let input = "normal text";
1543
1544		// Act
1545		let result = escape_like_pattern(input);
1546
1547		// Assert
1548		assert_eq!(result, "normal text");
1549	}
1550
1551	// ==================== escape_like_pattern regression tests (#632) ====================
1552
1553	/// Regression tests for issue #632: LIKE wildcard injection via unescaped metacharacters.
1554	/// Verifies that percent, underscore, and backslash in user input are always escaped
1555	/// so they cannot be used as LIKE wildcards or escape prefix injections.
1556	#[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		// Arrange: user-supplied string containing LIKE metacharacters
1566		// Act
1567		let escaped = escape_like_pattern(input);
1568		// Assert: output exactly matches fully-escaped form with no unescaped metacharacters
1569		assert_eq!(
1570			escaped, expected,
1571			"input={input:?} was not correctly escaped"
1572		);
1573	}
1574
1575	// ==================== build_composite_filter_condition tests ====================
1576
1577	#[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		// The condition should produce valid SQL when used
1592		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		// OR condition should produce SQL with OR keyword
1632		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		// AND condition should produce SQL with AND keyword
1667		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		// Build: (name LIKE '%Alice%' OR email LIKE '%alice%') AND is_active = true
1675		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		// Nested condition should contain both OR and AND
1711		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		// Empty OR should return Ok(None)
1725		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		// Empty AND should return Ok(None)
1736		assert!(result.is_ok());
1737		assert!(result.unwrap().is_none());
1738	}
1739
1740	#[test]
1741	fn test_build_composite_depth_overflow_returns_error() {
1742		// Build a filter condition that exceeds MAX_FILTER_DEPTH by nesting
1743		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		// Wrap in And() nesting MAX_FILTER_DEPTH + 1 times to exceed the limit
1750		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	// ==================== FieldRef/OuterRef/Expression filter tests ====================
1768
1769	#[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		// Test: price > (cost * 2)
1875		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		// Arrange
1923		let filter = reinhardt_db::orm::expressions::FieldRef::<(), i64>::new("created_at")
1924			.year()
1925			.range(2024, 2026);
1926
1927		// Act
1928		let result = build_single_filter_expr(&filter);
1929
1930		// Assert
1931		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		// Should return string representation, not panic
1949		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		// Should return string representation, not panic
1961		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		// Should return SQL string representation, not panic
1979		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	// ==================== insert values mismatch tests (#1551) ====================
1989
1990	#[rstest]
1991	fn test_insert_values_mismatch_returns_error_not_panic() {
1992		// Arrange
1993		// Simulate the scenario where columns and values count mismatch
1994		// by calling SeaQuery's values() with wrong number of values
1995		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())))]; // Only 1 value for 3 columns
2001
2002		// Act
2003		let result = query.columns(columns).values(values);
2004
2005		// Assert - should return Err, not panic
2006		assert!(result.is_err());
2007	}
2008
2009	#[rstest]
2010	fn test_insert_values_matching_count_succeeds() {
2011		// Arrange
2012		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		// Act
2023		let result = query.columns(columns).values(values);
2024
2025		// Assert
2026		assert!(result.is_ok());
2027	}
2028
2029	// ==================== SQL injection prevention tests ====================
2030
2031	#[test]
2032	fn test_outer_ref_filter_uses_safe_column_api() {
2033		// Arrange: OuterRef with a field name that could be an injection attempt
2034		let filter = Filter::new(
2035			"author_id".to_string(),
2036			FilterOperator::Eq,
2037			FilterValue::OuterRef(OuterRef::new("users.id")),
2038		);
2039
2040		// Act
2041		let result = build_single_filter_expr(&filter);
2042
2043		// Assert: should produce a valid expression using quoted identifiers
2044		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		// The field names should be quoted by SeaQuery's Alias, not raw interpolation
2052		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		// Arrange: attacker tries SQL injection through OuterRef field name
2062		let filter = Filter::new(
2063			"id".to_string(),
2064			FilterOperator::Eq,
2065			FilterValue::OuterRef(OuterRef::new("id; DROP TABLE users; --")),
2066		);
2067
2068		// Act
2069		let result = build_single_filter_expr(&filter);
2070
2071		// Assert: the injection string should be treated as a quoted identifier
2072		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		// SeaQuery's Alias wraps the name in double quotes, treating the entire
2080		// injection payload as a single identifier name (not executable SQL).
2081		// The right side of the equality uses Expr::col(Alias::new(...)) which
2082		// produces a quoted identifier instead of raw SQL interpolation.
2083		assert!(
2084			query.contains("\"id; DROP TABLE users; --\""),
2085			"Injection payload should be enclosed in double quotes as identifier: {}",
2086			query
2087		);
2088		// Verify the query is a valid single-statement SELECT (no semicolons
2089		// appear outside of the quoted identifier)
2090		let unquoted_parts: Vec<&str> = query.split('"').enumerate()
2091			.filter(|(i, _)| i % 2 == 0) // Even indices are outside quotes
2092			.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		// Arrange: arithmetic expression (price * quantity)
2107		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		// Act
2118		let result = build_single_filter_expr(&filter);
2119
2120		// Assert
2121		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		// Arrange: field + literal value
2140		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		// Act
2151		let result = build_single_filter_expr(&filter);
2152
2153		// Assert
2154		assert!(result.is_some());
2155	}
2156
2157	#[test]
2158	fn test_outer_ref_all_operators_use_safe_api() {
2159		// Arrange & Act & Assert: verify all comparison operators work with OuterRef
2160		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	// ==================== Case/Coalesce safe expression tests ====================
2185
2186	#[test]
2187	fn test_coalesce_expression_uses_safe_parameterized_api() {
2188		use reinhardt_db::orm::annotation::{AnnotationValue, Value as OrmValue};
2189
2190		// Arrange: COALESCE(field_a, 0)
2191		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		// Act
2202		let result = build_single_filter_expr(&filter);
2203
2204		// Assert
2205		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		// Arrange: CASE WHEN status = 'active' THEN 1 ELSE 0 END
2232		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		// Act
2246		let result = build_single_filter_expr(&filter);
2247
2248		// Assert
2249		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		// Arrange: COALESCE() with no values
2276		let expr = Expression::Coalesce(vec![]);
2277
2278		// Act
2279		let result = annotation_expr_to_safe_expr(&expr);
2280
2281		// Assert: should produce NULL expression without panicking
2282		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	// ==================== Aggregate safe expression tests ====================
2295
2296	#[test]
2297	fn test_aggregate_count_uses_safe_api() {
2298		use reinhardt_db::orm::aggregation::{Aggregate, AggregateFunc};
2299
2300		// Arrange: COUNT(*)
2301		let agg = Aggregate {
2302			func: AggregateFunc::Count,
2303			field: None,
2304			alias: None,
2305			distinct: false,
2306		};
2307
2308		// Act
2309		let result = aggregate_to_safe_expr(&agg);
2310
2311		// Assert
2312		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		// Arrange: SUM(price)
2328		let agg = Aggregate {
2329			func: AggregateFunc::Sum,
2330			field: Some("price".to_string()),
2331			alias: None,
2332			distinct: false,
2333		};
2334
2335		// Act
2336		let result = aggregate_to_safe_expr(&agg);
2337
2338		// Assert
2339		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		// Arrange: COUNT(DISTINCT category)
2360		let agg = Aggregate {
2361			func: AggregateFunc::CountDistinct,
2362			field: Some("category".to_string()),
2363			alias: None,
2364			distinct: false, // AggregateFunc::CountDistinct implies DISTINCT
2365		};
2366
2367		// Act
2368		let result = aggregate_to_safe_expr(&agg);
2369
2370		// Assert
2371		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		// Arrange: attacker tries injection via aggregate field name
2392		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		// Act
2400		let result = aggregate_to_safe_expr(&agg);
2401
2402		// Assert: injection payload should be treated as a quoted identifier
2403		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	// ==================== empty And/Or all-unsupported filter tests (#2943) ====================
2415
2416	#[rstest]
2417	fn test_build_composite_and_all_unsupported_returns_none() {
2418		// Arrange
2419		// Contains + Boolean is an unsupported combo that falls through to None
2420		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		// Act
2436		let result = build_composite_filter_condition(&condition);
2437
2438		// Assert
2439		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		// Arrange
2449		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		// Act
2465		let result = build_composite_filter_condition(&condition);
2466
2467		// Assert
2468		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		// Arrange
2478		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		// Act
2494		let result = build_composite_filter_condition(&condition);
2495
2496		// Assert
2497		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		// Arrange
2523		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		// Act
2539		let result = build_composite_filter_condition(&condition);
2540
2541		// Assert
2542		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		// Arrange
2568		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		// Act
2582		let result = build_filter_condition(&filters);
2583
2584		// Assert
2585		assert!(
2586			result.is_none(),
2587			"build_filter_condition with all unsupported filters should return None"
2588		);
2589	}
2590
2591	// ==================== extract_count_from_row tests (#2945) ====================
2592
2593	#[rstest]
2594	fn test_extract_count_from_row_with_count_key() {
2595		// Arrange
2596		let data = serde_json::json!({"count": 42});
2597
2598		// Act
2599		let result = extract_count_from_row(&data);
2600
2601		// Assert
2602		assert_eq!(result.unwrap(), 42);
2603	}
2604
2605	#[rstest]
2606	fn test_extract_count_from_row_without_count_key() {
2607		// Arrange
2608		let data = serde_json::json!({"total": 10});
2609
2610		// Act
2611		let result = extract_count_from_row(&data);
2612
2613		// Assert
2614		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		// Arrange
2625		let data = serde_json::json!({});
2626
2627		// Act
2628		let result = extract_count_from_row(&data);
2629
2630		// Assert
2631		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		// Arrange
2642		let data = serde_json::json!({"count": "abc"});
2643
2644		// Act
2645		let result = extract_count_from_row(&data);
2646
2647		// Assert
2648		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		// Arrange
2659		let data = serde_json::Value::Null;
2660
2661		// Act
2662		let result = extract_count_from_row(&data);
2663
2664		// Assert
2665		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		// Arrange
2676		let data = serde_json::json!({"count": 0});
2677
2678		// Act
2679		let result = extract_count_from_row(&data);
2680
2681		// Assert
2682		assert_eq!(result.unwrap(), 0);
2683	}
2684
2685	// ==================== AdminDatabase inject tests ====================
2686
2687	#[rstest]
2688	#[tokio::test]
2689	async fn test_admin_database_inject_error_hint_mentions_connection() {
2690		// Arrange
2691		let singleton = Arc::new(reinhardt_di::SingletonScope::new());
2692		let ctx = reinhardt_di::InjectionContext::builder(singleton).build();
2693
2694		// Act
2695		let result = AdminDatabase::inject(&ctx).await;
2696
2697		// Assert
2698		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		// Arrange - simulate pre-built AdminDatabase via configure_di pattern
2711		let singleton = Arc::new(reinhardt_di::SingletonScope::new());
2712		// We cannot create a real DatabaseConnection without a DB, so test
2713		// the prebuilt path by directly setting AdminDatabase in singleton
2714		// This verifies backward compat: pre-set AdminDatabase is found first
2715
2716		// Create a mock-like AdminDatabase would require DatabaseConnection,
2717		// so we just verify the error path when nothing is registered
2718		let ctx = reinhardt_di::InjectionContext::builder(singleton).build();
2719
2720		// Act
2721		let result = AdminDatabase::inject(&ctx).await;
2722
2723		// Assert - should fail with NotRegistered since no DatabaseConnection
2724		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	// ==================== FilterValue::Array In/NotIn tests (#2936) ====================
2767
2768	#[rstest]
2769	fn test_build_single_filter_expr_array_in() {
2770		// Arrange
2771		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		// Act
2778		let result = build_single_filter_expr(&filter);
2779
2780		// Assert
2781		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		// Arrange
2799		let filter = Filter::new(
2800			"status".to_string(),
2801			FilterOperator::NotIn,
2802			FilterValue::Array(vec!["x".to_string(), "y".to_string()]),
2803		);
2804
2805		// Act
2806		let result = build_single_filter_expr(&filter);
2807
2808		// Assert
2809		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		// Arrange
2829		let filter = Filter::new(
2830			"status".to_string(),
2831			FilterOperator::In,
2832			FilterValue::Array(vec![]),
2833		);
2834
2835		// Act
2836		let result = build_single_filter_expr(&filter);
2837
2838		// Assert
2839		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		// Arrange
2848		let filter = Filter::new(
2849			"category".to_string(),
2850			FilterOperator::In,
2851			FilterValue::Array(vec!["solo".to_string()]),
2852		);
2853
2854		// Act
2855		let result = build_single_filter_expr(&filter);
2856
2857		// Assert
2858		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		// Arrange
2874		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		// Act
2881		let result = build_single_filter_expr(&filter);
2882
2883		// Assert
2884		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		// SeaQuery's to_string with PostgresQueryBuilder escapes single quotes by doubling them
2895		assert!(
2896			query.contains("O''Brien"),
2897			"Single quote in value should be escaped, got: {}",
2898			query
2899		);
2900		// SQL injection attempt should be safely enclosed as a quoted string literal
2901		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	// ==================== Bug #2943: Composite filter WHERE TRUE tests ====================
2909
2910	#[rstest]
2911	fn test_and_with_all_unsupported_returns_none() {
2912		// Arrange: Contains with Integer is unsupported (only String is handled)
2913		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		// Act
2926		let result = build_composite_filter_condition(&condition);
2927
2928		// Assert: And with all unsupported sub-conditions returns None
2929		// (fixed in #2943: previously returned empty Condition::all() generating WHERE TRUE)
2930		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		// Arrange: Contains/StartsWith with Integer are unsupported
2941		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		// Act
2954		let result = build_composite_filter_condition(&condition);
2955
2956		// Assert: Or with all unsupported sub-conditions returns None
2957		// (fixed in #2943: previously returned empty Condition::any() generating WHERE FALSE)
2958		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		// Arrange: One supported (Eq + String), one unsupported (Contains + Integer)
2969		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		// Act
2982		let result = build_composite_filter_condition(&condition);
2983
2984		// Assert: Should keep the supported filter condition
2985		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		// Verify the supported condition is preserved by building SQL
2992		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		// Arrange
3007		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		// Act
3020		let result = build_composite_filter_condition(&condition);
3021
3022		// Assert
3023		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	// ==================== Bug #2945: extract_count_from_row tests ====================
3042
3043	#[rstest]
3044	fn test_extract_count_with_count_key() {
3045		// Arrange
3046		let data = serde_json::json!({"count": 42});
3047
3048		// Act
3049		let result = extract_count_from_row(&data);
3050
3051		// Assert
3052		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		// Arrange: Single non-"count" key
3059		let data = serde_json::json!({"total": 42});
3060
3061		// Act
3062		let result = extract_count_from_row(&data);
3063
3064		// Assert: Missing "count" key now returns error with available keys
3065		// (fixed in #2945: previously fell back to first value from iteration order)
3066		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		// Arrange: Multiple keys, no "count" key
3078		let data = serde_json::json!({"total": 42, "other": 99});
3079
3080		// Act
3081		let result = extract_count_from_row(&data);
3082
3083		// Assert: Missing "count" key returns error listing available keys
3084		// (fixed in #2945: previously used fragile obj.values().next() fallback)
3085		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		// Arrange
3097		let data = serde_json::json!({"count": "not_a_number"});
3098
3099		// Act
3100		let result = extract_count_from_row(&data);
3101
3102		// Assert
3103		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		// Arrange
3111		let data = serde_json::json!({"count": null});
3112
3113		// Act
3114		let result = extract_count_from_row(&data);
3115
3116		// Assert
3117		assert!(result.is_err());
3118	}
3119
3120	#[rstest]
3121	fn test_extract_count_empty_object_returns_error() {
3122		// Arrange
3123		let data = serde_json::json!({});
3124
3125		// Act
3126		let result = extract_count_from_row(&data);
3127
3128		// Assert
3129		assert!(result.is_err());
3130	}
3131
3132	#[rstest]
3133	fn test_extract_count_non_object_returns_error() {
3134		// Arrange: Array instead of object
3135		let data = serde_json::json!([1, 2, 3]);
3136
3137		// Act
3138		let result = extract_count_from_row(&data);
3139
3140		// Assert
3141		assert!(result.is_err());
3142	}
3143
3144	// ==================== parse_pk_value tests ====================
3145
3146	#[rstest]
3147	fn test_parse_pk_value_integer_falls_back_to_bigint() {
3148		// Arrange: No registry entry for this table, integer string input
3149
3150		// Act
3151		let val = parse_pk_value("nonexistent_table", "id", "42");
3152
3153		// Assert
3154		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		// Arrange: No registry entry, UUID string input
3160
3161		// Act
3162		let val = parse_pk_value(
3163			"nonexistent_table",
3164			"id",
3165			"c1a363b1-cc42-4dea-81f0-9dc1cedf0083",
3166		);
3167
3168		// Assert: Without registry metadata, UUID falls back to Value::String
3169		assert!(matches!(val, Value::String(Some(_))));
3170	}
3171
3172	#[rstest]
3173	fn test_parse_pk_value_non_numeric_string_falls_back_to_string() {
3174		// Arrange: No registry entry, non-numeric string input
3175
3176		// Act
3177		let val = parse_pk_value("nonexistent_table", "id", "hello-world");
3178
3179		// Assert
3180		assert!(matches!(val, Value::String(Some(_))));
3181	}
3182
3183	#[rstest]
3184	fn test_parse_pk_value_negative_integer() {
3185		// Arrange: Negative integer string
3186
3187		// Act
3188		let val = parse_pk_value("nonexistent_table", "id", "-1");
3189
3190		// Assert
3191		assert_eq!(val, Value::BigInt(Some(-1)));
3192	}
3193
3194	#[rstest]
3195	fn test_parse_pk_value_zero() {
3196		// Arrange: Zero as string
3197
3198		// Act
3199		let val = parse_pk_value("nonexistent_table", "id", "0");
3200
3201		// Assert
3202		assert_eq!(val, Value::BigInt(Some(0)));
3203	}
3204}