diff --git a/db/migrations/0003_cleanup_indexes.sql b/db/migrations/0003_cleanup_indexes.sql new file mode 100644 index 0000000..29f7968 --- /dev/null +++ b/db/migrations/0003_cleanup_indexes.sql @@ -0,0 +1,17 @@ +CREATE INDEX IF NOT EXISTS idx_sessions_state_expires + ON sessions (state, expires_at); + +CREATE INDEX IF NOT EXISTS idx_grants_session_id + ON grants (session_id); + +CREATE INDEX IF NOT EXISTS idx_grants_state_expires + ON grants (state, expires_at); + +CREATE INDEX IF NOT EXISTS idx_approvals_state_expires + ON approvals (state, expires_at); + +CREATE INDEX IF NOT EXISTS idx_artifacts_state_expires + ON artifacts (state, expires_at); + +CREATE INDEX IF NOT EXISTS idx_audit_events_created_at + ON audit_events (created_at); diff --git a/internal/store/postgres/repository.go b/internal/store/postgres/repository.go index 30f838c..7c0faae 100644 --- a/internal/store/postgres/repository.go +++ b/internal/store/postgres/repository.go @@ -137,12 +137,16 @@ func (r *Repository) GetSession(ctx context.Context, sessionID string) (*core.Se return &session, nil } +const maxGrantsBySessionLookup = 10000 + func (r *Repository) ListGrantsBySession(ctx context.Context, sessionID string) ([]*core.Grant, error) { rows, err := r.db.Query(ctx, ` SELECT id, tenant_id, session_id, tool, capability, resource_ref, delivery_mode, connector_kind, approval_id, artifact_ref, state, requested_ttl_seconds, effective_ttl_seconds, expires_at, created_at, reason FROM grants WHERE session_id = $1 - `, sessionID) + ORDER BY created_at ASC, id ASC + LIMIT $2 + `, sessionID, maxGrantsBySessionLookup) if err != nil { return nil, err } diff --git a/internal/store/postgres/repository_test.go b/internal/store/postgres/repository_test.go index 6a23003..87c9474 100644 --- a/internal/store/postgres/repository_test.go +++ b/internal/store/postgres/repository_test.go @@ -110,3 +110,41 @@ func TestRepository_UseArtifactMarksSingleUse(t *testing.T) { t.Fatalf("artifact state = %q, want %q", artifact.State, core.ArtifactStateUsed) } } + +func TestRepository_ListGrantsBySessionIsBounded(t *testing.T) { + t.Parallel() + + mock, err := pgxmock.NewPool() + if err != nil { + t.Fatalf("pgxmock.NewPool() error = %v", err) + } + defer mock.Close() + + repo := postgres.NewRepository(mock) + createdAt := time.Date(2026, 4, 15, 18, 0, 0, 0, time.UTC) + expiresAt := createdAt.Add(30 * time.Minute) + + mock.ExpectQuery("SELECT id, tenant_id, session_id, tool, capability, resource_ref, delivery_mode, connector_kind, approval_id, artifact_ref, state, requested_ttl_seconds, effective_ttl_seconds, expires_at, created_at, reason\\s+FROM grants\\s+WHERE session_id = \\$1\\s+ORDER BY created_at ASC, id ASC\\s+LIMIT \\$2"). + WithArgs("sess_abc", pgxmock.AnyArg()). + WillReturnRows(pgxmock.NewRows([]string{ + "id", "tenant_id", "session_id", "tool", "capability", "resource_ref", "delivery_mode", "connector_kind", "approval_id", "artifact_ref", "state", "requested_ttl_seconds", "effective_ttl_seconds", "expires_at", "created_at", "reason", + }).AddRow( + "gr_123", "t_acme", "sess_abc", "github", "repo.read", "repo:evalops/asb", "direct", "github", nil, nil, + string(core.GrantStateIssued), int32(300), int32(300), expiresAt, createdAt, "cleanup", + )) + + grants, err := repo.ListGrantsBySession(context.Background(), "sess_abc") + if err != nil { + t.Fatalf("ListGrantsBySession() error = %v", err) + } + if len(grants) != 1 { + t.Fatalf("len(grants) = %d, want 1", len(grants)) + } + if grants[0].ID != "gr_123" { + t.Fatalf("grant id = %q, want %q", grants[0].ID, "gr_123") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("ExpectationsWereMet() error = %v", err) + } +}