Building recursive CTEs

Version 0.3.0 Updated Jul 22, 2026

Recursive queries delegate the three constituent fragments (seed, recursive step, and final body) to a RecursiveParts struct. Each fragment can be a normal Diesel query builder expression, so Diesel validates the AST at compile time instead of leaving the CTE body as raw SQL.

use diesel::{allow_tables_to_appear_in_same_query, pg::PgConnection, prelude::*, table};
use diesel_cte_ext::{RecursiveCTEExt, RecursiveParts};

fn parent_category_ids(
    conn: &mut PgConnection,
    category_id: i64,
) -> diesel::QueryResult<Vec<i64>> {
    table! {
        categories (id) {
            id -> BigInt,
            parent_category_id -> Nullable<BigInt>,
        }
    }

    table! {
        parents (id) {
            id -> Nullable<BigInt>,
        }
    }

    allow_tables_to_appear_in_same_query!(categories, parents);

    conn.with_recursive_not_all(
        "parents",
        &["id"],
        RecursiveParts::new(
            categories::table
                .select(categories::parent_category_id)
                .filter(categories::id.eq(category_id)),
            categories::table
                .select(categories::parent_category_id)
                .inner_join(
                    parents::table.on(parents::id.assume_not_null().eq(categories::id)),
                ),
            parents::table
                .select(parents::id.assume_not_null())
                .filter(parents::id.is_not_null())
                .order(parents::id.desc()),
        ),
    )
    .load(conn)
}

with_recursive renders a recursive UNION ALL. Use with_recursive_not_all when the recursive term should deduplicate at each iteration using UNION, as shown above.

Async connections receive the same helpers once the async feature is enabled:

use diesel::{allow_tables_to_appear_in_same_query, prelude::*, table};
use diesel_async::{AsyncPgConnection, RunQueryDsl};
use diesel_cte_ext::{RecursiveCTEExt, RecursiveParts};

async fn parent_category_ids_async(
    conn: &mut AsyncPgConnection,
    category_id: i64,
) -> diesel::QueryResult<Vec<i64>> {
    table! {
        categories (id) {
            id -> BigInt,
            parent_category_id -> Nullable<BigInt>,
        }
    }

    table! {
        parents (id) {
            id -> Nullable<BigInt>,
        }
    }

    allow_tables_to_appear_in_same_query!(categories, parents);

    conn.with_recursive_not_all(
        "parents",
        &["id"],
        RecursiveParts::new(
            categories::table
                .select(categories::parent_category_id)
                .filter(categories::id.eq(category_id)),
            categories::table
                .select(categories::parent_category_id)
                .inner_join(
                    parents::table.on(parents::id.assume_not_null().eq(categories::id)),
                ),
            parents::table
                .select(parents::id.assume_not_null())
                .filter(parents::id.is_not_null())
                .order(parents::id.desc()),
        ),
    )
    .load(conn)
    .await
}