Skip to content

fix: gate matview read path on shape check to prevent "column does not exist" during rolling deploys - #5384

Merged
Pratham-Mishra04 merged 1 commit into
devfrom
07-20-fix_matview_ready_state
Jul 21, 2026
Merged

fix: gate matview read path on shape check to prevent "column does not exist" during rolling deploys#5384
Pratham-Mishra04 merged 1 commit into
devfrom
07-20-fix_matview_ready_state

Conversation

@impoiler

@impoiler impoiler commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

During a rolling deploy, a replica that loses the ensureMatViews advisory lock race could enable the matview read path while the lock-holding replica was still mid-repair. This caused queries like getModelHistogramFromMatView to hit columns (cancelled_count, throughput_*) that the old-schema view did not yet have, producing "column does not exist" errors.

Changes

  • ensureMatViews now returns (bool, error) — the bool indicates whether this process actually performed the create/repair work. A false return with a nil error means another replica held the lock; callers must not treat this as "views are usable."
  • Added matViewShapesReady, a read-only pg_catalog check that verifies every managed matview exists and carries the full column set this build requires. It is safe and cheap to call from replicas that skipped the lock, and is used as the gate before enabling the matview read path.
  • Added pqTextArray, a minimal helper that renders a []string as a Postgres text[] literal for use as a query parameter, avoiding a driver-specific array type dependency. Backslashes and double-quotes are escaped correctly.
  • In newPostgresLogStore, when ensureMatViews returns false (lock held elsewhere), the startup path now calls matViewShapesReady before enabling the read path. If the shapes are not current yet, it starts the refresher's recovery path instead of flipping matViewsReady immediately.
  • In startMatViewRefresher, a successful refreshMatViews tick no longer unconditionally flips matViewsReady. It now calls matViewShapesReady first, since REFRESH succeeds on an old-shape view and refreshMatViews also returns nil when it skipped due to the lock or activity gate.
  • Added pg_catalog.pg_table_is_visible filters to the existence and column-listing queries in matViewNeedsRebuild to avoid matching same-named views in other schemas.
  • Updated all call sites and tests to handle the new (bool, error) signature.

Type of change

  • Bug fix

Affected areas

  • Core (Go)

How to test

go test ./framework/logstore/...

The existing TestEnsureMatViewsSharesRefreshAdvisoryLock test now asserts the bool return value in both the lock-held and lock-free cases. The scale migration test and perf tests have been updated to match the new signature.

To validate the rolling-deploy scenario specifically: run two instances sharing the same Postgres database against a schema that is missing the new matview columns, confirm that the replica that loses the lock race does not enable the matview read path until matViewShapesReady returns true.

Breaking changes

  • No

Internal function signature change only (ensureMatViews return type). No exported API is affected.

Security considerations

pqTextArray escapes backslashes and double-quotes. Column names passed to it come from package-level constants, not user input, but the escaping is correct regardless.

Checklist

  • I added/updated tests where appropriate
  • I verified builds succeed (Go and UI)

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9dcbb9d4-3fd3-4977-b372-9f43fa7263fd

📥 Commits

Reviewing files that changed from the base of the PR and between a464240 and c6347af.

📒 Files selected for processing (6)
  • framework/logstore/matviews.go
  • framework/logstore/matviews_lock_test.go
  • framework/logstore/matviews_shape_test.go
  • framework/logstore/migrations_scale_test.go
  • framework/logstore/postgres.go
  • framework/logstore/rdb_postgres_perf_test.go

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved materialized-view maintenance so only the active replica performs startup/refresh, with clearer behavior when another replica holds the lock.
    • Added stronger schema-shape readiness checks (including visibility) before enabling view-dependent operations, and improved fallback when readiness can’t be confirmed.
  • Tests
    • Expanded tests to validate the new multi-value maintenance result, advisory-lock scenarios, and mvLogsHourly column consistency against its DDL.

Walkthrough

Changes

Materialized-view readiness

Layer / File(s) Summary
Maintenance ownership contract
framework/logstore/matviews.go, framework/logstore/*_test.go
ensureMatViews now returns maintenance ownership separately from errors, with callers and lock, migration, and performance tests updated for the new contract.
Visible shape validation
framework/logstore/matviews.go, framework/logstore/matviews_shape_test.go
Catalog checks require managed materialized views to be visible and contain every required column; the hourly view’s required columns are checked against its DDL.
Startup and recovery gating
framework/logstore/postgres.go, framework/logstore/matviews.go
Readiness is set only after maintenance or confirmed shapes; refresher recovery repeats the shape check before setting readiness.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PostgresLogStore
  participant EnsureMatViews
  participant ShapeCheck
  participant Refresher
  PostgresLogStore->>EnsureMatViews: maintain materialized views
  EnsureMatViews-->>PostgresLogStore: ownership and error
  PostgresLogStore->>ShapeCheck: validate shapes when not owner
  ShapeCheck-->>PostgresLogStore: ready status
  PostgresLogStore->>Refresher: start periodic refresh
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: gating matview read-path activation on shape readiness during rolling deploys.
Description check ✅ Passed The description follows the required template well, covering summary, changes, testing, type, affected areas, breaking changes, security, and checklist.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 07-20-fix_matview_ready_state

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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

impoiler commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

@impoiler impoiler changed the title fix: matview ready state fix: gate matview read path on shape check to prevent "column does not exist" during rolling deploys Jul 20, 2026
@impoiler impoiler self-assigned this Jul 20, 2026
@impoiler
impoiler force-pushed the 07-20-fix_matview_ready_state branch from 9df18eb to 1fda164 Compare July 20, 2026 14:18
@impoiler
impoiler marked this pull request as ready for review July 20, 2026 14:36
@greptile-apps

greptile-apps Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

This looks safe to merge.

  • The required-column set now covers the full hourly view schema and all reviewed reader columns.
  • Startup and recovery only enable materialized-view reads after the catalog confirms the expected shape.
  • No blocking issue remains in the updated code.

Important Files Changed

Filename Overview
framework/logstore/matviews.go Adds complete shape validation, maintenance status reporting, schema-aware catalog checks, and guarded refresher recovery.
framework/logstore/postgres.go Keeps materialized-view reads disabled when another replica is still repairing the views.
framework/logstore/matviews_shape_test.go Checks that the hourly view's required columns remain synchronized with its DDL output.

Reviews (4): Last reviewed commit: "fix: matview ready state" | Re-trigger Greptile

Comment thread framework/logstore/matviews.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

488-551: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding direct test coverage for matViewShapesReady/pqTextArray.

The ownership contract (ensureMatViews) is covered by matviews_lock_test.go, but matViewShapesReady — the function that actually gates enabling the read path when maintenance is owned by another replica — doesn't appear to have a dedicated test in this diff (e.g., create an old-shape matview missing a required column, assert false; then create the correct shape and assert true). Given this function directly controls whether a rolling-deploy replica serves matview reads before the shape is confirmed, a focused test would materially reduce regression risk. The existing setupPerfTestDB/testMatViewExists harness in rdb_postgres_perf_test.go could be reused for this.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/logstore/matviews.go` around lines 488 - 551, Add focused tests for
matViewShapesReady using the existing PostgreSQL test harness: verify it returns
false for a managed materialized view with a missing required column, then true
after the view has the complete required shape. Also add direct coverage for
pqTextArray, including representative strings requiring escaping, while
preserving existing behavior for non-Postgres databases and query errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@framework/logstore/matviews.go`:
- Around line 488-551: Add focused tests for matViewShapesReady using the
existing PostgreSQL test harness: verify it returns false for a managed
materialized view with a missing required column, then true after the view has
the complete required shape. Also add direct coverage for pqTextArray, including
representative strings requiring escaping, while preserving existing behavior
for non-Postgres databases and query errors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7cebd5fb-9c03-48fe-8ff3-0cddb8de8a3d

📥 Commits

Reviewing files that changed from the base of the PR and between 2bc571f and 1fda164.

📒 Files selected for processing (5)
  • framework/logstore/matviews.go
  • framework/logstore/matviews_lock_test.go
  • framework/logstore/migrations_scale_test.go
  • framework/logstore/postgres.go
  • framework/logstore/rdb_postgres_perf_test.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 20, 2026
@impoiler
impoiler force-pushed the 07-20-fix_matview_ready_state branch 2 times, most recently from 7ea3aad to 58a5441 Compare July 20, 2026 15:16

Pratham-Mishra04 commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Merge activity

  • Jul 21, 7:38 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jul 21, 7:39 AM UTC: Graphite rebased this pull request as part of a merge.
  • Jul 21, 7:39 AM UTC: @Pratham-Mishra04 merged this pull request with Graphite.

@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-20-fix_matview_ready_state branch from a464240 to c6347af Compare July 21, 2026 07:38
@Pratham-Mishra04
Pratham-Mishra04 merged commit 8354ca7 into dev Jul 21, 2026
14 of 15 checks passed
@Pratham-Mishra04
Pratham-Mishra04 deleted the 07-20-fix_matview_ready_state branch July 21, 2026 07:39
akshaydeo added a commit that referenced this pull request Jul 23, 2026
## Summary

This PR adds two improvements to the materialized view read path: cache-hit statistics are now served from the hybrid aggregate (interior buckets from `mv_logs_hourly`, boundary slivers classified raw) instead of a separate full-window raw scan, and a runtime self-heal mechanism automatically recovers from missing or stale-shaped materialized views without requiring a process restart.

## Changes

- **Cache hits in the hybrid aggregate**: Three new columns (`direct_cache_hits`, `semantic_cache_hits`, `cache_debug_count`) are added to `mv_logs_hourly` using the same `cacheDebugJSONGuard` and `cacheDebugHitTypeExpr` expressions shared across the matview DDL, boundary sliver queries, and `aggregateCacheHits`. `cache_debug_count` preserves the nil contract: when no row in the window carried valid `cache_debug` JSON the fields are omitted from the response, and when cache rows exist but none were direct/semantic explicit zeros are returned. The previous approach issued a separate full-window raw scan for cache hits after the hybrid aggregate completed; that scan is removed.

- **Runtime matview self-heal** (`matviewheal.go`): `isMatViewShapeError` classifies PostgreSQL error codes `42P01` (undefined table), `42703` (undefined column), and `55000` (object not in prerequisite state) as shape errors. `fallBackToRaw` is called at every matview dispatch site — on a shape error it disables the matview read path process-wide, logs a warning, and triggers a single-flight background repair via `triggerMatViewSelfHeal`. The repair runs `ensureMatViews` then `refreshMatViews` and re-enables the path on success. A 30-second cooldown (`matViewHealCooldown`) bounds repair frequency; while broken, every request continues succeeding via the raw fallback. Two new atomic fields (`matViewHealInFlight`, `matViewHealLastAttempt`) are added to `RDBLogStore`.

- **Shared SQL constants**: The inline regex strings for the cache debug guard and hit-type extractor are replaced with named constants `cacheDebugJSONGuard` and `cacheDebugHitTypeExpr`, used consistently across the matview DDL, `applyFilters`, `rawTerminalStatsAgg`, and `aggregateCacheHits`.

- **Tests**: `TestGetStatsMatViewCacheHitsHybrid` verifies the hybrid cache-hit path including nil and zero contracts and agreement with the raw path. `TestMatViewShapeErrorFallsBackAndSelfHeals`, `TestMatViewStaleShapeFallsBackAndSelfHeals`, and `TestFilterMatViewShapeErrorFallsBack` cover the 42P01, 42703, and filter-view drop scenarios end-to-end, including background self-heal convergence.

## Type of change

- [x] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./framework/logstore/... -run TestGetStatsMatViewCacheHitsHybrid
go test ./framework/logstore/... -run TestMatViewShapeErrorFallsBackAndSelfHeals
go test ./framework/logstore/... -run TestMatViewStaleShapeFallsBackAndSelfHeals
go test ./framework/logstore/... -run TestFilterMatViewShapeErrorFallsBack
go test ./framework/logstore/... -run TestIsMatViewShapeError
go test ./framework/logstore/...
```

The self-heal tests drop or replace `mv_logs_hourly` mid-run and assert that reads continue returning correct results from the raw table with no error, that `matViewsReady` is set to false immediately, and that the view is recreated with the correct shape within 90 seconds.

## Breaking changes

- [x] No

The new matview columns require a schema migration. On first deploy, `repairMatViewShapes` detects the missing columns, drops and recreates `mv_logs_hourly`, and the self-heal path handles any replica that reads before the rebuild completes.

## Related issues

Closes #5384

## Security considerations

No auth, secrets, PII, or sandboxing changes. The new SQL expressions are constants composed only of built-in PostgreSQL operators and are not user-controlled.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
akshaydeo added a commit that referenced this pull request Jul 24, 2026
## Summary

This PR adds two improvements to the materialized view read path: cache-hit statistics are now served from the hybrid aggregate (interior buckets from `mv_logs_hourly`, boundary slivers classified raw) instead of a separate full-window raw scan, and a runtime self-heal mechanism automatically recovers from missing or stale-shaped materialized views without requiring a process restart.

## Changes

- **Cache hits in the hybrid aggregate**: Three new columns (`direct_cache_hits`, `semantic_cache_hits`, `cache_debug_count`) are added to `mv_logs_hourly` using the same `cacheDebugJSONGuard` and `cacheDebugHitTypeExpr` expressions shared across the matview DDL, boundary sliver queries, and `aggregateCacheHits`. `cache_debug_count` preserves the nil contract: when no row in the window carried valid `cache_debug` JSON the fields are omitted from the response, and when cache rows exist but none were direct/semantic explicit zeros are returned. The previous approach issued a separate full-window raw scan for cache hits after the hybrid aggregate completed; that scan is removed.

- **Runtime matview self-heal** (`matviewheal.go`): `isMatViewShapeError` classifies PostgreSQL error codes `42P01` (undefined table), `42703` (undefined column), and `55000` (object not in prerequisite state) as shape errors. `fallBackToRaw` is called at every matview dispatch site — on a shape error it disables the matview read path process-wide, logs a warning, and triggers a single-flight background repair via `triggerMatViewSelfHeal`. The repair runs `ensureMatViews` then `refreshMatViews` and re-enables the path on success. A 30-second cooldown (`matViewHealCooldown`) bounds repair frequency; while broken, every request continues succeeding via the raw fallback. Two new atomic fields (`matViewHealInFlight`, `matViewHealLastAttempt`) are added to `RDBLogStore`.

- **Shared SQL constants**: The inline regex strings for the cache debug guard and hit-type extractor are replaced with named constants `cacheDebugJSONGuard` and `cacheDebugHitTypeExpr`, used consistently across the matview DDL, `applyFilters`, `rawTerminalStatsAgg`, and `aggregateCacheHits`.

- **Tests**: `TestGetStatsMatViewCacheHitsHybrid` verifies the hybrid cache-hit path including nil and zero contracts and agreement with the raw path. `TestMatViewShapeErrorFallsBackAndSelfHeals`, `TestMatViewStaleShapeFallsBackAndSelfHeals`, and `TestFilterMatViewShapeErrorFallsBack` cover the 42P01, 42703, and filter-view drop scenarios end-to-end, including background self-heal convergence.

## Type of change

- [x] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./framework/logstore/... -run TestGetStatsMatViewCacheHitsHybrid
go test ./framework/logstore/... -run TestMatViewShapeErrorFallsBackAndSelfHeals
go test ./framework/logstore/... -run TestMatViewStaleShapeFallsBackAndSelfHeals
go test ./framework/logstore/... -run TestFilterMatViewShapeErrorFallsBack
go test ./framework/logstore/... -run TestIsMatViewShapeError
go test ./framework/logstore/...
```

The self-heal tests drop or replace `mv_logs_hourly` mid-run and assert that reads continue returning correct results from the raw table with no error, that `matViewsReady` is set to false immediately, and that the view is recreated with the correct shape within 90 seconds.

## Breaking changes

- [x] No

The new matview columns require a schema migration. On first deploy, `repairMatViewShapes` detects the missing columns, drops and recreates `mv_logs_hourly`, and the self-heal path handles any replica that reads before the rebuild completes.

## Related issues

Closes #5384

## Security considerations

No auth, secrets, PII, or sandboxing changes. The new SQL expressions are constants composed only of built-in PostgreSQL operators and are not user-controlled.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
BearTS pushed a commit that referenced this pull request Jul 27, 2026
This PR adds two improvements to the materialized view read path: cache-hit statistics are now served from the hybrid aggregate (interior buckets from `mv_logs_hourly`, boundary slivers classified raw) instead of a separate full-window raw scan, and a runtime self-heal mechanism automatically recovers from missing or stale-shaped materialized views without requiring a process restart.

- **Cache hits in the hybrid aggregate**: Three new columns (`direct_cache_hits`, `semantic_cache_hits`, `cache_debug_count`) are added to `mv_logs_hourly` using the same `cacheDebugJSONGuard` and `cacheDebugHitTypeExpr` expressions shared across the matview DDL, boundary sliver queries, and `aggregateCacheHits`. `cache_debug_count` preserves the nil contract: when no row in the window carried valid `cache_debug` JSON the fields are omitted from the response, and when cache rows exist but none were direct/semantic explicit zeros are returned. The previous approach issued a separate full-window raw scan for cache hits after the hybrid aggregate completed; that scan is removed.

- **Runtime matview self-heal** (`matviewheal.go`): `isMatViewShapeError` classifies PostgreSQL error codes `42P01` (undefined table), `42703` (undefined column), and `55000` (object not in prerequisite state) as shape errors. `fallBackToRaw` is called at every matview dispatch site — on a shape error it disables the matview read path process-wide, logs a warning, and triggers a single-flight background repair via `triggerMatViewSelfHeal`. The repair runs `ensureMatViews` then `refreshMatViews` and re-enables the path on success. A 30-second cooldown (`matViewHealCooldown`) bounds repair frequency; while broken, every request continues succeeding via the raw fallback. Two new atomic fields (`matViewHealInFlight`, `matViewHealLastAttempt`) are added to `RDBLogStore`.

- **Shared SQL constants**: The inline regex strings for the cache debug guard and hit-type extractor are replaced with named constants `cacheDebugJSONGuard` and `cacheDebugHitTypeExpr`, used consistently across the matview DDL, `applyFilters`, `rawTerminalStatsAgg`, and `aggregateCacheHits`.

- **Tests**: `TestGetStatsMatViewCacheHitsHybrid` verifies the hybrid cache-hit path including nil and zero contracts and agreement with the raw path. `TestMatViewShapeErrorFallsBackAndSelfHeals`, `TestMatViewStaleShapeFallsBackAndSelfHeals`, and `TestFilterMatViewShapeErrorFallsBack` cover the 42P01, 42703, and filter-view drop scenarios end-to-end, including background self-heal convergence.

- [x] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

- [x] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

```sh
go test ./framework/logstore/... -run TestGetStatsMatViewCacheHitsHybrid
go test ./framework/logstore/... -run TestMatViewShapeErrorFallsBackAndSelfHeals
go test ./framework/logstore/... -run TestMatViewStaleShapeFallsBackAndSelfHeals
go test ./framework/logstore/... -run TestFilterMatViewShapeErrorFallsBack
go test ./framework/logstore/... -run TestIsMatViewShapeError
go test ./framework/logstore/...
```

The self-heal tests drop or replace `mv_logs_hourly` mid-run and assert that reads continue returning correct results from the raw table with no error, that `matViewsReady` is set to false immediately, and that the view is recreated with the correct shape within 90 seconds.

- [x] No

The new matview columns require a schema migration. On first deploy, `repairMatViewShapes` detects the missing columns, drops and recreates `mv_logs_hourly`, and the self-heal path handles any replica that reads before the rebuild completes.

Closes #5384

No auth, secrets, PII, or sandboxing changes. The new SQL expressions are constants composed only of built-in PostgreSQL operators and are not user-controlled.

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
akhsaul pushed a commit to akhsaul/bifrost that referenced this pull request Aug 27, 2026
…t exist" during rolling deploys (maximhq#5384)

## Summary

During a rolling deploy, a replica that loses the `ensureMatViews` advisory lock race could enable the matview read path while the lock-holding replica was still mid-repair. This caused queries like `getModelHistogramFromMatView` to hit columns (`cancelled_count`, `throughput_*`) that the old-schema view did not yet have, producing "column does not exist" errors.

## Changes

- `ensureMatViews` now returns `(bool, error)` — the bool indicates whether this process actually performed the create/repair work. A `false` return with a `nil` error means another replica held the lock; callers must not treat this as "views are usable."
- Added `matViewShapesReady`, a read-only `pg_catalog` check that verifies every managed matview exists and carries the full column set this build requires. It is safe and cheap to call from replicas that skipped the lock, and is used as the gate before enabling the matview read path.
- Added `pqTextArray`, a minimal helper that renders a `[]string` as a Postgres `text[]` literal for use as a query parameter, avoiding a driver-specific array type dependency. Backslashes and double-quotes are escaped correctly.
- In `newPostgresLogStore`, when `ensureMatViews` returns `false` (lock held elsewhere), the startup path now calls `matViewShapesReady` before enabling the read path. If the shapes are not current yet, it starts the refresher's recovery path instead of flipping `matViewsReady` immediately.
- In `startMatViewRefresher`, a successful `refreshMatViews` tick no longer unconditionally flips `matViewsReady`. It now calls `matViewShapesReady` first, since `REFRESH` succeeds on an old-shape view and `refreshMatViews` also returns `nil` when it skipped due to the lock or activity gate.
- Added `pg_catalog.pg_table_is_visible` filters to the existence and column-listing queries in `matViewNeedsRebuild` to avoid matching same-named views in other schemas.
- Updated all call sites and tests to handle the new `(bool, error)` signature.

## Type of change

- [x] Bug fix

## Affected areas

- [x] Core (Go)

## How to test

```sh
go test ./framework/logstore/...
```

The existing `TestEnsureMatViewsSharesRefreshAdvisoryLock` test now asserts the `bool` return value in both the lock-held and lock-free cases. The scale migration test and perf tests have been updated to match the new signature.

To validate the rolling-deploy scenario specifically: run two instances sharing the same Postgres database against a schema that is missing the new matview columns, confirm that the replica that loses the lock race does not enable the matview read path until `matViewShapesReady` returns true.

## Breaking changes

- [x] No

Internal function signature change only (`ensureMatViews` return type). No exported API is affected.

## Security considerations

`pqTextArray` escapes backslashes and double-quotes. Column names passed to it come from package-level constants, not user input, but the escaping is correct regardless.

## Checklist

- [x] I added/updated tests where appropriate
- [x] I verified builds succeed (Go and UI)
akhsaul pushed a commit to akhsaul/bifrost that referenced this pull request Aug 27, 2026
…hq#5507)

## Summary

This PR adds two improvements to the materialized view read path: cache-hit statistics are now served from the hybrid aggregate (interior buckets from `mv_logs_hourly`, boundary slivers classified raw) instead of a separate full-window raw scan, and a runtime self-heal mechanism automatically recovers from missing or stale-shaped materialized views without requiring a process restart.

## Changes

- **Cache hits in the hybrid aggregate**: Three new columns (`direct_cache_hits`, `semantic_cache_hits`, `cache_debug_count`) are added to `mv_logs_hourly` using the same `cacheDebugJSONGuard` and `cacheDebugHitTypeExpr` expressions shared across the matview DDL, boundary sliver queries, and `aggregateCacheHits`. `cache_debug_count` preserves the nil contract: when no row in the window carried valid `cache_debug` JSON the fields are omitted from the response, and when cache rows exist but none were direct/semantic explicit zeros are returned. The previous approach issued a separate full-window raw scan for cache hits after the hybrid aggregate completed; that scan is removed.

- **Runtime matview self-heal** (`matviewheal.go`): `isMatViewShapeError` classifies PostgreSQL error codes `42P01` (undefined table), `42703` (undefined column), and `55000` (object not in prerequisite state) as shape errors. `fallBackToRaw` is called at every matview dispatch site — on a shape error it disables the matview read path process-wide, logs a warning, and triggers a single-flight background repair via `triggerMatViewSelfHeal`. The repair runs `ensureMatViews` then `refreshMatViews` and re-enables the path on success. A 30-second cooldown (`matViewHealCooldown`) bounds repair frequency; while broken, every request continues succeeding via the raw fallback. Two new atomic fields (`matViewHealInFlight`, `matViewHealLastAttempt`) are added to `RDBLogStore`.

- **Shared SQL constants**: The inline regex strings for the cache debug guard and hit-type extractor are replaced with named constants `cacheDebugJSONGuard` and `cacheDebugHitTypeExpr`, used consistently across the matview DDL, `applyFilters`, `rawTerminalStatsAgg`, and `aggregateCacheHits`.

- **Tests**: `TestGetStatsMatViewCacheHitsHybrid` verifies the hybrid cache-hit path including nil and zero contracts and agreement with the raw path. `TestMatViewShapeErrorFallsBackAndSelfHeals`, `TestMatViewStaleShapeFallsBackAndSelfHeals`, and `TestFilterMatViewShapeErrorFallsBack` cover the 42P01, 42703, and filter-view drop scenarios end-to-end, including background self-heal convergence.

## Type of change

- [x] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./framework/logstore/... -run TestGetStatsMatViewCacheHitsHybrid
go test ./framework/logstore/... -run TestMatViewShapeErrorFallsBackAndSelfHeals
go test ./framework/logstore/... -run TestMatViewStaleShapeFallsBackAndSelfHeals
go test ./framework/logstore/... -run TestFilterMatViewShapeErrorFallsBack
go test ./framework/logstore/... -run TestIsMatViewShapeError
go test ./framework/logstore/...
```

The self-heal tests drop or replace `mv_logs_hourly` mid-run and assert that reads continue returning correct results from the raw table with no error, that `matViewsReady` is set to false immediately, and that the view is recreated with the correct shape within 90 seconds.

## Breaking changes

- [x] No

The new matview columns require a schema migration. On first deploy, `repairMatViewShapes` detects the missing columns, drops and recreates `mv_logs_hourly`, and the self-heal path handles any replica that reads before the rebuild completes.

## Related issues

Closes maximhq#5384

## Security considerations

No auth, secrets, PII, or sandboxing changes. The new SQL expressions are constants composed only of built-in PostgreSQL operators and are not user-controlled.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
occcat pushed a commit to occcat/bifrost that referenced this pull request Sep 2, 2026
…t exist" during rolling deploys (maximhq#5384)

## Summary

During a rolling deploy, a replica that loses the `ensureMatViews` advisory lock race could enable the matview read path while the lock-holding replica was still mid-repair. This caused queries like `getModelHistogramFromMatView` to hit columns (`cancelled_count`, `throughput_*`) that the old-schema view did not yet have, producing "column does not exist" errors.

## Changes

- `ensureMatViews` now returns `(bool, error)` — the bool indicates whether this process actually performed the create/repair work. A `false` return with a `nil` error means another replica held the lock; callers must not treat this as "views are usable."
- Added `matViewShapesReady`, a read-only `pg_catalog` check that verifies every managed matview exists and carries the full column set this build requires. It is safe and cheap to call from replicas that skipped the lock, and is used as the gate before enabling the matview read path.
- Added `pqTextArray`, a minimal helper that renders a `[]string` as a Postgres `text[]` literal for use as a query parameter, avoiding a driver-specific array type dependency. Backslashes and double-quotes are escaped correctly.
- In `newPostgresLogStore`, when `ensureMatViews` returns `false` (lock held elsewhere), the startup path now calls `matViewShapesReady` before enabling the read path. If the shapes are not current yet, it starts the refresher's recovery path instead of flipping `matViewsReady` immediately.
- In `startMatViewRefresher`, a successful `refreshMatViews` tick no longer unconditionally flips `matViewsReady`. It now calls `matViewShapesReady` first, since `REFRESH` succeeds on an old-shape view and `refreshMatViews` also returns `nil` when it skipped due to the lock or activity gate.
- Added `pg_catalog.pg_table_is_visible` filters to the existence and column-listing queries in `matViewNeedsRebuild` to avoid matching same-named views in other schemas.
- Updated all call sites and tests to handle the new `(bool, error)` signature.

## Type of change

- [x] Bug fix

## Affected areas

- [x] Core (Go)

## How to test

```sh
go test ./framework/logstore/...
```

The existing `TestEnsureMatViewsSharesRefreshAdvisoryLock` test now asserts the `bool` return value in both the lock-held and lock-free cases. The scale migration test and perf tests have been updated to match the new signature.

To validate the rolling-deploy scenario specifically: run two instances sharing the same Postgres database against a schema that is missing the new matview columns, confirm that the replica that loses the lock race does not enable the matview read path until `matViewShapesReady` returns true.

## Breaking changes

- [x] No

Internal function signature change only (`ensureMatViews` return type). No exported API is affected.

## Security considerations

`pqTextArray` escapes backslashes and double-quotes. Column names passed to it come from package-level constants, not user input, but the escaping is correct regardless.

## Checklist

- [x] I added/updated tests where appropriate
- [x] I verified builds succeed (Go and UI)
occcat pushed a commit to occcat/bifrost that referenced this pull request Sep 2, 2026
…hq#5507)

## Summary

This PR adds two improvements to the materialized view read path: cache-hit statistics are now served from the hybrid aggregate (interior buckets from `mv_logs_hourly`, boundary slivers classified raw) instead of a separate full-window raw scan, and a runtime self-heal mechanism automatically recovers from missing or stale-shaped materialized views without requiring a process restart.

## Changes

- **Cache hits in the hybrid aggregate**: Three new columns (`direct_cache_hits`, `semantic_cache_hits`, `cache_debug_count`) are added to `mv_logs_hourly` using the same `cacheDebugJSONGuard` and `cacheDebugHitTypeExpr` expressions shared across the matview DDL, boundary sliver queries, and `aggregateCacheHits`. `cache_debug_count` preserves the nil contract: when no row in the window carried valid `cache_debug` JSON the fields are omitted from the response, and when cache rows exist but none were direct/semantic explicit zeros are returned. The previous approach issued a separate full-window raw scan for cache hits after the hybrid aggregate completed; that scan is removed.

- **Runtime matview self-heal** (`matviewheal.go`): `isMatViewShapeError` classifies PostgreSQL error codes `42P01` (undefined table), `42703` (undefined column), and `55000` (object not in prerequisite state) as shape errors. `fallBackToRaw` is called at every matview dispatch site — on a shape error it disables the matview read path process-wide, logs a warning, and triggers a single-flight background repair via `triggerMatViewSelfHeal`. The repair runs `ensureMatViews` then `refreshMatViews` and re-enables the path on success. A 30-second cooldown (`matViewHealCooldown`) bounds repair frequency; while broken, every request continues succeeding via the raw fallback. Two new atomic fields (`matViewHealInFlight`, `matViewHealLastAttempt`) are added to `RDBLogStore`.

- **Shared SQL constants**: The inline regex strings for the cache debug guard and hit-type extractor are replaced with named constants `cacheDebugJSONGuard` and `cacheDebugHitTypeExpr`, used consistently across the matview DDL, `applyFilters`, `rawTerminalStatsAgg`, and `aggregateCacheHits`.

- **Tests**: `TestGetStatsMatViewCacheHitsHybrid` verifies the hybrid cache-hit path including nil and zero contracts and agreement with the raw path. `TestMatViewShapeErrorFallsBackAndSelfHeals`, `TestMatViewStaleShapeFallsBackAndSelfHeals`, and `TestFilterMatViewShapeErrorFallsBack` cover the 42P01, 42703, and filter-view drop scenarios end-to-end, including background self-heal convergence.

## Type of change

- [x] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./framework/logstore/... -run TestGetStatsMatViewCacheHitsHybrid
go test ./framework/logstore/... -run TestMatViewShapeErrorFallsBackAndSelfHeals
go test ./framework/logstore/... -run TestMatViewStaleShapeFallsBackAndSelfHeals
go test ./framework/logstore/... -run TestFilterMatViewShapeErrorFallsBack
go test ./framework/logstore/... -run TestIsMatViewShapeError
go test ./framework/logstore/...
```

The self-heal tests drop or replace `mv_logs_hourly` mid-run and assert that reads continue returning correct results from the raw table with no error, that `matViewsReady` is set to false immediately, and that the view is recreated with the correct shape within 90 seconds.

## Breaking changes

- [x] No

The new matview columns require a schema migration. On first deploy, `repairMatViewShapes` detects the missing columns, drops and recreates `mv_logs_hourly`, and the self-heal path handles any replica that reads before the rebuild completes.

## Related issues

Closes maximhq#5384

## Security considerations

No auth, secrets, PII, or sandboxing changes. The new SQL expressions are constants composed only of built-in PostgreSQL operators and are not user-controlled.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
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.

2 participants