#![cfg(any(
feature = "sqlite",
feature = "postgresql",
feature = "mysql",
feature = "mssql"
))]
pub mod _test_common;
use ormer::query::builder::Select;
use ormer::OrderBy;
#[cfg(any(
feature = "sqlite",
feature = "postgresql",
feature = "mssql",
feature = "questdb"
))]
use ormer::DbType;
#[cfg(feature = "postgresql")]
use ormer::FullTextMode;
#[cfg(any(feature = "sqlite", feature = "questdb"))]
use ormer::TimeUnit;
#[cfg(feature = "sqlite")]
use ormer::FullTextRank;
#[derive(Debug, Clone, ormer::Model)]
#[table = "afq_users"]
struct AfqUser {
#[primary]
id: i32,
name: String,
age: i32,
created_at: chrono::NaiveDateTime,
}
#[derive(Debug, Clone, ormer::Model)]
#[table = "afq_orders"]
struct AfqOrder {
#[primary]
id: i32,
user_id: i32,
status: String,
}
#[derive(Debug, Clone, ormer::Model)]
#[table = "afq_articles"]
struct AfqArticle {
#[primary]
id: i32,
#[index(method = "fulltext", columns = "(title, body)")]
title: String,
body: String,
}
#[derive(Debug, Clone, ormer::Model)]
#[table = "afq_products"]
struct AfqProduct {
#[primary]
id: i32,
price: rust_decimal::Decimal,
}
#[derive(Debug, Clone, ormer::Model)]
#[table = "afq_amounts"]
struct AfqAmount {
#[primary]
id: i32,
user_id: i32,
amount: i32,
}
#[derive(Debug, Clone, ormer::ViewModel)]
struct AfqUserTotal {
user_id: i32,
total: i64,
}
#[cfg(feature = "sqlite")]
fn decimal(value: &str) -> rust_decimal::Decimal {
use std::str::FromStr;
rust_decimal::Decimal::from_str(value).unwrap()
}
#[cfg(feature = "sqlite")]
#[test]
fn aggregate_try_to_sql_rejects_invalid_dynamic_field() {
let aggregate = Select::<AfqUser>::new()
.filter_dynamic(|p| p.field("typo").eq(1))
.count(|u| u.id);
let error = aggregate
.try_to_sql_with_params(DbType::Sqlite)
.expect_err("invalid dynamic field must be rejected");
assert!(
error.to_string().contains("does not exist"),
"unexpected error: {error}"
);
}
#[cfg(feature = "sqlite")]
#[test]
fn aggregate_try_to_sql_accepts_valid_filter() {
let aggregate = Select::<AfqUser>::new()
.filter(|u| u.age.gt(18))
.count(|u| u.id);
let (sql, params) = aggregate
.try_to_sql_with_params(DbType::Sqlite)
.expect("valid aggregate must render");
assert!(sql.contains("COUNT("), "{sql}");
assert_eq!(params.len(), 1);
}
#[cfg(feature = "sqlite")]
#[test]
fn invalid_dynamic_field_renders_error_marker_instead_of_panic() {
let (sql, params) = Select::<AfqUser>::new()
.filter_dynamic(|p| p.field("typo").eq(1))
.to_sql_with_params(DbType::Sqlite);
assert!(
sql.contains("__ormer_invalid_filter__"),
"expected error marker in SQL: {sql}"
);
assert!(params.is_empty());
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn aggregate_executor_validates_invalid_dynamic_field() {
let db = ormer::Database::connect(DbType::Sqlite, ":memory:")
.await
.unwrap();
let _ = db.drop_table::<AfqUser>().execute().await;
db.create_table::<AfqUser>().execute().await.unwrap();
let result: ormer::Result<usize> = db
.select::<AfqUser>()
.filter_dynamic(|p| p.field("typo").eq(1))
.count(|u| u.id)
.await;
let error = result.expect_err("invalid dynamic field must surface as an error");
assert!(
error.to_string().contains("does not exist"),
"error must name the invalid field: {error}"
);
assert!(
error.to_string().contains("typo"),
"error must name the invalid field: {error}"
);
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn related_executor_validates_invalid_dynamic_field() {
let db = ormer::Database::connect(DbType::Sqlite, ":memory:")
.await
.unwrap();
let _ = db.drop_table::<AfqUser>().execute().await;
let _ = db.drop_table::<AfqOrder>().execute().await;
db.create_table::<AfqUser>().execute().await.unwrap();
db.create_table::<AfqOrder>().execute().await.unwrap();
let result: ormer::Result<Vec<AfqUser>> = db
.select::<AfqUser>()
.filter_dynamic(|p| p.field("typo").eq(1))
.from::<AfqOrder>()
.collect::<Vec<AfqUser>>()
.await;
let error = result.expect_err("invalid dynamic field must surface as an error");
assert!(
error.to_string().contains("does not exist"),
"error must name the invalid field: {error}"
);
let naive = chrono::DateTime::from_timestamp(0, 0).unwrap().naive_utc();
db.insert(vec![
AfqUser { id: 1, name: "a".into(), age: 20, created_at: naive },
AfqUser { id: 2, name: "b".into(), age: 30, created_at: naive },
])
.execute()
.await
.unwrap();
db.insert(vec![AfqOrder { id: 1, user_id: 1, status: "paid".into() }])
.execute()
.await
.unwrap();
let rows: Vec<AfqUser> = db
.select::<AfqUser>()
.from::<AfqOrder>()
.filter(|u, o| u.id.eq(o.user_id))
.collect::<Vec<AfqUser>>()
.await
.unwrap();
assert_eq!(rows.len(), 1, "valid related query must still work");
}
#[cfg(feature = "sqlite")]
#[test]
fn derived_table_try_to_sql_validates_and_renders() {
let derived = Select::<AfqAmount>::new()
.select_column(|a| (a.user_id, a.amount.sum()))
.group_by(|a| a.user_id)
.as_model::<AfqUserTotal>();
let table = ormer::from_derived(derived)
.filter(|t| t.total.gt(50_i64))
.order_by_desc(|t| t.total);
let (sql, params) = table
.try_to_sql_with_params(DbType::Sqlite)
.expect("valid derived table query must render");
assert!(sql.contains("FROM ("), "{sql}");
assert!(sql.contains("AS t0"), "{sql}");
assert_eq!(params.len(), 1);
}
#[cfg(feature = "postgresql")]
#[test]
fn union_renumbers_placeholders_on_postgresql() {
let union = Select::<AfqUser>::new()
.filter(|u| u.age.gt(30))
.union(Select::<AfqUser>::new().filter(|u| u.name.eq("admin")));
let (sql, params) = union.to_sql_with_params(DbType::PostgreSQL);
assert!(sql.contains("> $1"), "{sql}");
assert!(sql.contains("= $2"), "{sql}");
assert_eq!(sql.matches("$1").count(), 1, "{sql}");
assert_eq!(params.len(), 2);
let (sql, _) = union
.try_to_sql_with_params(DbType::PostgreSQL)
.expect("valid union must render");
assert!(sql.contains("= $2"), "{sql}");
}
#[cfg(feature = "mssql")]
#[test]
fn union_unparenthesized_renumbers_placeholders_on_mssql() {
let union = Select::<AfqUser>::new()
.filter(|u| u.age.gt(30))
.union(Select::<AfqUser>::new().filter(|u| u.name.eq("admin")));
let (sql, params) = union.to_sql_with_params_unparenthesized(DbType::MSSQL);
assert!(sql.contains("> @P1"), "{sql}");
assert!(sql.contains("= @P2"), "{sql}");
assert_eq!(sql.matches("@P1").count(), 1, "{sql}");
assert_eq!(params.len(), 2);
}
#[cfg(feature = "sqlite")]
#[test]
fn sqlite_fulltext_relevance_with_order_and_range_renders_single_tail() {
let (sql, _) = Select::<AfqArticle>::new()
.fields(|a| (a.title,))
.query("rust")
.rank(FullTextRank::Relevance)
.order_by(|a| a.id)
.range(0..5)
.to_sql_with_params(DbType::Sqlite);
assert_eq!(sql.matches("ORDER BY").count(), 1, "{sql}");
assert_eq!(sql.matches("LIMIT").count(), 1, "{sql}");
assert!(sql.contains("__ormer_hits.__ormer_rank"), "{sql}");
let (sql, _) = Select::<AfqArticle>::new()
.fields(|a| (a.title,))
.query("rust")
.rank(FullTextRank::Relevance)
.limit(3)
.to_sql_with_params(DbType::Sqlite);
assert_eq!(sql.matches("ORDER BY").count(), 1, "{sql}");
assert_eq!(sql.matches("LIMIT").count(), 1, "{sql}");
assert!(sql.contains("__ormer_hits.__ormer_rank"), "{sql}");
}
#[cfg(feature = "sqlite")]
#[test]
fn query_without_fields_is_rejected_on_sqlite() {
let error = Select::<AfqArticle>::new()
.query("rust")
.try_to_sql_with_params(DbType::Sqlite)
.expect_err("query without fields must be rejected");
assert!(
error.to_string().contains("at least one field"),
"unexpected error: {error}"
);
}
#[cfg(feature = "postgresql")]
#[test]
fn query_without_fields_is_rejected_on_postgresql() {
let error = Select::<AfqArticle>::new()
.query("rust")
.try_to_sql_with_params(DbType::PostgreSQL)
.expect_err("query without fields must be rejected");
assert!(
error.to_string().contains("at least one field"),
"unexpected error: {error}"
);
}
#[cfg(feature = "sqlite")]
#[test]
fn fulltext_rank_before_fields_and_query_is_kept() {
let (sql, _) = Select::<AfqArticle>::new()
.rank(FullTextRank::Relevance)
.fields(|a| (a.title,))
.query("rust")
.to_sql_with_params(DbType::Sqlite);
assert!(sql.contains("bm25("), "{sql}");
}
#[cfg(feature = "postgresql")]
#[test]
fn fulltext_mode_and_language_before_fields_query_are_kept() {
let (sql, params) = Select::<AfqArticle>::new()
.mode(FullTextMode::Boolean)
.language("english")
.fields(|a| (a.title,))
.query("rust +ormer")
.to_sql_with_params(DbType::PostgreSQL);
assert!(sql.contains("to_tsquery"), "{sql}");
assert!(!sql.contains("plainto_tsquery"), "{sql}");
assert!(
params.iter().any(|value| matches!(value, ormer::Value::Text(language) if language == "english")),
"params: {params:?}"
);
}
#[cfg(feature = "sqlite")]
#[test]
fn date_add_validates_inner_expr() {
let error = Select::<AfqUser>::new()
.filter(|u| {
u.created_at
.at_time_zone("UTC")
.add(TimeUnit::Day, 1)
.gt(ormer::value(String::new()))
})
.try_to_sql_with_params(DbType::Sqlite)
.expect_err("unsupported inner expr must be rejected by validation");
assert!(
error.to_string().contains("timezone conversion"),
"unexpected error: {error}"
);
}
#[cfg(feature = "questdb")]
#[test]
fn date_add_validates_inner_expr_on_questdb() {
let error = Select::<AfqUser>::new()
.filter(|u| {
u.name
.json_text("k")
.add(TimeUnit::Day, 1)
.gt(ormer::raw::<String>("''"))
})
.try_to_sql_with_params(DbType::QuestDB)
.expect_err("QuestDB JSON text extraction inside DateAdd must be rejected");
assert!(
error.to_string().contains("JSON text extraction"),
"unexpected error: {error}"
);
}
#[test]
#[should_panic(expected = "LATERAL")]
fn where_expr_and_rejects_lateral_config_on_right_operand() {
let _ = Select::<AfqUser>::new().left_join::<AfqOrder>(|u, o| {
u.id.eq(o.user_id)
.and(o.status.eq("paid").order_by(o.id).range(0..3))
});
}
#[test]
#[should_panic(expected = "LATERAL")]
fn where_expr_or_rejects_lateral_config_on_right_operand() {
let _ = Select::<AfqUser>::new().left_join::<AfqOrder>(|u, o| {
u.id.eq(o.user_id)
.or(o.status.eq("paid").order_by(o.id).range(0..3))
});
}
#[cfg(feature = "sqlite")]
#[test]
fn sqlite_decimal_between_and_in_apply_numeric_cast() {
let (sql, params) = Select::<AfqProduct>::new()
.filter(|p| p.price.between(decimal("9.90"), decimal("10.10")))
.to_sql_with_params(DbType::Sqlite);
assert!(
sql.contains("AS NUMERIC) BETWEEN CAST("),
"BETWEEN must cast both bounds: {sql}"
);
assert_eq!(params.len(), 2);
let (sql, params) = Select::<AfqProduct>::new()
.filter(|p| p.price.is_in(vec![decimal("9.90"), decimal("10.10")]))
.to_sql_with_params(DbType::Sqlite);
assert!(
sql.contains("AS NUMERIC) IN (CAST("),
"IN must cast every placeholder: {sql}"
);
assert_eq!(params.len(), 2);
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn sqlite_decimal_between_matches_numerically() -> Result<(), Box<dyn std::error::Error>> {
let db = ormer::Database::connect(DbType::Sqlite, ":memory:").await?;
let _ = db.drop_table::<AfqProduct>().execute().await;
db.create_table::<AfqProduct>().execute().await?;
db.insert(vec![
AfqProduct { id: 1, price: decimal("9.50") },
AfqProduct { id: 2, price: decimal("10.00") },
AfqProduct { id: 3, price: decimal("10.50") },
])
.execute()
.await?;
let rows: Vec<AfqProduct> = db
.select::<AfqProduct>()
.filter(|p| p.price.between(decimal("9.90"), decimal("10.10")))
.collect()
.await?;
assert_eq!(rows.len(), 1, "numeric BETWEEN must match the 10.00 row");
assert_eq!(rows[0].id, 2);
let ge_rows: Vec<AfqProduct> = db
.select::<AfqProduct>()
.filter(|p| p.price.ge(decimal("9.90")))
.collect()
.await?;
assert_eq!(ge_rows.len(), 2);
Ok(())
}
#[derive(Debug, Clone, ormer::Model)]
#[table = "afq_single_col_models"]
struct AfqSingleColumn {
#[primary]
uid: i32,
}
#[test]
#[should_panic(expected = "expects 4 columns")]
fn map_to_model_rejects_column_count_mismatch() {
let _ = Select::<AfqUser>::new().map_to_model::<AfqUser, _, _>(|u| u.id);
}
#[cfg(feature = "sqlite")]
#[test]
fn map_to_model_accepts_single_column_target() {
let mapped = Select::<AfqUser>::new().map_to_model::<AfqSingleColumn, _, _>(|u| u.id);
let (sql, _) = mapped.to_sql_with_params(DbType::Sqlite);
assert!(sql.contains("AS uid"), "{sql}");
}
#[test]
fn order_by_to_sql_matches_to_sql_for_default_dialect() {
let order = OrderBy::asc("Order".into());
#[cfg(feature = "sqlite")]
assert_eq!(order.to_sql(), order.to_sql_for(DbType::Sqlite));
assert_ne!(order.to_sql(), "Order ASC");
assert!(order.to_sql().ends_with("ASC"));
}