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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ Pre-1.0 note: while `pg_durable` is in major version `0`, minor releases may inc

## [0.2.4] - Unreleased

### Added

- **`df.list_instances_paginated()`:** new monitoring function that lists instances with keyset (cursor) pagination, ordered by `(created_at DESC, id DESC)`. It returns the page rows plus a `total_count` and a `next_cursor` (pass the previous page's `next_cursor` as `after_cursor`, or `NULL` for the first page). A new `idx_instances_created_at_desc_id` index keeps paging an index scan.

### Changed

- **`df.list_instances()`:** now also returns the `created_at` and `completed_at` timestamps for each instance, and orders by `(created_at DESC, id DESC)` for a stable total order.
- **`df.grant_usage()` / `df.revoke_usage()`:** dropped the explicit per-function `EXECUTE` allowlist. Schema `USAGE` on `df` is the real access gate for ordinary `df.*` functions, so the helpers now grant/revoke schema `USAGE`, the table privileges, and `EXECUTE` only on the sensitive functions (`df.http`, `df.grant_usage`, `df.revoke_usage`). Function signatures are unchanged and existing privileges are unaffected (#242).

### Removed
Expand Down
26 changes: 25 additions & 1 deletion USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1433,7 +1433,31 @@ SELECT * FROM df.list_instances('failed');
SELECT * FROM df.list_instances(NULL, 10);
```

**Columns:** `instance_id`, `label`, `function_name`, `status`, `execution_count`, `output`
**Columns:** `instance_id`, `label`, `function_name`, `status`, `execution_count`, `output`, `created_at`, `completed_at`

Instances are ordered newest-first by `(created_at DESC, id DESC)`.

### List Instances with Pagination

For large instance counts, use cursor (keyset) pagination instead of a raw limit:

```sql
-- First page (no cursor)
SELECT * FROM df.list_instances_paginated(NULL, 100, NULL);

-- Filter by status, 50 per page
SELECT * FROM df.list_instances_paginated('completed', 50, NULL);

-- Next page: pass the previous page's next_cursor as after_cursor
SELECT * FROM df.list_instances_paginated(NULL, 100, '2026-06-23 12:00:00+00|a1b2c3d4');
```

**Columns:** `instance_id`, `label`, `function_name`, `status`, `execution_count`, `output`, `created_at`, `completed_at`, `total_count`, `next_cursor`

- `total_count` is the number of instances visible to the caller for the given `status_filter`.
- `next_cursor` is the value to pass as `after_cursor` for the next page; it is `NULL` once the last page has been returned.

Pages are ordered by `(created_at DESC, id DESC)` and backed by the `idx_instances_created_at_desc_id` index, so paging stays efficient regardless of offset.

### Instance Details

Expand Down
8 changes: 8 additions & 0 deletions docs/upgrade-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,14 @@ what the upgrade script handles, and any backward compatibility considerations.

### v0.2.3 → v0.2.4

#### Chronological instance listing — index, timestamp columns, cursor pagination
- **DDL change (df schema):** Adds the `idx_instances_created_at_desc_id` index on `df.instances(created_at DESC, id)`. Fresh installs create it in `src/lib.rs`; the upgrade script `sql/pg_durable--0.2.3--0.2.4.sql` creates it with `CREATE INDEX IF NOT EXISTS`. The resulting `pg_get_indexdef` is identical on both paths.
- **DDL change (df schema):** `df.list_instances(text, integer)` gains two output columns (`created_at`, `completed_at`). Because the return `TABLE` shape changed, the upgrade script `DROP`s and re-`CREATE`s the function (a `CREATE OR REPLACE` cannot change output columns). The function carries PostgreSQL's default PUBLIC `EXECUTE` and is referenced by no other object, so drop/recreate restores identical access. The argument signature `df.list_instances(text, integer)` is unchanged.
- **DDL change (df schema):** Adds `df.list_instances_paginated(text, integer, text)` returning the page rows plus `total_count` and `next_cursor`. Fresh installs create it from the generated function SQL (`src/monitoring.rs`); the upgrade script creates the matching binding to `list_instances_paginated_wrapper`.
- **Scenario A considerations:** Fresh-install and upgraded schemas both expose the new index, the extended `df.list_instances` return shape, and `df.list_instances_paginated`. The hand-written upgrade DDL mirrors the pgrx-generated install DDL (same arg types, defaults, and return columns), so the equivalence contract passes.
- **Scenario B1 considerations:** The new `.so` reads only `df.instances` columns (`id`, `label`, `status`, `created_at`, `completed_at`) that exist in all prior schemas in this provider line, so it runs against pre-0.2.4 schemas that have not applied `ALTER EXTENSION UPDATE`. The chronological index is a performance aid, not a correctness dependency — queries fall back to a sort when it is absent.
- **Scenario B2 considerations:** No data migration. Existing instances, nodes, and vars are untouched; the upgrade only adds one index and two function bindings.

#### Simplify `df.grant_usage()` — drop the explicit function allowlist
- **DDL change (df schema):** `df.grant_usage()` no longer loops over a hard-coded `func_sigs` array issuing `GRANT EXECUTE` per function. Fresh installs (`src/lib.rs`) and the upgrade script (`sql/pg_durable--0.2.3--0.2.4.sql`) both `CREATE OR REPLACE` the function with a body that grants `USAGE ON SCHEMA df` plus the table privileges, and conditionally grants `df.http()` / the admin helpers. The signature `df.grant_usage(text, boolean, boolean)` is unchanged.
- **DDL change (df schema):** `df.revoke_usage()` is made symmetric with the new `grant_usage()`. It no longer loops over every `df.*` function in `pg_proc` issuing `REVOKE EXECUTE` (which, post-simplification, only produced "no privileges could be revoked" warnings since ordinary functions are never granted per-function EXECUTE). The new body revokes only what `grant_usage()` grants: schema `USAGE`, EXECUTE on the sensitive functions (`df.http`, `df.grant_usage`, `df.revoke_usage`), and the table privileges. The signature `df.revoke_usage(text)` is unchanged.
Expand Down
9 changes: 9 additions & 0 deletions scripts/run-pgspot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ PGSPOT_ALLOW=(
# object, so this is safe. Scoped to these two functions only.
'^PS002: Unsafe function creation: df\.grant_usage\(p_role text,include_http boolean,with_grant boolean\) at line [0-9]+$'
'^PS002: Unsafe function creation: df\.revoke_usage\(p_role text\) at line [0-9]+$'
# Upgrade scripts create idx_instances_created_at_desc_id on df.instances. As
# with PS002 above, pgspot flags PS014 only because a standalone upgrade script
# has no `CREATE SCHEMA df` to prove df.instances is extension-owned (the
# install SQL does, so the same CREATE INDEX is not flagged there). The index
# is a plain btree on the columns (created_at DESC, id) with the default
# operator class -- it references no user-defined function or operator whose
# resolution could be hijacked via search_path -- so it is safe. Scoped to this
# one index only.
'^PS014: Unsafe index creation: idx_instances_created_at_desc_id at line [0-9]+$'
)

# Whole codes to suppress globally (pgspot --ignore). Prefer PGSPOT_ALLOW. Empty.
Expand Down
62 changes: 62 additions & 0 deletions sql/pg_durable--0.2.3--0.2.4.sql
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,65 @@ CREATE FUNCTION df."await_instance"(
STRICT
LANGUAGE c
AS 'MODULE_PATHNAME', 'await_instance_wrapper';

-- ============================================================================
-- Chronological instance listing: index + timestamp columns + cursor pagination.
--
-- Adds a chronological keyset index, extends df.list_instances() with the
-- created_at / completed_at timestamps, and introduces df.list_instances_paginated()
-- for cursor-based paging. Fresh 0.2.4 installs create all three via src/lib.rs
-- (the index) and the generated function SQL (src/monitoring.rs); this section
-- brings pre-existing installs to the same shape (Scenario A).
-- No data migration is required (Scenario B2); the new .so reads the same
-- df.instances columns that already exist in all prior schemas (Scenario B1).
-- ============================================================================

-- Index for efficient chronological (keyset) listing of instances. Matches the
-- ORDER BY (created_at DESC, id DESC) used by both listing functions so paging
-- stays an index scan instead of a sort.
CREATE INDEX IF NOT EXISTS idx_instances_created_at_desc_id
ON df.instances(created_at DESC, id);

-- df.list_instances() gains created_at / completed_at output columns. The return
-- TABLE shape changed, so the function must be dropped and recreated rather than
-- CREATE OR REPLACE'd. It carries PostgreSQL's default PUBLIC EXECUTE and is not
-- referenced by any other object, so the drop/recreate restores identical access.
DROP FUNCTION IF EXISTS df."list_instances"(TEXT, INT);
CREATE FUNCTION df."list_instances"(
"status_filter" TEXT DEFAULT NULL,
"limit_count" INT DEFAULT 100
) RETURNS TABLE (
"instance_id" TEXT,
"label" TEXT,
"function_name" TEXT,
"status" TEXT,
"execution_count" bigint,
"output" TEXT,
"created_at" timestamp with time zone,
"completed_at" timestamp with time zone
)
LANGUAGE c
AS 'MODULE_PATHNAME', 'list_instances_wrapper';

-- df.list_instances_paginated(): keyset (cursor) pagination ordered by
-- (created_at DESC, id DESC), returning the page rows plus total_count and the
-- next_cursor to fetch the following page. Bound to the C symbol
-- list_instances_paginated_wrapper exported by the new .so.
CREATE FUNCTION df."list_instances_paginated"(
"status_filter" TEXT DEFAULT NULL,
"limit_count" INT DEFAULT 100,
"after_cursor" TEXT DEFAULT NULL
) RETURNS TABLE (
"instance_id" TEXT,
"label" TEXT,
"function_name" TEXT,
"status" TEXT,
"execution_count" bigint,
"output" TEXT,
"created_at" timestamp with time zone,
"completed_at" timestamp with time zone,
"total_count" bigint,
"next_cursor" TEXT
)
LANGUAGE c
AS 'MODULE_PATHNAME', 'list_instances_paginated_wrapper';
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ COMMENT ON COLUMN df.instances.submitted_by IS
-- Index for finding pending instances
CREATE INDEX idx_instances_status ON df.instances(status);

-- Index for efficient chronological (keyset) listing of instances
CREATE INDEX idx_instances_created_at_desc_id ON df.instances(created_at DESC, id);

-- Index for finding nodes by instance
CREATE INDEX idx_nodes_instance ON df.nodes(instance_id);

Expand Down
Loading
Loading