use rust_ef::db_context::{DbContext, DbContextOptionsBuilder};
use rust_ef::linq;
use rust_ef::prelude::*;
use rust_ef_sqlite::DbContextOptionsBuilderExt as _;
#[derive(Debug, Clone, EntityType)]
#[table("sub_blogs")]
struct SubBlog {
#[primary_key]
#[auto_increment]
blog_id: i32,
#[required]
url: String,
#[navigation]
posts: HasMany<SubPost>,
}
#[derive(Debug, Clone, EntityType)]
#[table("sub_posts")]
struct SubPost {
#[primary_key]
#[auto_increment]
post_id: i32,
#[required]
title: String,
#[foreign_key(SubBlog)]
blog_id: i32,
published: bool,
#[navigation]
blog: BelongsTo<SubBlog>,
}
fn make_ctx() -> DbContext {
let mut builder = DbContextOptionsBuilder::new();
builder.use_sqlite_in_memory();
let options = builder.build();
DbContext::from_options(&options).unwrap()
}
async fn seed() -> DbContext {
let mut ctx = make_ctx();
ctx.set::<SubBlog>();
ctx.set::<SubPost>();
ctx.ensure_created().await.unwrap();
let blogs = [
(
"https://b1.example",
vec![("Post 1A", true), ("Post 1B", false)],
),
(
"https://b2.example",
vec![("rust intro", true), ("rust advanced", true)],
),
("https://b3.example", vec![("Post 3A", false)]),
("https://b4.example", vec![]),
];
for (url, posts) in &blogs {
ctx.set::<SubBlog>().add(SubBlog {
blog_id: 0,
url: (*url).into(),
posts: HasMany::new(),
});
ctx.save_changes().await.unwrap();
let inserted = ctx.set::<SubBlog>().query().to_list().await.unwrap();
let blog_id = inserted.last().unwrap().blog_id;
for (title, published) in posts {
ctx.set::<SubPost>().add(SubPost {
post_id: 0,
title: (*title).into(),
blog_id,
published: *published,
blog: BelongsTo::new(),
});
}
ctx.save_changes().await.unwrap();
}
ctx
}
#[tokio::test]
async fn test_any_published_post() {
let mut ctx = seed().await;
let blogs = linq!(ctx.set::<SubBlog>(), |b: SubBlog| b
.posts
.any(|p: SubPost| p.published))
.to_list()
.await
.unwrap();
let urls: Vec<String> = blogs.iter().map(|b| b.url.clone()).collect();
assert_eq!(blogs.len(), 2, "expected 2 blogs with published posts");
assert!(urls.contains(&"https://b1.example".to_string()));
assert!(urls.contains(&"https://b2.example".to_string()));
}
#[tokio::test]
async fn test_none_published_post() {
let mut ctx = seed().await;
let blogs = linq!(ctx.set::<SubBlog>(), |b: SubBlog| b
.posts
.none(|p: SubPost| p.published))
.to_list()
.await
.unwrap();
let urls: Vec<String> = blogs.iter().map(|b| b.url.clone()).collect();
assert_eq!(blogs.len(), 2, "expected 2 blogs with no published posts");
assert!(urls.contains(&"https://b3.example".to_string()));
assert!(urls.contains(&"https://b4.example".to_string()));
}
#[tokio::test]
async fn test_all_posts_published() {
let mut ctx = seed().await;
let blogs = linq!(ctx.set::<SubBlog>(), |b: SubBlog| b
.posts
.all(|p: SubPost| p.published))
.to_list()
.await
.unwrap();
let urls: Vec<String> = blogs.iter().map(|b| b.url.clone()).collect();
assert_eq!(blogs.len(), 2, "expected 2 blogs where all posts published");
assert!(urls.contains(&"https://b2.example".to_string()));
assert!(urls.contains(&"https://b4.example".to_string()));
}
#[tokio::test]
async fn test_not_any_published() {
let mut ctx = seed().await;
let blogs = linq!(ctx.set::<SubBlog>(), |b: SubBlog| !b
.posts
.any(|p: SubPost| p.published))
.to_list()
.await
.unwrap();
assert_eq!(
blogs.len(),
2,
"expected 2 blogs with no published posts (via !any)"
);
}
#[tokio::test]
async fn test_any_with_complex_predicate() {
let mut ctx = seed().await;
let blogs = linq!(ctx.set::<SubBlog>(), |b: SubBlog| b
.posts
.any(|p: SubPost| p.title.contains("rust")))
.to_list()
.await
.unwrap();
assert_eq!(blogs.len(), 1, "expected 1 blog with rust-titled posts");
assert_eq!(blogs[0].url, "https://b2.example");
}
#[tokio::test]
async fn test_any_combined_with_and() {
let mut ctx = seed().await;
let blogs = linq!(ctx.set::<SubBlog>(), |b: SubBlog| b.blog_id > 0
&& b.posts.any(|p: SubPost| p.published))
.to_list()
.await
.unwrap();
assert_eq!(
blogs.len(),
2,
"expected 2 blogs matching combined condition"
);
}
#[tokio::test]
async fn test_form_c_filter_subquery() {
let expr: BoolExpr = linq!(filter |b: SubBlog| b.posts.any(|p: SubPost| p.published));
assert!(
matches!(expr, BoolExpr::Exists(_)),
"Form C filter with .any() should produce BoolExpr::Exists"
);
let expr_none: BoolExpr = linq!(filter |b: SubBlog| b.posts.none(|p: SubPost| p.published));
assert!(
matches!(expr_none, BoolExpr::NotExists(_)),
"Form C filter with .none() should produce BoolExpr::NotExists"
);
}
#[tokio::test]
async fn test_not_all_published() {
let mut ctx = seed().await;
let blogs = linq!(ctx.set::<SubBlog>(), |b: SubBlog| !b
.posts
.all(|p: SubPost| p.published))
.to_list()
.await
.unwrap();
let urls: Vec<String> = blogs.iter().map(|b| b.url.clone()).collect();
assert_eq!(
blogs.len(),
2,
"expected 2 blogs where NOT all posts published"
);
assert!(urls.contains(&"https://b1.example".to_string()));
assert!(urls.contains(&"https://b3.example".to_string()));
}