#![allow(dead_code)]
#![allow(unused_imports)]
use std::borrow::Cow;
use std::sync::{Arc, Mutex};
use prax_orm::{Model, client};
use prax_query::capabilities::{SupportsNestedWrites, SupportsScalarSubqueryInSelect};
use prax_query::dialect::SqlDialect;
use prax_query::error::{QueryError, QueryResult};
use prax_query::filter::{Filter, FilterValue};
use prax_query::projection::ScalarProjection;
use prax_query::row::{FromRow, RowError, RowRef};
use prax_query::traits::{BoxFuture, Model as ModelTrait, QueryEngine};
use prax_query::types::{OrderBy, OrderByField, SortOrder};
type StatementLog = Arc<Mutex<Vec<(String, Vec<FilterValue>)>>>;
#[derive(Clone)]
struct RecordingEngine {
recorded: StatementLog,
}
impl RecordingEngine {
fn new() -> Self {
Self {
recorded: Arc::new(Mutex::new(Vec::new())),
}
}
fn statements(&self) -> Vec<(String, Vec<FilterValue>)> {
self.recorded.lock().unwrap().clone()
}
}
impl QueryEngine for RecordingEngine {
fn dialect(&self) -> &dyn SqlDialect {
&prax_query::dialect::Postgres
}
fn query_many<T: ModelTrait + FromRow + Send + 'static>(
&self,
sql: &str,
params: Vec<FilterValue>,
) -> BoxFuture<'_, QueryResult<Vec<T>>> {
let recorded = self.recorded.clone();
let sql = sql.to_string();
Box::pin(async move {
recorded.lock().unwrap().push((sql, params));
Ok(Vec::new())
})
}
fn query_one<T: ModelTrait + FromRow + Send + 'static>(
&self,
sql: &str,
params: Vec<FilterValue>,
) -> BoxFuture<'_, QueryResult<T>> {
let recorded = self.recorded.clone();
let sql = sql.to_string();
Box::pin(async move {
recorded.lock().unwrap().push((sql, params));
T::from_row(&CannedRow).map_err(|e| QueryError::internal(e.to_string()))
})
}
fn query_optional<T: ModelTrait + FromRow + Send + 'static>(
&self,
_sql: &str,
_params: Vec<FilterValue>,
) -> BoxFuture<'_, QueryResult<Option<T>>> {
Box::pin(async { Ok(None) })
}
fn execute_insert<T: ModelTrait + FromRow + Send + 'static>(
&self,
sql: &str,
params: Vec<FilterValue>,
) -> BoxFuture<'_, QueryResult<T>> {
let recorded = self.recorded.clone();
let sql = sql.to_string();
Box::pin(async move {
recorded.lock().unwrap().push((sql, params));
T::from_row(&CannedRow).map_err(|e| QueryError::internal(e.to_string()))
})
}
fn execute_update<T: ModelTrait + FromRow + Send + 'static>(
&self,
sql: &str,
params: Vec<FilterValue>,
) -> BoxFuture<'_, QueryResult<Vec<T>>> {
let recorded = self.recorded.clone();
let sql = sql.to_string();
Box::pin(async move {
recorded.lock().unwrap().push((sql, params));
Ok(Vec::new())
})
}
fn execute_delete(
&self,
_sql: &str,
_params: Vec<FilterValue>,
) -> BoxFuture<'_, QueryResult<u64>> {
Box::pin(async { Ok(0) })
}
fn execute_raw(&self, sql: &str, params: Vec<FilterValue>) -> BoxFuture<'_, QueryResult<u64>> {
let recorded = self.recorded.clone();
let sql = sql.to_string();
Box::pin(async move {
recorded.lock().unwrap().push((sql, params));
Ok(1)
})
}
fn count(&self, _sql: &str, _params: Vec<FilterValue>) -> BoxFuture<'_, QueryResult<u64>> {
Box::pin(async { Ok(0) })
}
}
impl SupportsNestedWrites for RecordingEngine {}
impl SupportsScalarSubqueryInSelect for RecordingEngine {}
struct CannedRow;
impl RowRef for CannedRow {
fn get_i32(&self, _column: &str) -> Result<i32, RowError> {
Ok(1)
}
fn get_i32_opt(&self, _column: &str) -> Result<Option<i32>, RowError> {
Ok(Some(1))
}
fn get_i64(&self, _column: &str) -> Result<i64, RowError> {
Ok(0)
}
fn get_i64_opt(&self, _column: &str) -> Result<Option<i64>, RowError> {
Ok(None)
}
fn get_f64(&self, _column: &str) -> Result<f64, RowError> {
Ok(0.0)
}
fn get_f64_opt(&self, _column: &str) -> Result<Option<f64>, RowError> {
Ok(None)
}
fn get_bool(&self, _column: &str) -> Result<bool, RowError> {
Ok(false)
}
fn get_bool_opt(&self, _column: &str) -> Result<Option<bool>, RowError> {
Ok(None)
}
fn get_str(&self, _column: &str) -> Result<&str, RowError> {
Ok("canned")
}
fn get_str_opt(&self, _column: &str) -> Result<Option<&str>, RowError> {
Ok(Some("canned"))
}
fn get_bytes(&self, _column: &str) -> Result<&[u8], RowError> {
Ok(b"")
}
fn get_bytes_opt(&self, _column: &str) -> Result<Option<&[u8]>, RowError> {
Ok(None)
}
}
#[derive(Model, Debug, Clone, Default)]
#[prax(table = "posts")]
pub struct Post {
#[prax(id, auto)]
pub id: i32,
pub author_id: i32,
pub title: String,
}
#[derive(Model, Debug, Clone, Default)]
#[prax(table = "users")]
pub struct User {
#[prax(id, auto)]
pub id: i32,
#[prax(unique)]
pub email: String,
pub first_name: String,
pub last_name: String,
#[prax(generated = "first_name || ' ' || last_name", stored)]
pub full_name: String,
#[prax(relation(target = "Post", foreign_key = "author_id"))]
pub posts: Vec<Post>,
#[prax(count(posts))]
pub post_count: i64,
}
client!(User, Post);
#[test]
fn filter_by_post_count_emits_scalar_subquery_in_where() {
let engine = RecordingEngine::new();
let c = prax_orm::PraxClient::new(engine.clone());
let subquery_sql = r#"(SELECT COUNT(*) FROM "posts" WHERE "posts"."author_id" = "users"."id")"#;
let op = c.user().find_many().r#where(Filter::ScalarSubquery {
sql: format!("{subquery_sql} > {{0}}").into(),
params: vec![FilterValue::Int(5)],
});
let (sql, params) = op.build_sql(&prax_query::dialect::Postgres);
assert!(sql.contains("WHERE"), "missing WHERE clause; got: {sql}");
assert!(
sql.contains("(SELECT COUNT(*) FROM"),
"missing scalar subquery in WHERE; got: {sql}"
);
assert!(
sql.contains("> $1"),
"expected `> $1` comparison; got: {sql}"
);
assert_eq!(
params,
vec![FilterValue::Int(5)],
"expected single Int(5) param; got: {params:?}"
);
}
#[test]
fn select_count_emits_scalar_projection_in_select() {
let engine = RecordingEngine::new();
let c = prax_orm::PraxClient::new(engine.clone());
let proj = ScalarProjection::new(
Cow::Borrowed(r#"SELECT COUNT(*) FROM "posts" WHERE "posts"."author_id" = "users"."id""#),
vec![],
"_count_posts",
);
let op = c.user().find_many().with_scalar_projection(proj);
let (sql, _params) = op.build_sql(&prax_query::dialect::Postgres);
assert!(
sql.contains("_count_posts"),
"missing `_count_posts` alias in SELECT; got: {sql}"
);
assert!(
sql.contains("(SELECT COUNT(*) FROM"),
"missing scalar subquery in SELECT; got: {sql}"
);
assert!(
sql.contains("AS \"_count_posts\""),
"missing aliased projection; got: {sql}"
);
}
#[test]
fn order_by_post_count_emits_scalar_subquery() {
let engine = RecordingEngine::new();
let c = prax_orm::PraxClient::new(engine.clone());
let subquery_sql = r#"(SELECT COUNT(*) FROM "posts" WHERE "posts"."author_id" = "users"."id")"#;
let op = c.user().find_many().order_by(OrderByField::new(
String::from(subquery_sql),
SortOrder::Desc,
));
let (sql, _params) = op.build_sql(&prax_query::dialect::Postgres);
assert!(
sql.contains("ORDER BY"),
"missing ORDER BY clause; got: {sql}"
);
assert!(
sql.contains("(SELECT COUNT(*) FROM"),
"ORDER BY must contain scalar subquery; got: {sql}"
);
assert!(
sql.contains("DESC"),
"missing DESC direction in ORDER BY; got: {sql}"
);
}
#[test]
fn create_omits_generated_and_aggregate_columns() {
let engine = RecordingEngine::new();
let c = prax_orm::PraxClient::new(engine.clone());
let op = c
.user()
.create()
.set("email", "ada@lovelace.io")
.set("first_name", "Ada");
let (sql, _params) = op.build_sql(&prax_query::dialect::Postgres);
assert!(
sql.starts_with("INSERT INTO"),
"expected INSERT INTO; got: {sql}"
);
assert!(
!sql.contains("full_name"),
"INSERT must omit @generated column `full_name`; got: {sql}"
);
assert!(
!sql.contains("post_count"),
"INSERT must omit @count column `post_count`; got: {sql}"
);
}