Skip to content

refactor(ingest)!: insert-only pipeline; mutations via /v1/query - #164

Merged
EricAndrechek merged 32 commits into
mainfrom
lock-ingest-inserts-only
May 20, 2026
Merged

refactor(ingest)!: insert-only pipeline; mutations via /v1/query#164
EricAndrechek merged 32 commits into
mainfrom
lock-ingest-inserts-only

Conversation

@taitelee

@taitelee taitelee commented May 19, 2026

Copy link
Copy Markdown
Member

Summary

Three-part lockdown of the data path on the way to the alpha release. Each part landed as its own commit on this branch.

1. Lock the Bento ingest pipeline to inserts only. The action: "delete" branch in jsInput.Read is gone; any envelope whose action isn't "insert" (or absent) is DoubleAck'd and dropped. The delete-envelope DLQ shape and the Wave-DLQ-Type header it required are gone too — dlq.<table> now carries only insert-failure payloads, so DLQ consumers no longer need to discriminate.

2. Move raw-SQL passthrough to POST /v1/admin/query under admin gating. Previously /v1/query was registered under /v1's auth middleware with the authorization decision deferred to the handler — any caller (or, with auth.enabled=false, anyone) could submit raw SQL, and a policy.RolePermissions.raw_sql: true grant on any table let non-admins through. The route now lives under /v1/admin/*, gated by the same RequireRole("admin","service") middleware that covers the rest of the admin tree. The policy.RolePermissions.raw_sql field is removed outright — equivalent capability is "issue a JWT with role=admin or role=service."

3. Replace the handler's clickhouse-go Query/Exec dispatch with a thin HTTP proxy. Instead of classifying the leading SQL verb client-side (~150 LOC of isMutation / CTE parser / comment stripper) and routing between driver.Exec and driver.Query, the handler now POSTs the SQL verbatim to ClickHouse's HTTP interface and forwards the response. Wins: multi-statement input works (SELECT 1; TRUNCATE t), any DDL/DML/SYSTEM verb (current or future) works without WaveHouse code changes, ClickHouse's own error messages reach the admin verbatim, no cache, no singleflight, Cache-Control: no-store on every response. The verb-classification primitives still exist (now in internal/api/clickhouse_exec.go) because the structured-query and pipes handlers still need them — clickhouse-go's native Query() errors on no-result-set statements, so explicit Exec-vs-Query routing remains correctness for those paths.

Why this shape: the policy engine authorizes mutations by inspecting the payload of an operation, which works for inserts but doesn't extend to predicate-driven mutations — we can't prove a WHERE clause is satisfiable only for rows the caller can touch, and WHERE 1=1 would otherwise nuke a table. Rather than ship a partial enforcement story, the pipelined surface narrows to what policy can authorize today, and the escape hatch for everything else lives behind admin-equivalent role gating with no per-statement scope check (the role gate is the entire authorization story; admins/service tokens see exactly what ClickHouse sees).

Paired fix (closes #118): the original /v1/query returned HTTP 500 for TRUNCATE/DROP/DELETE/etc. because clickhouse-go's driver.Query() errors on no-result-set statements. The verb-classification fix lived in this branch before the HTTP-proxy rewrite; the proxy now sidesteps the issue entirely (ClickHouse decides what to do natively), but the structured-query/pipes paths still rely on the same primitives via executeCHQuery.

Breaking surface

  • action: "delete" envelopes on ingest.<table> are no longer honored.
  • StartIngestWorker signature drops its chConn driver.Conn parameter (and jsInput.chConn / jsInput.js fields go with it).
  • Wave-DLQ-Type: delete-envelope NATS header is gone. DLQ consumers that branched on it should drop the branch.
  • POST /v1/queryPOST /v1/admin/query. The old URL returns 404.
  • policy.RolePermissions.raw_sql field removed from the policy schema. Operators with raw_sql: true in policy.yaml will see a YAML-load warning for the unknown field but it's otherwise ignored — the capability is now expressed solely via JWT role.
  • Request body to /v1/admin/query no longer accepts a params array. ClickHouse's HTTP interface uses named {name:Type} binding, not positional ?. Inline literals or use the structured query endpoint (POST /v1/tables/{table}/query) for safe binding from user inputs.
  • TypeScript SDK's client.sql(sql, params) drops the params argument.
  • api.NewQueryHandler signature is (endpoint, username, password, database string), replacing (driver.Conn).

Test plan

  • TestJsInput_Read_NonInsertActionRejected: table-driven across non-insert verbs; asserts DoubleAck (never Nak) and that Read advances.
  • TestNewRouter_RawSQLAdminGate: admin reaches handler, service reaches handler, viewer → 403, no-role+auth-off → reaches handler, no-role+auth-on → 401.
  • TestQueryHandler_* (proxy contract): SQL forwarded verbatim with the correct query-string params and headers, {meta,data,rows} envelope extracted to bare data array, empty-body mutation → [], ClickHouse error forwarded with its own message, Cache-Control: no-store set, no auth headers when blank, raw forward on unexpected non-JSON 200 body, request context cancellation propagates to upstream.
  • TestExecuteCHQuery_MutationRoutesToExec / SelectRoutesToQuery (in clickhouse_exec_test.go): keeps the still-required structured-query + pipes Exec/Query dispatch covered.
  • TestIngest_NonInsertActionDropped: publishes a delete envelope to JetStream after a successful insert; asserts the row stays and nothing leaks to the DLQ.
  • TestQuery_TruncateReturnsEmptyArray: pins bug: TRUNCATE via /v1/query returns HTTP 500 #118 end-to-end through the proxy now (HTTP 200 + [], row actually disappears).
  • TestQuery_DeleteReturnsEmptyArray: predicate-driven DELETE through /v1/admin/query; targeted row goes, sibling row stays. Updated to inline the literal since positional ? is gone.
  • SDK tests: client.sql() posts to /v1/admin/query and never sends a params field.
  • make verify, make test-unit, make test-sdk, go build -tags=integration ./..., go vet -tags=integration ./... all green locally before push.

Related Issues

Summary by CodeRabbit

  • Breaking Changes

    • Ingest is now insert-only: non-"insert" actions are dropped (acknowledged). Non-insert mutations/DDL must use POST /v1/admin/query. SDK sql() no longer accepts positional params; role permission raw_sql removed.
  • New Behavior

    • POST /v1/admin/query proxies SQL to ClickHouse over HTTP, returns [] for successful mutations/DDL, preserves non-JSON formats, enforces 30s timeout, request/response size caps, and sets Cache-Control/X-Content-Type-Options.
  • Tests & Docs

    • Tests and documentation updated to reflect ingest, proxy, auth, DLQ, cache, and SDK contract changes.

Review Change Stack

@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/docs Documentation, site/, README labels May 19, 2026
@github-actions
github-actions Bot requested a review from EricAndrechek May 19, 2026 02:21
@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Move raw SQL to an admin/service-gated ClickHouse HTTP proxy, add native ClickHouse exec/query dispatch and deterministic query cache-keying, convert Bento ingest to insert-only with DoubleAck-drop on non-insert actions, and update router, policy, SDK, tests, integration wiring, and documentation to match.

Changes

Admin HTTP proxy & tests

Layer / File(s) Summary
Admin HTTP proxy and tests
internal/api/query.go, internal/api/query_test.go
Replaces handler with an HTTP ClickHouse proxy accepting { "sql" }, enforces strict JSON validation, request/response size and time caps, security headers, upstream header/query-params forwarding, response shaping (return upstream data or [], passthrough non-JSON FORMAT), error/status mapping (ClickHouse 4xx→400, others→502), and adds validation & proxy contract tests.

Native ClickHouse exec & cache key

Layer / File(s) Summary
Native exec, mutation classification, transforms
internal/api/clickhouse_exec.go, internal/api/clickhouse_exec_test.go
Adds executeCHQuery to dispatch Exec vs Query using isMutation and CTE-aware scanning, reflectively scans rows, normalizes UUID/time, and includes unit tests for classification and transforms.
Deterministic query cache key
internal/api/cache_key.go, internal/api/cache_key_test.go
Introduces framed SHA-256 queryCacheKey(sql, params) to avoid boundary collisions and regression tests validating determinism and non-collisions.

Pipes / structured queries

Layer / File(s) Summary
Singleflight/cache integration
internal/api/pipes.go, internal/api/structured_query.go
Switches structured/pipes execution to call executeCHQuery directly while preserving singleflight/cache semantics and X-Cache HIT/MISS behavior.

Router, policy, SDK & clients

Layer / File(s) Summary
Router & admin gate
internal/api/router.go, internal/api/router_test.go
Moves raw SQL route under /v1/admin/*, applies shared admin/service RequireRole middleware to the admin subtree, and adds router-level gating tests.
Policy model
internal/policy/policy.go, internal/policy/policy_test.go
Removes RawSQL from RolePermissions/ResolvedPermissions and updates evaluation/tests to drop RawSQL assertions.
TS SDK / client
clients/ts/src/sql.ts, clients/ts/src/client.ts, clients/ts/src/*test.ts
Drops positional params argument, always POSTs { sql } to /v1/admin/query, documents admin/service requirement and lack of positional binding, adds runtime migration guard, and updates tests to assert single-field JSON body.

Ingest worker (Bento) changes

Layer / File(s) Summary
Insert-only ingest worker
internal/ingest/bento.go, internal/ingest/bento_test.go
Enforces insert-only EventMessage ({table_name, received_timestamp, data}), DoubleAck-drops non-"insert" actions, removes delete execution/drain logic, simplifies batch ack behavior, and updates tests for DLQ/DoubleAck behavior.
Integration test removals
tests/integration/dlq_test.go, tests/integration/ingest_test.go
Removes end-to-end delete-path integration tests and related helpers now irrelevant under insert-only ingest.

cmd wiring, integration and e2e tests

Layer / File(s) Summary
Main wiring & integration
cmd/wavehouse/main.go, tests/integration/setup_test.go
Start ingest worker without native chConn, build server with ClickHouse HTTP endpoint via NewQueryHandler, and update integration setup to use full ClickHouse instance wiring.
Integration contract test
tests/integration/query_test.go
Adds TestQuery_MutationsReturnEmptyArray asserting /v1/admin/query returns HTTP 200 with [] for mutation/DDL success and verifies effects in ClickHouse.
E2E SDK updates
tests/e2e/sdk/*
Remove raw_sql permission from test policies and adjust e2e setup/comments to reflect admin gating.

Docs, changelog, README, security, and checklist

Layer / File(s) Summary
Docs & changelog
docs/src/content/docs/*, CHANGELOG.md, AGENTS.md
Document insert-only ingest, admin raw-SQL proxy semantics (headers, caps, error mapping, passthrough, no-cache), DLQ payload rules, and API/SDK breaking changes.
Security/style/checklist
.github/prompts/pr-review.md, .gemini/styleguide.md, SECURITY.md, README.md
Scope raw-SQL security guidance to /v1/admin/query gated by admin/service role and update PR checklist and README quickstart notes.

Sequence Diagram (admin proxy flow)

sequenceDiagram
  participant Client
  participant WaveHouseAPI
  participant ClickHouse
  Client->>WaveHouseAPI: POST /v1/admin/query { "sql": "..." }
  WaveHouseAPI->>ClickHouse: HTTP POST (body=raw SQL, X-ClickHouse-User/Key, params=default_format=JSON,date_time_output_format=iso)
  ClickHouse-->>WaveHouseAPI: 200 + JSON { "data": [...] } OR 200 + raw body OR non-200 + plain text
  WaveHouseAPI->>Client: 200 + application/json (data) OR 200 + [] OR pass-through raw Content-Type OR JSON error (mapped 400/502)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • Wave-RF/WaveHouse#122 — Prior work on jsInput/delete/DLQ semantics related to ingest delete handling.

"I hopped through code and schema, carrot in paw,
Insert-only paths now guard the raw SQL door.
Admin gate hums, ClickHouse listens near,
DLQs quieted, tests sing — a tidy cheer! 🐰"

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lock-ingest-inserts-only

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

@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 significantly refactors the data mutation strategy by enforcing an insert-only ingest pipeline and centralizing all other mutation operations to the /v1/query endpoint. This change enhances security by ensuring that predicate-driven mutations are handled through an explicitly admin-gated interface, addressing limitations in the policy engine's ability to authorize complex WHERE clauses. Additionally, it rectifies an issue where /v1/query would incorrectly return HTTP 500 for successful mutation statements, providing a consistent and expected response for all SQL operations.

Highlights

  • Ingest Pipeline is now Insert-Only: The Bento ingest pipeline (/v1/ingest/{table}) has been locked down to accept only 'insert' actions. Any other action (e.g., 'delete', 'update', 'truncate') will be DoubleAck'd and dropped, preventing unintended mutations through this path.
  • Mutations Centralized to /v1/query: All non-insert mutations (such as DELETE, UPDATE, TRUNCATE, DROP, ALTER, REPLACE) must now be performed exclusively through the POST /v1/query endpoint. This endpoint is gated by admin/service roles or policies with RawSQL: true for enhanced security and control.
  • /v1/query Handles Mutations Correctly: The /v1/query endpoint now correctly routes mutation and DDL statements through driver.Exec and returns an HTTP 200 status with an empty JSON array ([]) on success, resolving issue bug: TRUNCATE via /v1/query returns HTTP 500 #118. Previously, these operations would result in an HTTP 500 error.
  • Removed Delete-Specific Logic and DLQ Header: The ingest pipeline's internal logic for handling 'delete' actions has been removed, along with the chConn driver.Conn parameter from StartIngestWorker. Consequently, the Wave-DLQ-Type: delete-envelope NATS header is no longer used, and the DLQ (dlq.<table>) now exclusively carries insert-failure payloads.
  • Enhanced SQL Parsing for /v1/query: New logic has been added to /v1/query to strip leading whitespace and SQL comments before classifying the leading SQL verb, ensuring accurate routing of statements to either driver.Query (for reads) or driver.Exec (for mutations).
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.

@taitelee taitelee changed the title refactor(ingest)!: lock pipeline to inserts only; mutations require /v1/query refactor(ingest)!: insert-only pipeline; mutations via /v1/query May 19, 2026
@coderabbitai coderabbitai Bot added the area/query Structured query AST, SQL builder label May 19, 2026
@taitelee taitelee linked an issue May 19, 2026 that may be closed by this pull request
6 tasks
@github-actions github-actions Bot removed the area/query Structured query AST, SQL builder label May 19, 2026
Comment thread internal/api/query.go Outdated
Comment thread tests/integration/ingest_test.go Outdated
@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown

1 [MUST], 1 [SHOULD], 0 [MAY] — see inline threads for detail.

Iterate — fix the mutation caching bug before merge.

The isMutation routing in executeQuery is correct, but Handle wraps it with the same TieredCache/singleflight path used for SELECTs. A TRUNCATE TABLE t executed once caches [] for DefaultTTL; a second identical request within that window hits the cache and returns success without touching ClickHouse. Same for INSERT, DROP, and any other mutation verb routed through /v1/query. Details and a concrete fix sketch are in the inline thread on internal/api/query.go:116.

The integration test quality note (tests/integration/ingest_test.go:125) is a [SHOULD] and not a blocker.

@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: 2

Caution

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

⚠️ Outside diff range comments (2)
internal/api/query.go (1)

74-104: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Mutation SQL is being cached and singleflight-collapsed before execution

Line 74 through Line 104 applies cache lookup/write and singleflight to all SQL, including mutating statements now routed via /v1/query. That can cause identical DELETE/UPDATE/TRUNCATE/... requests to return cached [] (or share one in-flight result) without executing every call, which is unsafe for write semantics.

Suggested fix
@@
 	if req.SQL == "" {
 		writeJSONError(w, http.StatusBadRequest, "missing sql")
 		return
 	}
+	isMut := isMutation(req.SQL)
 
 	cacheKey := queryCacheKey(req.SQL, req.Params)
 
-	// Try cache.
-	if h.Cache != nil {
+	// Try cache for read-only queries only.
+	if !isMut && h.Cache != nil {
 		if data, _, err := h.Cache.Get(r.Context(), cacheKey); err == nil && data != nil {
 			w.Header().Set("Content-Type", "application/json")
 			w.Header().Set("X-Cache", "HIT")
 			_, _ = w.Write(data)
 			return
 		}
 	}
 
-	// Execute query with singleflight to protect ClickHouse from thundering herds.
-	v, err, _ := h.sf.Do(cacheKey, func() (interface{}, error) {
+	exec := func() (interface{}, error) {
 		queryCtx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
 		defer cancel()
 		result, err := h.executeQuery(queryCtx, req.SQL, req.Params)
 		if err != nil {
 			return nil, err
@@
-		if h.Cache != nil {
+		if !isMut && h.Cache != nil {
 			_ = h.Cache.Set(r.Context(), cacheKey, data, h.DefaultTTL)
 		}
 		return data, nil
-	})
+	}
+
+	var (
+		v   interface{}
+		err error
+	)
+	if isMut {
+		v, err = exec()
+	} else {
+		// Read-only queries only: collapse duplicate in-flight work.
+		v, err, _ = h.sf.Do(cacheKey, exec)
+	}
 	if err != nil {
 		writeJSONError(w, http.StatusInternalServerError, err.Error())
 		return
 	}
@@
-	w.Header().Set("X-Cache", "MISS")
+	if isMut {
+		w.Header().Set("X-Cache", "BYPASS")
+	} else {
+		w.Header().Set("X-Cache", "MISS")
+	}
 	_, _ = w.Write(v.([]byte))
 }
🤖 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 `@internal/api/query.go` around lines 74 - 104, The current flow uses
queryCacheKey, h.Cache and h.sf.Do for every req.SQL including mutating
statements; change it so mutating SQL (e.g., statements starting with INSERT,
UPDATE, DELETE, TRUNCATE, ALTER, CREATE, DROP, MERGE, etc.) bypass both the
cache and singleflight. Add a small helper (e.g., isMutatingSQL(sql string)
bool) and in the handler branch: if isMutatingSQL(req.SQL) then call
h.executeQuery with a timed context (like queryCtx used now), json.Marshal the
result and write the response directly without calling h.sf.Do, without reading
from or writing to h.Cache (and optionally set X-Cache: MISS), otherwise keep
the existing singleflight + cache behavior that uses h.sf.Do, queryCacheKey,
DefaultTTL and Cache.Set.
internal/ingest/bento_test.go (1)

183-213: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tests currently encode legacy “missing action is allowed” behavior

TestJsInput_Read_InsertMessage uses an envelope with no action, and TestJsInput_Read_NonInsertActionRejected omits an empty/missing-action case. That leaves the insert-only contract unpinned for the “absent action” path.

Please make insert-path tests send explicit "action":"insert" and add rejection coverage for missing/empty action (DoubleAck + drop + advance-to-next-insert).

As per coding guidelines: internal/ingest/*.go: “The ingest pipeline is insert-only: envelopes with any action other than 'insert' (or absent) are DoubleAck'd and dropped”.

Also applies to: 722-748

🤖 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 `@internal/ingest/bento_test.go` around lines 183 - 213, Update the insert-path
test to explicitly set "action":"insert" in the EventMessage used by
TestJsInput_Read_InsertMessage (modify the evt variable in that test) so the
insert contract is pinned, and add/extend a test (e.g.,
TestJsInput_Read_MissingOrEmptyActionRejected) that sends an envelope with no
action or an empty action string and asserts the ingest pipeline rejects it:
Read should return a nil message, the returned ack function should DoubleAck the
underlying bentoMockMsg (check natsMsg.doubleAcked == true), the message should
be dropped (no inFlight increment on jsInput.inFlight), and processing should
advance to the next message; reference jsInput.Read,
TestJsInput_Read_InsertMessage, and the bentoMockMsg.doubleAcked flag when
locating changes.
🤖 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.

Inline comments:
In `@docs/src/content/docs/api.md`:
- Line 123: The sentence under the "Insert-only." paragraph is too long; split
the second sentence into two for readability by breaking at the semicolon (after
"`REPLACE`, etc.") and making the second sentence start with "These mutations
must be issued..." while preserving the reference to `POST /v1/query`, the
`admin` / `service` role (or policy role with `RawSQL: true`), and the
explanation that the policy engine authorizes mutations by inspecting written
columns and cannot handle predicate-driven mutations like `DELETE … WHERE`; keep
all technical terms and links intact (e.g., the `POST /v1/query` anchor and
`RawSQL: true`) and ensure punctuation and capitalization are adjusted for two
clear sentences.

In `@internal/ingest/bento.go`:
- Around line 121-122: The current check allows an empty raw.Action as valid;
update the condition so any action other than the literal "insert" — including
the empty string — is rejected and DoubleAck'd. Locate the branch using
raw.Action and slog.WarnContext (the code that logs "rejecting non-insert
message: ingest pipeline is insert-only") and change the if condition to treat
raw.Action == "" the same as other non-"insert" values (e.g., if raw.Action !=
"insert" { ... } or explicitly if raw.Action == "" || raw.Action != "insert" {
... }), then ensure the existing DoubleAck/drop logic is executed for that case.

---

Outside diff comments:
In `@internal/api/query.go`:
- Around line 74-104: The current flow uses queryCacheKey, h.Cache and h.sf.Do
for every req.SQL including mutating statements; change it so mutating SQL
(e.g., statements starting with INSERT, UPDATE, DELETE, TRUNCATE, ALTER, CREATE,
DROP, MERGE, etc.) bypass both the cache and singleflight. Add a small helper
(e.g., isMutatingSQL(sql string) bool) and in the handler branch: if
isMutatingSQL(req.SQL) then call h.executeQuery with a timed context (like
queryCtx used now), json.Marshal the result and write the response directly
without calling h.sf.Do, without reading from or writing to h.Cache (and
optionally set X-Cache: MISS), otherwise keep the existing singleflight + cache
behavior that uses h.sf.Do, queryCacheKey, DefaultTTL and Cache.Set.

In `@internal/ingest/bento_test.go`:
- Around line 183-213: Update the insert-path test to explicitly set
"action":"insert" in the EventMessage used by TestJsInput_Read_InsertMessage
(modify the evt variable in that test) so the insert contract is pinned, and
add/extend a test (e.g., TestJsInput_Read_MissingOrEmptyActionRejected) that
sends an envelope with no action or an empty action string and asserts the
ingest pipeline rejects it: Read should return a nil message, the returned ack
function should DoubleAck the underlying bentoMockMsg (check natsMsg.doubleAcked
== true), the message should be dropped (no inFlight increment on
jsInput.inFlight), and processing should advance to the next message; reference
jsInput.Read, TestJsInput_Read_InsertMessage, and the bentoMockMsg.doubleAcked
flag when locating changes.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 17e11d35-e61a-4283-b7e0-62009163f6a2

📥 Commits

Reviewing files that changed from the base of the PR and between e8b2870 and cd2a8ba.

📒 Files selected for processing (13)
  • AGENTS.md
  • CHANGELOG.md
  • cmd/wavehouse/main.go
  • docs/src/content/docs/api.md
  • docs/src/content/docs/architecture.md
  • internal/api/query.go
  • internal/api/query_test.go
  • internal/ingest/bento.go
  • internal/ingest/bento_test.go
  • tests/integration/dlq_test.go
  • tests/integration/ingest_test.go
  • tests/integration/query_test.go
  • tests/integration/setup_test.go
💤 Files with no reviewable changes (3)
  • cmd/wavehouse/main.go
  • tests/integration/dlq_test.go
  • tests/integration/setup_test.go

Comment thread docs/src/content/docs/api.md Outdated
Comment thread internal/ingest/bento.go Outdated
@github-project-automation github-project-automation Bot moved this from Backlog to In review in WaveHouse Task Board May 19, 2026

@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 transitions the ingest pipeline to an insert-only model, removing inline delete handling and requiring all non-insert mutations (DELETE, UPDATE, TRUNCATE, etc.) to be performed via the /v1/query endpoint. The query handler is updated to classify SQL verbs and route mutations through Exec to return an empty JSON array instead of an error. Feedback identifies a critical caching bug where mutation results could be incorrectly persisted in the API layer, suggests extending SQL comment stripping to support the # prefix used in ClickHouse, and notes a minor typo in the documentation comments. Iterate.

Comment thread internal/api/query.go Outdated
Comment thread internal/api/query.go Outdated
Comment thread internal/api/query.go Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 19, 2026
@coderabbitai coderabbitai Bot added the area/query Structured query AST, SQL builder label May 19, 2026
@github-actions github-actions Bot removed the area/query Structured query AST, SQL builder label May 19, 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: 2

🤖 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.

Inline comments:
In `@internal/api/query_test.go`:
- Around line 171-217: Add table-driven test rows in TestIsMutation to cover the
remaining mutation verbs ATTACH, DETACH, KILL, SET, and USE: for each verb add
at least one case (e.g., "ATTACH TABLE t", "DETACH TABLE t", "KILL QUERY 1",
"SET x = 1", "USE db") and assert they return true via isMutation; place them
alongside the other true cases so the test exercises leading whitespace/comments
behavior similarly. Ensure the test names are unique (e.g., "attach", "detach",
"kill", "set", "use") and follow the existing t.Run/t.Parallel pattern.

In `@tests/integration/ingest_test.go`:
- Around line 86-87: The test's context timeout created via context.WithTimeout
(ctx, 60*time.Second) is too short for two sequential require.Eventually blocks
plus HTTP/JetStream/DB overhead; update the timeout duration to a larger value
(e.g., 120s or 180s) by changing the call to context.WithTimeout where ctx and
cancel are defined so the test has sufficient headroom for both Eventually
blocks and external delays.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 8dd924a8-767c-46d3-8874-f1cd88a438ee

📥 Commits

Reviewing files that changed from the base of the PR and between cd2a8ba and 50af71d.

📒 Files selected for processing (9)
  • AGENTS.md
  • CHANGELOG.md
  • docs/src/content/docs/api.md
  • docs/src/content/docs/architecture.md
  • internal/api/query.go
  • internal/api/query_test.go
  • internal/ingest/bento.go
  • internal/ingest/bento_test.go
  • tests/integration/ingest_test.go

Comment thread internal/api/query_test.go Outdated
Comment thread tests/integration/ingest_test.go Outdated
Comment thread docs/src/content/docs/architecture.md Outdated
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/docs Documentation, site/, README area/infra CI, build, deploy, Docker, release area/ingest Ingest pipeline (Bento, batching, DLQ) area/policy Access control policies (Hasura-style) area/sdk TypeScript SDK (clients/ts/) documentation Improvements or additions to documentation go Pull requests that update go code

Projects

Archived in project

2 participants