Skip to content

fix(server): aggregate validated run statistics in BigInt, not SQL - #221

Closed
hyperpolymath wants to merge 5 commits into
mainfrom
arena/0ff97d53-metamanifold-webui
Closed

hyperpolymath wants to merge 5 commits into
mainfrom
arena/0ff97d53-metamanifold-webui

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

The failure

main's Julia tests job fails in step Run validated-statistics tests first, and the run-statistics testset is the reason:

Read-only run statistics and BigInt aggregation: Test Failed at test/unit/test_validated_statistics.jl:180
  Expression: response.status == 200
   Evaluated: 500 == 200

POST /api/v1/studies/{study}/runs/{run}/statistics/summaries builds

SELECT <feature> AS feature, "s1", "s2", ... FROM "merged" GROUP BY <feature>

and DuckDB refuses to bind it:

Binder Error: column "s1" must appear in the GROUP BY clause or must be part of an aggregate function.

Nothing in _statistics_guard maps a DuckDB.BinderException, so it left the handler as an uncaught exception and Oxygen answered 500. Reproduced directly against DuckDB with the fixture's schema (SeqName VARCHAR, Genus VARCHAR, Pident DOUBLE, s1 BIGINT, s2 HUGEINT, Empty BIGINT) and the generated SQL.

Why not just add the columns to GROUP BY, or SUM them

Adding the count columns to GROUP BY would fix the binder error but turn the endpoint's output into a deduplication of rows rather than an aggregate. SUM is what the endpoint exists to avoid: SUM over BIGINT/HUGEINT is evaluated in Int64/Int128, and the regression fixture deliberately has two rows carrying 2^126 reads, whose sample total is 2^127 — one past typemax(Int128). The test asserts exactly that (s2.counts[1] == string(big(2)^127), s2.total == string(big(2)^127 + 1)).

The fix

The query now projects the rows and only orders them by feature; every count is added by the existing Julia accumulation, in BigInt:

  • one row per input row, ORDER BY feature, so equal labels are adjacent and the accumulator does not depend on row order;
  • per-feature sums live in BigInt from the first addition, so no fixed-width boundary is ever crossed;
  • the MAX_CELLS cap now bounds source rows as well as the features × samples result, and the cell check happens while accumulating, so an oversized result is refused before it is allocated;
  • the misattached docstring ("Only GROUP BY is done in SQL") is replaced and now sits on the function it describes, next to an explanation of why SQL arithmetic is not used here.

The null/negative/float rejection paths keep their existing error types, so merged_null → 422, merged_negative → 400 and merged_float → 422 are unchanged.

Verification

  • tree-sitter parse of the file: no errors.
  • The DuckDB binder error is reproduced and the replacement projection verified against DuckDB 2.0 with the fixture's schema.
  • No Julia toolchain is available in this environment, so the suite itself is verified by CI on this PR; the assertions in test/unit/test_validated_statistics.jl (counts [3, 4], 2^127, "3/7", partial, persistence and idempotency) all follow from the accumulation and the unchanged exact_summaries storage path.

Fixes the failing Julia tests job on main (Occupancy disagreement test fails only as its downstream gate).

`POST /api/v1/studies/{study}/runs/{run}/statistics/summaries` answered every
request with 500 on a table that had more than one row per feature. The query
was

    SELECT <feature>, "s1", "s2", ... FROM "merged" GROUP BY <feature>

which DuckDB refuses to bind: the count columns are neither grouped nor
aggregated ("column \"s1\" must appear in the GROUP BY clause or must be part
of an aggregate function"). Nothing mapped a binder error, so it left the
handler as an uncaught exception.

Aggregating those columns in SQL is not an option. The endpoint exists to keep
counts exact, and `SUM` over BIGINT/HUGEINT is evaluated in Int64/Int128: a
merged table with two rows carrying 2^126 reads per sample sums to 2^127, one
past `typemax(Int128)`, which is exactly what the regression fixture asserts.

The query now projects the rows, orders them by feature, and the existing Julia
accumulation sums equal labels into `BigInt`, so no arithmetic crosses a
fixed-width boundary. Row and cell limits are unchanged in effect: the same
`MAX_CELLS` cap that bounded grouped features now bounds source rows and the
per-feature totals, and the cell check moves inside the accumulation so an
oversized result is refused before it is allocated.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-23T01:16:13.568122Z
We couldn’t complete this review. Comment @codex review to try again.
e681237 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 40 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: ce56d183-a40c-4f7f-b969-b30420da586c
📥 Commits

Reviewing files that changed from the base of the PR and between 7bb716d and c1848ae.

📒 Files selected for processing: 10
  • .github/workflows/ci.yml
  • src/analysis/clade_cumulus.jl
  • src/pipeline/phylogeny.jl
  • src/server/routes/clade_cumulus.jl
  • src/server/routes/trees.jl
  • src/server/routes/validated_statistics.jl
  • src/server/server.jl
  • test/unit/test_clade_cumulus.jl
  • test/unit/test_exact_summaries.jl
  • test/unit/test_validated_statistics.jl
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Fork governance

Lane Status Run
DEED syntax (repo deed) 🟢 conformant run 39891424151
Hypatia verdict grid purple (job: success) same run

Hypatia grid purple

🟣 critical · commit c1848ae5a128 · run 39891424151

Reporter Status Critical High Medium Minor Since previous revision
Hypatia scan 🟣 critical 47 63 32 0 ↔ unchanged
panic-attack assail 🔴 high 0 6 13 0 ↔ unchanged
pons asinorum 🟡 medium 0 0 10 0 ↔ unchanged
rhodibot (RSR compliance) 🟠 indeterminate 0 0 0 0 — indeterminate
panicbot (panic recovery, error handling) 🟠 indeterminate 0 0 0 0 — indeterminate
echidnabot (formal verification: proofs/) 🟠 indeterminate 0 0 0 0 — indeterminate
seambot (integration health: API routes) 🟠 indeterminate 0 0 0 0 — indeterminate
finishingbot (release readiness) 🟠 indeterminate 0 0 0 0 — indeterminate
accessibilitybot (WCAG on the React UI) 🟠 indeterminate 0 0 0 0 — indeterminate
glambot (presentation quality) 🟠 indeterminate 0 0 0 0 — indeterminate
cipherbot ⚪ not applicable 0 0 0 0 ↔ unchanged
sustainabot ⚪ not applicable 0 0 0 0 ↔ unchanged
Hypatia scan: 142 finding(s)
  • critical cicd_rules/banned_language_file — frontend/src/views/run/parseFilterYaml.ts — TypeScript file detected -- banned language
  • critical cicd_rules/banned_language_file — frontend/src/figure/encode.ts — TypeScript file detected -- banned language
  • critical cicd_rules/banned_language_file — frontend/src/figure/charts.ts — TypeScript file detected -- banned language
  • critical cicd_rules/banned_language_file — frontend/src/figure/export.ts — TypeScript file detected -- banned language
  • critical cicd_rules/banned_language_file — frontend/src/figure/figure.test.ts — TypeScript file detected -- banned language
  • critical cicd_rules/banned_language_file — frontend/src/figure/legend.ts — TypeScript file detected -- banned language
  • critical cicd_rules/banned_language_file — frontend/src/figure/types.ts — TypeScript file detected -- banned language
  • critical cicd_rules/banned_language_file — frontend/src/utils/download.ts — TypeScript file detected -- banned language
  • critical cicd_rules/banned_language_file — frontend/src/utils/text.test.ts — TypeScript file detected -- banned language
  • critical cicd_rules/banned_language_file — frontend/src/utils/timeago.ts — TypeScript file detected -- banned language
  • critical cicd_rules/banned_language_file — frontend/src/utils/text.ts — TypeScript file detected -- banned language
  • critical cicd_rules/banned_language_file — frontend/src/hooks/useUnsavedGuard.ts — TypeScript file detected -- banned language
  • critical cicd_rules/banned_language_file — frontend/src/hooks/useApi.test.ts — TypeScript file detected -- banned language
  • critical cicd_rules/banned_language_file — frontend/src/hooks/useApi.ts — TypeScript file detected -- banned language
  • critical cicd_rules/banned_language_file — frontend/src/hooks/useAnalysis.ts — TypeScript file detected -- banned language
  • critical cicd_rules/banned_language_file — frontend/src/hooks/useSSE.ts — TypeScript file detected -- banned language
  • critical cicd_rules/banned_language_file — frontend/src/hooks/useTabParam.ts — TypeScript file detected -- banned language
  • critical cicd_rules/banned_language_file — frontend/src/hooks/useNavRefresh.ts — TypeScript file detected -- banned language
  • critical cicd_rules/banned_language_file — frontend/src/hooks/useJobEvents.ts — TypeScript file detected -- banned language
  • critical cicd_rules/banned_language_file — frontend/src/about.ts — TypeScript file detected -- banned language
  • 122 more; see the structured report in the artifact.
panic-attack assail: 19 finding(s)
  • high DynamicCodeExecution — src/pipeline/dada2/context.jl — eval/Meta.parse in src/pipeline/dada2/context.jl
  • high DynamicCodeExecution — src/analysis/analysis.jl — eval/Meta.parse in src/analysis/analysis.jl
  • high DynamicCodeExecution — src/analysis/differential.jl — eval/Meta.parse in src/analysis/differential.jl
  • high DynamicCodeExecution — src/core/provenance.jl — eval/Meta.parse in src/core/provenance.jl
  • high DynamicCodeExecution — scripts/check-julia-syntax.jl — eval/Meta.parse in scripts/check-julia-syntax.jl
  • high DynamicCodeExecution — test/unit/test_permanova.jl — eval/Meta.parse in test/unit/test_permanova.jl
  • medium UnsafeFFI — src/core/memory_probe.jl — 3 ccall/FFI calls in src/core/memory_probe.jl
  • medium InputBoundary — src/server/routes/runs.jl — 2 JSON3.read/JSON.parse call(s) with 0 try block(s) in src/server/routes/runs.jl — these throw on malformed input; wrap in try/catch
  • medium InputBoundary — src/server/routes/composition.jl — 6 JSON3.read/JSON.parse call(s) with 2 try block(s) in src/server/routes/composition.jl — these throw on malformed input; wrap in try/catch
  • medium InputBoundary — src/server/routes/studies.jl — 4 JSON3.read/JSON.parse call(s) with 2 try block(s) in src/server/routes/studies.jl — these throw on malformed input; wrap in try/catch
  • medium InputBoundary — src/server/routes/analysis.jl — 9 JSON3.read/JSON.parse call(s) with 3 try block(s) in src/server/routes/analysis.jl — these throw on malformed input; wrap in try/catch
  • medium UnsafeFFI — install.jl — 1 ccall/FFI calls in install.jl
  • medium ProofDrift — test/unit/test_occupancy_grade.jl — 2 `@test x isa Y` assertion(s) with no value check — type-only tests may substitute for an unwritten formal theorem in test/unit/test_occupancy_grade.jl
  • medium ProofDrift — test/unit/test_evidence.jl — 1 `@test x isa Y` assertion(s) with no value check — type-only tests may substitute for an unwritten formal theorem in test/unit/test_evidence.jl
  • medium InputBoundary — bench/advanced_zero_handling/benchmark.jl — 1 JSON3.read/JSON.parse call(s) with 0 try block(s) in bench/advanced_zero_handling/benchmark.jl — these throw on malformed input; wrap in try/catch
  • medium InputBoundary — frontend/src/figure/charts.ts — 1 JSON.parse call(s) with 0 try block(s) in frontend/src/figure/charts.ts — JSON.parse throws SyntaxError on malformed input; wrap in try-catch
  • medium InputBoundary — frontend/src/tree/model.ts — 1 JSON.parse call(s) with 0 try block(s) in frontend/src/tree/model.ts — JSON.parse throws SyntaxError on malformed input; wrap in try-catch
  • medium InputBoundary — frontend/src/api/events.ts — 2 JSON.parse call(s) with 0 try block(s) in frontend/src/api/events.ts — JSON.parse throws SyntaxError on malformed input; wrap in try-catch
  • medium InputBoundary — frontend/src/components/alphaMetrics.tsx — 1 JSON.parse call(s) with 0 try block(s) in frontend/src/components/alphaMetrics.tsx — JSON.parse throws SyntaxError on malformed input; wrap in try-catch
pons asinorum: 10 finding(s)
  • medium string-concat-in-loop — ./frontend/src/tree/model.ts:122 — string built by repeated concatenation in a loop (T0, HEURISTIC)
  • medium string-concat-in-loop — ./frontend/src/tree/model.ts:118 — string built by repeated concatenation in a loop (T0, HEURISTIC)
  • medium string-concat-in-loop — ./frontend/src/tree/TreeShapes.tsx:69 — string built by repeated concatenation in a loop (T0, HEURISTIC)
  • medium string-concat-in-loop — ./frontend/src/tree/TreeShapes.tsx:66 — string built by repeated concatenation in a loop (T0, HEURISTIC)
  • medium string-concat-in-loop — ./frontend/src/tree/TreeShapes.tsx:61 — string built by repeated concatenation in a loop (T0, HEURISTIC)
  • medium string-concat-in-loop — ./frontend/src/tree/TreeShapes.tsx:53 — string built by repeated concatenation in a loop (T0, HEURISTIC)
  • medium string-concat-in-loop — ./frontend/src/tree/TreeShapes.tsx:43 — string built by repeated concatenation in a loop (T0, HEURISTIC)
  • medium string-concat-in-loop — ./frontend/src/tree/TreeShapes.tsx:42 — string built by repeated concatenation in a loop (T0, HEURISTIC)
  • medium unreachable-after-jump — ./frontend/src/figure/PaneEditor.tsx:45 — statement is unreachable after an unconditional jump (T0, HEURISTIC)
  • medium unreachable-after-jump — ./frontend/src/components/PublicationTablesPanel.tsx:208 — statement is unreachable after an unconditional jump (T0, HEURISTIC)
rhodibot (RSR compliance): not_run — enrolled, but no report-back was received
  • 🟠 indeterminate: enrolled, but no report-back was received
panicbot (panic recovery, error handling): not_run — enrolled, but no report-back was received
  • 🟠 indeterminate: enrolled, but no report-back was received
echidnabot (formal verification: proofs/): not_run — enrolled, but no report-back was received
  • 🟠 indeterminate: enrolled, but no report-back was received
seambot (integration health: API routes): not_run — enrolled, but no report-back was received
  • 🟠 indeterminate: enrolled, but no report-back was received
finishingbot (release readiness): not_run — enrolled, but no report-back was received
  • 🟠 indeterminate: enrolled, but no report-back was received
accessibilitybot (WCAG on the React UI): not_run — enrolled, but no report-back was received
  • 🟠 indeterminate: enrolled, but no report-back was received
glambot (presentation quality): not_run — enrolled, but no report-back was received
  • 🟠 indeterminate: enrolled, but no report-back was received
cipherbot: not_applicable — no cryptography is implemented in this repository
  • ⚪ not applicable: no cryptography is implemented in this repository
sustainabot: not_applicable — fleet adapter slot reserved; no adapter exists yet (gitbot-fleet README)
  • ⚪ not applicable: fleet adapter slot reserved; no adapter exists yet (gitbot-fleet README)

Scope: static findings reported by each row's tool at the named commit. Purple, red, yellow and blue grade the worst finding present; orange is evidence missing, malformed or a scanner that did not run; grey is an explicit scope exclusion with its reason. No row is a security certification. report.json, badge.json and the raw findings are in the verdict-grid artifact on the linked run.

hyperpolymath and others added 4 commits October 23, 2026 01:38
…portions

Three things the failing `Julia tests` job exposed once the run-statistics 500
stopped masking everything after it:

* `server.jl` included `routes/cladistics.jl` twice, so HTTP.jl printed
  "replacing existing registered route" for all six cladistics endpoints on
  every server start. The second include is gone; the routes are unchanged.

* `test_exact_summaries.jl` expected the proportions of zero counts in a sample
  with reads to be `nothing`. They are not: `_summarise` fills every
  proportion with `nothing` only when the sample total is zero, `has_defined_
  proportions` tests `!iszero(total)`, and the summary's own warning reads
  "has zero reads: relative abundances are undefined, not zero". A zero count
  in a sample of 2 is `0//1`; the test now says so, and its storage assertion
  checks all three rendered proportions instead of one null.

* the validated-statistics step published at most 3.5 KB of the tail of its
  log, which hid every failure but the last. It now posts a digest of every
  failing assertion (testset, expression and evaluated values) in
  annotation-sized chunks before the log tail, so the checks API shows the
  whole failure list.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The route cannot distinguish `1`, `1.0` and `1e0`: JSON3 normalises every
integral numeric token to Int64 while parsing (its `read!` turns an integral
Float64 into Int64 when the value fits), so all three arrive as `1`. The test
demanded a 422 for `1.0` and for `1e0`, which can only be met by refusing a
token whose value is exactly the integer it denotes.

The test now states the property the endpoint actually guarantees: anything
that is not exactly an integer of Int64 size is refused - a fractional token
(`1.5`, `1.0000000001`), a negative count, and a token past the JSON-safe
integer range (`2^53 + 1`) - while an integral token is accepted as the
identical integer. The comment in `_json_exact_count` said JSON3 keeps a
decimal token as a float; that was wrong, and it is corrected.

This is the last failure in the validated-statistics step: with it, the step
reports 617 passing assertions and no failure, so the full suite can run.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The full Julia suite reaches the CladeCumulus tests for the first time - the
earlier failures stopped the run before them - and they expose six defects.
None of them is new: the feature landed in #200/#207 with these paths already
broken.

* `Phylogeny` imported five names from `Tools` but not the module itself, so
  `PH.Tools._tools` (how the FastTree provenance test swaps in a mock binary)
  raised `UndefVarError`.

* `_parse_label_and_support` never saw a node's NHX support when the comment
  came before the label or the branch length, which is where every writer puts
  it: `(...)[&&NHX:B=92]:0.1`. `read_label_and_quoted` skipped that comment
  without capturing it, so `support` stayed `nothing`. The jplace edge tag has
  the same problem. Both are now captured before the label is read.

* `_graft_jplace_queries` summed `candidate.weight`, but `QueryCandidate` calls
  the field `like_weight`, so every grafted jplace tree died with a
  `FieldError`.

* `PLACEMENT_WEIGHT_SUM_TOL` was 1e-5. jplace writers round
  `like_weight_ratio` and drop placements under their own cutoff, so a genuine
  single placement carries `0.98` and was marked `undecided` with no best
  weight. A 5% shortfall is rounding; more is a missing placement.

* `_load_run_table_df` required `table::String`, but the route passes
  `strip(string(...))`, a `SubString`, so every POST to
  `/clade-cumulus` failed with a `MethodError`. It now accepts any
  `AbstractString` and narrows once.

* The sources route enumerated `SHOW TABLES`, which includes views, and read
  each with `COUNT(*)`. A DuckDB view over a replacement scan is bound to the
  connection that created it, so an unreadable object aborted the whole
  listing with `KeyError: key "src_df" not found`. Such an object is now
  skipped with a warning, and the test fixture drops the helper view it
  registers instead of leaving it in the results database.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…ree names

Two leftovers from the same paths:

* `_graft_jplace_queries` still read `candidate.weight` in the projections that
  populate `query_lwr` and the grafted-tip spec; the struct field is
  `like_weight`, so every jplace graft still raised a `FieldError`.

* `_valid_tree_file` and `_tree_format` were typed `::String`, but request
  bodies hand them `strip(string(...))`, a `SubString`, so a
  `treeSource = "study_tree"` request died with a `MethodError` before the tree
  was ever read. Both accept any `AbstractString` now.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented Oct 23, 2026

Copy link
Copy Markdown

Add Carrot credits or activate Agent usage billing to use Autofix

Copy link
Copy Markdown
Owner Author

I resolved the conflicts with current main on the session branch and opened #224 as a replacement PR. It merges the full #221 head (including cladistics fixes) and reconciles the four overlapping files; GitHub reports #224 mergeable. I preserved main's documented strict JSON count-token validation, adopted #221's early BigInt aggregation cell-limit check, and combined the CI diagnostics. Julia CI is pending there. Please review #224 instead of trying to merge the conflicted #221.

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.

1 participant