use rust_ef::db_context::{DbContext, DbContextOptionsBuilder};
use rust_ef::linq;
use rust_ef::prelude::*;
use rust_ef::provider::DbValue;
use rust_ef_sqlite::DbContextOptionsBuilderExt as _;
#[derive(Debug, Clone, EntityType)]
#[table("cte_employees")]
struct CteEmployee {
#[primary_key]
#[auto_increment]
emp_id: i32,
#[required]
name: String,
#[required]
dept: String,
#[required]
salary: i64,
}
fn build_ctx() -> DbContext {
let mut builder = DbContextOptionsBuilder::new();
builder.use_sqlite_in_memory();
let options = builder.build();
DbContext::from_options(&options).expect("DbContext")
}
async fn seed(ctx: &mut DbContext) {
ctx.set::<CteEmployee>();
ctx.ensure_created().await.unwrap();
let employees = [
("Alice", "Engineering", 100_000),
("Bob", "Engineering", 90_000),
("Carol", "Engineering", 110_000),
("Dave", "Sales", 80_000),
("Eve", "Sales", 85_000),
("Frank", "Sales", 80_000),
];
for (name, dept, salary) in &employees {
ctx.set::<CteEmployee>().add(CteEmployee {
emp_id: 0,
name: (*name).into(),
dept: (*dept).into(),
salary: *salary,
});
}
ctx.save_changes().await.unwrap();
}
#[test]
fn test_typed_cte_sql_generation() {
let mut ctx = build_ctx();
let sql = linq!(
ctx.set::<CteEmployee>();
with high_earners as |e: CteEmployee| e.salary > 85_000;
from high_earners
)
.to_sql();
assert!(
sql.contains("WITH high_earners AS ("),
"expected `WITH high_earners AS (...)` in SQL, got: {sql}"
);
assert!(
sql.contains("SELECT * FROM \"cte_employees\" WHERE"),
"expected typed CTE body `SELECT * FROM \"cte_employees\" WHERE ...`, got: {sql}"
);
assert!(
sql.contains("?"),
"expected ? placeholder in typed CTE body, got: {sql}"
);
assert!(
sql.contains("FROM high_earners"),
"expected main query to reference CTE by name, got: {sql}"
);
}
#[test]
fn test_typed_cte_compound_where() {
let mut ctx = build_ctx();
let sql = linq!(
ctx.set::<CteEmployee>();
with eng_high as |e: CteEmployee| e.salary > 85_000 && e.dept == "Engineering";
from eng_high
)
.to_sql();
assert!(
sql.contains("WITH eng_high AS ("),
"expected `WITH eng_high AS (...)`, got: {sql}"
);
assert!(
sql.contains("AND"),
"expected AND in compound WHERE CTE body, got: {sql}"
);
assert!(
sql.contains("salary") && sql.contains("dept"),
"expected column names in CTE body, got: {sql}"
);
}
#[test]
fn test_typed_cte_multiple_ctes() {
let mut ctx = build_ctx();
let sql = linq!(
ctx.set::<CteEmployee>();
with high_earners as |e: CteEmployee| e.salary > 85_000;
with eng_earners as |e: CteEmployee| e.dept == "Engineering";
from high_earners
)
.to_sql();
assert!(
sql.contains("WITH high_earners AS ("),
"expected first CTE, got: {sql}"
);
assert!(
sql.contains(", eng_earners AS ("),
"expected second CTE with comma separator, got: {sql}"
);
assert!(
sql.contains("\"cte_employees\""),
"expected both CTEs to reference the source table, got: {sql}"
);
}
#[test]
fn test_typed_cte_parameter_ordering() {
let mut ctx = build_ctx();
let query = linq!(
ctx.set::<CteEmployee>(),
|e: CteEmployee| e.emp_id > 0;
with high_earners as |e: CteEmployee| e.salary > 85_000;
from high_earners
);
let params = query.state().all_params();
assert_eq!(
params.len(),
2,
"expected 2 params (CTE + main WHERE), got {}",
params.len()
);
assert!(
matches!(params[0], DbValue::I32(85_000)),
"expected CTE param (85000) first, got {:?}",
params[0]
);
assert!(
matches!(params[1], DbValue::I32(0)),
"expected main query param (0) second, got {:?}",
params[1]
);
}
#[tokio::test]
async fn test_typed_cte_execution() {
let mut ctx = build_ctx();
seed(&mut ctx).await;
let employees = linq!(
ctx.set::<CteEmployee>();
with high_earners as |e: CteEmployee| e.salary > 85_000;
from high_earners
)
.to_list()
.await
.unwrap();
assert_eq!(employees.len(), 3, "typed CTE should return 3 high earners");
}
#[tokio::test]
async fn test_typed_cte_compound_execution() {
let mut ctx = build_ctx();
seed(&mut ctx).await;
let employees = linq!(
ctx.set::<CteEmployee>();
with eng_high as |e: CteEmployee| e.salary > 85_000 && e.dept == "Engineering";
from eng_high
)
.to_list()
.await
.unwrap();
assert_eq!(
employees.len(),
3,
"compound CTE should return 3 Engineering high earners"
);
}
#[tokio::test]
async fn test_typed_cte_with_main_where() {
let mut ctx = build_ctx();
seed(&mut ctx).await;
let employees = linq!(
ctx.set::<CteEmployee>(),
|e: CteEmployee| e.dept == "Engineering";
with high_earners as |e: CteEmployee| e.salary > 80_000;
from high_earners
)
.to_list()
.await
.unwrap();
assert_eq!(
employees.len(),
3,
"CTE + main WHERE should return 3 Engineering high earners"
);
}
#[tokio::test]
async fn test_typed_cte_with_order_by() {
let mut ctx = build_ctx();
seed(&mut ctx).await;
let employees = linq!(
ctx.set::<CteEmployee>();
with high_earners as |e: CteEmployee| e.salary > 85_000;
from high_earners;
order_by e.salary desc
)
.to_list()
.await
.unwrap();
assert_eq!(employees.len(), 3);
assert_eq!(employees[0].name, "Carol");
assert_eq!(employees[1].name, "Alice");
assert_eq!(employees[2].name, "Bob");
}
#[tokio::test]
async fn test_typed_cte_with_or_condition() {
let mut ctx = build_ctx();
seed(&mut ctx).await;
let employees = linq!(
ctx.set::<CteEmployee>();
with special as |e: CteEmployee| e.salary > 100_000 || e.dept == "Sales";
from special
)
.to_list()
.await
.unwrap();
assert_eq!(
employees.len(),
4,
"OR condition CTE should return 4 employees"
);
}
use rust_ef::provider::ISqlGenerator;
struct PgLikeGenerator;
impl ISqlGenerator for PgLikeGenerator {
fn select(&self, _: &str, _: &[&str]) -> String {
String::new()
}
fn insert(&self, _: &str, _: &[&str], _: bool) -> String {
String::new()
}
fn update(&self, _: &str, _: &[&str], _: &str) -> String {
String::new()
}
fn delete(&self, _: &str, _: &str) -> String {
String::new()
}
fn create_table(&self, _: &str, _: &[(String, String)]) -> String {
String::new()
}
fn drop_table(&self, _: &str) -> String {
String::new()
}
fn pagination(&self, _: Option<usize>, _: Option<usize>) -> String {
String::new()
}
fn parameter_placeholder(&self, index: usize) -> String {
format!("${index}")
}
fn quote_identifier(&self, identifier: &str) -> String {
format!("\"{identifier}\"")
}
fn auto_increment_syntax(&self) -> &'static str {
"SERIAL"
}
}
#[test]
fn test_pg_single_typed_cte_uses_dollar_n() {
let mut ctx = build_ctx();
let query = linq!(
ctx.set::<CteEmployee>();
with high_earners as |e: CteEmployee| e.salary > 85_000;
from high_earners
);
let sql = query.state().to_sql_with(&PgLikeGenerator);
assert!(
sql.contains("WHERE salary > $1"),
"PG typed CTE body should use $1 placeholder, got: {sql}"
);
assert!(
!sql.contains('?'),
"PG typed CTE body must not contain `?` placeholder, got: {sql}"
);
}
#[test]
fn test_pg_multiple_typed_ctes_contiguous_placeholders() {
let mut ctx = build_ctx();
let query = linq!(
ctx.set::<CteEmployee>();
with high_earners as |e: CteEmployee| e.salary > 85_000;
with eng_earners as |e: CteEmployee| e.dept == "Engineering";
from high_earners
);
let sql = query.state().to_sql_with(&PgLikeGenerator);
assert!(
sql.contains("salary > $1"),
"first CTE should use $1, got: {sql}"
);
assert!(
sql.contains("dept = $2"),
"second CTE should use $2 (regression: was $1 before fix), got: {sql}"
);
let second_cte_start = sql
.find("eng_earners AS (")
.unwrap_or_else(|| panic!("expected `eng_earners AS (` in SQL, got: {sql}"));
let second_cte_body = &sql[second_cte_start..];
assert!(
!second_cte_body.contains("= $1"),
"second CTE must not reuse $1 (regression), got: {sql}"
);
assert!(
second_cte_body.contains("= $2"),
"second CTE should use $2, got: {sql}"
);
}
#[test]
fn test_pg_multi_cte_with_main_where_contiguous() {
let mut ctx = build_ctx();
let query = linq!(
ctx.set::<CteEmployee>(),
|e: CteEmployee| e.emp_id > 0;
with high_earners as |e: CteEmployee| e.salary > 85_000;
with eng_earners as |e: CteEmployee| e.dept == "Engineering";
from high_earners
);
let sql = query.state().to_sql_with(&PgLikeGenerator);
assert!(
sql.contains("salary > $1"),
"CTE1 should use $1, got: {sql}"
);
assert!(sql.contains("dept = $2"), "CTE2 should use $2, got: {sql}");
assert!(
sql.contains("emp_id > $3"),
"main WHERE should use $3 (continuity from CTEs), got: {sql}"
);
let params = query.state().all_params();
assert_eq!(
params.len(),
3,
"expected 3 params (2 CTE + 1 main), got {}",
params.len()
);
assert!(
matches!(params[0], DbValue::I32(85_000)),
"param[0] should be CTE1's 85000, got {:?}",
params[0]
);
assert!(
matches!(params[1], DbValue::String(_)),
"param[1] should be CTE2's dept string, got {:?}",
params[1]
);
assert!(
matches!(params[2], DbValue::I32(0)),
"param[2] should be main WHERE's 0, got {:?}",
params[2]
);
}
#[test]
fn test_pg_compound_where_cte_placeholder_count() {
let mut ctx = build_ctx();
let query = linq!(
ctx.set::<CteEmployee>();
with eng_high as |e: CteEmployee| e.salary > 85_000 && e.dept == "Engineering";
from eng_high
);
let sql = query.state().to_sql_with(&PgLikeGenerator);
assert!(
sql.contains("salary > $1"),
"compound CTE first param should be $1, got: {sql}"
);
assert!(
sql.contains("dept = $2"),
"compound CTE second param should be $2, got: {sql}"
);
assert!(
!sql.contains("$3"),
"compound CTE with 2 params must not emit $3, got: {sql}"
);
}