df.if() vs df.if_rows() in the pg_durable DSL: Conditional Branching Explained
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, 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 (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:
// 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:
// 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:
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:
-- 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:
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 viadf.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 incondition_nodewithin theDurofutstructure.df.if_rows()checks cached result metadata forrow_count > 0, usingcondition_type: "result_has_rows"without additional SQL.- Source location: Both implementations reside in
src/dsl.rs(lines 369-387 forif_fn, lines 389-410 forif_rows_fn). - Performance:
df.if_rows()offers zero-cost branching compared to the query execution overhead ofdf.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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →