use sea_query::{PostgresQueryBuilder, SelectStatement};
use sea_query_binder::SqlxBinder;
use umbral::orm::Model;
#[derive(Debug, Clone, sqlx::FromRow, umbral::orm::Model)]
#[umbral(table = "umbral_phase421_event")]
pub struct Event {
pub id: i64,
pub tags: Vec<String>,
pub scores: Option<Vec<i64>>,
}
fn pg_sql_for(query: SelectStatement) -> String {
let (sql, _values) = query.build_sqlx(PostgresQueryBuilder);
sql
}
fn manager_sql_contains_operator(sql: &str, op: &str) -> bool {
sql.contains(op)
}
#[test]
fn contains_renders_at_arrow_operator() {
let qs = Event::objects().filter(event::TAGS.contains("hello"));
let sql = qs.to_sql();
assert!(
manager_sql_contains_operator(&sql, "@>"),
"expected `@>` operator in rendered SQL; got {sql}"
);
assert!(
sql.contains("\"tags\""),
"column identifier should appear quoted; got {sql}"
);
assert!(
sql.contains("ARRAY"),
"rendered SQL should include the ARRAY literal; got {sql}"
);
}
#[test]
fn contains_all_renders_multi_element_array() {
let qs = Event::objects().filter(event::TAGS.contains_all(["alpha", "beta", "gamma"]));
let sql = qs.to_sql_pg();
assert!(sql.contains("@>"), "expected `@>`; got {sql}");
assert!(
sql.contains("ARRAY"),
"should render an ARRAY literal; got {sql}"
);
assert!(sql.contains("$1"), "expected $1 placeholder; got {sql}");
assert!(sql.contains("$2"), "expected $2 placeholder; got {sql}");
assert!(sql.contains("$3"), "expected $3 placeholder; got {sql}");
}
#[test]
fn contained_by_renders_subset_arrow_operator() {
let qs = Event::objects().filter(event::TAGS.contained_by(["a", "b", "c"]));
let sql = qs.to_sql();
assert!(sql.contains("<@"), "expected `<@` operator; got {sql}");
}
#[test]
fn overlaps_renders_amp_amp_operator() {
let qs = Event::objects().filter(event::SCORES.overlaps([10i64, 20, 30]));
let sql = qs.to_sql();
assert!(sql.contains("&&"), "expected `&&` operator; got {sql}");
assert!(
sql.contains("\"scores\""),
"scores column should appear quoted; got {sql}"
);
}
#[test]
fn empty_contained_by_returns_false_predicate() {
let empty: Vec<&str> = Vec::new();
let qs = Event::objects().filter(event::TAGS.contained_by(empty));
let sql = qs.to_sql();
assert!(
sql.contains("1 = 0"),
"empty contained_by should render `1 = 0`; got {sql}"
);
assert!(
!sql.contains("<@"),
"empty contained_by should NOT emit the operator; got {sql}"
);
}
#[test]
fn empty_overlaps_returns_false_predicate() {
let empty: Vec<i64> = Vec::new();
let qs = Event::objects().filter(event::SCORES.overlaps(empty));
let sql = qs.to_sql();
assert!(
sql.contains("1 = 0"),
"empty overlaps should render `1 = 0`; got {sql}"
);
}
#[test]
fn empty_contains_all_renders_tautology() {
let empty: Vec<&str> = Vec::new();
let qs = Event::objects().filter(event::TAGS.contains_all(empty));
let sql = qs.to_sql();
assert!(
sql.contains("1 = 1"),
"empty contains_all should render `1 = 1`; got {sql}"
);
}
#[test]
fn to_sql_pg_emits_dollar_placeholders_and_at_arrow() {
let qs = Event::objects().filter(event::TAGS.contains_all(["x", "y"]));
let sql = qs.to_sql_pg();
assert!(sql.contains("@>"), "got {sql}");
assert!(sql.contains("$1") && sql.contains("$2"), "got {sql}");
}
#[test]
fn array_op_predicates_typecheck_through_fetch_pg() {
#[allow(dead_code)]
async fn _unreachable(pg_pool: &sqlx::PgPool) -> Result<(), sqlx::Error> {
let _v: Vec<Event> = Event::objects()
.filter(event::TAGS.contains("alpha"))
.filter(event::SCORES.overlaps([1i64, 2, 3]))
.fetch_pg(pg_pool)
.await?;
Ok(())
}
let _ = <Event as Model>::TABLE;
}
#[tokio::test]
#[ignore = "needs UMBRAL_TEST_POSTGRES_URL pointing at a Postgres server"]
async fn array_operators_filter_real_postgres_rows() {
let url =
std::env::var("UMBRAL_TEST_POSTGRES_URL").expect("UMBRAL_TEST_POSTGRES_URL must be set");
let pool = sqlx::PgPool::connect(&url).await.unwrap();
sqlx::query("DROP TABLE IF EXISTS umbral_phase421_event")
.execute(&pool)
.await
.unwrap();
sqlx::query(
"CREATE TABLE umbral_phase421_event ( \
id BIGSERIAL PRIMARY KEY, \
tags TEXT[] NOT NULL, \
scores BIGINT[] \
)",
)
.execute(&pool)
.await
.unwrap();
sqlx::query("INSERT INTO umbral_phase421_event (tags, scores) VALUES ($1, $2)")
.bind(vec!["info".to_string(), "boot".to_string()])
.bind(Option::<Vec<i64>>::Some(vec![10, 20, 30]))
.execute(&pool)
.await
.unwrap();
sqlx::query("INSERT INTO umbral_phase421_event (tags, scores) VALUES ($1, $2)")
.bind(vec!["wip".to_string()])
.bind(Option::<Vec<i64>>::None)
.execute(&pool)
.await
.unwrap();
let info_only = Event::objects()
.filter(event::TAGS.contains("info"))
.fetch_pg(&pool)
.await
.unwrap();
assert_eq!(info_only.len(), 1);
assert_eq!(
info_only[0].tags,
vec!["info".to_string(), "boot".to_string()]
);
let overlap_hits = Event::objects()
.filter(event::SCORES.overlaps([20i64, 99]))
.fetch_pg(&pool)
.await
.unwrap();
assert_eq!(overlap_hits.len(), 1);
let subset = Event::objects()
.filter(event::TAGS.contained_by(["info", "boot", "extra"]))
.fetch_pg(&pool)
.await
.unwrap();
assert_eq!(subset.len(), 1);
let _unused = pg_sql_for(SelectStatement::new());
}