Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/skills/pg-durable-sql/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,13 @@ df.signal(

Use a JSON object when workflow SQL expects structured fields; use plain text for simple opaque values.

-- Query status
-- Query status by instance ID (returned by df.start())
df.status(instance_id TEXT) → TEXT -- 'Running', 'Completed', 'Failed', 'Cancelled'

-- Query status by label (most recently started instance with that label)
-- NOTE: df.status() requires an instance_id, NOT a label — use df.status_by_label() when you only have the label
df.status_by_label(label TEXT) → TEXT -- 'Running', 'Completed', 'Failed', 'Cancelled', or NULL

-- Get result
df.result(instance_id TEXT) → TEXT -- JSON result from final node

Expand Down
10 changes: 9 additions & 1 deletion USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1442,13 +1442,21 @@ SELECT * FROM df.metrics();
### Quick Status Check

```sql
-- Status only
-- Status by instance ID (returned by df.start())
SELECT df.status('a1b2c3d4');

-- Status by label — returns the most recently started instance with that label
SELECT df.status_by_label('my-workflow');

-- Result only
SELECT df.result('a1b2c3d4');
```

> **Tip:** `df.status()` requires the `instance_id` returned by `df.start()`, not a label.
> Passing a label to `df.status()` returns `NULL` because no instance ID matches that string.
> Use `df.status_by_label()` when you only have the label, and save the `instance_id` from
> `df.start()` when you need to track a specific run.

### Worker Liveness

Check whether the background worker is alive and healthy:
Expand Down
27 changes: 27 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,12 +320,39 @@ Gets instance status.
|-----------|------|-----------|-------------|
| `instance_id` | TEXT | ❌ Literal | Target instance ID |

> **Note:** `df.status()` requires an `instance_id` (the unique identifier returned by
> `df.start()`), **not** a label. Passing a label returns `NULL` because no instance ID
> matches that string. Use `df.status_by_label()` to look up status by label instead.

```sql
SELECT df.status('a1b2c3d4'); -- Returns: 'Running', 'Completed', etc.
```

---

### df.status_by_label(label)

Gets the status of the **most recently started** instance with the given label.
Returns `NULL` when no matching instance exists or is visible to the calling user.

Labels are not unique — if multiple instances share the same label, only the
status of the most recently created one is returned. To target a specific run,
capture the `instance_id` from `df.start()` and use `df.status()` instead.

| Parameter | Type | Auto-wrap | Description |
|-----------|------|-----------|-------------|
| `label` | TEXT | ❌ Literal | Label passed to `df.start()` |

```sql
-- Start a labeled workflow
SELECT df.start('SELECT 1', 'my-workflow');

-- Check its status by label
SELECT df.status_by_label('my-workflow'); -- Returns: 'Running', 'Completed', etc.
```

---

### df.result(instance_id)

Gets instance result (for completed instances).
Expand Down
2 changes: 1 addition & 1 deletion docs/upgrade-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ We test against all previous versions in the same provider compatibility line. T
| Variable capture | `df.start()` with vars set |
| DSL construction | `df.sql()`, `df.seq()`, `df.if()`, `df.loop()`, `df.sleep()`, `df.http()` |
| Execution | Starting and completing orchestrations |
| Monitoring | `df.status()`, `df.result()`, `df.list_instances()`, `df.instance_info()` |
| Monitoring | `df.status()`, `df.status_by_label()`, `df.result()`, `df.list_instances()`, `df.instance_info()` |
| In-flight work | Orchestrations started before `.so` swap complete after swap |

**What it catches:**
Expand Down
11 changes: 11 additions & 0 deletions sql/pg_durable--0.1.1.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3713,6 +3713,17 @@ LANGUAGE c /* Rust */
AS 'MODULE_PATHNAME', 'instance_info_wrapper';
/* </end connected objects> */

/* <begin connected objects> */
-- src/monitoring.rs:111
-- pg_durable::monitoring::status_by_label
CREATE FUNCTION df."status_by_label"(
"label" TEXT /* &str */
) RETURNS TEXT /* core::option::Option<alloc::string::String> */
STRICT
LANGUAGE c /* Rust */
AS 'MODULE_PATHNAME', 'status_by_label_wrapper';
/* </end connected objects> */

/* <begin connected objects> */
-- src/dsl.rs:714
-- pg_durable::dsl::status
Expand Down
14 changes: 14 additions & 0 deletions sql/pg_durable--0.2.1--0.2.2.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
-- The fix runs the name through quote_ident() first so regrole_in() sees a
-- properly quoted identifier and resolves the role without case folding.
-- See issue #161 / PR #162.
--
-- Add: df.status_by_label(label TEXT) — ergonomic status lookup by label.
-- See issue #165.

-- ----------------------------------------------------------------------------
-- df.instances policy
Expand Down Expand Up @@ -39,3 +42,14 @@ CREATE POLICY vars_user_isolation ON df.vars

ALTER TABLE df.vars
ALTER COLUMN owner SET DEFAULT quote_ident(current_user)::regrole;

-- ----------------------------------------------------------------------------
-- df.status_by_label(label TEXT) — new in 0.2.2
-- Returns the status of the most recently started instance with the given label.
-- Returns NULL when no matching instance is visible to the calling user (RLS).
-- ----------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION df.status_by_label("label" TEXT)
RETURNS TEXT
STRICT
LANGUAGE c
AS 'pg_durable', 'status_by_label_wrapper';
23 changes: 23 additions & 0 deletions src/monitoring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,29 @@ pub fn list_instances(
TableIterator::new(results)
}

/// Gets the status of the most recently started instance with the given label.
///
/// Labels are not unique — multiple instances may share the same label. This
/// function always returns the status of the **most recently created** matching
/// instance. If no instance with the given label exists (or is visible to the
/// calling user), `NULL` is returned.
///
/// To check the status of a specific run, use `df.status(instance_id)` with
/// the `instance_id` returned by `df.start()`.
///
/// Note: SPI errors are mapped to `NULL` (same behaviour as `df.status()`).
/// A `NULL` return means either "no matching instance" or an internal error;
/// use `df.list_instances()` when you need to distinguish the two.
#[pg_extern(schema = "df")]
pub fn status_by_label(label: &str) -> Option<String> {
Spi::get_one_with_args::<String>(
"SELECT status FROM df.instances WHERE label = $1 ORDER BY created_at DESC LIMIT 1",
&[label.into()],
)
.ok()
.flatten()
}

/// Get detailed info about a specific durable function instance.
#[pg_extern(schema = "df")]
pub fn instance_info(
Expand Down
22 changes: 21 additions & 1 deletion tests/e2e/sql/05_monitoring_and_explain.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
-- Merged from: 09_monitoring, 10_explain, 31_explain_plain_sql
-- Tests: list_instances, instance_info, status, result, df.explain() on live and dry-run,
-- Tests: list_instances, instance_info, status, status_by_label, result, df.explain() on live and dry-run,
-- df.explain() on plain SQL auto-wrap
SET SESSION AUTHORIZATION df_e2e_user;

Expand All @@ -16,6 +16,8 @@ DECLARE
found BOOLEAN;
info_status TEXT;
result TEXT;
label_status TEXT;
missing_status TEXT;
BEGIN
SELECT instance_id INTO inst_id FROM _test_state;
RAISE NOTICE 'Testing instance: %', inst_id;
Expand Down Expand Up @@ -48,6 +50,24 @@ BEGIN
IF result NOT LIKE '%123%' THEN
RAISE EXCEPTION 'TEST FAILED: result should contain 123, got %', result;
END IF;

-- Test status_by_label returns the same status as df.status()
-- (uses the same label passed to df.start() above: 'test-monitoring-label')
SELECT df.status_by_label('test-monitoring-label') INTO label_status;
IF label_status IS NULL THEN
RAISE EXCEPTION 'TEST FAILED: status_by_label returned NULL for known label';
END IF;
IF lower(label_status) != lower(status) THEN
RAISE EXCEPTION 'TEST FAILED: status_by_label (%) != status (%) for same instance',
label_status, status;
END IF;

-- Test status_by_label returns NULL for an unknown label
SELECT df.status_by_label('__nonexistent_label_xyz__') INTO missing_status;
IF missing_status IS NOT NULL THEN
RAISE EXCEPTION 'TEST FAILED: status_by_label should return NULL for unknown label, got %',
missing_status;
END IF;

RAISE NOTICE 'TEST PASSED: monitoring';
END $$;
Expand Down
Loading