diff --git a/backend/migrations/20261009173546_agent_owner_prefix_index.down.sql b/backend/migrations/20261009173546_agent_owner_prefix_index.down.sql new file mode 100644 index 0000000000..d3e9318571 --- /dev/null +++ b/backend/migrations/20261009173546_agent_owner_prefix_index.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS idx_resource_agent_owner_prefix; diff --git a/backend/migrations/20261009173546_agent_owner_prefix_index.up.sql b/backend/migrations/20261009173546_agent_owner_prefix_index.up.sql new file mode 100644 index 0000000000..be1fd5813f --- /dev/null +++ b/backend/migrations/20261009173546_agent_owner_prefix_index.up.sql @@ -0,0 +1,7 @@ +-- Backs the agent branch of /runnables/list and /runnables/counts, which reach the +-- `ai_agent` rows of `resource` by workspace and byte-ordered path prefix. Partial, so +-- neither walks the workspace's other resources (state, cache, ...). +-- Created CONCURRENTLY via the OVERRIDDEN_MIGRATIONS rewrite in windmill-api/src/db.rs. +CREATE INDEX IF NOT EXISTS idx_resource_agent_owner_prefix + ON resource (workspace_id, path text_pattern_ops) + WHERE resource_type = 'ai_agent'; diff --git a/backend/windmill-api/src/db.rs b/backend/windmill-api/src/db.rs index 9619e96ebc..a6a8ee2469 100644 --- a/backend/windmill-api/src/db.rs +++ b/backend/windmill-api/src/db.rs @@ -111,6 +111,9 @@ lazy_static::lazy_static! { (20260909163047, include_str!( "../../migrations/20260909163047_workspace_delete_cascade_indexes.up.sql" ).replace("CREATE INDEX", "CREATE INDEX CONCURRENTLY").replace("DROP INDEX", "DROP INDEX CONCURRENTLY")), + (20261009173546, include_str!( + "../../migrations/20261009173546_agent_owner_prefix_index.up.sql" + ).replace("CREATE INDEX", "CREATE INDEX CONCURRENTLY")), ].into_iter().collect(); } diff --git a/backend/windmill-api/src/runnables.rs b/backend/windmill-api/src/runnables.rs index e45b06bdcb..c97bdcd120 100644 --- a/backend/windmill-api/src/runnables.rs +++ b/backend/windmill-api/src/runnables.rs @@ -16,7 +16,8 @@ //! Efficiency: each kind is a UNION ALL branch ordered by an index on //! `(workspace_id, archived, )` (created_at / edited_at, or the lowered //! summary-or-path expression for name orders); Postgres merges the ordered -//! branches and stops at the page limit. Pagination is keyset — a +//! branches and stops at the page limit. Agents, which are few, are instead +//! reached through the partial `idx_resource_agent_owner_prefix` and sorted whole. Pagination is keyset — a //! `(sort_key, path, kind, tiebreak)` cursor, where `tiebreak` (a script's hash, //! 0 for flow/app) is a stable final key that keeps the order total even when rows //! tie on (sort_key, path, kind) — so deep pages don't re-scan. Visibility is @@ -858,7 +859,7 @@ struct OwnerCount { /// `owner/` is >= `owner || '/'` and, since '/' (0x2F) is immediately /// followed by '0' (0x30), < `owner || '0'`. `~>=~` / `~<~` are the /// text_pattern_ops operators, which is what lets `idx__owner_prefix` -/// answer the count with an index-only scan — the default opclass sorts by the +/// (`idx_resource_agent_owner_prefix` for agents) answer the count with an index-only scan — the default opclass sorts by the /// database collation and cannot serve a byte-prefix range. fn owner_prefix_range(alias: &str) -> String { format!("{alias}.path ~>=~ (o.owner || '/') AND {alias}.path ~<~ (o.owner || '0')")