chore: Remove dead code - delete obsolete money.rs file
- Remove src/db/money.rs which contained the old Decimal-based MoneyAmount implementation - This file was leftover from the INTEGER cents refactoring and was not referenced in mod.rs - Only money_amount.rs with INTEGER storage is now active - Compilation confirmed successful after removal
This commit is contained in:
730
src/db/money.rs
730
src/db/money.rs
@@ -1,730 +0,0 @@
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{
|
||||
encode::IsNull, error::BoxDynError, sqlite::SqliteTypeInfo, Decode, Encode, Sqlite, Type,
|
||||
TypeInfo,
|
||||
};
|
||||
use std::ops::{Add, Sub};
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::currency_type::CurrencyType;
|
||||
|
||||
/// Newtype wrapper around Decimal for SQLx integration
|
||||
/// Stores as NUMERIC (TEXT) in SQLite to maintain exact precision
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct MoneyAmount(Decimal);
|
||||
|
||||
impl MoneyAmount {
|
||||
pub fn new(decimal: Decimal) -> Self {
|
||||
Self(decimal)
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Result<Self, rust_decimal::Error> {
|
||||
Ok(Self(Decimal::from_str(s)?))
|
||||
}
|
||||
|
||||
pub fn zero() -> Self {
|
||||
Self(Decimal::ZERO)
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> Decimal {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
// Allow easy conversion from Decimal
|
||||
impl From<Decimal> for MoneyAmount {
|
||||
fn from(decimal: Decimal) -> Self {
|
||||
Self(decimal)
|
||||
}
|
||||
}
|
||||
|
||||
// Allow easy conversion to Decimal
|
||||
impl From<MoneyAmount> for Decimal {
|
||||
fn from(money: MoneyAmount) -> Self {
|
||||
money.0
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Display for easy printing
|
||||
impl std::fmt::Display for MoneyAmount {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
// Implement arithmetic operations
|
||||
impl Add for MoneyAmount {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for MoneyAmount {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 - rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
// Implement SQLx Type trait for MoneyAmount with SQLite (store as TEXT with NUMERIC affinity)
|
||||
impl Type<Sqlite> for MoneyAmount {
|
||||
fn type_info() -> SqliteTypeInfo {
|
||||
// Use TEXT type but this will work with NUMERIC affinity columns
|
||||
<&str as Type<Sqlite>>::type_info()
|
||||
}
|
||||
|
||||
fn compatible(ty: &SqliteTypeInfo) -> bool {
|
||||
// Accept both TEXT and NUMERIC/REAL types
|
||||
<&str as Type<Sqlite>>::compatible(ty)
|
||||
|| <f64 as Type<Sqlite>>::compatible(ty)
|
||||
|| ty.name() == "NUMERIC"
|
||||
|| ty.name() == "DECIMAL"
|
||||
}
|
||||
}
|
||||
|
||||
// Implement SQLx Encode trait for MoneyAmount with SQLite
|
||||
impl<'q> Encode<'q, Sqlite> for MoneyAmount {
|
||||
fn encode_by_ref(
|
||||
&self,
|
||||
args: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>,
|
||||
) -> Result<IsNull, BoxDynError> {
|
||||
// Store decimal as string - SQLite NUMERIC affinity will handle the conversion
|
||||
let decimal_str = self.0.to_string();
|
||||
args.push(sqlx::sqlite::SqliteArgumentValue::Text(decimal_str.into()));
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
// Implement SQLx Decode trait for MoneyAmount with SQLite
|
||||
impl<'r> Decode<'r, Sqlite> for MoneyAmount {
|
||||
fn decode(value: sqlx::sqlite::SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
// Decode as string from NUMERIC affinity column (stored as TEXT for precision)
|
||||
let decimal_str = <&str as Decode<Sqlite>>::decode(value)?;
|
||||
let decimal = Decimal::from_str(decimal_str)?;
|
||||
Ok(MoneyAmount(decimal))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rstest::rstest;
|
||||
use sqlx::{Row, SqlitePool};
|
||||
|
||||
#[test]
|
||||
fn test_money_amount_creation() {
|
||||
let money1 = MoneyAmount::from(Decimal::new(12345, 2)); // 123.45
|
||||
assert_eq!(money1.to_string(), "123.45");
|
||||
|
||||
let money2 = MoneyAmount::from_str("456.789123").unwrap();
|
||||
assert_eq!(money2.to_string(), "456.789123");
|
||||
|
||||
let zero_money = MoneyAmount::zero();
|
||||
assert_eq!(zero_money.to_string(), "0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_money_amount_precision() {
|
||||
let precise_money = MoneyAmount::from_str("0.000001").unwrap(); // 1 micro-unit
|
||||
assert_eq!(precise_money.to_string(), "0.000001");
|
||||
|
||||
let large_money = MoneyAmount::from_str("999999.999999").unwrap(); // Near max precision
|
||||
assert_eq!(large_money.to_string(), "999999.999999");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_money_amount_conversions() {
|
||||
let decimal = Decimal::new(12345, 2);
|
||||
let money: MoneyAmount = decimal.into();
|
||||
let back_to_decimal: Decimal = money.into();
|
||||
assert_eq!(decimal, back_to_decimal);
|
||||
}
|
||||
|
||||
// Database integration test utilities
|
||||
async fn create_test_pool() -> SqlitePool {
|
||||
// Create an in-memory SQLite database for testing
|
||||
SqlitePool::connect("sqlite::memory:")
|
||||
.await
|
||||
.expect("Failed to create in-memory database")
|
||||
}
|
||||
|
||||
async fn setup_test_table(pool: &SqlitePool) {
|
||||
// Create a test table with MoneyAmount and CurrencyType fields
|
||||
// Use TEXT for MoneyAmount fields in STRICT mode (stores as TEXT, preserves precision)
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE test_money (
|
||||
id INTEGER PRIMARY KEY,
|
||||
amount TEXT NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
optional_amount TEXT,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
) STRICT
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect("Failed to create test table");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("0")]
|
||||
#[case("123.45")]
|
||||
#[case("0.000001")] // Micro precision
|
||||
#[case("999999.999999")] // Large with precision
|
||||
#[case("1234567890.123456")] // Very large
|
||||
#[case("0.1")]
|
||||
#[case("0.01")]
|
||||
#[case("0.001")]
|
||||
#[case("1.0")]
|
||||
#[case("1.00")]
|
||||
#[case("1.10")]
|
||||
#[case("1.01")]
|
||||
#[tokio::test]
|
||||
async fn test_money_amount_database_encode_decode(#[case] amount_str: &str) {
|
||||
let pool = create_test_pool().await;
|
||||
setup_test_table(&pool).await;
|
||||
|
||||
let amount = MoneyAmount::from_str(amount_str).unwrap();
|
||||
|
||||
// Insert test data
|
||||
sqlx::query("INSERT INTO test_money (amount, currency) VALUES (?, ?)")
|
||||
.bind(&amount)
|
||||
.bind(CurrencyType::USD)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Failed to insert test data");
|
||||
|
||||
// Retrieve and verify data
|
||||
let row = sqlx::query("SELECT amount, currency FROM test_money LIMIT 1")
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("Failed to fetch test data");
|
||||
|
||||
let retrieved_amount: MoneyAmount = row.get("amount");
|
||||
let retrieved_currency: CurrencyType = row.get("currency");
|
||||
|
||||
assert_eq!(retrieved_amount, amount);
|
||||
assert_eq!(retrieved_currency, CurrencyType::USD);
|
||||
|
||||
// Verify string representation matches exactly
|
||||
assert_eq!(retrieved_amount.to_string(), amount.to_string());
|
||||
assert_eq!(retrieved_amount.to_string(), amount_str);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_optional_money_amount_database() {
|
||||
let pool = create_test_pool().await;
|
||||
setup_test_table(&pool).await;
|
||||
|
||||
// Test with NULL (None) value
|
||||
sqlx::query("INSERT INTO test_money (amount, currency, optional_amount) VALUES (?, ?, ?)")
|
||||
.bind(MoneyAmount::from_str("50.00").unwrap())
|
||||
.bind(CurrencyType::USD)
|
||||
.bind(None::<MoneyAmount>)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Failed to insert null optional amount");
|
||||
|
||||
// Test with Some value
|
||||
let optional_amount = Some(MoneyAmount::from_str("25.75").unwrap());
|
||||
sqlx::query("INSERT INTO test_money (amount, currency, optional_amount) VALUES (?, ?, ?)")
|
||||
.bind(MoneyAmount::from_str("100.00").unwrap())
|
||||
.bind(CurrencyType::USD)
|
||||
.bind(&optional_amount)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Failed to insert some optional amount");
|
||||
|
||||
// Retrieve and verify
|
||||
let rows =
|
||||
sqlx::query("SELECT amount, currency, optional_amount FROM test_money ORDER BY id")
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.expect("Failed to fetch optional amount test data");
|
||||
|
||||
assert_eq!(rows.len(), 2);
|
||||
|
||||
// First row - NULL optional_amount
|
||||
let first_optional: Option<MoneyAmount> = rows[0].get("optional_amount");
|
||||
assert!(first_optional.is_none());
|
||||
|
||||
// Second row - Some optional_amount
|
||||
let second_optional: Option<MoneyAmount> = rows[1].get("optional_amount");
|
||||
assert_eq!(second_optional, optional_amount);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("0", "0")]
|
||||
#[case("1", "1")]
|
||||
#[case("1.0", "1.0")]
|
||||
#[case("1.00", "1.00")]
|
||||
#[case("1.10", "1.10")]
|
||||
#[case("1.01", "1.01")]
|
||||
#[case("0.1", "0.1")]
|
||||
#[case("0.01", "0.01")]
|
||||
#[case("0.001", "0.001")]
|
||||
#[case("0.0001", "0.0001")]
|
||||
#[case("0.00001", "0.00001")]
|
||||
#[case("0.000001", "0.000001")]
|
||||
#[case("123.456789", "123.456789")]
|
||||
#[tokio::test]
|
||||
async fn test_money_precision_preservation(
|
||||
#[case] input_str: &str,
|
||||
#[case] expected_str: &str,
|
||||
) {
|
||||
let pool = create_test_pool().await;
|
||||
setup_test_table(&pool).await;
|
||||
|
||||
let amount = MoneyAmount::from_str(input_str).unwrap();
|
||||
|
||||
// Insert into database
|
||||
sqlx::query("INSERT INTO test_money (amount, currency) VALUES (?, ?)")
|
||||
.bind(&amount)
|
||||
.bind(CurrencyType::USD)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Failed to insert precision test data");
|
||||
|
||||
// Retrieve from database
|
||||
let row = sqlx::query("SELECT amount FROM test_money LIMIT 1")
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("Failed to fetch precision test data");
|
||||
|
||||
let retrieved_amount: MoneyAmount = row.get("amount");
|
||||
|
||||
// Verify exact string representation is preserved
|
||||
assert_eq!(
|
||||
retrieved_amount.to_string(),
|
||||
expected_str,
|
||||
"Precision not preserved for input: {}",
|
||||
input_str
|
||||
);
|
||||
}
|
||||
|
||||
// ========== ARITHMETIC OPERATIONS TESTS ==========
|
||||
|
||||
#[rstest]
|
||||
#[case("0", "0", "0")]
|
||||
#[case("1", "2", "3")]
|
||||
#[case("10.50", "5.25", "15.75")]
|
||||
#[case("0.000001", "0.000002", "0.000003")]
|
||||
#[case("999999.999999", "0.000001", "1000000")]
|
||||
#[case("123.45", "876.55", "1000")]
|
||||
#[case("0.1", "0.2", "0.3")]
|
||||
#[case("0.33", "0.67", "1.00")]
|
||||
fn test_money_addition(#[case] a_str: &str, #[case] b_str: &str, #[case] expected: &str) {
|
||||
let a = MoneyAmount::from_str(a_str).unwrap();
|
||||
let b = MoneyAmount::from_str(b_str).unwrap();
|
||||
let result = a + b;
|
||||
let expected_amount = MoneyAmount::from_str(expected).unwrap();
|
||||
assert_eq!(result, expected_amount);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("10", "5", "5")]
|
||||
#[case("100.50", "0.50", "100")]
|
||||
#[case("1", "1", "0")]
|
||||
#[case("0.000003", "0.000001", "0.000002")]
|
||||
#[case("1000000", "0.000001", "999999.999999")]
|
||||
#[case("1000", "123.45", "876.55")]
|
||||
#[case("0.3", "0.1", "0.2")]
|
||||
fn test_money_subtraction(#[case] a_str: &str, #[case] b_str: &str, #[case] expected: &str) {
|
||||
let a = MoneyAmount::from_str(a_str).unwrap();
|
||||
let b = MoneyAmount::from_str(b_str).unwrap();
|
||||
let result = a - b;
|
||||
let expected_amount = MoneyAmount::from_str(expected).unwrap();
|
||||
assert_eq!(result, expected_amount);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_money_arithmetic_precision() {
|
||||
let a = MoneyAmount::from_str("0.1").unwrap();
|
||||
let b = MoneyAmount::from_str("0.2").unwrap();
|
||||
let sum = a + b;
|
||||
|
||||
// This should be exactly 0.3, not 0.30000000000000004 like with f64
|
||||
assert_eq!(sum.to_string(), "0.3");
|
||||
|
||||
let c = MoneyAmount::from_str("1.0").unwrap();
|
||||
let d = MoneyAmount::from_str("0.9").unwrap();
|
||||
let diff = c - d;
|
||||
|
||||
// This should be exactly 0.1
|
||||
assert_eq!(diff.to_string(), "0.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_money_chained_operations() {
|
||||
let a = MoneyAmount::from_str("100").unwrap();
|
||||
let b = MoneyAmount::from_str("25.50").unwrap();
|
||||
let c = MoneyAmount::from_str("10.25").unwrap();
|
||||
|
||||
let result = a + b - c;
|
||||
let expected = MoneyAmount::from_str("115.25").unwrap();
|
||||
assert_eq!(result, expected);
|
||||
|
||||
let result2 = a - b + c;
|
||||
let expected2 = MoneyAmount::from_str("84.75").unwrap();
|
||||
assert_eq!(result2, expected2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_money_zero_operations() {
|
||||
let zero = MoneyAmount::zero();
|
||||
let amount = MoneyAmount::from_str("42.50").unwrap();
|
||||
|
||||
assert_eq!(zero + amount, amount);
|
||||
assert_eq!(amount + zero, amount);
|
||||
assert_eq!(amount - zero, amount);
|
||||
assert_eq!(amount - amount, zero);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_money_negative_results() {
|
||||
let a = MoneyAmount::from_str("10").unwrap();
|
||||
let b = MoneyAmount::from_str("15").unwrap();
|
||||
let result = a - b;
|
||||
let expected = MoneyAmount::from_str("-5").unwrap();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
// ========== DATABASE QUERY OPERATIONS TESTS ==========
|
||||
|
||||
async fn setup_test_data_for_queries(pool: &SqlitePool) {
|
||||
// Create test table for query operations
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE test_bids (
|
||||
id INTEGER PRIMARY KEY,
|
||||
listing_id INTEGER NOT NULL,
|
||||
buyer_id INTEGER NOT NULL,
|
||||
bid_amount TEXT NOT NULL,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
) STRICT
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect("Failed to create test_bids table");
|
||||
|
||||
// Insert test data with various amounts
|
||||
let test_amounts = [
|
||||
"10.00", "25.50", "100.00", "15.75", "200.25", "0.01", "999.99", "50.00", "75.25",
|
||||
"125.50",
|
||||
];
|
||||
|
||||
for (i, amount_str) in test_amounts.iter().enumerate() {
|
||||
let amount = MoneyAmount::from_str(amount_str).unwrap();
|
||||
sqlx::query(
|
||||
"INSERT INTO test_bids (listing_id, buyer_id, bid_amount) VALUES (?, ?, ?)",
|
||||
)
|
||||
.bind(1) // listing_id
|
||||
.bind(i as i64 + 1) // buyer_id
|
||||
.bind(amount)
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect("Failed to insert test bid data");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_money_database_max_min() {
|
||||
let pool = create_test_pool().await;
|
||||
setup_test_data_for_queries(&pool).await;
|
||||
|
||||
// Test MAX
|
||||
let max_row = sqlx::query("SELECT MAX(bid_amount) as max_bid FROM test_bids")
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("Failed to fetch max bid");
|
||||
|
||||
let max_amount: MoneyAmount = max_row.get("max_bid");
|
||||
let expected_max = MoneyAmount::from_str("999.99").unwrap();
|
||||
assert_eq!(max_amount, expected_max);
|
||||
|
||||
// Test MIN
|
||||
let min_row = sqlx::query("SELECT MIN(bid_amount) as min_bid FROM test_bids")
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("Failed to fetch min bid");
|
||||
|
||||
let min_amount: MoneyAmount = min_row.get("min_bid");
|
||||
let expected_min = MoneyAmount::from_str("0.01").unwrap();
|
||||
assert_eq!(min_amount, expected_min);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(">", "50.00", 5)] // amounts > 50.00: 100.00, 200.25, 999.99, 75.25, 125.50
|
||||
#[case(">=", "50.00", 6)] // amounts >= 50.00: includes 50.00
|
||||
#[case("<", "50.00", 4)] // amounts < 50.00: 10.00, 25.50, 15.75, 0.01
|
||||
#[case("<=", "50.00", 5)] // amounts <= 50.00: includes 50.00
|
||||
#[tokio::test]
|
||||
async fn test_money_database_comparison_operators(
|
||||
#[case] operator: &str,
|
||||
#[case] threshold: &str,
|
||||
#[case] expected_count: usize,
|
||||
) {
|
||||
let pool = create_test_pool().await;
|
||||
setup_test_data_for_queries(&pool).await;
|
||||
|
||||
let threshold_amount = MoneyAmount::from_str(threshold).unwrap();
|
||||
let query = format!(
|
||||
"SELECT COUNT(*) as count FROM test_bids WHERE CAST(bid_amount AS REAL) {} CAST(? AS REAL)",
|
||||
operator
|
||||
);
|
||||
|
||||
let count_row = sqlx::query(&query)
|
||||
.bind(threshold_amount)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("Failed to fetch count with comparison");
|
||||
|
||||
let count: i64 = count_row.get("count");
|
||||
assert_eq!(
|
||||
count as usize, expected_count,
|
||||
"Comparison {} {} failed",
|
||||
operator, threshold
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("25.00", "100.00", 4)] // amounts between 25.00 and 100.00: 25.50, 100.00, 50.00, 75.25
|
||||
#[case("0.00", "20.00", 3)] // amounts between 0.00 and 20.00: 10.00, 15.75, 0.01
|
||||
#[case("200.00", "1000.00", 2)] // amounts between 200.00 and 1000.00: 200.25, 999.99
|
||||
#[tokio::test]
|
||||
async fn test_money_database_between_operator(
|
||||
#[case] min_amount: &str,
|
||||
#[case] max_amount: &str,
|
||||
#[case] expected_count: usize,
|
||||
) {
|
||||
let pool = create_test_pool().await;
|
||||
setup_test_data_for_queries(&pool).await;
|
||||
|
||||
let min = MoneyAmount::from_str(min_amount).unwrap();
|
||||
let max = MoneyAmount::from_str(max_amount).unwrap();
|
||||
|
||||
let count_row =
|
||||
sqlx::query("SELECT COUNT(*) as count FROM test_bids WHERE CAST(bid_amount AS REAL) BETWEEN CAST(? AS REAL) AND CAST(? AS REAL)")
|
||||
.bind(min)
|
||||
.bind(max)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("Failed to fetch count with BETWEEN");
|
||||
|
||||
let count: i64 = count_row.get("count");
|
||||
assert_eq!(
|
||||
count as usize, expected_count,
|
||||
"BETWEEN {} AND {} failed",
|
||||
min_amount, max_amount
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_money_database_ordering() {
|
||||
let pool = create_test_pool().await;
|
||||
setup_test_data_for_queries(&pool).await;
|
||||
|
||||
// Test ORDER BY ASC
|
||||
let rows_asc =
|
||||
sqlx::query("SELECT bid_amount FROM test_bids ORDER BY CAST(bid_amount AS REAL) ASC")
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.expect("Failed to fetch ordered data ASC");
|
||||
|
||||
let amounts_asc: Vec<MoneyAmount> = rows_asc
|
||||
.iter()
|
||||
.map(|row| row.get::<MoneyAmount, _>("bid_amount"))
|
||||
.collect();
|
||||
|
||||
// Verify ascending order
|
||||
for i in 1..amounts_asc.len() {
|
||||
assert!(
|
||||
amounts_asc[i - 1] <= amounts_asc[i],
|
||||
"ASC order violated: {} should be <= {}",
|
||||
amounts_asc[i - 1],
|
||||
amounts_asc[i]
|
||||
);
|
||||
}
|
||||
|
||||
// Test ORDER BY DESC
|
||||
let rows_desc =
|
||||
sqlx::query("SELECT bid_amount FROM test_bids ORDER BY CAST(bid_amount AS REAL) DESC")
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.expect("Failed to fetch ordered data DESC");
|
||||
|
||||
let amounts_desc: Vec<MoneyAmount> = rows_desc
|
||||
.iter()
|
||||
.map(|row| row.get::<MoneyAmount, _>("bid_amount"))
|
||||
.collect();
|
||||
|
||||
// Verify descending order
|
||||
for i in 1..amounts_desc.len() {
|
||||
assert!(
|
||||
amounts_desc[i - 1] >= amounts_desc[i],
|
||||
"DESC order violated: {} should be >= {}",
|
||||
amounts_desc[i - 1],
|
||||
amounts_desc[i]
|
||||
);
|
||||
}
|
||||
|
||||
// Verify first and last elements
|
||||
let expected_min = MoneyAmount::from_str("0.01").unwrap();
|
||||
let expected_max = MoneyAmount::from_str("999.99").unwrap();
|
||||
|
||||
assert_eq!(amounts_asc[0], expected_min);
|
||||
assert_eq!(amounts_asc[amounts_asc.len() - 1], expected_max);
|
||||
assert_eq!(amounts_desc[0], expected_max);
|
||||
assert_eq!(amounts_desc[amounts_desc.len() - 1], expected_min);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_money_database_aggregation_functions() {
|
||||
let pool = create_test_pool().await;
|
||||
setup_test_data_for_queries(&pool).await;
|
||||
|
||||
// Test SUM, AVG, COUNT
|
||||
let agg_row = sqlx::query(
|
||||
"SELECT
|
||||
SUM(bid_amount) as total_sum,
|
||||
AVG(bid_amount) as average,
|
||||
COUNT(bid_amount) as count
|
||||
FROM test_bids",
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("Failed to fetch aggregation data");
|
||||
|
||||
let total_sum: MoneyAmount = agg_row.get("total_sum");
|
||||
let average: MoneyAmount = agg_row.get("average");
|
||||
let count: i64 = agg_row.get("count");
|
||||
|
||||
// Verify count
|
||||
assert_eq!(count, 10);
|
||||
|
||||
// Calculate expected sum manually
|
||||
let expected_sum = MoneyAmount::from_str("10.00").unwrap()
|
||||
+ MoneyAmount::from_str("25.50").unwrap()
|
||||
+ MoneyAmount::from_str("100.00").unwrap()
|
||||
+ MoneyAmount::from_str("15.75").unwrap()
|
||||
+ MoneyAmount::from_str("200.25").unwrap()
|
||||
+ MoneyAmount::from_str("0.01").unwrap()
|
||||
+ MoneyAmount::from_str("999.99").unwrap()
|
||||
+ MoneyAmount::from_str("50.00").unwrap()
|
||||
+ MoneyAmount::from_str("75.25").unwrap()
|
||||
+ MoneyAmount::from_str("125.50").unwrap();
|
||||
|
||||
assert_eq!(total_sum, expected_sum);
|
||||
|
||||
// Verify average is approximately correct (may have rounding differences)
|
||||
let expected_avg_decimal = expected_sum.inner() / Decimal::new(10, 0);
|
||||
let avg_diff = (average.inner() - expected_avg_decimal).abs();
|
||||
assert!(
|
||||
avg_diff < Decimal::new(1, 2),
|
||||
"Average calculation too far off: expected ~{}, got {}",
|
||||
expected_avg_decimal,
|
||||
average
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_money_database_group_by_with_aggregation() {
|
||||
let pool = create_test_pool().await;
|
||||
|
||||
// Create a more complex test table
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE test_listing_bids (
|
||||
id INTEGER PRIMARY KEY,
|
||||
listing_id INTEGER NOT NULL,
|
||||
bid_amount TEXT NOT NULL
|
||||
) STRICT
|
||||
"#,
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Failed to create test_listing_bids table");
|
||||
|
||||
// Insert test data for multiple listings
|
||||
let test_data = [
|
||||
(1, "10.00"),
|
||||
(1, "20.00"),
|
||||
(1, "30.00"), // Listing 1: total 60.00, max 30.00
|
||||
(2, "50.00"),
|
||||
(2, "75.00"), // Listing 2: total 125.00, max 75.00
|
||||
(3, "100.00"), // Listing 3: total 100.00, max 100.00
|
||||
];
|
||||
|
||||
for (listing_id, amount_str) in test_data {
|
||||
let amount = MoneyAmount::from_str(amount_str).unwrap();
|
||||
sqlx::query("INSERT INTO test_listing_bids (listing_id, bid_amount) VALUES (?, ?)")
|
||||
.bind(listing_id)
|
||||
.bind(amount)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Failed to insert listing bid data");
|
||||
}
|
||||
|
||||
// Test GROUP BY with aggregation
|
||||
let grouped_rows = sqlx::query(
|
||||
"SELECT
|
||||
listing_id,
|
||||
SUM(bid_amount) as total_bids,
|
||||
MAX(bid_amount) as highest_bid,
|
||||
COUNT(*) as bid_count
|
||||
FROM test_listing_bids
|
||||
GROUP BY listing_id
|
||||
ORDER BY listing_id",
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.expect("Failed to fetch grouped data");
|
||||
|
||||
assert_eq!(grouped_rows.len(), 3);
|
||||
|
||||
// Verify listing 1
|
||||
let listing1 = &grouped_rows[0];
|
||||
assert_eq!(listing1.get::<i64, _>("listing_id"), 1);
|
||||
assert_eq!(
|
||||
listing1.get::<MoneyAmount, _>("total_bids"),
|
||||
MoneyAmount::from_str("60.00").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
listing1.get::<MoneyAmount, _>("highest_bid"),
|
||||
MoneyAmount::from_str("30.00").unwrap()
|
||||
);
|
||||
assert_eq!(listing1.get::<i64, _>("bid_count"), 3);
|
||||
|
||||
// Verify listing 2
|
||||
let listing2 = &grouped_rows[1];
|
||||
assert_eq!(listing2.get::<i64, _>("listing_id"), 2);
|
||||
assert_eq!(
|
||||
listing2.get::<MoneyAmount, _>("total_bids"),
|
||||
MoneyAmount::from_str("125.00").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
listing2.get::<MoneyAmount, _>("highest_bid"),
|
||||
MoneyAmount::from_str("75.00").unwrap()
|
||||
);
|
||||
assert_eq!(listing2.get::<i64, _>("bid_count"), 2);
|
||||
|
||||
// Verify listing 3
|
||||
let listing3 = &grouped_rows[2];
|
||||
assert_eq!(listing3.get::<i64, _>("listing_id"), 3);
|
||||
assert_eq!(
|
||||
listing3.get::<MoneyAmount, _>("total_bids"),
|
||||
MoneyAmount::from_str("100.00").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
listing3.get::<MoneyAmount, _>("highest_bid"),
|
||||
MoneyAmount::from_str("100.00").unwrap()
|
||||
);
|
||||
assert_eq!(listing3.get::<i64, _>("bid_count"), 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user