toasty 0.2.0

An async ORM for Rust supporting SQL and NoSQL databases
Documentation
use crate as toasty;
use crate::engine::simplify::Simplify;
use crate::schema::Register;
use toasty_core::{
    driver::Capability,
    schema::{Builder, app, app::FieldId, app::ModelId},
    stmt::{self, Association, Expr, ExprInSubquery, Path, Query, SourceModel, Value},
};

#[allow(dead_code)]
#[derive(toasty::Model)]
struct User {
    #[key]
    id: i64,

    #[has_many(pair = author)]
    posts: toasty::HasMany<Post>,
}

#[allow(dead_code)]
#[derive(toasty::Model)]
struct Post {
    #[key]
    id: i64,

    #[index]
    user_id: i64,

    #[belongs_to(key = user_id, references = id)]
    author: toasty::BelongsTo<User>,
}

struct UserPostSchema {
    schema: toasty_core::Schema,
    user_model: ModelId,
    user_id: FieldId,
    user_posts: FieldId,
    post_model: ModelId,
    post_author: FieldId,
}

impl UserPostSchema {
    fn new() -> Self {
        let app_schema = app::Schema::from_macro(&[User::schema(), Post::schema()])
            .expect("schema should build from macro");

        let schema = Builder::new()
            .build(app_schema, &Capability::SQLITE)
            .expect("schema should build");

        let user_model = User::id();
        let post_model = Post::id();

        // Find field IDs by name from the generated schema
        let user_id = schema
            .app
            .model(user_model)
            .as_root_unwrap()
            .field_by_name("id")
            .unwrap()
            .id;
        let user_posts = schema
            .app
            .model(user_model)
            .as_root_unwrap()
            .field_by_name("posts")
            .unwrap()
            .id;
        let post_author = schema
            .app
            .model(post_model)
            .as_root_unwrap()
            .field_by_name("author")
            .unwrap()
            .id;

        Self {
            schema,
            user_model,
            user_id,
            user_posts,
            post_model,
            post_author,
        }
    }
}

#[test]
fn has_many_via_becomes_in_subquery() {
    // `select(Post, via(User.posts)) → select(Post, in_subquery(author, user_query))`
    let s = UserPostSchema::new();
    let mut simplify = Simplify::new(&s.schema);

    let user_filter = Expr::eq(
        Expr::ref_self_field(s.user_id),
        Expr::Value(Value::from(42i64)),
    );
    let user_query = Query::new_select(s.user_model, user_filter);

    let association = Association {
        source: Box::new(user_query),
        path: Path::field(s.user_model, s.user_posts.index),
    };

    let mut query = Query::new_select(s.post_model, Expr::Value(Value::Bool(true)));
    if let stmt::ExprSet::Select(select) = &mut query.body
        && let stmt::Source::Model(model) = &mut select.source
    {
        model.via = Some(association);
    }

    simplify.simplify_via_association_for_query(&mut query);

    let stmt::ExprSet::Select(select) = &query.body else {
        panic!("expected Select");
    };
    assert!(matches!(
        &select.source,
        stmt::Source::Model(SourceModel { via: None, .. })
    ));

    let filter_expr = select.filter.as_expr();
    let Expr::InSubquery(ExprInSubquery {
        expr,
        query: subquery,
    }) = filter_expr
    else {
        panic!("expected filter expression to be an `Expr::InSubquery`");
    };

    // The expression should reference the pair field (`post_author`).
    assert!(matches!(
        &**expr,
        Expr::Reference(stmt::ExprReference::Field { index, .. }) if *index == s.post_author.index
    ));

    // The subquery should be the user query.
    let stmt::ExprSet::Select(select) = &subquery.body else {
        panic!("expected subquery body to be a `ExprSet::Select`");
    };

    // Ensure the source of the subquery is the user model.
    assert!(matches!(
        &select.source,
        stmt::Source::Model(SourceModel { id, .. }) if *id == s.user_model
    ));
}