Skip to content

dashboard query optimizations#2193

Merged
akshaydeo merged 1 commit intomainfrom
03-21-dashboard_query_optimizations
Mar 22, 2026
Merged

dashboard query optimizations#2193
akshaydeo merged 1 commit intomainfrom
03-21-dashboard_query_optimizations

Conversation

@akshaydeo
Copy link
Copy Markdown
Contributor

Summary

Briefly explain the purpose of this PR and the problem it solves.

Changes

  • What was changed and why
  • Any notable design decisions or trade-offs

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (Next.js)
  • Docs

How to test

Describe the steps to validate this change. Include commands and expected outcomes.

# Core/Transports
go version
go test ./...

# UI
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build

If adding new configs or environment variables, document them here.

Screenshots/Recordings

If UI changes, add before/after screenshots or short clips.

Breaking changes

  • Yes
  • No

If yes, describe impact and migration instructions.

Related issues

Link related issues and discussions. Example: Closes #123

Security considerations

Note any security implications (auth, secrets, PII, sandboxing, etc.).

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 20, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7867977e-25c6-44c6-adb8-a490d1fbd8c5

📥 Commits

Reviewing files that changed from the base of the PR and between 4fbb670 and 0a443f4.

📒 Files selected for processing (8)
  • framework/go.mod
  • framework/logstore/matviews.go
  • framework/logstore/migrations.go
  • framework/logstore/postgres.go
  • framework/logstore/rdb.go
  • framework/logstore/sqlite.go
  • ui/app/workspace/dashboard/page.tsx
  • ui/components/filters/filterPopover.tsx

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Adds materialized-view-backed analytics for faster, pre-aggregated dashboard metrics and distinct filter data.
  • Performance

    • Dashboard query paths now delegate to pre-aggregated data where applicable; background refresher keeps analytics up to date.
    • Concurrent pagination/count fetching improves log search responsiveness.
  • Bug Fixes

    • Fixed filter selection and badge counts when options share duplicate display names.
  • User Experience

    • Dashboard tabs load data on-demand with background pre-warming for quicker tab switches.

Walkthrough

Adds PostgreSQL materialized views and refresh lifecycle plus advisory-locked periodic refresher; routes eligible dashboard analytics queries to mat-views; implements mat-view-backed aggregation/read helpers and eligibility checks; introduces concurrent COUNT + page fetch; frontend tab-scoped lazy loaders and filter-name deduplication; promotes golang.org/x/sync to a direct dependency.

Changes

Cohort / File(s) Summary
Go Module
framework/go.mod
Promoted golang.org/x/sync v0.20.0 from indirect to direct dependency.
Materialized Views & Read Paths
framework/logstore/matviews.go
New Postgres-only DDL for mv_logs_hourly and mv_logs_filterdata, unique indexes, ensure/refresh lifecycle, advisory-lock guarded refresher, mat-view eligibility checks, filter→matview translation, and a suite of mat-view-backed aggregation/read helpers (stats, histograms, rankings, distinct lists).
Orchestration & Migrations
framework/logstore/postgres.go, framework/logstore/migrations.go
Added matviewRefreshAdvisoryLockKey; startup now ensures matviews, runs initial refresh, and starts periodic refresher; reordered dashboard/index goroutine to run matview path first; minor whitespace normalization in migrations.
Query Routing & Concurrency
framework/logstore/rdb.go
SearchLogs now runs COUNT and page fetch concurrently via errgroup; multiple stats/histogram/ranking/distinct endpoints route to mat-view implementations when eligible; histogram mat-view branch requires bucketSizeSeconds >= 3600; gorm.ErrRecordNotFound treated as non-fatal for page queries.
SQLite Minor
framework/logstore/sqlite.go
Whitespace-only: inserted blank line after migration error-check.
Dashboard Frontend Loading
ui/app/workspace/dashboard/page.tsx
Split overview/provider fetching into tab-scoped loaders; per-tab fetched/loading/gen refs with ensure*DataLoaded; reset flags on filter changes; active-tab-only immediate load plus 150ms background warm-up for other tabs.
Filter UI Deduplication & Selection
ui/components/filters/filterPopover.tsx
Group options by display name -> ids[]; resolveValuesForCategory returns arrays; selecting a displayed name toggles all backing IDs; isSelected requires all matched IDs; badge counts show unique visible names (deduped).

Sequence Diagram(s)

sequenceDiagram
    participant App as Dashboard App
    participant Store as RDBLogStore
    participant DB as PostgreSQL
    participant BG as MatView Refresher

    Note over App,Store: Startup
    App->>Store: Initialize LogStore
    Store->>DB: ensureMatViews() (CREATE MATERIALIZED VIEW / INDEX)
    Store->>DB: refreshMatViews() (initial REFRESH CONCURRENTLY with advisory lock)
    Store->>BG: startMatViewRefresher(interval)

    Note over App,Store: Query Time
    App->>Store: GetStats(filters)
    alt canUseMatView(filters) && dialect==postgres
        Store->>DB: query mv_logs_hourly / mv_logs_filterdata
        DB-->>Store: aggregated results
    else
        Store-->>DB: query raw log tables
        DB-->>Store: raw results
    end
    Store-->>App: Stats response

    Note over BG,DB: Background Maintenance
    BG->>DB: refreshMatViews() (periodic, advisory-locked)
    DB->>DB: REFRESH MATERIALIZED VIEW CONCURRENTLY
Loading
sequenceDiagram
    participant User as Dashboard User
    participant Page as Dashboard Page
    participant API as LogStore API

    User->>Page: Open Dashboard
    Page->>Page: init per-tab refs
    Page->>API: fetchOverviewData()
    API-->>Page: Overview payloads
    Page->>User: Render Overview

    User->>Page: Switch to Provider tab
    Page->>Page: reset/load flags on filter change
    Page->>API: fetchProviderData()
    API-->>Page: Provider payloads
    Page->>User: Render Provider tab

    Note over Page: Background warm-up
    Page->>Page: wait 150ms
    Page->>API: warm other tabs in background
    API-->>Page: cached responses
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped through schemas, indices in tow,
Views wake hourly where log-rivers flow.
Tabs fetch only what each view will need,
Names grouped tight so duplicates recede.
The refresher hums, locked, steady and slow.

🚥 Pre-merge checks | ✅ 1 | ❌ 4

❌ Failed checks (3 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is entirely a blank template with no actual content filled in; all sections (Summary, Changes, Type of change, Affected areas, etc.) lack concrete implementation details. Complete the description by filling in each section: explain the materialized views approach, list all file modifications, select appropriate change type and affected areas, provide testing steps, and clarify any breaking changes or security considerations.
Linked Issues check ⚠️ Warning The linked issue #123 requires File API support for providers (OpenAI, Anthropic), but the PR changes implement PostgreSQL materialized views and dashboard query optimizations; no File API implementation is present.
Out of Scope Changes check ⚠️ Warning The entire PR appears out of scope: it implements dashboard query optimizations and materialized views, which are unrelated to the linked issue #123 that requires File API support for providers.
Title check ❓ Inconclusive The title 'dashboard query optimizations' is vague and generic; it does not convey specific details about materialized views, the refactored fetching logic, or filter deduplication. Provide a more descriptive title that highlights the main optimization, such as 'Add PostgreSQL materialized views for dashboard analytics queries' or 'Optimize dashboard queries with materialized views and tab-specific data fetching'.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 03-21-dashboard_query_optimizations

Comment @coderabbitai help to get the list of available commands and usage tips.

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Copy Markdown
Contributor Author

akshaydeo commented Mar 20, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@akshaydeo akshaydeo force-pushed the 03-21-dashboard_query_optimizations branch from dc92c0b to 61f5fe3 Compare March 21, 2026 08:27
@akshaydeo akshaydeo marked this pull request as ready for review March 21, 2026 08:28
@akshaydeo akshaydeo force-pushed the 03-21-dashboard_query_optimizations branch from 61f5fe3 to 06df0f6 Compare March 21, 2026 08:31
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
ui/app/workspace/dashboard/page.tsx (1)

279-381: ⚠️ Potential issue | 🟠 Major

The generation refs no longer stop stale responses from painting old data.

gen is only checked when flipping the *FetchedRef / *LoadingRef flags. The awaited fetchers still call setHistogramData, setProviderCostData, setMcpHistogramData, setRankingsData, etc. unconditionally, so a slower request for old filters can resolve last and overwrite the newer selection.

Also applies to: 400-446

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/dashboard/page.tsx` around lines 279 - 381, The fetch
helpers (fetchOverviewData, fetchProviderData, fetchMcpData, fetchRankingsData)
unconditionally call set*Data after awaiting concurrent requests so a stale
slower response can overwrite newer data; capture the current generation
token/ref (the same `gen` checked when flipping the *FetchedRef/*LoadingRef
flags) at the start of each fetch, and before calling each set*Data or clearing
the loading flag verify the captured gen still matches the latest gen (or that
the corresponding isCurrent helper returns true); only apply state updates when
the gen matches and move loading flag clearing into a finally or guarded block
so stale results cannot paint old data.
framework/logstore/postgres.go (1)

80-89: ⚠️ Potential issue | 🟠 Major

This hard version gate breaks backward compatibility with PostgreSQL < 16 without a migration path.

The constructor now rejects PG15 and older at startup, but the logstore migrations still carry a PG<16 fallback path (Go-based isValidJSON() validation instead of server-side IS NOT JSON OBJECT). That fallback code is now unreachable—existing deployments on PG15 cannot upgrade to this version without first upgrading PostgreSQL itself. If the codebase is already requiring PG16+ as a breaking change across all services, document it explicitly; otherwise, this needs a phased rollout or a backward-compatible flag to allow PG<16 deployments additional time.

🧹 Nitpick comments (1)
framework/logstore/rdb.go (1)

409-413: Unnecessary ErrRecordNotFound check after Find().

GORM's Find() returns an empty slice and nil error when no records match—it does not return gorm.ErrRecordNotFound. This check is dead code.

Consider removing it for clarity:

♻️ Proposed simplification
-		err := dataQuery.Find(&logs).Error
-		if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
-			return nil
-		}
-		return err
+		return dataQuery.Find(&logs).Error
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/logstore/rdb.go` around lines 409 - 413, The code checks for
gorm.ErrRecordNotFound after calling dataQuery.Find(&logs).Error, but GORM's
Find returns an empty slice and nil error when nothing matches, so the
errors.Is(err, gorm.ErrRecordNotFound) branch is dead code; remove that
conditional and simply propagate the error from dataQuery.Find(&logs).Error
(e.g., if err != nil { return err } or just return err) and keep the rest of the
surrounding logic unchanged—look for the dataQuery.Find(&logs).Error call and
the logs variable to locate the exact spot.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@framework/logstore/matviews.go`:
- Around line 102-134: refreshMatViews is being executed concurrently by every
replica; wrap the refresh sequence in the same advisory-lock pattern used
elsewhere (the ensureMatViews/create logic) so only one instance refreshes at a
time: acquire the Postgres advisory lock (using the same lock key/value and
try-lock semantics as the existing pattern), check success and skip refresh if
lock not acquired, defer releasing the advisory lock, then call db.Exec("REFRESH
MATERIALIZED VIEW CONCURRENTLY ...") for each view; update startMatViewRefresher
to rely on the modified refreshMatViews (no extra changes needed) so refreshes
across replicas are serialized.
- Around line 18-21: The mvLogsHourlyDDL materialized view currently stores
exact p90/p95/p99 per hour which cannot be re-aggregated by weighted average;
remove the per-hour percentile columns from mvLogsHourlyDDL and from the other
similar materialized view(s) in the provider-latency path, and instead either
(a) store a mergeable sketch/histogram column (e.g., t-digest/tdigest,
HDRHistogram, or a serialized histogram type) such as latency_histogram that can
be unioned and re-queried for percentiles, or (b) stop precomputing percentiles
and run percentile queries directly against raw logs; update the DDL and any
downstream code that reads p90/p95/p99 to use the new histogram column and
aggregation functions (or raw-log queries) so multi-hour percentile queries are
mathematically correct.
- Around line 136-160: canUseMatView currently allows second-precision start/end
times which mismatch applyMatViewFilters' hour-truncation and causes inflated
metrics from mv_logs_hourly; update canUseMatView to only return true when the
time window is hour-aligned (i.e., if f.StartTime and f.EndTime are non-nil they
must have minutes, seconds, and nanoseconds == 0 and EndTime >= StartTime),
otherwise fall back to raw logs; reference the canUseMatView(SearchFilters)
function, the StartTime/EndTime fields, and mv_logs_hourly/applyMatViewFilters
when implementing the guard.
- Around line 56-80: The unique index mv_logs_filterdata_uniq does not include
the name columns causing REFRESH MATERIALIZED VIEW CONCURRENTLY to fail when an
ID appears with different names; update the mvLogsFilterdataUniqueIdx definition
to add selected_key_name, virtual_key_name, and routing_rule_name (matching the
SELECT DISTINCT columns in mvLogsFilterdataDDL) to the index so the uniqueness
covers both IDs and their corresponding name fields.

In `@ui/app/workspace/dashboard/page.tsx`:
- Around line 467-487: The lazy-load and warm-up useEffect blocks check
urlState.tab against the wrong literal "providers" causing the Provider Usage
tab to be treated as inactive; update both effects to use the actual tab key
"provider-usage" wherever they compare tab (i.e., replace the checks comparing
tab === "providers" and tab !== "providers" with "provider-usage") so
ensureProviderDataLoaded is called eagerly when the Provider Usage tab is active
and excluded from warm-up when it is the current tab.

In `@ui/components/filters/filterPopover.tsx`:
- Around line 39-50: The current dedup function (used when building
FILTER_OPTIONS for "Selected Keys", "Virtual Keys", and "Routing Rules")
collapses multiple entries with the same name into a single label, but
resolveValueForCategory still expects to map a label to one ID; to fix, stop
collapsing rows to a single name: change FILTER_OPTIONS to keep items keyed by
their unique IDs (e.g., use the original
availableSelectedKeys/availableVirtualKeys/availableRoutingRules arrays of
{id,name}) or else change the mapping so each label in FILTER_OPTIONS maps to an
array of IDs and update resolveValueForCategory to toggle all IDs for that
label; update references to dedup, FILTER_OPTIONS, and resolveValueForCategory
accordingly so duplicate names remain reachable via their distinct IDs.

---

Outside diff comments:
In `@ui/app/workspace/dashboard/page.tsx`:
- Around line 279-381: The fetch helpers (fetchOverviewData, fetchProviderData,
fetchMcpData, fetchRankingsData) unconditionally call set*Data after awaiting
concurrent requests so a stale slower response can overwrite newer data; capture
the current generation token/ref (the same `gen` checked when flipping the
*FetchedRef/*LoadingRef flags) at the start of each fetch, and before calling
each set*Data or clearing the loading flag verify the captured gen still matches
the latest gen (or that the corresponding isCurrent helper returns true); only
apply state updates when the gen matches and move loading flag clearing into a
finally or guarded block so stale results cannot paint old data.

---

Nitpick comments:
In `@framework/logstore/rdb.go`:
- Around line 409-413: The code checks for gorm.ErrRecordNotFound after calling
dataQuery.Find(&logs).Error, but GORM's Find returns an empty slice and nil
error when nothing matches, so the errors.Is(err, gorm.ErrRecordNotFound) branch
is dead code; remove that conditional and simply propagate the error from
dataQuery.Find(&logs).Error (e.g., if err != nil { return err } or just return
err) and keep the rest of the surrounding logic unchanged—look for the
dataQuery.Find(&logs).Error call and the logs variable to locate the exact spot.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0dddbadb-b263-46e6-be17-1e73f2927f99

📥 Commits

Reviewing files that changed from the base of the PR and between 3d406f8 and 06df0f6.

📒 Files selected for processing (8)
  • framework/go.mod
  • framework/logstore/matviews.go
  • framework/logstore/migrations.go
  • framework/logstore/postgres.go
  • framework/logstore/rdb.go
  • framework/logstore/sqlite.go
  • ui/app/workspace/dashboard/page.tsx
  • ui/components/filters/filterPopover.tsx

Comment thread framework/logstore/matviews.go
Comment thread framework/logstore/matviews.go
Comment thread framework/logstore/matviews.go
Comment thread framework/logstore/matviews.go
Comment thread ui/app/workspace/dashboard/page.tsx
Comment thread ui/components/filters/filterPopover.tsx
@akshaydeo akshaydeo force-pushed the 03-21-dashboard_query_optimizations branch 2 times, most recently from e9ec7d7 to c4bcb14 Compare March 21, 2026 11:06
@akshaydeo akshaydeo force-pushed the 03-21-go_mod_fixes_-_updates_dashboard_migrations branch from 3d406f8 to 5fe923c Compare March 21, 2026 11:06
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@framework/logstore/matviews.go`:
- Around line 745-748: The current prevMap uses a string key built by
concatenating r.Model + ":" + r.Provider which can collide if fields contain
":"; define a small composite key type (e.g., modelProvKey struct{ Model,
Provider string }) and change prevMap to map[modelProvKey]int, populate it with
prevMap[modelProvKey{Model: r.Model, Provider: r.Provider}] = i in the loop that
builds prevMap, and update all lookups (the code that reads prevMap later,
including the similar usage at the other occurrence) to use the same composite
key type instead of the concatenated string.
- Around line 260-268: The matview-based histogram functions
(getHistogramFromMatView, getTokenHistogramFromMatView,
getCostHistogramFromMatView, getModelHistogramFromMatView,
getLatencyHistogramFromMatView, getProviderCostHistogramFromMatView,
getProviderTokenHistogramFromMatView, getProviderLatencyHistogramFromMatView)
currently re-bucket hourly materialized data into arbitrary bucketSizeSeconds
which breaks sub-hour resolutions; add a guard at the start of each of these
functions to reject bucketSizeSeconds < 3600 (return an error) or,
alternatively, detect that case and route to the raw-log query path (the
existing non-matview logic) so finer resolutions use raw logs instead of
mv_logs_hourly, ensuring you check and handle bucketSizeSeconds before building
the SQL SELECT/CAST re-bucketing expression.
- Around line 106-115: refreshMatViews currently uses separate db.WithContext
calls so the pg_try_advisory_lock and pg_advisory_unlock may run on different
pooled connections; change refreshMatViews to obtain a dedicated *sql.Conn from
db (following the advisoryLock pattern in migrations.go) and run the acquire
(pg_try_advisory_lock), the refresh work, and the release (pg_advisory_unlock)
on that same connection so the session-scoped lock is correctly held and
released; also check and handle the error/result from pg_advisory_unlock (do not
ignore its return value) and reference matviewRefreshAdvisoryLockKey in the
queries.

In `@ui/components/filters/filterPopover.tsx`:
- Around line 56-59: The active-filter badge currently counts raw ID arrays
(selected_key_ids, virtual_key_ids, routing_rule_ids) which inflates the visible
count; change the badge/count logic to map those ID arrays to their visible
names using the same lookups used to render options (availableSelectedKeys,
availableVirtualKeys, availableRoutingRules / dedup), collapse/uniq by the
visible label, and then sum those unique labels for the badge. Update both
places noted (the badge computation and the other count usage around the block
that renders options) so the badge uses deduped visible labels instead of raw ID
length.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e12e2cb6-df3b-49f0-aeae-a6e6e3284803

📥 Commits

Reviewing files that changed from the base of the PR and between e9ec7d7 and c4bcb14.

📒 Files selected for processing (8)
  • framework/go.mod
  • framework/logstore/matviews.go
  • framework/logstore/migrations.go
  • framework/logstore/postgres.go
  • framework/logstore/rdb.go
  • framework/logstore/sqlite.go
  • ui/app/workspace/dashboard/page.tsx
  • ui/components/filters/filterPopover.tsx
✅ Files skipped from review due to trivial changes (3)
  • framework/logstore/sqlite.go
  • framework/go.mod
  • framework/logstore/rdb.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • framework/logstore/postgres.go
  • ui/app/workspace/dashboard/page.tsx
  • framework/logstore/migrations.go

Comment thread framework/logstore/matviews.go Outdated
Comment thread framework/logstore/matviews.go Outdated
Comment thread ui/components/filters/filterPopover.tsx
@akshaydeo akshaydeo force-pushed the 03-21-dashboard_query_optimizations branch from c4bcb14 to dd966fd Compare March 21, 2026 11:43
@akshaydeo akshaydeo force-pushed the 03-21-go_mod_fixes_-_updates_dashboard_migrations branch from 5fe923c to da62438 Compare March 21, 2026 11:43
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
framework/logstore/matviews.go (1)

106-123: ⚠️ Potential issue | 🟠 Major

Session-scoped advisory lock may leak due to connection pooling.

pg_try_advisory_lock and pg_advisory_unlock are session-scoped, but each db.WithContext(ctx) call can acquire a different connection from GORM's pool. If the acquire (line 109) and unlock (line 115) run on different connections, the original lock is never released and all future refresh attempts will silently skip forever.

Use a dedicated *sql.Conn (similar to the advisoryLock pattern in migrations.go) to pin acquire, refresh, and unlock to the same database session.

🔧 Suggested fix using dedicated connection
 func refreshMatViews(ctx context.Context, db *gorm.DB) error {
-	// Try to acquire advisory lock; skip refresh if another replica holds it.
-	var acquired bool
-	if err := db.WithContext(ctx).Raw("SELECT pg_try_advisory_lock(?)", matviewRefreshAdvisoryLockKey).Row().Scan(&acquired); err != nil {
-		return fmt.Errorf("failed to try advisory lock for matview refresh: %w", err)
+	sqlDB, err := db.DB()
+	if err != nil {
+		return fmt.Errorf("failed to get sql.DB: %w", err)
 	}
-	if !acquired {
-		return nil // another replica is refreshing
+	conn, err := sqlDB.Conn(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to get dedicated connection for matview refresh: %w", err)
 	}
-	defer db.WithContext(ctx).Exec("SELECT pg_advisory_unlock(?)", matviewRefreshAdvisoryLockKey)
+	defer conn.Close()
+
+	// Try to acquire advisory lock; skip refresh if another replica holds it.
+	var acquired bool
+	if err := conn.QueryRowContext(ctx, "SELECT pg_try_advisory_lock($1)", matviewRefreshAdvisoryLockKey).Scan(&acquired); err != nil {
+		return fmt.Errorf("failed to try advisory lock for matview refresh: %w", err)
+	}
+	if !acquired {
+		return nil // another replica is refreshing
+	}
+	defer func() {
+		_, _ = conn.ExecContext(ctx, "SELECT pg_advisory_unlock($1)", matviewRefreshAdvisoryLockKey)
+	}()
 
 	for _, view := range []string{"mv_logs_hourly", "mv_logs_filterdata"} {
-		if err := db.WithContext(ctx).Exec("REFRESH MATERIALIZED VIEW CONCURRENTLY " + view).Error; err != nil {
+		if _, err := conn.ExecContext(ctx, "REFRESH MATERIALIZED VIEW CONCURRENTLY "+view); err != nil {
 			return fmt.Errorf("failed to refresh %s: %w", view, err)
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/logstore/matviews.go` around lines 106 - 123, refreshMatViews
currently uses session-scoped advisory locks across multiple db.WithContext
calls which may pick different pooled connections and leak the lock; change it
to obtain and use a single dedicated SQL connection for the entire operation
(pin the session) similar to the advisoryLock pattern in migrations.go: get the
underlying *sql.DB (via db.DB()), call .Conn(ctx) to get a *sql.Conn, run the
pg_try_advisory_lock(...) on that conn, run the REFRESH MATERIALIZED VIEW
CONCURRENTLY commands on that same conn, and finally run pg_advisory_unlock(...)
and close the *sql.Conn in a defer; ensure all Exec/Query calls reference that
*sql.Conn so the acquire, refresh, and unlock happen on the same session and
errors are handled/wrapped (refer to refreshMatViews and
matviewRefreshAdvisoryLockKey for placement).
🧹 Nitpick comments (1)
framework/logstore/matviews.go (1)

745-748: String key collision possible when model/provider contains ":".

The prevMap key is built as r.Model+":"+r.Provider, which can collide if either field contains :. For example, model "a:b" with provider "c" collides with model "a" and provider "b:c".

♻️ Use a struct key to avoid collision
+	type rankingKey struct {
+		model    string
+		provider string
+	}
-	prevMap := make(map[string]int, len(prevResults))
+	prevMap := make(map[rankingKey]int, len(prevResults))
 	for i, r := range prevResults {
-		prevMap[r.Model+":"+r.Provider] = i
+		prevMap[rankingKey{model: r.Model, provider: r.Provider}] = i
 	}
 ...
-		if idx, ok := prevMap[r.Model+":"+r.Provider]; ok {
+		if idx, ok := prevMap[rankingKey{model: r.Model, provider: r.Provider}]; ok {

Also applies to: 767-768

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/logstore/matviews.go` around lines 745 - 748, The map key
construction using string concatenation prevMap[r.Model+":"+r.Provider] can
produce collisions when Model or Provider contain ":"; replace the string key
with a composite struct key (e.g., type prevKey struct { Model, Provider string
}) and use prevMap[prevKey{r.Model, r.Provider}] when populating and looking up
entries (apply the same change to the other instance at the similar block around
the code that mirrors lines 767-768), ensuring all accesses use the new struct
key to avoid collisions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@framework/logstore/matviews.go`:
- Around line 564-576: The SELECT includes SUM(total_cached_read_tokens) AS
cached_read_tokens but the destination scan struct used to populate results
doesn't have a corresponding field, so add a field (e.g., CachedReadTokens int64
or appropriate type) to the result struct that backs the results variable and
ensure its db/json/gorm tag matches "cached_read_tokens" (or remove the SUM(...)
AS cached_read_tokens from the q.Select call if you intentionally don't need
that value); update the type/tag to match how other aggregated fields
(prompt_tokens/completion_tokens/total_tkns) are declared so the value is
captured into results after q.Select(...).Find(&results).
- Around line 260-268: getHistogramFromMatView must defend against sub-hour
buckets because mv_logs_hourly contains hourly rows; add a check at the start of
getHistogramFromMatView that if bucketSizeSeconds > 0 and bucketSizeSeconds <
3600 you immediately return an error (or normalize to 3600) instead of running
the SQL that rebuckets hourly data. Reference the function
getHistogramFromMatView and the mv_logs_hourly aggregation and validate
bucketSizeSeconds before the SELECT/CAST(...) SQL block so callers cannot pass 0
< bucketSizeSeconds < 3600.

---

Duplicate comments:
In `@framework/logstore/matviews.go`:
- Around line 106-123: refreshMatViews currently uses session-scoped advisory
locks across multiple db.WithContext calls which may pick different pooled
connections and leak the lock; change it to obtain and use a single dedicated
SQL connection for the entire operation (pin the session) similar to the
advisoryLock pattern in migrations.go: get the underlying *sql.DB (via db.DB()),
call .Conn(ctx) to get a *sql.Conn, run the pg_try_advisory_lock(...) on that
conn, run the REFRESH MATERIALIZED VIEW CONCURRENTLY commands on that same conn,
and finally run pg_advisory_unlock(...) and close the *sql.Conn in a defer;
ensure all Exec/Query calls reference that *sql.Conn so the acquire, refresh,
and unlock happen on the same session and errors are handled/wrapped (refer to
refreshMatViews and matviewRefreshAdvisoryLockKey for placement).

---

Nitpick comments:
In `@framework/logstore/matviews.go`:
- Around line 745-748: The map key construction using string concatenation
prevMap[r.Model+":"+r.Provider] can produce collisions when Model or Provider
contain ":"; replace the string key with a composite struct key (e.g., type
prevKey struct { Model, Provider string }) and use prevMap[prevKey{r.Model,
r.Provider}] when populating and looking up entries (apply the same change to
the other instance at the similar block around the code that mirrors lines
767-768), ensuring all accesses use the new struct key to avoid collisions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2599ea4c-1233-4b7d-a0bc-0d8c8429435f

📥 Commits

Reviewing files that changed from the base of the PR and between c4bcb14 and dd966fd.

📒 Files selected for processing (8)
  • framework/go.mod
  • framework/logstore/matviews.go
  • framework/logstore/migrations.go
  • framework/logstore/postgres.go
  • framework/logstore/rdb.go
  • framework/logstore/sqlite.go
  • ui/app/workspace/dashboard/page.tsx
  • ui/components/filters/filterPopover.tsx
✅ Files skipped from review due to trivial changes (2)
  • framework/logstore/sqlite.go
  • framework/go.mod
🚧 Files skipped from review as they are similar to previous changes (3)
  • ui/components/filters/filterPopover.tsx
  • framework/logstore/postgres.go
  • framework/logstore/rdb.go

Comment thread framework/logstore/matviews.go
Comment thread framework/logstore/matviews.go
@akshaydeo akshaydeo force-pushed the 03-21-dashboard_query_optimizations branch from dd966fd to 4fbb670 Compare March 22, 2026 06:35
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ui/components/filters/filterPopover.tsx (1)

124-171: ⚠️ Potential issue | 🟠 Major

Don't let grouped filters enter a partial visible state.

Line 125 and Line 144 require every backing ID to be selected, but Lines 149-156 count a grouped label as active when any backing ID matches. If selected_key_ids, virtual_key_ids, or routing_rule_ids arrive with only one duplicate ID set, the badge shows an active filter, the row renders unchecked, and the first click broadens the filter instead of clearing it.

🐛 One way to keep the row state aligned with the active filter state
-		// Check if ALL resolved IDs are already selected (toggle all together)
-		const allSelected = resolvedIds.every((id) => currentValues.includes(id));
-		const newValues = allSelected
+		const anySelected = resolvedIds.some((id) => currentValues.includes(id));
+		const newValues = anySelected
 			? currentValues.filter((v) => !resolvedIds.includes(v))
 			: [...currentValues, ...resolvedIds.filter((id) => !currentValues.includes(id))];
@@
-		return Array.isArray(currentValues) && resolvedIds.every((id) => currentValues.includes(id));
+		return Array.isArray(currentValues) && resolvedIds.some((id) => currentValues.includes(id));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/components/filters/filterPopover.tsx` around lines 124 - 171, The
badge/count logic is inconsistent with selection logic: isSelected requires ALL
backing IDs for a grouped label to be present, but countUniqueNames currently
treats a name as active if ANY backing ID matches, causing mismatched row state.
Update countUniqueNames (used with dedupedCountKeys for selected_key_ids,
virtual_key_ids, routing_rule_ids) to count a name only when every mappedId for
that name is included in the current ids (i.e., use mappedIds.every(id =>
ids.includes(id))) so the deduped count and isSelected semantics align.
🧹 Nitpick comments (2)
ui/components/filters/filterPopover.tsx (1)

34-59: Derive the visible labels from the grouped maps.

groupByName() and dedup() currently encode the same grouping rule twice. If name normalization ever changes in only one path, the rendered options, ID resolution, and badge counting will drift apart. Reusing the Map keys here keeps the visible labels on the same source of truth as the selection logic.

♻️ Suggested cleanup
-	// Deduplicate by name to avoid React key collisions (e.g. multiple deleted keys with the same name)
-	const dedup = (items: { name: string }[]) => [...new Map(items.map((i) => [i.name, i])).values()].map((i) => i.name);
-
 	const FILTER_OPTIONS: Record<string, string[]> = {
 		Status: [...Statuses],
 		Providers: providersLoading ? [] : availableProviders.map((provider) => provider.name),
 		Type: [...RequestTypes],
 		Models: filterDataLoading ? [] : availableModels,
-		"Selected Keys": filterDataLoading ? [] : dedup(availableSelectedKeys),
-		"Virtual Keys": filterDataLoading ? [] : dedup(availableVirtualKeys),
+		"Selected Keys": filterDataLoading ? [] : [...selectedKeyNameToIds.keys()],
+		"Virtual Keys": filterDataLoading ? [] : [...virtualKeyNameToIds.keys()],
 		"Routing Engines": filterDataLoading ? [] : availableRoutingEngines,
-		"Routing Rules": filterDataLoading ? [] : dedup(availableRoutingRules),
+		"Routing Rules": filterDataLoading ? [] : [...routingRuleNameToIds.keys()],
 	};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/components/filters/filterPopover.tsx` around lines 34 - 59, The dedup
logic duplicates the name-normalization already produced by groupByName, causing
drift between rendered labels and selection maps; replace uses of
dedup(availableSelectedKeys), dedup(availableVirtualKeys), and
dedup(availableRoutingRules) in FILTER_OPTIONS with the keys from the
corresponding grouped maps (selectedKeyNameToIds, virtualKeyNameToIds,
routingRuleNameToIds) — e.g., when filterDataLoading is false use
Array.from(selectedKeyNameToIds.keys()) (and similarly for virtualKeyNameToIds
and routingRuleNameToIds) so the visible labels are derived from the same Map
used for ID resolution and badge counting.
framework/logstore/matviews.go (1)

815-823: Whitelist the allowed (idCol, nameCol) pairs.

idCol and nameCol are interpolated directly into both SQL fragments here. Even if current callers only pass constants, this helper makes identifier injection one refactor away. Constrain it to the supported column pairs and reject anything else.

Possible guard
 func (s *RDBLogStore) getDistinctKeyPairsFromMatView(ctx context.Context, idCol, nameCol string) ([]KeyPairResult, error) {
+	validPairs := map[string]string{
+		"selected_key_id": "selected_key_name",
+		"virtual_key_id":  "virtual_key_name",
+		"routing_rule_id": "routing_rule_name",
+	}
+	if validPairs[idCol] != nameCol {
+		return nil, fmt.Errorf("unsupported key pair columns")
+	}
+
 	var results []KeyPairResult
 	if err := s.db.WithContext(ctx).Table("mv_logs_filterdata").
 		Select(fmt.Sprintf("DISTINCT %s AS id, %s AS name", idCol, nameCol)).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/logstore/matviews.go` around lines 815 - 823, The helper
getDistinctKeyPairsFromMatView currently interpolates idCol and nameCol into SQL
directly; restrict allowed column pairs by adding a whitelist (e.g., only allow
the known safe pairs used elsewhere) and validate the incoming idCol/nameCol
against that set before building the query; if the pair is not allowed return a
clear error and do not execute the query (use the function name
getDistinctKeyPairsFromMatView and reference the mv_logs_filterdata table when
adding the check so reviewers can find it).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@framework/logstore/matviews.go`:
- Around line 56-74: The materialized view mv_logs_filterdata only keeps 30 days
which causes getDistinctModelsFromMatView, getDistinctKeyPairsFromMatView, and
getDistinctRoutingEnginesFromMatView to lose filter values for dashboard
lookbacks up to 60 days; update the mvLogsFilterdataDDL WHERE clause to retain
the full dashboard-supported horizon (e.g., change INTERVAL '30 days' to
INTERVAL '60 days' or make the interval configurable) so the mat view matches
the dashboard max lookback, or alternatively implement a fallback in those
functions to run raw distinct queries when the requested window exceeds the
mat-view retention.

---

Outside diff comments:
In `@ui/components/filters/filterPopover.tsx`:
- Around line 124-171: The badge/count logic is inconsistent with selection
logic: isSelected requires ALL backing IDs for a grouped label to be present,
but countUniqueNames currently treats a name as active if ANY backing ID
matches, causing mismatched row state. Update countUniqueNames (used with
dedupedCountKeys for selected_key_ids, virtual_key_ids, routing_rule_ids) to
count a name only when every mappedId for that name is included in the current
ids (i.e., use mappedIds.every(id => ids.includes(id))) so the deduped count and
isSelected semantics align.

---

Nitpick comments:
In `@framework/logstore/matviews.go`:
- Around line 815-823: The helper getDistinctKeyPairsFromMatView currently
interpolates idCol and nameCol into SQL directly; restrict allowed column pairs
by adding a whitelist (e.g., only allow the known safe pairs used elsewhere) and
validate the incoming idCol/nameCol against that set before building the query;
if the pair is not allowed return a clear error and do not execute the query
(use the function name getDistinctKeyPairsFromMatView and reference the
mv_logs_filterdata table when adding the check so reviewers can find it).

In `@ui/components/filters/filterPopover.tsx`:
- Around line 34-59: The dedup logic duplicates the name-normalization already
produced by groupByName, causing drift between rendered labels and selection
maps; replace uses of dedup(availableSelectedKeys), dedup(availableVirtualKeys),
and dedup(availableRoutingRules) in FILTER_OPTIONS with the keys from the
corresponding grouped maps (selectedKeyNameToIds, virtualKeyNameToIds,
routingRuleNameToIds) — e.g., when filterDataLoading is false use
Array.from(selectedKeyNameToIds.keys()) (and similarly for virtualKeyNameToIds
and routingRuleNameToIds) so the visible labels are derived from the same Map
used for ID resolution and badge counting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 297bcd65-7e4c-401e-9f47-f5868bb4a667

📥 Commits

Reviewing files that changed from the base of the PR and between dd966fd and 4fbb670.

📒 Files selected for processing (8)
  • framework/go.mod
  • framework/logstore/matviews.go
  • framework/logstore/migrations.go
  • framework/logstore/postgres.go
  • framework/logstore/rdb.go
  • framework/logstore/sqlite.go
  • ui/app/workspace/dashboard/page.tsx
  • ui/components/filters/filterPopover.tsx
✅ Files skipped from review due to trivial changes (4)
  • framework/logstore/sqlite.go
  • framework/go.mod
  • ui/app/workspace/dashboard/page.tsx
  • framework/logstore/postgres.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • framework/logstore/migrations.go
  • framework/logstore/rdb.go

Comment thread framework/logstore/matviews.go
@akshaydeo akshaydeo force-pushed the 03-21-dashboard_query_optimizations branch from 4fbb670 to 6a3ccfc Compare March 22, 2026 07:25
@akshaydeo akshaydeo force-pushed the 03-21-go_mod_fixes_-_updates_dashboard_migrations branch from da62438 to 8c647b1 Compare March 22, 2026 07:25
@akshaydeo akshaydeo force-pushed the 03-21-dashboard_query_optimizations branch from 6a3ccfc to 642faa7 Compare March 22, 2026 07:40
Copy link
Copy Markdown
Contributor Author

akshaydeo commented Mar 22, 2026

Merge activity

  • Mar 22, 7:52 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Mar 22, 7:54 AM UTC: Graphite rebased this pull request as part of a merge.
  • Mar 22, 7:54 AM UTC: @akshaydeo merged this pull request with Graphite.

@akshaydeo akshaydeo changed the base branch from 03-21-go_mod_fixes_-_updates_dashboard_migrations to graphite-base/2193 March 22, 2026 07:52
@akshaydeo akshaydeo changed the base branch from graphite-base/2193 to main March 22, 2026 07:53
@akshaydeo akshaydeo force-pushed the 03-21-dashboard_query_optimizations branch from 642faa7 to 0a443f4 Compare March 22, 2026 07:53
@akshaydeo akshaydeo merged commit 67552ae into main Mar 22, 2026
7 of 8 checks passed
@akshaydeo akshaydeo deleted the 03-21-dashboard_query_optimizations branch March 22, 2026 07:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Files API Support

2 participants