#![cfg(test)]
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use prax_postgres::{PgPool, PgPoolBuilder};
static TABLE_COUNTER: AtomicU32 = AtomicU32::new(0);
fn unique_table(prefix: &str) -> String {
let n = TABLE_COUNTER.fetch_add(1, Ordering::SeqCst);
let pid = std::process::id();
format!("cf_{prefix}_{pid}_{n}")
}
fn skip_unless_e2e() -> Option<String> {
if std::env::var("PRAX_E2E").ok().as_deref() != Some("1") {
return None;
}
std::env::var("POSTGRES_URL").ok()
}
async fn pool() -> PgPool {
let url = skip_unless_e2e().expect("PRAX_E2E=1 and POSTGRES_URL required");
PgPoolBuilder::new()
.url(url)
.max_connections(4)
.connection_timeout(Duration::from_secs(10))
.build()
.await
.expect("connect to postgres")
}
async fn drop_table(pool: &PgPool, table: &str) {
let conn = pool.get().await.expect("acquire conn for cleanup");
let _ = conn
.batch_execute(&format!("DROP TABLE IF EXISTS {table}"))
.await;
}
#[tokio::test]
#[ignore = "requires running PostgreSQL via docker-compose (PRAX_E2E=1 + POSTGRES_URL)"]
async fn generated_column_round_trip() {
if skip_unless_e2e().is_none() {
eprintln!("skipping: PRAX_E2E not set");
return;
}
let pool = pool().await;
let table = unique_table("generated");
drop_table(&pool, &table).await;
let conn = pool.get().await.expect("conn");
conn.batch_execute(&format!(
"CREATE TABLE {table} (
id SERIAL PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
full_name TEXT GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED
)"
))
.await
.expect("create table with generated column");
let n = conn
.execute(
&format!("INSERT INTO {table} (first_name, last_name) VALUES ($1, $2)"),
&[&"Ada", &"Lovelace"],
)
.await
.expect("insert row");
assert_eq!(n, 1);
let row = conn
.query_one(
&format!("SELECT first_name, last_name, full_name FROM {table} WHERE first_name = $1"),
&[&"Ada"],
)
.await
.expect("query_one");
let first: &str = row.get(0);
let last: &str = row.get(1);
let full: &str = row.get(2);
assert_eq!(first, "Ada");
assert_eq!(last, "Lovelace");
assert_eq!(
full, "Ada Lovelace",
"generated column should concatenate names"
);
let bad = conn
.execute(
&format!("INSERT INTO {table} (first_name, last_name, full_name) VALUES ($1, $2, $3)"),
&[&"Grace", &"Hopper", &"override"],
)
.await;
assert!(
bad.is_err(),
"inserting into a GENERATED ALWAYS column should be rejected"
);
drop_table(&pool, &table).await;
}
#[tokio::test]
#[ignore = "requires running PostgreSQL via docker-compose (PRAX_E2E=1 + POSTGRES_URL)"]
async fn count_scalar_projection_round_trip() {
use prax_postgres::PgEngine;
use prax_query::filter::FilterValue;
use prax_query::row::{FromRow, RowError, RowRef};
use prax_query::traits::{Model, QueryEngine};
#[derive(Debug, PartialEq)]
struct Author {
id: i32,
email: String,
post_count: i64,
}
impl Model for Author {
const MODEL_NAME: &'static str = "Author";
const TABLE_NAME: &'static str = ""; const PRIMARY_KEY: &'static [&'static str] = &["id"];
const COLUMNS: &'static [&'static str] = &["id", "email", "_count_posts"];
}
impl FromRow for Author {
fn from_row(row: &impl RowRef) -> Result<Self, RowError> {
Ok(Author {
id: row.get_i32("id")?,
email: row.get_string("email")?,
post_count: row.get_i64("_count_posts")?,
})
}
}
if skip_unless_e2e().is_none() {
eprintln!("skipping: PRAX_E2E not set");
return;
}
let pool = pool().await;
let authors_table = unique_table("authors");
let posts_table = unique_table("posts");
{
let conn = pool.get().await.expect("conn");
let _ = conn
.batch_execute(&format!(
"DROP TABLE IF EXISTS {posts_table}; DROP TABLE IF EXISTS {authors_table};"
))
.await;
}
{
let conn = pool.get().await.expect("conn");
conn.batch_execute(&format!(
"CREATE TABLE {authors_table} (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL
);
CREATE TABLE {posts_table} (
id SERIAL PRIMARY KEY,
author_id INT REFERENCES {authors_table}(id),
views INT NOT NULL DEFAULT 0
);"
))
.await
.expect("create tables");
}
let author_id: i32 = {
let conn = pool.get().await.expect("conn");
let row = conn
.query_one(
&format!("INSERT INTO {authors_table} (email) VALUES ($1) RETURNING id"),
&[&"ada@example.com"],
)
.await
.expect("insert author");
row.get(0)
};
{
let conn = pool.get().await.expect("conn");
for _ in 0..3_i32 {
conn.execute(
&format!("INSERT INTO {posts_table} (author_id, views) VALUES ($1, 100)"),
&[&author_id],
)
.await
.expect("insert post");
}
}
let sql = format!(
"SELECT a.id, a.email, \
(SELECT COUNT(*)::BIGINT FROM {posts_table} p WHERE p.author_id = a.id) \
AS _count_posts \
FROM {authors_table} a \
WHERE a.id = $1"
);
let engine = PgEngine::new(pool.clone());
let rows = engine
.query_many::<Author>(&sql, vec![FilterValue::Int(author_id as i64)])
.await
.expect("query_many with scalar projection");
assert_eq!(
rows.len(),
1,
"should return exactly the one matching author"
);
assert_eq!(rows[0].email, "ada@example.com");
assert_eq!(
rows[0].post_count, 3,
"_count_posts scalar subquery should return 3"
);
{
let conn = pool.get().await.expect("conn");
let _ = conn
.batch_execute(&format!(
"DROP TABLE IF EXISTS {posts_table}; DROP TABLE IF EXISTS {authors_table};"
))
.await;
}
}