Search code examples
rustrust-diesel

Diesel delete from with count related records in where clause


I am using the Rust Diesel library and have a schema that looks like this:

The schema looks like this:

table! {
    table1 (id) {
        id -> Uuid,
        ...
    }
}

table! {
    table2 (id) {
        id -> Uuid,
        ...
    }
}

table! {
    table3 (id) {
        id -> Uuid,
        table1_id -> Uuid,
        table2_id -> Uuid,
        ...
    }
}

...

allow_tables_to_appear_in_same_query!(table1, table2, table3, ...);

I am struggling to represent this SQL query with Rust Diesel:

DELETE FROM table1 t1
WHERE (
  SELECT COUNT(*) FROM table3 t3 WHERE t3.table2_id = {id_value}
  AND t1.id = t3.table1_id
) = 1;

The desired result of the query is to delete records from table1 when the count of related records in table3 that are also associated with a specific record from table2 is 1. The real tables 1, 2, and 3 are called budgets, users, and user_budgets, respectively, if that helps with intuition for what I am doing. I want to be able to delete all budgets for a user that aren't "shared," meaning they are also associated with other users via the user_budgets table.

I'm not sure how to approach doing this in Diesel. I have tried doing this:

diesel::delete(
   table1.filter(
        dsl::count(
            table3_fields::table2_id
                .eq({id_value})
                .and(table1_fields::id.eq(table3_fields::table1_id)),
        )
        .eq(1),
    ),
)

That doesn't work; I get the following error (edited with some search/replace to match my hypothetical schema):

error[E0277]: the trait bound `diesel::expression::is_aggregate::Yes: MixedAggregates<diesel::expression::is_aggregate::No>` is not satisfied
   --> path/to/file.rs:345:21
    |
345 |             table1.filter(
    |                     ^^^^^^ the trait `MixedAggregates<diesel::expression::is_aggregate::No>` is not implemented for `diesel::expression::is_aggregate::Yes`
    |
    = help: the following other types implement trait `MixedAggregates<Other>`:
              <diesel::expression::is_aggregate::Yes as MixedAggregates<diesel::expression::is_aggregate::Never>>
              <diesel::expression::is_aggregate::Yes as MixedAggregates<diesel::expression::is_aggregate::Yes>>
    = note: required for `diesel::expression::grouped::Grouped<diesel::expression::operators::Eq<diesel::expression::count::count::count<diesel::sql_types::Bool, diesel::expression::grouped::Grouped<diesel::expression::operators::And<diesel::expression::grouped::Grouped<diesel::expression::operators::Eq<table3::columns::table2_id, diesel::expression::bound::Bound<diesel::sql_types::Uuid, uuid::Uuid>>>, diesel::expression::grouped::Grouped<diesel::expression::operators::Eq<table1::columns::id, table3::columns::table1_id>>>>>, diesel::expression::bound::Bound<BigInt, i64>>>` to implement `NonAggregate`
    = note: required for `SelectStatement<FromClause<table1::table>>` to implement `FilterDsl<diesel::expression::grouped::Grouped<diesel::expression::operators::Eq<diesel::expression::count::count::count<diesel::sql_types::Bool, diesel::expression::grouped::Grouped<diesel::expression::operators::And<diesel::expression::grouped::Grouped<diesel::expression::operators::Eq<table3::columns::table2_id, diesel::expression::bound::Bound<diesel::sql_types::Uuid, uuid::Uuid>>>, diesel::expression::grouped::Grouped<diesel::expression::operators::Eq<table1::columns::id, table3::columns::table1_id>>>>>, diesel::expression::bound::Bound<BigInt, i64>>>>`

Is there a way to do this without resorting sql_query?

(FYI, I am using a Postgres backend)


Solution

  • This should accomplish that:

    diesel::delete(table1::table).filter(
        table3::table
            .filter(table3::table2_id.eq(id_value))
            .filter(table1::id.eq(table3::table1_id))
            .count()
            .single_value()
            .eq(1),
    );
    

    The output from debug_query() looks pretty much identical:

    DELETE  FROM "table1" WHERE ((SELECT COUNT(*) FROM "table3" WHERE (("table3"."table2_id" = $1) AND ("table1"."id" = "table3"."table1_id")) LIMIT $2) = $3) -- binds: [78e50fcc-6b13-4ee2-aece-4959f3a67f76, 1, 1]
    

    Tripped me up for a while because I forgot that .single_value() is needed to treat a SELECT as an expression. After that it was fairly straightforward. Full code available here on the playground.

    See also: