Files
foxhunt/database/src/query.rs
jgrusewski 6bd5b18465 🔧 Wave 33: Test Compilation Improvements - 57 errors remaining
**Progress: 1,178 → 57 test errors (95% reduction)**

## Status Summary
-  Production code: Compiles cleanly (0 errors)
- ⚠️  Test code: 57 errors remain (massive improvement)
- ⚙️  All services build successfully
- 📊 Warning count: 253 (target: <20) - AGENTS WILL FIX

## Remaining Test Errors (57 total)
### Primary Issues:
1. 23× E0308 mismatched types
2. 17× E0433 undeclared Decimal
3. 15× E0433 compliance module not found
4. 6× E0624 private method access
5. Various import and type issues

## Next Phase: Wave 33-2
Launch 10+ parallel agents to:
- Fix remaining 57 test compilation errors
- Reduce 253 warnings to <20
- Achieve 95% test coverage
- Ensure all tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-01 21:24:28 +02:00

1172 lines
32 KiB
Rust

use crate::error::{DatabaseError, DatabaseResult, ErrorContext};
use sqlx::postgres::{PgArguments, PgRow};
use sqlx::{Arguments, Executor, FromRow, Postgres};
use std::fmt;
/// Type-safe query builder for PostgreSQL
///
/// Provides a fluent interface for building SQL queries with parameter binding
/// and type safety. Supports SELECT, INSERT, UPDATE, and DELETE operations.
#[derive(Debug, Clone)]
pub struct QueryBuilder {
/// The SQL query string being built
query: String,
/// Bound arguments for the query
args: PgArguments,
}
impl QueryBuilder {
/// Create a new empty query builder
///
/// # Returns
///
/// A new `QueryBuilder` instance ready for query construction
///
/// # Examples
///
/// ```rust
/// use database::QueryBuilder;
///
/// let builder = QueryBuilder::new();
/// ```
pub fn new() -> Self {
Self {
query: String::new(),
args: PgArguments::default(),
}
}
/// Create a SELECT query builder
///
/// # Arguments
///
/// * `columns` - Array of column names to select. If empty, selects all columns (*)
///
/// # Returns
///
/// A `SelectBuilder` for constructing the SELECT query
///
/// # Examples
///
/// ```rust
/// use database::QueryBuilder;
///
/// let query = QueryBuilder::select(&["id", "name"])
/// .from("users")
/// .build()
/// .unwrap();
/// ```
pub fn select<T: AsRef<str>>(columns: &[T]) -> SelectBuilder {
let columns_str = if columns.is_empty() {
"*".to_string()
} else {
columns
.iter()
.map(|c| c.as_ref())
.collect::<Vec<_>>()
.join(", ")
};
SelectBuilder {
query: QueryBuilder::new(),
columns: columns_str,
from_clause: None,
where_conditions: Vec::new(),
order_by: Vec::new(),
group_by: Vec::new(),
having_conditions: Vec::new(),
limit_clause: None,
offset_clause: None,
joins: Vec::new(),
}
}
/// Create an INSERT query builder
///
/// # Arguments
///
/// * `table` - The table name to insert into
///
/// # Returns
///
/// An `InsertBuilder` for constructing the INSERT query
///
/// # Examples
///
/// ```rust
/// use database::QueryBuilder;
///
/// let query = QueryBuilder::insert("users")
/// .values(&[("name", "John"), ("email", "john@example.com")])
/// .build()
/// .unwrap();
/// ```
pub fn insert(table: &str) -> InsertBuilder {
InsertBuilder {
query: QueryBuilder::new(),
table: table.to_string(),
columns: Vec::new(),
values: Vec::new(),
on_conflict: None,
returning: None,
}
}
/// Create an UPDATE query builder
///
/// # Arguments
///
/// * `table` - The table name to update
///
/// # Returns
///
/// An `UpdateBuilder` for constructing the UPDATE query
///
/// # Examples
///
/// ```rust
/// use database::QueryBuilder;
///
/// let query = QueryBuilder::update("users")
/// .set("name", "Jane")
/// .where_eq("id", 1)
/// .build()
/// .unwrap();
/// ```
pub fn update(table: &str) -> UpdateBuilder {
UpdateBuilder {
query: QueryBuilder::new(),
table: table.to_string(),
set_clauses: Vec::new(),
where_conditions: Vec::new(),
returning: None,
}
}
/// Create a DELETE query builder
///
/// # Arguments
///
/// * `table` - The table name to delete from
///
/// # Returns
///
/// A `DeleteBuilder` for constructing the DELETE query
///
/// # Examples
///
/// ```rust
/// use database::QueryBuilder;
///
/// let query = QueryBuilder::delete("users")
/// .where_eq("id", 1)
/// .build()
/// .unwrap();
/// ```
pub fn delete(table: &str) -> DeleteBuilder {
DeleteBuilder {
query: QueryBuilder::new(),
table: table.to_string(),
where_conditions: Vec::new(),
returning: None,
}
}
/// Add a parameter to the query
///
/// Binds a value as a parameter to the query, using PostgreSQL's
/// parameter placeholder system ($1, $2, etc.).
///
/// # Arguments
///
/// * `value` - The value to bind as a parameter
///
/// # Returns
///
/// A mutable reference to self for method chaining
///
/// # Type Parameters
///
/// * `T` - Must implement SQLx encoding and type traits for PostgreSQL
pub fn bind<T>(&mut self, value: T) -> &mut Self
where
T: for<'q> sqlx::Encode<'q, Postgres> + sqlx::Type<Postgres> + Send,
{
let _result = self.args.add(value);
self
}
/// Get the generated SQL query string
///
/// # Returns
///
/// The SQL query string that has been built
pub fn sql(&self) -> &str {
&self.query
}
/// Get the bound arguments for the query
///
/// # Returns
///
/// A reference to the PostgreSQL arguments that have been bound
pub fn arguments(&self) -> &PgArguments {
&self.args
}
/// Execute the query and return the number of affected rows
///
/// # Arguments
///
/// * `executor` - Database executor (pool, connection, or transaction)
///
/// # Returns
///
/// The number of rows affected by the query
///
/// # Errors
///
/// Returns `DatabaseError` if the query execution fails
pub async fn execute<'e, E>(&self, executor: E) -> DatabaseResult<u64>
where
E: Executor<'e, Database = Postgres>,
{
let result = sqlx::query_with(&self.query, self.args.clone())
.execute(executor)
.await
.with_query_context(&self.query)?;
Ok(result.rows_affected())
}
/// Execute the query and fetch all matching rows
///
/// # Arguments
///
/// * `executor` - Database executor (pool, connection, or transaction)
///
/// # Returns
///
/// A vector of all rows returned by the query
///
/// # Errors
///
/// Returns `DatabaseError` if the query execution fails
///
/// # Type Parameters
///
/// * `T` - The type to deserialize rows into (must implement `FromRow`)
pub async fn fetch_all<'e, E, T>(&self, executor: E) -> DatabaseResult<Vec<T>>
where
E: Executor<'e, Database = Postgres>,
T: for<'r> FromRow<'r, PgRow> + Send + Unpin,
{
let rows = sqlx::query_as_with(&self.query, self.args.clone())
.fetch_all(executor)
.await
.with_query_context(&self.query)?;
Ok(rows)
}
/// Execute the query and fetch exactly one row
///
/// # Arguments
///
/// * `executor` - Database executor (pool, connection, or transaction)
///
/// # Returns
///
/// The single row returned by the query
///
/// # Errors
///
/// Returns `DatabaseError` if:
/// - The query execution fails
/// - No rows are found
/// - More than one row is found
///
/// # Type Parameters
///
/// * `T` - The type to deserialize the row into (must implement `FromRow`)
pub async fn fetch_one<'e, E, T>(&self, executor: E) -> DatabaseResult<T>
where
E: Executor<'e, Database = Postgres>,
T: for<'r> FromRow<'r, PgRow> + Send + Unpin,
{
let row = sqlx::query_as_with(&self.query, self.args.clone())
.fetch_one(executor)
.await
.with_query_context(&self.query)?;
Ok(row)
}
/// Execute the query and fetch an optional row
///
/// # Arguments
///
/// * `executor` - Database executor (pool, connection, or transaction)
///
/// # Returns
///
/// `Some(row)` if a row is found, `None` if no rows match
///
/// # Errors
///
/// Returns `DatabaseError` if:
/// - The query execution fails
/// - More than one row is found
///
/// # Type Parameters
///
/// * `T` - The type to deserialize the row into (must implement `FromRow`)
pub async fn fetch_optional<'e, E, T>(&self, executor: E) -> DatabaseResult<Option<T>>
where
E: Executor<'e, Database = Postgres>,
T: for<'r> FromRow<'r, PgRow> + Send + Unpin,
{
let row = sqlx::query_as_with(&self.query, self.args.clone())
.fetch_optional(executor)
.await
.with_query_context(&self.query)?;
Ok(row)
}
}
impl Default for QueryBuilder {
fn default() -> Self {
Self::new()
}
}
/// SELECT query builder
///
/// Provides a fluent interface for building SELECT queries with support for
/// WHERE conditions, JOINs, ORDER BY, GROUP BY, LIMIT, and OFFSET clauses.
#[derive(Debug, Clone)]
pub struct SelectBuilder {
/// The underlying query builder
query: QueryBuilder,
/// Comma-separated column names to select
columns: String,
/// FROM table clause
from_clause: Option<String>,
/// WHERE conditions that will be joined with AND
where_conditions: Vec<String>,
/// ORDER BY clauses
order_by: Vec<String>,
/// GROUP BY columns
group_by: Vec<String>,
/// HAVING conditions for grouped queries
having_conditions: Vec<String>,
/// LIMIT clause for result count
limit_clause: Option<u64>,
/// OFFSET clause for result pagination
offset_clause: Option<u64>,
/// JOIN clauses (INNER, LEFT, etc.)
joins: Vec<String>,
}
impl SelectBuilder {
/// Add FROM clause to specify the source table
///
/// # Arguments
///
/// * `table` - The table name to select from
///
/// # Returns
///
/// Self for method chaining
///
/// # Examples
///
/// ```rust
/// use database::QueryBuilder;
///
/// let query = QueryBuilder::select(&["*"])
/// .from("users")
/// .build()
/// .unwrap();
/// ```
pub fn from(mut self, table: &str) -> Self {
self.from_clause = Some(table.to_string());
self
}
/// Add WHERE equality condition
///
/// # Arguments
///
/// * `column` - The column name to filter on
/// * `value` - The value to compare against
///
/// # Returns
///
/// Self for method chaining
///
/// # Type Parameters
///
/// * `T` - Must implement SQLx encoding and type traits for PostgreSQL
///
/// # Examples
///
/// ```rust
/// use database::QueryBuilder;
///
/// let query = QueryBuilder::select(&["*"])
/// .from("users")
/// .where_eq("active", true)
/// .build()
/// .unwrap();
/// ```
pub fn where_eq<T>(mut self, column: &str, value: T) -> Self
where
T: for<'q> sqlx::Encode<'q, Postgres> + sqlx::Type<Postgres> + Send,
{
let placeholder = format!("${}", self.query.args.len() + 1);
self.where_conditions
.push(format!("{} = {}", column, placeholder));
self.query.bind(value);
self
}
/// Add WHERE IN condition for multiple values
///
/// # Arguments
///
/// * `column` - The column name to filter on
/// * `values` - Vector of values to match against
///
/// # Returns
///
/// Self for method chaining
///
/// # Type Parameters
///
/// * `T` - Must implement SQLx encoding and type traits for PostgreSQL
///
/// # Examples
///
/// ```rust
/// use database::QueryBuilder;
///
/// let query = QueryBuilder::select(&["*"])
/// .from("users")
/// .where_in("status", vec!["active", "pending"])
/// .build()
/// .unwrap();
/// ```
pub fn where_in<T>(mut self, column: &str, values: Vec<T>) -> Self
where
T: for<'q> sqlx::Encode<'q, Postgres> + sqlx::Type<Postgres> + Send,
{
if values.is_empty() {
return self;
}
let placeholders: Vec<String> = values
.into_iter()
.map(|value| {
let placeholder = format!("${}", self.query.args.len() + 1);
self.query.bind(value);
placeholder
})
.collect();
self.where_conditions
.push(format!("{} IN ({})", column, placeholders.join(", ")));
self
}
/// Add custom WHERE condition with raw SQL
///
/// # Arguments
///
/// * `condition` - Raw SQL condition string
///
/// # Returns
///
/// Self for method chaining
///
/// # Safety
///
/// This method allows raw SQL which could be vulnerable to SQL injection.
/// Only use with trusted input or properly escaped values.
///
/// # Examples
///
/// ```rust
/// use database::QueryBuilder;
///
/// let query = QueryBuilder::select(&["*"])
/// .from("users")
/// .where_raw("created_at > NOW() - INTERVAL '1 day'")
/// .build()
/// .unwrap();
/// ```
pub fn where_raw(mut self, condition: &str) -> Self {
self.where_conditions.push(condition.to_string());
self
}
/// Add INNER JOIN clause
///
/// # Arguments
///
/// * `table` - The table to join with
/// * `on` - The join condition
///
/// # Returns
///
/// Self for method chaining
///
/// # Examples
///
/// ```rust
/// use database::QueryBuilder;
///
/// let query = QueryBuilder::select(&["u.name", "p.title"])
/// .from("users u")
/// .inner_join("posts p", "u.id = p.user_id")
/// .build()
/// .unwrap();
/// ```
pub fn inner_join(mut self, table: &str, on: &str) -> Self {
self.joins.push(format!("INNER JOIN {} ON {}", table, on));
self
}
/// Add LEFT JOIN clause
///
/// # Arguments
///
/// * `table` - The table to join with
/// * `on` - The join condition
///
/// # Returns
///
/// Self for method chaining
///
/// # Examples
///
/// ```rust
/// use database::QueryBuilder;
///
/// let query = QueryBuilder::select(&["u.name", "p.title"])
/// .from("users u")
/// .left_join("posts p", "u.id = p.user_id")
/// .build()
/// .unwrap();
/// ```
pub fn left_join(mut self, table: &str, on: &str) -> Self {
self.joins.push(format!("LEFT JOIN {} ON {}", table, on));
self
}
/// Add ORDER BY clause for result sorting
///
/// # Arguments
///
/// * `column` - The column name to sort by
/// * `direction` - Sort direction (ASC or DESC)
///
/// # Returns
///
/// Self for method chaining
///
/// # Examples
///
/// ```rust
/// use database::{QueryBuilder, OrderDirection};
///
/// let query = QueryBuilder::select(&["*"])
/// .from("users")
/// .order_by("created_at", OrderDirection::Desc)
/// .build()
/// .unwrap();
/// ```
pub fn order_by(mut self, column: &str, direction: OrderDirection) -> Self {
self.order_by.push(format!("{} {}", column, direction));
self
}
/// Add GROUP BY clause for result grouping
///
/// # Arguments
///
/// * `column` - The column name to group by
///
/// # Returns
///
/// Self for method chaining
///
/// # Examples
///
/// ```rust
/// use database::QueryBuilder;
///
/// let query = QueryBuilder::select(&["status", "COUNT(*)"])
/// .from("users")
/// .group_by("status")
/// .build()
/// .unwrap();
/// ```
pub fn group_by(mut self, column: &str) -> Self {
self.group_by.push(column.to_string());
self
}
/// Add HAVING condition for grouped results
///
/// # Arguments
///
/// * `condition` - The HAVING condition
///
/// # Returns
///
/// Self for method chaining
///
/// # Examples
///
/// ```rust
/// use database::QueryBuilder;
///
/// let query = QueryBuilder::select(&["status", "COUNT(*) as count"])
/// .from("users")
/// .group_by("status")
/// .having("COUNT(*) > 10")
/// .build()
/// .unwrap();
/// ```
pub fn having(mut self, condition: &str) -> Self {
self.having_conditions.push(condition.to_string());
self
}
/// Add LIMIT clause to restrict number of results
///
/// # Arguments
///
/// * `count` - Maximum number of rows to return
///
/// # Returns
///
/// Self for method chaining
///
/// # Examples
///
/// ```rust
/// use database::QueryBuilder;
///
/// let query = QueryBuilder::select(&["*"])
/// .from("users")
/// .limit(10)
/// .build()
/// .unwrap();
/// ```
pub fn limit(mut self, count: u64) -> Self {
self.limit_clause = Some(count);
self
}
/// Add OFFSET clause for result pagination
///
/// # Arguments
///
/// * `count` - Number of rows to skip
///
/// # Returns
///
/// Self for method chaining
///
/// # Examples
///
/// ```rust
/// use database::QueryBuilder;
///
/// let query = QueryBuilder::select(&["*"])
/// .from("users")
/// .limit(10)
/// .offset(20)
/// .build()
/// .unwrap();
/// ```
pub fn offset(mut self, count: u64) -> Self {
self.offset_clause = Some(count);
self
}
/// Build the final SELECT query
///
/// Constructs the complete SQL query string from all the added clauses.
///
/// # Returns
///
/// A `QueryBuilder` ready for execution
///
/// # Errors
///
/// Returns `DatabaseError::Query` if the query is incomplete (missing FROM clause)
///
/// # Examples
///
/// ```rust
/// use database::QueryBuilder;
///
/// let query = QueryBuilder::select(&["id", "name"])
/// .from("users")
/// .where_eq("active", true)
/// .build()
/// .unwrap();
/// ```
pub fn build(mut self) -> DatabaseResult<QueryBuilder> {
let mut query_parts = vec![format!("SELECT {}", self.columns)];
if let Some(from) = &self.from_clause {
query_parts.push(format!("FROM {}", from));
} else {
return Err(DatabaseError::Query {
query: "incomplete".to_string(),
message: "SELECT query must have FROM clause".to_string(),
});
}
// Add JOINs
query_parts.extend(self.joins);
// Add WHERE conditions
if !self.where_conditions.is_empty() {
query_parts.push(format!("WHERE {}", self.where_conditions.join(" AND ")));
}
// Add GROUP BY
if !self.group_by.is_empty() {
query_parts.push(format!("GROUP BY {}", self.group_by.join(", ")));
}
// Add HAVING
if !self.having_conditions.is_empty() {
query_parts.push(format!("HAVING {}", self.having_conditions.join(" AND ")));
}
// Add ORDER BY
if !self.order_by.is_empty() {
query_parts.push(format!("ORDER BY {}", self.order_by.join(", ")));
}
// Add LIMIT
if let Some(limit) = self.limit_clause {
query_parts.push(format!("LIMIT {}", limit));
}
// Add OFFSET
if let Some(offset) = self.offset_clause {
query_parts.push(format!("OFFSET {}", offset));
}
self.query.query = query_parts.join(" ");
Ok(self.query)
}
}
/// INSERT query builder
///
/// Provides a fluent interface for building INSERT queries with support for
/// multiple value sets, conflict resolution, and RETURNING clauses.
#[derive(Debug, Clone)]
pub struct InsertBuilder {
/// The underlying query builder
query: QueryBuilder,
/// Target table name
table: String,
/// Column names for insertion
columns: Vec<String>,
/// Value placeholders for each row to insert
values: Vec<Vec<String>>,
/// ON CONFLICT clause for handling constraint violations
on_conflict: Option<String>,
/// RETURNING clause for getting inserted data back
returning: Option<String>,
}
impl InsertBuilder {
/// Add a single row of values
pub fn values<T>(mut self, values: &[(&str, T)]) -> Self
where
T: for<'q> sqlx::Encode<'q, Postgres> + sqlx::Type<Postgres> + Send + Clone,
{
if self.columns.is_empty() {
self.columns = values.iter().map(|(col, _)| col.to_string()).collect();
}
let placeholders: Vec<String> = values
.iter()
.map(|(_, value)| {
let placeholder = format!("${}", self.query.args.len() + 1);
self.query.bind(value.clone());
placeholder
})
.collect();
self.values.push(placeholders);
self
}
/// Add ON CONFLICT clause
pub fn on_conflict(mut self, conflict: &str) -> Self {
self.on_conflict = Some(conflict.to_string());
self
}
/// Add RETURNING clause
pub fn returning(mut self, columns: &str) -> Self {
self.returning = Some(columns.to_string());
self
}
/// Build the final query
pub fn build(mut self) -> DatabaseResult<QueryBuilder> {
if self.columns.is_empty() || self.values.is_empty() {
return Err(DatabaseError::Query {
query: "incomplete".to_string(),
message: "INSERT query must have columns and values".to_string(),
});
}
let mut query_parts = vec![
format!("INSERT INTO {} ({})", self.table, self.columns.join(", ")),
format!(
"VALUES {}",
self.values
.iter()
.map(|row| format!("({})", row.join(", ")))
.collect::<Vec<_>>()
.join(", ")
),
];
if let Some(conflict) = &self.on_conflict {
query_parts.push(format!("ON CONFLICT {}", conflict));
}
if let Some(returning) = &self.returning {
query_parts.push(format!("RETURNING {}", returning));
}
self.query.query = query_parts.join(" ");
Ok(self.query)
}
}
/// UPDATE query builder
///
/// Provides a fluent interface for building UPDATE queries with support for
/// SET clauses, WHERE conditions, and RETURNING clauses.
#[derive(Debug, Clone)]
pub struct UpdateBuilder {
/// The underlying query builder
query: QueryBuilder,
/// Target table name
table: String,
/// SET clauses for column updates
set_clauses: Vec<String>,
/// WHERE conditions to filter rows for update
where_conditions: Vec<String>,
/// RETURNING clause for getting updated data back
returning: Option<String>,
}
impl UpdateBuilder {
/// Add SET clause
pub fn set<T>(mut self, column: &str, value: T) -> Self
where
T: for<'q> sqlx::Encode<'q, Postgres> + sqlx::Type<Postgres> + Send,
{
let placeholder = format!("${}", self.query.args.len() + 1);
self.set_clauses
.push(format!("{} = {}", column, placeholder));
self.query.bind(value);
self
}
/// Add WHERE condition
pub fn where_eq<T>(mut self, column: &str, value: T) -> Self
where
T: for<'q> sqlx::Encode<'q, Postgres> + sqlx::Type<Postgres> + Send,
{
let placeholder = format!("${}", self.query.args.len() + 1);
self.where_conditions
.push(format!("{} = {}", column, placeholder));
self.query.bind(value);
self
}
/// Add RETURNING clause
pub fn returning(mut self, columns: &str) -> Self {
self.returning = Some(columns.to_string());
self
}
/// Build the final query
pub fn build(mut self) -> DatabaseResult<QueryBuilder> {
if self.set_clauses.is_empty() {
return Err(DatabaseError::Query {
query: "incomplete".to_string(),
message: "UPDATE query must have SET clauses".to_string(),
});
}
let mut query_parts = vec![
format!("UPDATE {}", self.table),
format!("SET {}", self.set_clauses.join(", ")),
];
if !self.where_conditions.is_empty() {
query_parts.push(format!("WHERE {}", self.where_conditions.join(" AND ")));
}
if let Some(returning) = &self.returning {
query_parts.push(format!("RETURNING {}", returning));
}
self.query.query = query_parts.join(" ");
Ok(self.query)
}
}
/// DELETE query builder
///
/// Provides a fluent interface for building DELETE queries with support for
/// WHERE conditions and RETURNING clauses.
#[derive(Debug, Clone)]
pub struct DeleteBuilder {
/// The underlying query builder
query: QueryBuilder,
/// Target table name
table: String,
/// WHERE conditions to filter rows for deletion
where_conditions: Vec<String>,
/// RETURNING clause for getting deleted data back
returning: Option<String>,
}
impl DeleteBuilder {
/// Add WHERE condition
pub fn where_eq<T>(mut self, column: &str, value: T) -> Self
where
T: for<'q> sqlx::Encode<'q, Postgres> + sqlx::Type<Postgres> + Send,
{
let placeholder = format!("${}", self.query.args.len() + 1);
self.where_conditions
.push(format!("{} = {}", column, placeholder));
self.query.bind(value);
self
}
/// Add custom WHERE condition
pub fn where_raw(mut self, condition: &str) -> Self {
self.where_conditions.push(condition.to_string());
self
}
/// Add RETURNING clause
pub fn returning(mut self, columns: &str) -> Self {
self.returning = Some(columns.to_string());
self
}
/// Build the final query
pub fn build(mut self) -> DatabaseResult<QueryBuilder> {
let mut query_parts = vec![format!("DELETE FROM {}", self.table)];
if !self.where_conditions.is_empty() {
query_parts.push(format!("WHERE {}", self.where_conditions.join(" AND ")));
}
if let Some(returning) = &self.returning {
query_parts.push(format!("RETURNING {}", returning));
}
self.query.query = query_parts.join(" ");
Ok(self.query)
}
}
/// Order direction for ORDER BY clauses
///
/// Specifies whether to sort in ascending or descending order.
#[derive(Debug, Clone, Copy)]
pub enum OrderDirection {
/// Ascending order (smallest to largest)
Asc,
/// Descending order (largest to smallest)
Desc,
}
impl fmt::Display for OrderDirection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OrderDirection::Asc => write!(f, "ASC"),
OrderDirection::Desc => write!(f, "DESC"),
}
}
}
/// Helper for building dynamic WHERE conditions
///
/// Provides a convenient way to build complex WHERE clauses dynamically
/// with proper parameter binding and type safety.
#[derive(Debug, Clone)]
pub struct WhereBuilder {
/// List of WHERE conditions that will be joined with AND
conditions: Vec<String>,
/// Bound arguments for the WHERE conditions
args: PgArguments,
}
impl WhereBuilder {
/// Create a new WHERE builder
pub fn new() -> Self {
Self {
conditions: Vec::new(),
args: PgArguments::default(),
}
}
/// Add an equality condition
pub fn eq<T>(&mut self, column: &str, value: T) -> &mut Self
where
T: for<'q> sqlx::Encode<'q, Postgres> + sqlx::Type<Postgres> + Send,
{
let placeholder = format!("${}", self.args.len() + 1);
self.conditions
.push(format!("{} = {}", column, placeholder));
let _result = self.args.add(value);
self
}
/// Add an IN condition
pub fn in_values<T>(&mut self, column: &str, values: Vec<T>) -> &mut Self
where
T: for<'q> sqlx::Encode<'q, Postgres> + sqlx::Type<Postgres> + Send,
{
if values.is_empty() {
return self;
}
let placeholders: Vec<String> = values
.into_iter()
.map(|value| {
let placeholder = format!("${}", self.args.len() + 1);
let _result = self.args.add(value);
placeholder
})
.collect();
self.conditions
.push(format!("{} IN ({})", column, placeholders.join(", ")));
self
}
/// Add a custom condition
pub fn custom(&mut self, condition: &str) -> &mut Self {
self.conditions.push(condition.to_string());
self
}
/// Build the WHERE clause
pub fn build(self) -> (String, PgArguments) {
let clause = if self.conditions.is_empty() {
String::new()
} else {
format!("WHERE {}", self.conditions.join(" AND "))
};
(clause, self.args)
}
}
impl Default for WhereBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_select_builder() {
let query = QueryBuilder::select(&["id", "name"])
.from("users")
.where_eq("active", true)
.order_by("name", OrderDirection::Asc)
.limit(10)
.build()
.unwrap();
assert!(query.sql().contains("SELECT id, name"));
assert!(query.sql().contains("FROM users"));
assert!(query.sql().contains("WHERE active = $1"));
assert!(query.sql().contains("ORDER BY name ASC"));
assert!(query.sql().contains("LIMIT 10"));
}
#[test]
fn test_insert_builder() {
let query = QueryBuilder::insert("users")
.values(&[("name", "John"), ("email", "john@example.com")])
.returning("id")
.build()
.unwrap();
assert!(query.sql().contains("INSERT INTO users"));
assert!(query.sql().contains("(name, email)"));
assert!(query.sql().contains("VALUES ($1, $2)"));
assert!(query.sql().contains("RETURNING id"));
}
#[test]
fn test_update_builder() {
let query = QueryBuilder::update("users")
.set("name", "Jane")
.set("updated_at", "NOW()")
.where_eq("id", 1)
.build()
.unwrap();
assert!(query.sql().contains("UPDATE users"));
assert!(query.sql().contains("SET name = $1"));
assert!(query.sql().contains("WHERE id = $"));
}
#[test]
fn test_delete_builder() {
let query = QueryBuilder::delete("users")
.where_eq("id", 1)
.returning("name")
.build()
.unwrap();
assert!(query.sql().contains("DELETE FROM users"));
assert!(query.sql().contains("WHERE id = $1"));
assert!(query.sql().contains("RETURNING name"));
}
#[test]
fn test_where_builder() {
let mut where_builder = WhereBuilder::new();
where_builder.eq("active", true);
where_builder.in_values("status", vec!["active", "pending"]);
where_builder.custom("created_at > NOW() - INTERVAL '1 day'");
let (clause, _) = where_builder.build();
assert!(clause.contains("WHERE"));
assert!(clause.contains("active = $1"));
assert!(clause.contains("status IN"));
assert!(clause.contains("created_at >"));
}
}