Skip to content

feat: caching for local table/scope invalidation - #177

Merged
EricAndrechek merged 7 commits into
mainfrom
cache-invalidation
May 24, 2026
Merged

feat: caching for local table/scope invalidation#177
EricAndrechek merged 7 commits into
mainfrom
cache-invalidation

Conversation

@EricAndrechek

Copy link
Copy Markdown
Member

Summary

Refactored caching package to handle table (and eventually table + scope) based cache invalidation, wiring it up to Bento ingest worker to invalidate the relevant caches when new data is pushed to clickhouse successfully.

Related Issues

Closes #85, #73

@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Failed to post review comments

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Optional scope routing in ingest and streaming; automatic cache invalidation on ingest; ClickHouse query timeout configurable.
  • Bug Fixes

    • Cache TTLs now derived from observed query durations for more accurate expiry; cache reads/writes respect table+scope.
  • Documentation

    • API examples use query-parameter table form; removed request-level cache_ttl examples; documented scope and query_timeout.
  • Refactor

    • Cache subsystem reworked to a scope-aware interface and simplified local versioned keys.
  • Tests

    • Added unit and end-to-end tests covering cache behavior, invalidation, and timeouts.

Walkthrough

Redesigns cache to be namespace+scope-aware with VersionManager and LocalCache; propagates scope through ingest and streaming subjects; invalidates cache after ClickHouse inserts; removes TieredCache; adds per-handler ClickHouse query timeouts; removes client-side cache_ttl; updates docs and tests.

Changes

Cache architecture and ingest integration

Layer / File(s) Summary
Cache interface and helpers
internal/cache/cache.go, internal/cache/cache_test.go
Cache API now uses Get(ctx, key, namespace, scope) and Set(ctx, key, namespace, scope, data, ttl), adds InvalidateCache, QueryTimeToTTL, and helpers for version/invalidation keys.
VersionManager
internal/cache/version_manager.go, internal/cache/version_manager_test.go
New in-memory VersionManager produces versioned cache keys and supports IncrementVersion to invalidate prior keys.
LocalCache implementation
internal/cache/local.go, internal/cache/local_test.go
LocalCache switched to versioned keys via VersionManager, uses ristretto TTLs, exposes namespaced Get/Set, and adds InvalidateCache.
Remove TieredCache
internal/cache/tiered.go, internal/cache/tiered_test.go
TieredCache wrapper and its tests removed; call sites now use cache.Cache directly.
Ingest wire format and ident rename
internal/ingest/types.go, internal/query/ident.go, internal/query/ident_test.go
Add optional scope to EventMessage JSON and rename EncodeTable/DecodeTable → SafeEncodeNATS/SafeDecodeNATS for NATS-safe segments.
Ingest API subject construction
internal/api/ingest.go
Populate EventMessage.Scope (currently placeholder), build NATS publish subject with SafeEncodeNATS(table)[.SafeEncodeNATS(scope)], and log subject+scope.
Bento ClickHouse output + invalidation
internal/ingest/bento.go, internal/ingest/bento_test.go
clickhouseOutput now accepts cache dependency, collects batch scopes, removes timestamp injection, and calls InvalidateCache in background after successful inserts; tests updated accordingly.
PipesHandler -> cache.Cache
internal/api/pipes.go
PipesHandler now depends on cache.Cache, uses pipe name as namespace and scope in Get/Set, measures query duration and computes TTL via QueryTimeToTTL, and uses per-handler maxQueryTimeout.
StructuredQueryHandler -> cache.Cache
internal/api/structured_query.go, internal/api/structured_query_test.go
StructuredQueryHandler uses cache.Cache with table namespace and scope, computes TTL from execution time, and caps execution time with configured maxQueryTimeout.
Streaming handlers subject updates
internal/api/stream_sse.go, internal/api/stream_ws.go
SSE/WS subscribe/unsubscribe/gap-fill now use SafeEncodeNATS(table) and optionally append encoded scope when non-empty; comments clarify scope expectations.
QueryHandler timeout wiring
internal/api/query.go, internal/api/query_test.go
Add maxQueryTimeout to QueryHandler and pass configured ClickHouse query timeout into NewQueryHandler; upstream calls use this timeout rather than a fixed 30s.
Config: query_timeout and removed default_ttl
internal/config/config.go, internal/config/config_test.go, config.yaml
Add clickhouse.query_timeout (30s default) and remove cache.default_ttl from config and validation.
Remove client-side cache_ttl
internal/query/ast.go, clients/ts/src/query-builder.ts, clients/ts/src/types.ts, clients/ts/src/query-builder.test.ts
Remove cache_ttl from StructuredQuery AST and TS types; QueryBuilder no longer emits cache_ttl and related tests/docs updated.
Docs updates
docs/src/content/docs/*
Update architecture, API, configuration, and SDK docs to document scope field, clickhouse.query_timeout, and removal of cache_ttl/default_ttl.
main.go: direct LocalCache & timeout wiring
cmd/wavehouse/main.go
Main now uses LocalCache directly (no TieredCache), closes local cache on shutdown, wires cache into StartIngestWorker and handlers, and passes cfg.ClickHouse.QueryTimeout to NewQueryHandler and other handlers.
Integration and E2E tests
tests/integration/setup_test.go, tests/integration/dlq_test.go, tests/e2e/sdk/*.ts
Integration setup initializes and wires LocalCache; DLQ tests use SafeEncodeNATS/SafeDecodeNATS; add e2e tests for cache invalidation and TTL expiry and a policy timeout enforcement test.
Test util cleanup
internal/testutil/mocks.go
Remove MockCache test double and related imports.

Sequence Diagram (cache invalidation flow):

sequenceDiagram
  participant Bento as clickhouseOutput
  participant CH as ClickHouse
  participant Cache as LocalCache
  Bento->>CH: INSERT batch (table, rows)
  CH-->>Bento: 200 OK
  Bento->>Cache: InvalidateCache(table, scopes)
  Cache->>Cache: IncrementVersion(table[.scope]) for each scope
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • taitelee
🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The implementation significantly deviates from issue #85 requirements; the PR uses scope/namespace-based invalidation instead of tag-based invalidation as specified. The PR implements scope-aware cache keys and InvalidateCache(table, scopes) instead of the required InvalidateByTags(ctx, tags). Align implementation with issue #85's tag-based tagging/invalidation design, ensure raw SQL queries return X-Cache: BYPASS, and validate Bento synchronously invalidates on successful inserts.
Out of Scope Changes check ⚠️ Warning Multiple file changes (TypeScript SDK, documentation updates, query handler timeouts) exceed the scope of table/scope invalidation requirements. Remove out-of-scope changes: TypeScript QueryBuilder cacheTTL removal, QueryHandler timeout parameter addition, and documentation updates unrelated to cache invalidation. Focus PR on scope-based invalidation wiring and Bento integration only.
Docstring Coverage ⚠️ Warning Docstring coverage is 21.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main change: refactoring cache to support table/scope invalidation wiring.
Description check ✅ Passed The PR description clearly relates to the changeset, summarizing cache refactoring and Bento ingest worker integration.

✏️ 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 cache-invalidation
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch cache-invalidation

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation go Pull requests that update go code area/api HTTP handlers, routing, middleware area/ingest Ingest pipeline (Bento, batching, DLQ) area/query Structured query AST, SQL builder area/cache Local / shared / tiered caching area/docs Documentation, site/, README area/infra CI, build, deploy, Docker, release labels May 23, 2026
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a robust caching invalidation mechanism designed to improve data consistency within the system. By refactoring the cache layer to support table and scope-based invalidation and wiring this into the Bento ingest pipeline, the system now ensures that cached query results are invalidated immediately upon successful data ingestion. These changes also lay the groundwork for future multi-tenancy support through the introduction of scope-based metadata.

Highlights

  • Cache Refactoring: Refactored the caching package to support table and scope-based invalidation, replacing the previous tiered cache implementation with a more flexible interface.
  • Ingest Pipeline Integration: Integrated the cache invalidation logic directly into the Bento ingest worker, ensuring that caches are invalidated automatically when new data is successfully pushed to ClickHouse.
  • Scope Support: Added support for 'scope' metadata to event messages, allowing for more granular cache management and future multi-tenancy support.
  • API and Query Updates: Updated API handlers and query execution paths to utilize the new cache interface, including specific methods for query and pipe caching.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
coderabbitai Bot requested a review from taitelee May 23, 2026 17:29

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the caching architecture by replacing TieredCache with a generic Cache interface and introducing scoped invalidation. The ingest pipeline now supports a scope field, and cache invalidation is triggered after ClickHouse inserts. However, several critical issues were identified: using an evicting Ristretto cache for the versionTable risks serving stale data, and generateInvalidationKeys fails to invalidate global table entries during scoped inserts. Furthermore, the removal of received_timestamp injection may impact analytics, and the scope identifier lacks regex validation. Performance improvements are also suggested for Ristretto cost configurations and memory allocation across sub-caches. Iterate.

Comment thread internal/cache/local.go Outdated
Comment thread internal/ingest/bento.go
Comment thread internal/cache/cache.go
Comment thread internal/cache/cache.go
Comment thread internal/cache/cache.go
Comment thread internal/cache/local.go Outdated
Comment thread internal/cache/local.go Outdated
Comment thread internal/cache/local.go Outdated
@github-project-automation github-project-automation Bot moved this from Backlog to In progress in WaveHouse Task Board May 23, 2026
@github-actions github-actions Bot added the area/sdk TypeScript SDK (clients/ts/) label May 23, 2026
@EricAndrechek
EricAndrechek marked this pull request as ready for review May 23, 2026 21:57
@EricAndrechek

Copy link
Copy Markdown
Member Author

/review
/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant overhaul of the caching system, moving from a tiered cache to a version-based invalidation strategy using a VersionManager. It also adds support for query timeouts and scopes (tenancy) across the ingest and query paths. Key changes include the removal of the TieredCache, updates to the Cache interface to support namespaces and scopes, and the introduction of a QueryTimeToTTL function for dynamic cache expiration. However, several critical issues were identified: a regression where received_timestamp is no longer injected into ClickHouse inserts, a bug in the LocalCache.Get implementation that causes misses for keys with zero TTL, and a potential memory leak in the VersionManager due to unbounded map growth. Additionally, redundant version increments were found in the invalidation logic. Iterate.

Comment thread internal/ingest/bento.go
Comment thread internal/cache/local.go
Comment thread internal/cache/version_manager.go
Comment thread internal/cache/cache.go
Comment thread internal/cache/cache.go
Comment thread internal/ingest/bento.go
Comment thread internal/query/ast.go
Comment thread internal/config/config.go
Comment thread internal/api/stream_sse.go Outdated
@claude

claude Bot commented May 23, 2026

Copy link
Copy Markdown

Review summary — 4 [MUST], 1 [SHOULD], 0 [MAY]

See inline threads for detail. Highlights:

# Severity Finding
1 [MUST] generateInvalidationKeys double-bumps the global version when scope is "" — always the case today
2 [MUST] received_timestamp injection silently removed — breaking for tables with a non-nullable received_timestamp column; dead metadata code left behind
3 [MUST] cache_ttl removed from StructuredQuery JSON — breaking API change with no CHANGELOG / docs / SDK update
4 [MUST] WH_CACHE_DEFAULT_TTL config option removed — breaking config change with no CHANGELOG / docs / compose-file update
5 [SHOULD] SSE and WS subscribers use the exact subject ingest.<table>; scoped publishes go to ingest.<table>.<scope> — latent silent-miss bug when scopes are activated

Iterate — address the received_timestamp + docs gaps and the duplicate-version-bump before merge; the SSE wildcard is a [SHOULD] that can follow as a fast-follow if scope is still stub-only.

coderabbitai[bot]
coderabbitai Bot previously requested changes May 23, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

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

⚠️ Outside diff range comments (7)
docs/src/content/docs/api.md (1)

568-606: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

EventMessage docs are missing the scope field and now violate the wire-format contract.

The Internal Wire Format and Client-Facing SSE/WS examples still omit scope, which makes this page inconsistent with the current event payload shape.

Proposed doc fix
 {
   "table_name": "clicks",
+  "scope": "",
   "received_timestamp": "2026-03-24T12:00:00.123456789Z",
   "data": {
     "page": "/home",
     "button": "signup",
     "score": 42.5
   }
 }

 | Field | Type | Description |
 | ----- | ---- | ----------- |
 | `table_name` | string | Target ClickHouse table (from URL). |
+| `scope` | string | Optional scope namespace used for subject routing/cache invalidation context. |
 | `received_timestamp` | string | RFC 3339 nano timestamp when WaveHouse received the event. |
 | `data` | object | The original flat JSON body. |

As per coding guidelines, “internal/ingest/types.go: EventMessage JSON tags must agree with docs/src/content/docs/api.md event format, SSE/WS examples, and ClickHouse INSERT columns”.

docs/src/content/docs/architecture.md (1)

194-195: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Query timeout is documented as fixed 30s, but it is now configurable.

This section should reference clickhouse.query_timeout instead of a hardcoded 30s value to match current behavior and the configuration reference.

docs/src/content/docs/configuration.md (1)

168-172: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Example config still includes removed cache.default_ttl.

The cache section in the example YAML is stale and contradicts the updated reference table. Please remove default_ttl from the example block.

Proposed doc fix
 cache:
   l1_max_cost: 67108864
-  default_ttl: 300
   timestamp_bucket_seconds: 60

As per coding guidelines, “internal/config/**/*.go: Config struct tags in internal/config/config.go must agree with docs/src/content/docs/configuration.md, config.yaml, and compose env blocks”.

internal/ingest/bento.go (1)

390-399: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

registerOnce makes the injected cache effectively process-global.

Line 398 closes over cache inside a factory that is registered only once. After the first StartIngestWorker call, later workers will keep using the first call's cache/JetStream wiring, so restarts and tests become order-dependent and can invalidate the wrong cache instance.

As per coding guidelines "Do not use global state; pass dependencies explicitly through constructor injection".

internal/api/pipes.go (1)

88-103: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix empty-role authorization fail-open in allowlist checks.

Line 90 currently skips allowlist enforcement when role == "", which can permit unintended access. Also guard against empty allowlist entries matching empty roles.

🔒 Proposed fix
 	// Check role permissions.
 	if len(q.AllowedRoles) > 0 {
 		role := RoleFromContext(r.Context())
-		if role != "" {
-			allowed := false
-			for _, ar := range q.AllowedRoles {
-				if ar == role || ar == "*" {
-					allowed = true
-					break
-				}
-			}
-			if !allowed {
-				writeJSONError(w, http.StatusForbidden, "forbidden")
-				return
-			}
-		}
+		allowed := false
+		for _, ar := range q.AllowedRoles {
+			if ar == "*" || (ar != "" && ar == role) {
+				allowed = true
+				break
+			}
+		}
+		if !allowed {
+			writeJSONError(w, http.StatusForbidden, "forbidden")
+			return
+		}
 	}

Based on learnings: remove any outer guard that skips allowlist evaluation for role == "", and require only non-empty allowlist entries to match (ar != "" && ar == role).

internal/api/query.go (1)

92-96: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update constructor docs to match configurable timeout behavior.

Line 92 still documents a fixed 30s request deadline, but NewQueryHandler now accepts queryTimeout (Line 97), so this comment is outdated.

internal/api/query_test.go (1)

511-512: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Replace this TODO with an actual short-timeout expiry test.

Please add a deterministic test that sets a very small queryTimeout, blocks the fake ClickHouse handler, and asserts timeout-driven failure status/body.

As per coding guidelines: "Every new function should have corresponding test cases."


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8bcb45f4-437d-45e2-af7e-8e0c2cdc0cb1

📥 Commits

Reviewing files that changed from the base of the PR and between ff8399b and ef305f3.

📒 Files selected for processing (38)
  • clients/ts/src/query-builder.test.ts
  • clients/ts/src/query-builder.ts
  • clients/ts/src/types.ts
  • cmd/wavehouse/main.go
  • config.yaml
  • docs/src/content/docs/api.md
  • docs/src/content/docs/architecture.md
  • docs/src/content/docs/configuration.md
  • docs/src/content/docs/sdk.md
  • internal/api/ingest.go
  • internal/api/pipes.go
  • internal/api/query.go
  • internal/api/query_test.go
  • internal/api/stream_sse.go
  • internal/api/stream_ws.go
  • internal/api/structured_query.go
  • internal/api/structured_query_test.go
  • internal/cache/cache.go
  • internal/cache/cache_test.go
  • internal/cache/local.go
  • internal/cache/local_test.go
  • internal/cache/tiered.go
  • internal/cache/tiered_test.go
  • internal/cache/version_manager.go
  • internal/cache/version_manager_test.go
  • internal/config/config.go
  • internal/config/config_test.go
  • internal/ingest/bento.go
  • internal/ingest/bento_test.go
  • internal/ingest/types.go
  • internal/query/ast.go
  • internal/query/ident.go
  • internal/query/ident_test.go
  • internal/testutil/mocks.go
  • tests/e2e/sdk/cache.test.ts
  • tests/e2e/sdk/query.test.ts
  • tests/integration/dlq_test.go
  • tests/integration/setup_test.go
💤 Files with no reviewable changes (9)
  • clients/ts/src/types.ts
  • internal/ingest/bento_test.go
  • internal/cache/tiered_test.go
  • internal/cache/tiered.go
  • internal/query/ast.go
  • internal/testutil/mocks.go
  • clients/ts/src/query-builder.ts
  • docs/src/content/docs/sdk.md
  • clients/ts/src/query-builder.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (14)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Use Go interfaces for core behaviors (Cache, Deduplicator, Publisher, Subscriber) to enable interface-first design with multiple implementations for standalone and clustered modes
Apply gofumpt strict formatting enforced by CI
Use structured logging with log/slog (JSON handler) instead of other logging approaches
Return errors and wrap with fmt.Errorf("context: %w", err) instead of panicking
Do not use global state; pass dependencies explicitly through constructor injection
Use lowercase, single-word (or abbreviated) package names in Go
Every new function should have corresponding test cases
Aim for 80%+ coverage on new code; project-wide CI enforces 80% minimum via merged unit + integration + e2e profiles
Go version 1.26 with strict gofumpt formatting enforced by CI; use golangci-lint v2.11.4 pinned in Makefile; most dev tools pinned in go.mod via tool directives

Files:

  • internal/api/stream_ws.go
  • internal/query/ident_test.go
  • internal/ingest/types.go
  • internal/api/stream_sse.go
  • internal/api/ingest.go
  • internal/cache/cache_test.go
  • internal/api/query.go
  • tests/integration/setup_test.go
  • internal/api/query_test.go
  • internal/api/structured_query_test.go
  • internal/query/ident.go
  • internal/config/config.go
  • internal/cache/version_manager_test.go
  • internal/cache/cache.go
  • internal/api/structured_query.go
  • tests/integration/dlq_test.go
  • cmd/wavehouse/main.go
  • internal/cache/version_manager.go
  • internal/config/config_test.go
  • internal/api/pipes.go
  • internal/ingest/bento.go
  • internal/cache/local.go
  • internal/cache/local_test.go
internal/api/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

internal/api/**/*.go: Use Chi v5 for HTTP routing in WaveHouse
Use Bearer-token-only CORS posture: never emit Access-Control-Allow-Credentials, rely on Authorization: Bearer headers, not cookies
Schema-driven ingest: POST /v1/ingest/{table} accepts flat JSON body, validates against discovered schema (unknown fields rejected, types checked, nullable constraints enforced), returns 200 immediately after optional dedup + MQ publish
/health returns 200 after first successful schema Refresh; /ready returns 503 with latest diagnostic until Refresh succeeds

Files:

  • internal/api/stream_ws.go
  • internal/api/stream_sse.go
  • internal/api/ingest.go
  • internal/api/query.go
  • internal/api/query_test.go
  • internal/api/structured_query_test.go
  • internal/api/structured_query.go
  • internal/api/pipes.go
internal/api/**/*stream*.go

📄 CodeRabbit inference engine (AGENTS.md)

Use NATS DeliverByStartTime for SSE/WS gap-fill; no in-process ring buffer

Files:

  • internal/api/stream_ws.go
  • internal/api/stream_sse.go
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*_test.go: Use table-driven tests with t.Run(tt.name, ...) for test cases in Go
Use shared mocks from internal/testutil/ (MockPublisher, MockCache, MockDeduplicator, MockSubscriber) instead of creating ad-hoc mocks
Use testutil.NewTestSchemaRegistry(tables) or discovery.NewSchemaRegistryFromMap(tables) for schema-aware tests

Files:

  • internal/query/ident_test.go
  • internal/cache/cache_test.go
  • tests/integration/setup_test.go
  • internal/api/query_test.go
  • internal/api/structured_query_test.go
  • internal/cache/version_manager_test.go
  • tests/integration/dlq_test.go
  • internal/config/config_test.go
  • internal/cache/local_test.go
internal/query/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Structured queries (POST /v1/tables/{table}/query) are type-safe query AST validated against schema, with permission enforcement, timestamp bucketing for cache optimization, and 10,000 row DefaultMaxRows limit cap

Files:

  • internal/query/ident_test.go
  • internal/query/ident.go
internal/ingest/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

internal/ingest/**/*.go: Ingest is insert-only: the EventMessage wire format carries {table_name, received_timestamp, data} and nothing else; non-insert mutations (DELETE/UPDATE/TRUNCATE) go through POST /v1/admin/query under admin/service role
Per-table batching: the Bento ingest pipeline groups events by table name and performs dynamic INSERTs using schema column order; each table batch is independent
Failed batch inserts are published to a separate NATS stream (WAVEHOUSE_DLQ) with subjects dlq.

to prevent silent data loss; DLQ is controlled by dlq.enabled config
Active Sweeper purges NATS messages that are both ACKed (written to ClickHouse) and older than the gap window

Files:

  • internal/ingest/types.go
  • internal/ingest/bento.go
internal/ingest/types.go

📄 CodeRabbit inference engine (AGENTS.md)

EventMessage JSON tags must agree with docs/src/content/docs/api.md event format, SSE/WS examples, and ClickHouse INSERT columns

Files:

  • internal/ingest/types.go
tests/e2e/sdk/**/*.test.ts

📄 CodeRabbit inference engine (AGENTS.md)

E2E tests in tests/e2e/sdk/*.test.ts exercise the full pipeline and use helpers from tests/e2e/sdk/helpers.ts; run with make test-e2e

Files:

  • tests/e2e/sdk/query.test.ts
  • tests/e2e/sdk/cache.test.ts
internal/api/**/*ingest*.go

📄 CodeRabbit inference engine (AGENTS.md)

If NATS stream is full on ingest, return 503 + Retry-After instead of blocking

Files:

  • internal/api/ingest.go
internal/cache/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

TieredCache uses golang.org/x/sync/singleflight to prevent cache stampede

Files:

  • internal/cache/cache_test.go
  • internal/cache/version_manager_test.go
  • internal/cache/cache.go
  • internal/cache/version_manager.go
  • internal/cache/local.go
  • internal/cache/local_test.go
tests/integration/**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

Integration tests must be marked with //go:build integration and use Docker testcontainers for ClickHouse; E2E tests use the SDK as the primary test harness in tests/e2e/sdk/

Files:

  • tests/integration/setup_test.go
  • tests/integration/dlq_test.go
**/api/**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

**/api/**/*_test.go: Use testutil.MakeJWT(t, claims) and testutil.MakeExpiredJWT(t, claims) for JWT authentication tests
Use testutil.AssertJSONResponse(t, rec, status, expected) and testutil.AssertJSONContains(t, rec, status, substring) for HTTP handler assertions

Files:

  • internal/api/query_test.go
  • internal/api/structured_query_test.go
internal/config/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Config struct tags in internal/config/config.go must agree with docs/src/content/docs/configuration.md, config.yaml, and compose env blocks

Files:

  • internal/config/config.go
  • internal/config/config_test.go
cmd/wavehouse/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Schema-discovery failure on boot is non-fatal: record api.BootState diagnostic, bind :8080, and retry via SchemaRegistry.RetryRefresh (exp backoff 2s → 60s) in background

Files:

  • cmd/wavehouse/main.go
🧠 Learnings (3)
📚 Learning: 2026-05-20T01:02:00.784Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 164
File: internal/api/router_test.go:289-350
Timestamp: 2026-05-20T01:02:00.784Z
Learning: In WaveHouse’s internal API tests (files matching internal/api/**/*_test.go), follow the existing separation-of-concerns convention for testing the RequireRole middleware: inject `ContextKeyRole` directly into the request `context.Context` instead of using `testutil.MakeJWT`/JWT-driven flows. Do not refactor role-gate tests to use JWT tokens—JWT parsing and token handling are covered separately in `middleware_test.go` (the dedicated JWT parsing tests), and mixing those concerns would expand the failure surface and reduce isolation.

Applied to files:

  • internal/api/query_test.go
  • internal/api/structured_query_test.go
📚 Learning: 2026-05-23T01:23:59.268Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 174
File: internal/api/ingest_test.go:111-111
Timestamp: 2026-05-23T01:23:59.268Z
Learning: In WaveHouse Go tests in internal/api/**/*_test.go, use internal/testutil.AssertJSONErrorResponse(t, w) for HTTP error-path JSON assertions. Do not use (or reintroduce) package-local assertJSONErrorResponse helpers. AssertJSONErrorResponse verifies the response Content-Type is application/json, includes the X-Content-Type-Options: nosniff header, and that the JSON body contains an "error" field.

Applied to files:

  • internal/api/query_test.go
  • internal/api/structured_query_test.go
📚 Learning: 2026-05-20T20:30:15.808Z
Learnt from: taitelee
Repo: Wave-RF/WaveHouse PR: 172
File: internal/api/pipes_test.go:106-118
Timestamp: 2026-05-20T20:30:15.808Z
Learning: For WaveHouse pipes authorization allowlist checks, fix the empty-role fail-open behavior by (1) removing any outer guard that prevents allowlist evaluation when the incoming `role` is `""` (e.g., don’t short-circuit with `if role != "" { ... }`), and (2) during allowlist scanning, ensure only non-empty allowlist entries can match—e.g., require `ar != "" && ar == role` (so a malformed allowlist like `["" ]` cannot grant access to an empty incoming role via `"" == ""`).

Applied to files:

  • internal/api/pipes.go
🪛 LanguageTool
docs/src/content/docs/architecture.md

[style] ~108-~108: Consider using the typographical ellipsis character here instead.
Context: ...which publishes the inner data payload ({"id":"abc","field":...}) to dlq.{table} NATS subjects when ...

(ELLIPSIS)

🔇 Additional comments (7)
internal/cache/cache.go (1)

10-34: LGTM!

Also applies to: 37-80

internal/cache/version_manager.go (1)

13-49: LGTM!

internal/cache/local_test.go (1)

18-22: LGTM!

Also applies to: 31-41, 51-61, 70-78, 87-98, 100-131

tests/integration/setup_test.go (1)

319-319: LGTM!

tests/integration/dlq_test.go (1)

62-62: LGTM!

Also applies to: 106-106, 131-131

tests/e2e/sdk/query.test.ts (1)

190-225: LGTM!

config.yaml (1)

46-46: LGTM!

Comment thread internal/api/pipes.go
Comment thread internal/api/stream_sse.go
Comment thread internal/cache/cache_test.go
Comment thread internal/cache/local.go
Comment thread internal/cache/version_manager_test.go
Comment thread internal/config/config.go Outdated
Comment thread internal/ingest/bento.go
Comment thread tests/e2e/sdk/cache.test.ts Outdated
Comment thread tests/e2e/sdk/cache.test.ts
Comment thread tests/integration/setup_test.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (5)
docs/src/content/docs/api.md (1)

117-122: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Endpoint shape drift: document ingest as path param, not query param.

Line 117 (and the example at Line 626) documents POST /v1/ingest?table={table}, which conflicts with the API contract using POST /v1/ingest/{table}. This will send users to the wrong URL shape.

Suggested doc fix
-### `POST /v1/ingest?table={table}` — Ingest Data
+### `POST /v1/ingest/{table}` — Ingest Data
@@
-curl -X POST http://localhost:8080/v1/ingest?table=clicks \
+curl -X POST http://localhost:8080/v1/ingest/clicks \
@@
-curl -X POST http://localhost:8080/v1/ingest?table=clicks \
+curl -X POST http://localhost:8080/v1/ingest/clicks \

As per coding guidelines: “Schema-driven ingest: POST /v1/ingest/{table} accepts flat JSON body…”.

Also applies to: 626-626

internal/cache/local.go (2)

19-23: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

NumCounters becomes zero for small maxCost values due to integer division.

The expression maxCost / 100 * 10 uses integer division. If maxCost < 100, this evaluates to 0, which may cause Ristretto initialization to fail or behave unexpectedly.

Proposed fix
 func NewLocal(maxCost int64) (*LocalCache, error) {
 	cache, err := ristretto.NewCache(&ristretto.Config[string, []byte]{
-		NumCounters: maxCost / 100 * 10,
+		NumCounters: maxCost / 10, // Simplified; consider max(maxCost/10, 1000) for small caches
 		MaxCost:     maxCost,
 		BufferItems: 64,
 	})

53-60: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Unused ctx parameter in InvalidateCache.

The ctx parameter is accepted but never used. While this matches the interface signature (needed for future Redis implementations), consider adding a brief comment or using _ context.Context to signal intent.

Suggested clarification
-func (l *LocalCache) InvalidateCache(ctx context.Context, table string, scopes map[string]struct{}) (uint64, error) {
+func (l *LocalCache) InvalidateCache(_ context.Context, table string, scopes map[string]struct{}) (uint64, error) {
+	// ctx unused for local cache; required by interface for distributed implementations
 	keys := generateInvalidationKeys(table, scopes)
internal/ingest/bento.go (1)

391-410: 🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

registerOnce.Do closure captures cache from first invocation only.

The registerOnce.Do block captures cache, host, chHTTPPort, and other parameters from the first call to StartIngestWorker. Subsequent calls with different values will silently use the original configuration. While this is likely intentional (single worker per process), consider documenting this constraint or returning an error on subsequent calls with different parameters.

internal/api/pipes.go (1)

88-103: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Enforce allowlist checks even when the incoming role is empty.

Line 90 skips authorization when role == "", which creates a fail-open path for pipes with AllowedRoles. Evaluate the allowlist unconditionally and only allow non-empty exact matches (plus *).

🔒 Proposed fix
 if len(q.AllowedRoles) > 0 {
 	role := RoleFromContext(r.Context())
-	if role != "" {
-		allowed := false
-		for _, ar := range q.AllowedRoles {
-			if ar == role || ar == "*" {
-				allowed = true
-				break
-			}
-		}
-		if !allowed {
-			writeJSONError(w, http.StatusForbidden, "forbidden")
-			return
-		}
-	}
+	allowed := false
+	for _, ar := range q.AllowedRoles {
+		if ar == "*" || (ar != "" && ar == role) {
+			allowed = true
+			break
+		}
+	}
+	if !allowed {
+		writeJSONError(w, http.StatusForbidden, "forbidden")
+		return
+	}
 }
Based on learnings: remove outer guards that skip allowlist checks for empty roles, and require non-empty allowlist entries for exact role matches.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 61ddf52e-b286-45cf-b053-79c96cc196c6

📥 Commits

Reviewing files that changed from the base of the PR and between ef305f3 and 7cfe381.

📒 Files selected for processing (12)
  • docs/src/content/docs/api.md
  • docs/src/content/docs/architecture.md
  • docs/src/content/docs/configuration.md
  • internal/api/pipes.go
  • internal/api/query.go
  • internal/api/stream_sse.go
  • internal/cache/local.go
  • internal/config/config.go
  • internal/config/config_test.go
  • internal/ingest/bento.go
  • tests/e2e/sdk/cache.test.ts
  • tests/integration/setup_test.go
💤 Files with no reviewable changes (1)
  • docs/src/content/docs/configuration.md
📜 Review details
🧰 Additional context used
📓 Path-based instructions (9)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Use Go interfaces for core behaviors (Cache, Deduplicator, Publisher, Subscriber) to enable interface-first design with multiple implementations for standalone and clustered modes
Apply gofumpt strict formatting enforced by CI
Use structured logging with log/slog (JSON handler) instead of other logging approaches
Return errors and wrap with fmt.Errorf("context: %w", err) instead of panicking
Do not use global state; pass dependencies explicitly through constructor injection
Use lowercase, single-word (or abbreviated) package names in Go
Every new function should have corresponding test cases
Aim for 80%+ coverage on new code; project-wide CI enforces 80% minimum via merged unit + integration + e2e profiles
Go version 1.26 with strict gofumpt formatting enforced by CI; use golangci-lint v2.11.4 pinned in Makefile; most dev tools pinned in go.mod via tool directives

Files:

  • internal/api/query.go
  • internal/api/pipes.go
  • internal/api/stream_sse.go
  • internal/config/config.go
  • internal/cache/local.go
  • internal/ingest/bento.go
  • internal/config/config_test.go
  • tests/integration/setup_test.go
internal/api/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

internal/api/**/*.go: Use Chi v5 for HTTP routing in WaveHouse
Use Bearer-token-only CORS posture: never emit Access-Control-Allow-Credentials, rely on Authorization: Bearer headers, not cookies
Schema-driven ingest: POST /v1/ingest/{table} accepts flat JSON body, validates against discovered schema (unknown fields rejected, types checked, nullable constraints enforced), returns 200 immediately after optional dedup + MQ publish
/health returns 200 after first successful schema Refresh; /ready returns 503 with latest diagnostic until Refresh succeeds

Files:

  • internal/api/query.go
  • internal/api/pipes.go
  • internal/api/stream_sse.go
tests/e2e/sdk/**/*.test.ts

📄 CodeRabbit inference engine (AGENTS.md)

E2E tests in tests/e2e/sdk/*.test.ts exercise the full pipeline and use helpers from tests/e2e/sdk/helpers.ts; run with make test-e2e

Files:

  • tests/e2e/sdk/cache.test.ts
internal/api/**/*stream*.go

📄 CodeRabbit inference engine (AGENTS.md)

Use NATS DeliverByStartTime for SSE/WS gap-fill; no in-process ring buffer

Files:

  • internal/api/stream_sse.go
internal/config/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Config struct tags in internal/config/config.go must agree with docs/src/content/docs/configuration.md, config.yaml, and compose env blocks

Files:

  • internal/config/config.go
  • internal/config/config_test.go
internal/cache/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

TieredCache uses golang.org/x/sync/singleflight to prevent cache stampede

Files:

  • internal/cache/local.go
internal/ingest/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

internal/ingest/**/*.go: Ingest is insert-only: the EventMessage wire format carries {table_name, received_timestamp, data} and nothing else; non-insert mutations (DELETE/UPDATE/TRUNCATE) go through POST /v1/admin/query under admin/service role
Per-table batching: the Bento ingest pipeline groups events by table name and performs dynamic INSERTs using schema column order; each table batch is independent
Failed batch inserts are published to a separate NATS stream (WAVEHOUSE_DLQ) with subjects dlq.

to prevent silent data loss; DLQ is controlled by dlq.enabled config
Active Sweeper purges NATS messages that are both ACKed (written to ClickHouse) and older than the gap window

Files:

  • internal/ingest/bento.go
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*_test.go: Use table-driven tests with t.Run(tt.name, ...) for test cases in Go
Use shared mocks from internal/testutil/ (MockPublisher, MockCache, MockDeduplicator, MockSubscriber) instead of creating ad-hoc mocks
Use testutil.NewTestSchemaRegistry(tables) or discovery.NewSchemaRegistryFromMap(tables) for schema-aware tests

Files:

  • internal/config/config_test.go
  • tests/integration/setup_test.go
tests/integration/**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

Integration tests must be marked with //go:build integration and use Docker testcontainers for ClickHouse; E2E tests use the SDK as the primary test harness in tests/e2e/sdk/

Files:

  • tests/integration/setup_test.go
🧠 Learnings (1)
📚 Learning: 2026-05-20T20:30:15.808Z
Learnt from: taitelee
Repo: Wave-RF/WaveHouse PR: 172
File: internal/api/pipes_test.go:106-118
Timestamp: 2026-05-20T20:30:15.808Z
Learning: For WaveHouse pipes authorization allowlist checks, fix the empty-role fail-open behavior by (1) removing any outer guard that prevents allowlist evaluation when the incoming `role` is `""` (e.g., don’t short-circuit with `if role != "" { ... }`), and (2) during allowlist scanning, ensure only non-empty allowlist entries can match—e.g., require `ar != "" && ar == role` (so a malformed allowlist like `["" ]` cannot grant access to an empty incoming role via `"" == ""`).

Applied to files:

  • internal/api/pipes.go
🔇 Additional comments (13)
tests/integration/setup_test.go (1)

165-170: LGTM!

Also applies to: 175-175

tests/e2e/sdk/cache.test.ts (1)

76-76: LGTM!

docs/src/content/docs/architecture.md (1)

194-195: LGTM!

docs/src/content/docs/api.md (1)

577-577: LGTM!

Also applies to: 590-590

internal/ingest/bento.go (3)

331-332: The past review concern about returning success when invalidation fails is acknowledged in the TODO comment. For LocalCache, InvalidateCache always returns nil, so this error path is currently unreachable. The TODO correctly identifies that this must be addressed before adding distributed cache implementations like Redis.


159-183: LGTM!


254-279: LGTM!

internal/cache/local.go (1)

47-50: LGTM!

internal/api/pipes.go (1)

131-132: Known invalidation gap is already tracked.

internal/api/query.go (1)

92-97: LGTM!

Also applies to: 196-197

internal/config/config.go (1)

172-173: LGTM!

internal/config/config_test.go (1)

186-208: ⚡ Quick win

Merge query-timeout boundary checks into a table-driven t.Run test.

internal/config/config_test.go:186-208 has two standalone TestValidate_* cases covering the same clickhouse.query_timeout validation (negative and zero). Combine them into one table-driven test using t.Run(tt.name, ...) per the **/*_test.go testing guideline.

internal/api/stream_sse.go (1)

48-53: LGTM!

@github-actions github-actions Bot removed the dependencies Pull requests that update a dependency file label May 24, 2026
@github-project-automation github-project-automation Bot moved this from In review to In progress in WaveHouse Task Board May 24, 2026
@EricAndrechek
EricAndrechek merged commit 65cb8ad into main May 24, 2026
9 checks passed
@EricAndrechek
EricAndrechek deleted the cache-invalidation branch May 24, 2026 15:07
@github-project-automation github-project-automation Bot moved this from In progress to Done in WaveHouse Task Board May 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/api HTTP handlers, routing, middleware area/cache Local / shared / tiered caching area/docs Documentation, site/, README area/infra CI, build, deploy, Docker, release area/ingest Ingest pipeline (Bento, batching, DLQ) area/query Structured query AST, SQL builder area/sdk TypeScript SDK (clients/ts/) documentation Improvements or additions to documentation go Pull requests that update go code

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

feat(ingest): implement per-table cache invalidation on Bento batch flush perf(cache): query cache returns stale data after writes (read-your-writes)

2 participants