[OPIK-7772] [BE] refactor: make the resolver the only way to name a trace mutation's table - #7953
Conversation
⏱️ pre-commit per-hook timing
⏭️ 42 skipped (no matching files changed)
|
|
No test needed here. Behaviour-preserving: Advisory, from the QA test radar. Nothing here blocks this PR, and anything it proposes is a draft for review. Re-checked after a push on 21 Aug 19:17 UTC — nothing the verdict depends on changed. |
f664c8a to
14239c5
Compare
2a9dd49 to
6422807
Compare
d9a13e7 to
039831f
Compare
6422807 to
28fe5d5
Compare
d8363c5 to
c62abc2
Compare
6904e7b to
e27cf4a
Compare
c62abc2 to
3e6f158
Compare
| void tracesIsAPlainMergeTreePreCutover() { | ||
| // Pinned, not merely "not Distributed": the helper returns "" for a missing table and any other engine | ||
| // (Memory, a plain MergeTree) would have satisfied a negative check, so an absent or wrong table passed. | ||
| assertThat(engineOf("traces")) | ||
| .as("pre-cutover `traces` must be the live ReplicatedReplacingMergeTree") | ||
| .isEqualTo("ReplicatedReplacingMergeTree"); |
There was a problem hiding this comment.
Topology documentation contradicts assertion
MergeTree in the test name and documentation misstates the table topology, while the assertion requires ReplicatedReplacingMergeTree, so the test contract conflicts with the class description — should we rename the test and update the related comments/Javadoc to describe ReplicatedReplacingMergeTree consistently?
Want Baz to fix this for you? Activate Fixer
Other fix methods
Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
apps/opik-backend/src/test/java/com/comet/opik/infrastructure/TracesUnwrappedMutationTest.java
around lines 227-232, rename `tracesIsAPlainMergeTreePreCutover` and update its comments
to consistently describe the asserted `ReplicatedReplacingMergeTree` topology. Also
correct the class-level documentation around lines 50-51 and any related wording so it
no longer calls `traces` a plain `MergeTree` and accurately distinguishes `traces` from
the absent `traces_local` table.
…race mutation's table Post-cutover `traces` is a Distributed table, which rejects mutations (code 36 / 48), so every trace mutation must target the `traces_local` shard once the wrap is live and `traces` while it is not. Routing was a two-branch `<if(distributed_wrap)>traces_local<else>traces<endif>` conditional repeated in every mutation template, plus one site that hand-rolled the same ternary in a StringBuilder — which made a correct new mutation a matter of remembering to copy the branch, and an incorrect one indistinguishable from a correct one at a glance. Funnels the decision into TraceDAOImpl#tracesMutationTable(), the single place the name is chosen: * the mutation templates become topology-agnostic (`DELETE FROM <traces_mutation_table>`), with the resolver binding the resolved name; * deleteForRetentionBounded appends the resolver's result instead of branching on the flag itself — it was the one site outside the resolver reading it. Two guards keep it that way. TraceMutationRoutingArchTest asserts the flag is read in exactly one place and the routing decision made in exactly one place; these rules select the guarded method rather than its callers, so unlike TraceDeletionEventArchTest they deliberately omit allowEmptyShould — an empty selection would mean the method was renamed and the rule had stopped guarding. TraceMutationSqlRoutingTest covers what a call-graph rule cannot see, a mutation that hardcodes a table without consulting the flag at all: it reads the SQL constants reflectively and also scans the source's single-line string literals, which is the form the previous StringBuilder site took. All four rules were verified to fail on injected violations before being committed green, and TracesDistributedWrapMutationTest — which drives the delete and retention paths against a real Distributed wrapper — passes unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e unwrapped branch Review feedback on the mutation-routing PR. All three findings were real. **The bounded retention delete is now a declared template.** It was still built with a StringBuilder, which SKILL.md forbids outright and which hid the statement from both its declaration site and the routing guard that reads these constants. DELETE_FOR_RETENTION_BOUNDED now sits beside its unbounded sibling, with the OR-ed per-workspace predicates as a `getQueryItemPlaceHolder` template loop — the same idiom BATCH_INSERT and the other variable-arity queries in this DAO use. Every value stays bound; the table still comes from the resolver placeholder. **The unwrapped branch of the resolver had no coverage at all.** The reviewer was right, and more sharply than it first appeared: `TracesDistributedWrapMutationTest` only runs with the wrap on, and nothing else in the repository calls `deleteForRetentionBounded` — so the SQL this PR rewrites had no test on the default topology. `TracesUnwrappedMutationTest` is its pre-cutover counterpart: delete-by-id, both retention sweeps, a multi-workspace case that forces the template loop to render more than one branch and its separator, and a guard asserting `traces` is a MergeTree and `traces_local` does not exist, so a mis-routed mutation could not have quietly succeeded. Verified by inverting the resolver's ternary: 4 of 5 tests fail with "Table opik.traces_local does not exist", which is precisely the bug that used to stay green. **The SQL detector missed qualified and quoted targets.** `analytics.traces` reduced to `analytics` and was not flagged, so a qualified mutation escaped the guard entirely. `normalizeTarget` now strips quoting, drops the database qualifier and trims trailing punctuation, with 16 parameterized cases covering qualified, backtick-quoted, double-quoted, upper-case and semicolon-terminated forms, plus the complement that must not be flagged — the resolver placeholder, `traces_local_v2`, `traces_pre_cutover_backup`, `trace_threads` and `spans`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ention delete Review feedback: the multi-workspace test seeded traces in only one workspace, so it rendered the extra OR branches without ever verifying they select correctly. A broken separator, a mis-numbered bind, or a single shared floor would all have passed it. It now seeds two workspaces through the public API and asserts *selective* deletion: both traces sit inside the shared week window, so the toMonday bounds cannot be what separates them — the only thing that can is each workspace's own `:lb_i`. The first workspace's floor sits below its trace, the second's above its own, and one call must delete the first and spare the second. Verified load-bearing by collapsing the per-workspace bind to the first workspace's floor: the test fails on "its workspace's floor sits above this trace, so the same statement must spare it". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… its engine
Review feedback, both fair.
The suite asserts the pre-cutover topology as a *precondition*, so it has to own
that topology rather than inherit whatever a shared container is in. Reuse is
enabled in CI, so a container left wrapped by anything would have failed this
suite for environmental reasons rather than real ones. Now dedicated, non-reused
ClickHouse and ZooKeeper on their own network, matching
TracesDistributedWrapMutationTest, and stopped in afterAll.
The topology guard also accepted too much: `doesNotContain("Distributed")` passes
for the helper's "" sentinel when the table is missing, and for any other engine.
Pinned to ReplicatedReplacingMergeTree, so an absent or wrong table fails.
Renamed deleteForRetentionBoundedAppliesPerWorkspaceLowerBounds — the previous
name mangled the possessive.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3e6f158 to
f084c37
Compare
Details
Stack 3/4 for OPIK-7772 — base #7952. Post-cutover
tracesis aDistributedtable, which rejects mutations (code 36 / 48), so every trace mutation must target thetraces_localshard once the wrap is live andtraceswhile it is not. Routing was a two-branch<if(distributed_wrap)>traces_local<else>traces<endif>conditional repeated in each mutation template, plus one site that hand-rolled the same ternary in aStringBuilder— which made a correct new mutation a matter of remembering to copy the branch, and an incorrect one indistinguishable from a correct one at a glance. This funnels the decision into one method and guards it.TraceDAOImpl#tracesMutationTable()is now the single place the physical table name is decided. The mutation templates become topology-agnostic (DELETE FROM <traces_mutation_table>) and the resolver binds the resolved name.deleteForRetentionBoundedappends the resolver's result instead of branching on the flag itself — it was the one site outside the resolver reading it.TraceMutationRoutingArchTest(ArchUnit) asserts the config flag is read in exactly one place and the routing decision made in exactly one place. These rules select the guarded method rather than its callers, so unlikeTraceDeletionEventArchTestthey deliberately omitallowEmptyShould— an empty selection would mean the method was renamed and the rule had silently stopped guarding.TraceMutationSqlRoutingTestcovers what a call-graph rule cannot see: a mutation that hardcodes a table without consulting the flag at all. It reads the SQL constants reflectively and also scans the source's single-line string literals, which is precisely the form the previousStringBuildersite took.traces, which is theDistributedwrapper post-cutover and theMergeTreebefore it, and is correct either way.Change checklist
Issues
AI-WATERMARK
AI-WATERMARK: yes
TraceDAOrefactor (resolver, two table-name constants, template placeholders, thedeleteForRetentionBoundedcall site) and both guard test classes. The ArchUnit formulation was corrected after the first attempt failed to compile —noCodeUnits()exposes nocallMethod, so the rules useonlyBeCalled().byMethodsThat(...), which is also stricter.Testing
Environment: local, Docker 29.7.2 (linux/aarch64), Corretto 25.0.3, Maven 3.9.9, from
apps/opik-backend.Results:
TracesDistributedWrapMutationTest4/4 unchanged — this is the suite that drives the delete and retention paths against a realDistributedwrapper, so it is the load-bearing regression check for this refactor. New guards 4/4.Scenarios validated:
Distributedtracesovertraces_local. A delete that still hit the wrapper would surface as a 500.TraceDAOImpl#tracesDistributedWrapEnabled()→ routing rule fires, naming the method and line;DatabaseAnalyticsDataModelConfig#tracesDistributedWrapEnabled()directly → flag rule fires;DELETE FROM traces→ constants scan fires, naming the constant;new StringBuilder("DELETE FROM traces_local WHERE (")→ literal scan fires. This is the exact pre-refactor form, so the guard is confirmed to reject the code it replaced.Not run: the full backend suite (CI runs it). No video — non-visual change.
Documentation
None in this PR. The runtime-routing rule is written up in stack 4/4's playbook, and
TraceDAOImpl's Javadoc now points at the guard that enforces it.