Skip to content

docs(AGENTS): capture recurring PR review feedback as conventions - #487

Merged
alex-clickhouse merged 3 commits into
mainfrom
polyglot/agents-review-feedback-conventions
Jul 31, 2026
Merged

docs(AGENTS): capture recurring PR review feedback as conventions#487
alex-clickhouse merged 3 commits into
mainfrom
polyglot/agents-review-feedback-conventions

Conversation

@polyglotAI-bot

Copy link
Copy Markdown
Collaborator

I reviewed the review comments on my last 10 PRs in this repo (#467, #469, #473, #474, #477, #479, #480, #481, #482, #484, plus #445/#446) and pulled out the feedback that recurred. Each rule below was asked for at least twice, or once in a way that killed a PR. Documenting them so they're followed the first time instead of costing a review round-trip.

What was repeated, and what I added

Feedback Where it came up Added to
"Make the changelog/releasenotes vastly more concise", "one short sentence is enough" #474, #477, #480, #481, #482 Changelog and release notes
"Let's not include the benchmark" #481, #482 Performance Characteristics
"Those test cases are already covered by existing ones, check for duplicates (probably in TestCases.cs)" #480, #482 Testing Discipline
"We don't need #if NET6_0_OR_GREATER, we already only target .net 6+" #446, #484 Code Style
"Versions before 25.8 LTS are not officially supported, we're going to skip this" — closed the PR #469 Project Context
"I would strongly prefer integration tests over the unit tests we have" #479, and #477 where mock tests passed against a byte layout the server never emits Testing Discipline
"The comments say the CRLF is optional, is that actually true?" / changelog over-promising what the code guarantees #477, #469, #479 Code Style, Changelog
"What are the performance implications? Is there some alternative approach?" on adding a guard to every numeric accessor #445 Performance Characteristics

Two of these narrow existing guidance rather than adding new rules: the integration-test bullet already existed but didn't say why a mock passes when the server disagrees, and the framework list already said net6+ but not that guards below it are dead.

Notes

  • The 25.8 floor is stated against .github/workflows/tests.yml, so it stays checkable rather than becoming stale prose.
  • Utilities/TestCases.cs / GetDataTypeSamples() verified as the shared source feeding the select, parameter, bulk-copy, serialisation, Dapper and DataAdapter suites — that's why it's the first place to check before adding a type test.
  • /benchmark-compare verified as a maintainer-gated PR-comment trigger (stresshouse-benchmark-compare.yml requires OWNER/MEMBER/COLLABORATOR), so the wording says to ask a maintainer for it.
  • Docs-only; no changelog entry, matching docs(AGENTS): test isolation, determinism & parametrization conventions #429.

Six rules that reviewers have had to repeat across recent PRs, so they are
followed the first time instead of in a review round-trip:

- Supported ClickHouse versions floor at 25.8 LTS (matches the CI matrix)
- Don't tax a common path for a niche case; where benchmarks belong
- Integration tests over mocks; don't restate TestCases.cs coverage
- No redundant #if NET5/6_0_OR_GREATER guards; comments must be verified
- Changelog and release-notes entries stay short
Copilot AI review requested due to automatic review settings July 31, 2026 10:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates AGENTS.md (the repository’s development guide) to codify several recurring review points as explicit conventions, aiming to reduce review round-trips by making expectations discoverable up-front.

Changes:

  • Document the supported ClickHouse version floor (25.8 LTS+) and explicitly de-scope fixes/workarounds for older server behaviors.
  • Add clearer performance guidance (avoid taxing hot paths for niche cases; prefer maintainer-run /benchmark-compare over committing benchmark additions).
  • Expand testing/style guidance (integration-test rationale, avoid duplicating existing type coverage, avoid redundant NET6_0_OR_GREATER-style guards, and avoid over-claiming in comments/changelog entries).

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@polyglotAI-bot

Copy link
Copy Markdown
Collaborator Author

Pushed 2a53f9d — sharpened the redundant-coverage rule to name the specific shape that keeps recurring.

The two review comments behind it (#480 "I don't think the new test cases cover anything that pre-existing tests don't", #482 "check for duplicates, probably in TestCases.cs") were both really about control cases: tests that pin behavior the change never touched, added as a safety net. Those are exactly what TestCases.cs already covers, so the rule now calls that out rather than leaving "check for duplicates" as a judgement call.

Self-audit while writing it: my own open #484 has one — Format_TimeSpanBoundToTime_ReturnsFormattedTime pins TimeSpanTime, which that PR doesn't change, and TestCases.cs:354-361 already round-trips Time/Time64(N) with TimeSpan through SqlParameterizedSelectTests into the same formatter path. I'll drop it there.

Performance-related changes should be measured with BenchmarkDotNet. The
rule is about what ships, not whether to benchmark: an ad-hoc benchmark
written to answer one question can stay out of the PR, but one worth
re-running later belongs in the repo.
@polyglotAI-bot

Copy link
Copy Markdown
Collaborator Author

Pushed 44576a0 — corrected the benchmark rule, which I'd overstated.

I'd written "don't commit a new benchmark file", but that conflates two things. Performance-related changes should be measured with BenchmarkDotNet — the ask on #481/#482 was about what ships, not whether to benchmark. And #445 was the opposite ask ("can you do a quick benchmark?"), so a blanket prohibition would have contradicted it.

Now: measure perf changes with BenchmarkDotNet and put the numbers in the PR description; an ad-hoc benchmark written to answer one question doesn't need to ship, but one worth re-running later belongs in the repo.

@alex-clickhouse
alex-clickhouse merged commit d9998a3 into main Jul 31, 2026
17 checks passed
@alex-clickhouse
alex-clickhouse deleted the polyglot/agents-review-feedback-conventions branch July 31, 2026 16:20
alex-clickhouse added a commit that referenced this pull request Aug 4, 2026
Review feedback, and now also the house rules AGENTS.md picked up in #487:
comments stay short and assert only what has been verified, and changelog
entries stay to the user-visible change with the detail left to the PR.

The two XML doc blocks in JsonType.cs kept only the non-obvious part -- that
the text/bytes decision has to come from the ClickHouse type, since
Array(UInt8) also reads as a byte[]. The changelog entry goes from 73 words
to 52, identical in both files. No behaviour or test-assertion change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alex-clickhouse added a commit that referenced this pull request Aug 5, 2026
…#485)

* Fix JSON string values base64-corrupted under ReadStringsAsByteArrays

`ReadStringsAsByteArrays` propagates into the JSON decoder, so every string
leaf inside a `JSON` column was read as a `byte[]`. `ReadJsonValue`'s type
switch had no arm for one, so it fell through to the
`JsonSerializer.SerializeToElement` default, which renders a byte array as
base64: `payload["event"].GetValue<string>()` returned `"aW5mbw=="` instead of
`"info"`, with no exception and no visible change to the node's type. Numeric
and boolean leaves were unaffected, which made the corruption easy to miss,
and reading a document then writing it back persisted the base64 text into
ClickHouse.

A `Map(String, ...)` path failed differently: `ReadJsonMap` cast the decoded
key straight to `string`, throwing `InvalidCastException`.

Both now go through a shared `DecodeString` helper, which is what
`ReadJsonFixedString` already did four lines away. Because `ReadJsonArray` and
`ReadJsonMap` recurse through `ReadJsonNode` -> `ReadJsonValue`, the single new
switch arm covers top-level, array-nested and map-nested strings at once,
including `LowCardinality(String)` and `Nullable(String)`.

Behavioural change, deliberately not honouring the flag inside JSON: RFC 8259
defines JSON strings as text, so the "a ClickHouse String is arbitrary bytes"
rationale behind `ReadStringsAsByteArrays` does not apply within a JSON
document, and `JsonValue` has no byte-array representation to expose instead.
Decoding is lenient (invalid sequences yield U+FFFD rather than failing the
row), matching `ReadJsonFixedString`. The flag is unchanged for ordinary
`String`/`FixedString`/`Dynamic` columns.

Adds `JsonStringAsByteArrayTests` (15 cases), covering the gap that allowed
this: no existing test combined the flag with a `JSON` column. 12 of the 15
fail before this change; the other three are regression guards and the source
documents which are which and why. They assert the decoded values, that string
leaves are backed by `string` rather than `JsonElement` (the exact
discriminator between the two code paths), that flag-on output is
byte-identical to flag-off across ten JSON shapes covering both the hinted and
the dynamic path-type construction sites, and that a read-then-write round trip
no longer stores base64.

No public API change; `PublicAPI/*.txt` is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Only decode a byte[] as text when it came from a string type

Review caught a regression in the previous commit. The new `byte[]` arm keyed
off the CLR type of the decoded value, but a `byte[]` is not on its own
evidence of a string: `ArrayType.Read` allocates by
`UnderlyingType.FrameworkType`, so `Array(UInt8)` materializes as a real
`byte[]` too - with `ReadStringsAsByteArrays` on *or off*.

`ReadJsonNode` intercepts `ArrayType` directly, so a plain `Array(UInt8)` hint
was fine, but any wrapper it does not intercept passes the value through to
`ReadJsonValue`, where the arm reinterpreted those bytes as UTF-8:

    JSON(v Variant(Array(UInt8), String))  [1, 2]      "AQI=" -> "��"
    JSON(s SimpleAggregateFunction(anyLast, Array(UInt8)))    "AQI=" -> "��"
    JSON(v Variant(Array(UInt8), String))  [255, 254]  "//4=" -> two U+FFFD

Two things made this worse than the bug being fixed. It fired at the *default*
flag setting, so it was an undisclosed behaviour change for everyone rather
than for opt-in users. And it was lossy where the old output was not: `"//4="`
decodes back to `[255, 254]`, whereas the replacement characters have destroyed
the bytes.

The arm is now gated on `IsTextBacked(type)`, which decides from the
originating ClickHouse type: `String`/`FixedString`, and those wrapped in
`LowCardinality`, `Nullable` or `SimpleAggregateFunction`. `Variant` and
`Dynamic` answer false deliberately - their subtype is chosen per value at read
time and is not knowable from the static type, so they keep their existing
behaviour instead of risking the same misinterpretation. Every shape the tests
and release notes actually claim still decodes as text.

The four new cases assert against fixed expected values rather than comparing
flag-on to flag-off, because `Array(UInt8)` reads identically under both
settings - the flag-on-equals-flag-off invariant is structurally incapable of
catching this, and only pinning the literal output does. Verified: they fail
with the guard removed and pass with it.

Also corrects two claims the review found inaccurate:
- the notes implied `Map` keys work under `LowCardinality`; `ReadJsonMap`'s
  guard is an exact `is not StringType` test, so `Map(LowCardinality(String),
  String)` still throws `NotSupportedException`. Pre-existing, now stated.
- `Dynamic` was listed as unaffected, but a `Dynamic` column holding a `JSON`
  value does get the fix, since its leaves go through the JSON reader. Only a
  `Dynamic` holding a string still yields `byte[]`.

And renames four `using var connection` locals that shadowed the inherited
fixture field, since one test deliberately uses the inherited one to mean the
flag-off connection.

net9.0: 0 failed, 9584 passed, 142 skipped (baseline 9565/142, so exactly +19).
Green on net6.0 and net10.0 too. Full build 0 errors, 293 warnings, matching
baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Cover JsonReadMode.None and .String, and note which modes are affected

The fix applies to whichever read mode decodes a JSON document structurally, and
that is two of the three modes, not just the default. Tests only exercised
`Binary`, so `None` was fixed without being covered and `String` was untouched
without that being pinned. Verified against pristine main:

    JsonReadMode  flag   main                       this branch
    Binary        true   {"n":42,"s":"aW5mbw=="}    {"n":42,"s":"info"}
    Binary        false  {"n":42,"s":"info"}        unchanged
    None          true   {"n":42,"s":"aW5mbw=="}    {"n":42,"s":"info"}
    None          false  {"n":42,"s":"info"}        unchanged
    String        true   {"n":42,"s":"info"}        unchanged
    String        false  {"n":42,"s":"info"}        unchanged

`None` differs from `Binary` only in not sending the server-side format setting
(for read-only connections that cannot set one), so it decodes through the same
path and carried the same bug. Its new case fails without the fix, alongside
`Binary`'s.

`String` never had the bug: the server sends the whole document as a single
string and `JsonType.Read` returns it via `ExtendedBinaryReader.ReadString()`
before any per-path type dispatch, so `ReadStringsAsByteArrays` cannot reach it.
Pinned with a guard test rather than left implicit.

That last point is worth more than a footnote, so the release notes now say it:
one of the driver's three JSON read modes *already* returned real text
regardless of the flag. Treating JSON strings as text is therefore not a new
convention invented by this PR - `Binary` and `None` were the inconsistent ones.
The lenient decoding lines up too, since `ExtendedBinaryReader` is constructed
with a replacement-fallback UTF-8 decoder, which is exactly what
`Encoding.UTF8.GetString` does.

net9.0: 0 failed, 9587 passed, 142 skipped (baseline 9565/142, so exactly +22).
Green on net6.0 and net10.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Condense the changelog entry and match it across both files

The entry had grown to 273 words in CHANGELOG.md and longer still in
RELEASENOTES.md, where it had also diverged into a differently-worded lead
bullet with four sub-bullets. Both are wrong for this repo:

- Every other Bug Fixes entry in `Unreleased` is byte-identical between the two
  files. Mine was the only one that differed.
- Existing entries run 15-102 words (median ~40). At 273 it was nearly 3x the
  longest one in the file.

Now one 73-word entry, identical in both files, covering just what a reader
needs: the symptom, that a round trip persisted it, the map-key exception, the
behavioural change with its precedent, and the action to take. The details that
were in the sub-bullets - affected read modes, the `Array(UInt8)` exclusion,
`Variant`/`Dynamic` scope, U+FFFD on invalid UTF-8 - stay in the PR description
and the code comments, which is where that depth belongs. Notably most of them
described things that did *not* change, which do not warrant changelog space.

Docs only; no code or test changes. Verified: the `Unreleased` Bug Fixes blocks
of the two files now diff clean, build 0 errors / 293 warnings, fixture 22/22.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Trim the code comments and the changelog entry

Review feedback, and now also the house rules AGENTS.md picked up in #487:
comments stay short and assert only what has been verified, and changelog
entries stay to the user-visible change with the detail left to the PR.

The two XML doc blocks in JsonType.cs kept only the non-obvious part -- that
the text/bytes decision has to come from the ClickHouse type, since
Array(UInt8) also reads as a byte[]. The changelog entry goes from 73 words
to 52, identical in both files. No behaviour or test-assertion change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Alex Soffronow Pagonidis <237136924+alex-clickhouse@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

3 participants