# df.if() vs df.if_rows() in the pg_durable DSL: Conditional Branching Explained

> Understand the difference between df.if() and df.if_rows() in pg_durable. Learn how to optimize conditional branching and eliminate extra database queries for better performance.

- Repository: [Microsoft/pg_durable](https://github.com/microsoft/pg_durable)
- Tags: deep-dive
- Published: 2026-06-08

---

**`df.if()` executes a fresh SQL query at runtime to determine branching logic, while `df.if_rows()` performs a zero-cost check against cached named result metadata, eliminating extra database round-trips.**

The **pg_durable** DSL provides powerful workflow branching capabilities through two distinct conditional functions. Understanding the difference between `df.if()` and `df.if_rows()` is essential for optimizing your durable workflows in the `microsoft/pg_durable` repository, as choosing the wrong method can introduce unnecessary latency or redundant query execution.

## Core Architectural Differences

At the implementation level in [`src/dsl.rs`](https://github.com/microsoft/pg_durable/blob/main/src/dsl.rs), these functions build entirely different execution nodes despite sharing the same branching semantics.

**`df.if()`** treats the condition as a **SQL query string** that gets executed during workflow evaluation. According to the source code in [`src/dsl.rs`](https://github.com/microsoft/pg_durable/blob/main/src/dsl.rs) (lines 369-387), the `if_fn` implementation creates a `Durofut` node containing a `condition_node` that stores the full SQL text. This triggers an actual database query every time the branch is evaluated.

**`df.if_rows()`** operates on **named results** previously captured via `df.as()` or the `|=>` operator. The `if_rows_fn` implementation (lines 389-410) omits the `condition_node` entirely, instead storing JSON configuration with `"condition_type": "result_has_rows"` and the result name. The orchestration engine simply inspects the in-memory JSON metadata for `row_count > 0` without issuing additional SQL.

## Implementation Details in src/dsl.rs

### How df.if() Works

The `if_fn` function constructs a condition sub-graph:

```rust
// src/dsl.rs lines 369-387
#[pg_extern(name = "if", schema = "df")]
pub fn if_fn(condition: &str, then_branch: &str, else_branch: &str) -> String {
    let condition_fut = Durofut::ensure(condition);
    let then_fut     = Durofut::ensure(then_branch);
    let else_fut     = Durofut::ensure(else_branch);

    let config = serde_json::json!({
        "condition_node": condition_fut
    });

    Durofut {
        node_type: "IF".to_string(),
        left_node: Some(Box::new(then_fut)),
        right_node: Some(Box::new(else_fut)),
        query: Some(config.to_string()),
        ..Default::default()
    }
    .to_json()
}

```

This implementation requires the orchestration engine to execute the `condition_fut` query string at runtime to obtain a boolean-like value.

### How df.if_rows() Works

The `if_rows_fn` function takes a result name instead of SQL:

```rust
// src/dsl.rs lines 389-410
#[pg_extern(name = "if_rows", schema = "df")]
pub fn if_rows_fn(result_name: &str, then_branch: &str, else_branch: &str) -> String {
    let then_fut = Durofut::ensure(then_branch);
    let else_fut = Durofut::ensure(else_branch);

    let config = serde_json::json!({
        "condition_type": "result_has_rows",
        "result_name": result_name
    });

    Durofut {
        node_type: "IF".to_string(),
        left_node: Some(Box::new(then_fut)),
        right_node: Some(Box::new(else_fut)),
        query: Some(config.to_string()),
        ..Default::default()
    }
    .to_json()
}

```

The engine checks the cached result's `row_count` field in the JSON metadata, providing **zero-cost** branching.

## Practical Usage Examples

### Dynamic Conditions with df.if()

Use `df.if()` when the branching decision requires a calculation or lookup not present in previous results:

```sql
SELECT df.if(
    'SELECT EXISTS (SELECT 1 FROM orders WHERE status = ''pending'')',
    'SELECT process_pending_orders()',
    'SELECT ''no pending orders''::text'
) AS graph;

```

This executes the `EXISTS` query at runtime to determine the branch.

### Efficient Branching with df.if_rows()

Use `df.if_rows()` to check if a previous query returned any rows without additional SQL:

```sql
-- Capture the result first
SELECT df.as(
    'SELECT * FROM orders WHERE status = ''pending''', 
    'pending_orders'
) AS graph;

-- Branch based on row count
SELECT df.if_rows(
    'pending_orders',
    'SELECT process_pending_orders()',
    'SELECT ''no pending orders''::text'
) AS graph;

```

The second call inspects the `pending_orders` metadata instead of querying the database again.

### Combining Both Approaches

For complex workflows, nest `df.if()` inside `df.if_rows()` to avoid expensive condition checks when no data exists:

```sql
SELECT df.if_rows(
    'pending_orders',
    df.if(
        'SELECT pg_catalog.current_setting(''enable_feature_x'', true) = ''on''',
        'SELECT enable_feature_x()',
        'SELECT disable_feature_x()'
    ),
    'SELECT ''nothing to do''::text'
) AS graph;

```

## Performance and Use Case Guidelines

- **Use `df.if()`** when: The condition depends on dynamic system state, current timestamps, or calculations requiring real-time database evaluation.
- **Use `df.if_rows()`** when: You need to verify whether a specific query returned data, and that query result is already cached as a named result via `df.as()`.
- **Latency impact**: `df.if()` adds a database round-trip per evaluation; `df.if_rows()` operates entirely in-memory on the orchestration node's cached JSON.

## Summary

- **`df.if()`** executes SQL queries at runtime to determine branching, stored in `condition_node` within the `Durofut` structure.
- **`df.if_rows()`** checks cached result metadata for `row_count > 0`, using `condition_type: "result_has_rows"` without additional SQL.
- **Source location**: Both implementations reside in [`src/dsl.rs`](https://github.com/microsoft/pg_durable/blob/main/src/dsl.rs) (lines 369-387 for `if_fn`, lines 389-410 for `if_rows_fn`).
- **Performance**: `df.if_rows()` offers zero-cost branching compared to the query execution overhead of `df.if()`.

## Frequently Asked Questions

### Can I use df.if_rows() with any named result?

Yes, `df.if_rows()` works with any result previously captured using `df.as()` or the pipe-arrow operator. The function specifically checks the `row_count` field in the result's JSON metadata, making it compatible with any stored query output regardless of the specific columns or data types returned.

### Does df.if_rows() evaluate the actual row content?

No, `df.if_rows()` only verifies whether the row count is greater than zero. It does not inspect individual column values or execute the original query again. If you need to branch based on specific data values within the rows, you must use `df.if()` with a SQL condition that queries the named result or the underlying tables.

### Which function should I use for expensive conditional checks?

Always prefer `df.if_rows()` when checking for the existence of previously queried data, as it performs zero-cost metadata inspection. Reserve `df.if()` for conditions that genuinely require fresh database evaluation, such as checking current system settings, timestamps, or cross-referencing external tables that may have changed since the named result was cached.

### Can these branching functions be nested within each other?

Yes, the pg_durable DSL supports arbitrary nesting. You can pass the JSON output of `df.if()` as the then-branch or else-branch argument to `df.if_rows()`, and vice versa. The orchestration engine recursively evaluates the nested `Durofut` nodes according to the outer condition's result.