TCP B9: Run the integration suite against Cloud - #611
Conversation
f23e741 to
50a78cf
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2d1478b. Configure here.
72c8c7a to
3598070
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
🟡 Changes recommended
TcpServerFeatures performs sync-over-async blocking in a static initializer, which can deadlock under a SynchronizationContext and/or stall discovery longer than necessary without bounded timeouts.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR wires the native TCP integration test suite to run against a real ClickHouse Cloud service by (1) teaching the integration fixture to resolve a server via a full connection string (including TLS), (2) tagging a subset of integration fixtures as Category("Cloud") and adjusting/skipping cases that can’t work on multi-replica Cloud, and (3) adding a reusable GitHub Actions workflow to execute that Cloud category.
Changes:
- Extend
TcpServerFixtureto resolve the test server fromCLICKHOUSE_TCP_CONNECTION/ host overrides / Testcontainers, and enforce TLS invariants in Cloud mode. - Mark selected integration fixtures as
Category("Cloud")and add Cloud-specific skips/settings (e.g., locked settings; sequential consistency for replica-read correctness). - Add a new
tests-cloud-tcp.ymlreusable workflow and wire it intotests-tcp.ymlfor push/dispatch runs.
File summaries
| File | Description |
|---|---|
| ClickHouse.Driver.Tcp.Tests/Utilities/TcpServerFeatures.cs | Falls back to querying the configured server for version() when CLICKHOUSE_VERSION is not pinned. |
| ClickHouse.Driver.Tcp.Tests/Utilities/QueryLog.cs | Skips query-log-based assertions on Cloud where system.query_log is replica-local. |
| ClickHouse.Driver.Tcp.Tests/Integration/TlsTransportIntegrationTests.cs | Updates documentation comment to reflect Cloud-category coverage expectations. |
| ClickHouse.Driver.Tcp.Tests/Integration/TimezoneColumnIntegrationTests.cs | Adds Category("Cloud"). |
| ClickHouse.Driver.Tcp.Tests/Integration/TcpServerFixture.cs | Implements Cloud connection-string precedence, TLS enforcement in Cloud mode, and Cloud-locked-settings skipping; centralizes options/factory creation. |
| ClickHouse.Driver.Tcp.Tests/Integration/PocoWriteIntegrationTests.cs | Adds Category("Cloud") and skips cases requiring Cloud-locked settings. |
| ClickHouse.Driver.Tcp.Tests/Integration/PocoReadIntegrationTests.cs | Adds Category("Cloud") and skips cases requiring Cloud-locked settings. |
| ClickHouse.Driver.Tcp.Tests/Integration/NestedArrayInsertIntegrationTests.cs | Adds Category("Cloud"). |
| ClickHouse.Driver.Tcp.Tests/Integration/MultiBlockStateIntegrationTests.cs | Adds Category("Cloud"). |
| ClickHouse.Driver.Tcp.Tests/Integration/MidStreamFailureIntegrationTests.cs | Adds Category("Cloud"). |
| ClickHouse.Driver.Tcp.Tests/Integration/InsertSlicingIntegrationTests.cs | Adds Category("Cloud") and skips cases requiring Cloud-locked settings. |
| ClickHouse.Driver.Tcp.Tests/Integration/InsertInterruptionIntegrationTests.cs | Adds Category("Cloud"). |
| ClickHouse.Driver.Tcp.Tests/Integration/ConnectionPoolIntegrationTests.cs | Reads row counts with select_sequential_consistency to be correct on Cloud replicas. |
| ClickHouse.Driver.Tcp.Tests/Integration/CompressionIntegrationTests.cs | Adds Category("Cloud"). |
| ClickHouse.Driver.Tcp.Tests/Integration/ColumnReadProjectionIntegrationTests.cs | Adds Category("Cloud") and skips a Cloud-locked LowCardinality setting case. |
| ClickHouse.Driver.Tcp.Tests/Integration/ColumnarReadSurfaceIntegrationTests.cs | Adds Category("Cloud"). |
| ClickHouse.Driver.Tcp.Tests/Integration/ClickHouseTcpTracingIntegrationTests.cs | Adds Category("Cloud"). |
| ClickHouse.Driver.Tcp.Tests/Integration/ClickHouseTcpSessionIntegrationTests.cs | Adds Category("Cloud"). |
| ClickHouse.Driver.Tcp.Tests/Integration/ClickHouseTcpParameterIntegrationTests.cs | Adds Category("Cloud"). |
| ClickHouse.Driver.Tcp.Tests/Integration/ClickHouseTcpExceptionIntegrationTests.cs | Adds Category("Cloud"). |
| ClickHouse.Driver.Tcp.Tests/Integration/ClickHouseTcpConnectionQueryIntegrationTests.cs | Adds Category("Cloud"). |
| ClickHouse.Driver.Tcp.Tests/Integration/ClickHouseTcpConnectionMetadataIntegrationTests.cs | Adds Category("Cloud"). |
| ClickHouse.Driver.Tcp.Tests/Integration/ClickHouseTcpConnectionIntegrationTests.cs | Avoids Cloud endpoint timeouts in an unreachable-port test by using loopback. |
| ClickHouse.Driver.Tcp.Tests/Integration/ClickHouseTcpConnectionInsertIntegrationTests.cs | Adds Category("Cloud"), skips Cloud-locked settings cases, and switches some Memory tables to MergeTree for multi-connection reads. |
| ClickHouse.Driver.Tcp.Tests/Integration/ClickHouseTcpClientQueryIntegrationTests.cs | Adds Category("Cloud"). |
| ClickHouse.Driver.Tcp.Tests/Integration/ClickHouseTcpClientIntegrationTests.cs | Adds Category("Cloud") and routes some option construction through TcpServerFixture.Options(). |
| ClickHouse.Driver.Tcp.Tests/Integration/ClickHouseTcpCancellationIntegrationTests.cs | Adds Category("Cloud"). |
| ClickHouse.Driver.Tcp.Tests/Integration/ClickHouseTcpCallbackIntegrationTests.cs | Adds Category("Cloud"). |
| .github/workflows/tests-tcp.yml | Triggers Cloud TCP workflow on push/dispatch and includes it in path filters. |
| .github/workflows/tests-cloud-tcp.yml | New reusable workflow to run Category=Cloud TCP tests against ClickHouse Cloud using a TLS connection string. |
| .github/workflows/reusable-tcp.yml | Updates setup-dotnet action version and ensures Cloud category also runs in container jobs (no exclusion filter). |
Review details
- Files reviewed: 31/31 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| using var client = new ClickHouseTcpClient(ClickHouseTcpClientOptions.FromConnectionString(connectionString)); | ||
| object version = client.ExecuteScalarAsync("SELECT version()").GetAwaiter().GetResult(); | ||
| return Parse(version as string); | ||
| } |
Split out of the B9 TLS change, which needed a Cloud service to prove the transport but shipped only a five-test smoke set to do it. The suite's usual connection string points at the service instead. CLICKHOUSE_TCP_CONNECTION supersedes the CLICKHOUSE_TCP_HOST variables and the container, and carries TLS, so no second fixture and no second code path is needed. CLICKHOUSE_TEST_ENVIRONMENT=cloud makes the fixture refuse a connection string without UseTls, so a run cannot pass in the clear against a public endpoint. The Cloud category marks the tests the Cloud job also runs, not tests that need a Cloud service, matching ClickHouse.Driver.Tests. The standard matrix runs them too, against the container, so reusable-tcp.yml drops its Category!=Cloud filter. Tests/Cloud goes: the integration suite already covers ping, a multi-block query, a MergeTree round trip, session reuse over a TEMPORARY table, and concurrency. Two things a Cloud service does that a container does not, both found by running this: Cloud refuses to let a query change allow_suspicious_low_cardinality_types, answering "should not be changed" rather than applying a value. SkipIfCloudLocksASetting ignores a test asking for a setting on that list, keyed on the settings a test applies rather than on test names. Concurrent inserts commit on whichever replica their connection reached, so a count needs select_sequential_consistency to include all of them. On a single server there is nothing to wait for and the setting does nothing. Cloud: 1357 passed, 11 skipped, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ten more fixtures, chosen for what a Cloud service does differently rather than for coverage: session identity through a load balancer, the Cancel packet reaching the server through it, an interrupted insert giving its pool slot back, compression in both directions inside the tunnel, an unframed Exception packet among framed data blocks, per-block prefixes where the server decides the block split, a non-UTC column read back against the server's own rendering, the trace id the server records for a client that set one, the error codes a different server build carries, and the schema of the Log and ProfileEvents blocks it sends. Left out on purpose. Per-type and conversion fixtures repeat what the corpus already round-trips there. Version-gated types (QBit, Geometry) would run unconditionally, because a Cloud run pins no CLICKHOUSE_VERSION and unpinned means "assume every feature". ReadonlyUserIntegrationTests needs CREATE USER and a readonly profile, which is Cloud's access model rather than the client's. Client-side lifecycle, dependency injection and logging fixtures do not depend on the deployment. TlsTransportIntegrationTests stays out for a concrete reason, now recorded on it: its proxy dials the fixture's host and port in the clear, which on Cloud is the secure port. Cloud category: 1368 -> 1806 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first run of the widened category failed 27. Four causes, all in what the service permits rather than in the client. Ask the server for its version when nothing pins one. TcpServerFeatures read CLICKHOUSE_VERSION and treated "unset" as "assume every feature", which is right for a container tagged latest and wrong for Cloud, where no version can be pinned in the workflow without guessing. QBit(Int8) is gated at 26.7 and the service is older, so eight corpus cases ran and were rejected. It now asks the configured server, which also means a local run no longer needs the variable set by hand. allow_experimental_nullable_tuple_type is locked as well, under either of its two names, which takes out Nullable(Tuple(...)) and Nullable(Point) -- a Point being a Tuple underneath. Three tests build their own settings dictionary rather than carrying the corpus one, so they now call the guard directly. system.query_log is local to each replica. Both the flush and the lookup go over whichever connection the pool hands out, so a record written on one replica is looked for on another, and no retry budget fixes it. QueryLog ignores the calling test there, with clusterAllReplicas noted as what would be needed to keep the assertion. InsertAsync_DenseVariantOfAnotherVariantsAlternatives reads what it wrote over a second connection, which is one of the two shapes a Memory table cannot survive on a multi-replica service. Its two tables are MergeTree now. That shape was recorded on TcpServerFixture as hypothetical; it was not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
InsertAsync_DenseVariantWhoseValueNoTargetAlternativeAccepts reads its source over a second connection and counts the target over a third, so its two Memory tables read empty on a multi-replica service: the refusal under test never happened, because the loop that would have triggered it had no rows. MergeTree, like its sibling. Cloud: 27 failures down to this one, now 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3598070 to
44a6d8b
Compare

Sits at the top of the
tcp/**stack, on #610 (tcp/epic-t4-docs). This diff is only the Cloud test wiring.Split out of #563, which needed a Cloud service to prove the TLS transport but shipped a five-test smoke set with its own fixture and its own environment variable to do it. It sits at the top rather than next to #563 so that it can see every integration fixture in the stack, not just the ones that existed at B9.
Split out of B9. That change needed a Cloud service to prove the TLS transport, but shipped a five-test smoke set with its own fixture and its own environment variable to do it. This runs the whole integration suite there instead, through the plumbing every other run already uses.
What this does
The suite's usual connection string points at the service.
TcpServerFixtureresolves its server in three steps, in precedence order:CLICKHOUSE_TCP_CONNECTIONCLICKHOUSE_TCP_HOST(+_PORT,_USER,_PASSWORD)CLICKHOUSE_VERSION, matching the main suite.CLICKHOUSE_TEST_ENVIRONMENT=cloudmakes the fixture refuse a connection string that does not setUseTls, or that bypasses certificate validation. Without that a misconfigured job would keep passing over a plaintext connection to a public endpoint, proving nothing about the transport it exists to cover.Category("Cloud")marks the tests the Cloud job also runs, not tests that need a Cloud service. This is the conventionClickHouse.Driver.Testsalready uses: the standard matrix applies no filter and the Cloud job selectsCategory=Cloud. Soreusable-tcp.ymldrops itsCategory!=Cloudfilter, and these tests now run on the container matrix too. Genuinely Cloud-only tests, if JWT ever needs them, gate on their own environment variable withAssert.Ignore, the way the HTTP suite's bearer-token tests do.Tests/Cloudis deleted rather than widened. Everything the smoke set asserted is already in the integration suite: handshake and ping, a multi-block query, a MergeTree insert round trip, pooled session reuse over aTEMPORARYtable (ConnectionPoolIntegrationTests), and concurrency. Keeping it would have restated that coverage over a slower transport.Three tests hand-built
ClickHouseTcpClientOptionsfrom the fixture's host and port, which silently drops TLS. They derive fromTcpServerFixture.Options()now.Two things a Cloud service does that a container does not
Both were found by running this, not by reading docs.
allow_suspicious_low_cardinality_typescannot be changed on Cloud. The server answersSetting ... should not be changedrather than applying its own value, so the eleven tests needing aLowCardinalityover a numeric orDateTimeinner cannot run there at all.TcpServerFixture.SkipIfCloudLocksASettingignores a test that asks for a setting on that list. It is keyed on the settings a test applies, not on a list of test names, so a new case carrying a locked setting is covered by writing it.flatten_nestedand theVariant/Dynamicsettings are all changeable; the lock is specific to this one.Worth knowing for the next person who debugs this: the rejection terminates the connection, so ten of the eleven surfaced as
ObjectDisposedException: The connection has been terminatedand the real error appeared once. Grep forshould not be changed, not for the first-reported failure.Concurrent inserts read short.
InsertRowsAsync_ConcurrentInsertsIntoOneTable_AllRowsLandcounted 200 of 400. All eight inserts reported success, so the rows were committed and the read saw half: each insert commits on whichever replica its connection reached. The count reads withselect_sequential_consistencynow, which keeps the assertion live on Cloud rather than skipping it. It passes there, and the setting does nothing on a single server.The
Memoryengine, which B9 predicted would block thisB9 kept the Cloud set to five tests on the grounds that most integration tables are
ENGINE = Memory, whose data is node-local, while a pooled client would spread an insert and its read-back across replicas. That turned out not to hold.MinPoolSizedefaults to 0, so nothing is prewarmed, and a checkout takes from the idle list — which for sequential operations holds the connection just returned. One connection is one session and one replica. Two full Cloud runs agree, with 53Memorytables in the suite.The shape that would break is a test that makes its connection retire — a server error, an enumerator dropped mid-response — and then reads a
Memorytable it wrote before the retirement. No test does that today, so this documents the constraint onTcpServerFixturerather than converting 53CREATE TABLEs. Converting them was the alternative and I decided against it:MergeTreewith no ordering key would put the read-back order of every one of those tests at the mercy of part-level parallelism.Testing
Cloud, by
workflow_dispatchon this branch: 1357 passed, 11 skipped, 0 failed, with all nine container jobs green alongside it.Locally, all three server-resolution paths pass at 2801 tests each. The widened set was also run over a real TLS transport — a container with
tcp_port_secureand a self-signed certificate, reached throughCLICKHOUSE_TCP_CONNECTIONwithTlsCaCertificatePathpinned to it — where all 1368 pass. That run is what caughtTcpServerFixture.ConnectionStringdropping the TLS keys, which failed exactly one test and nothing else. TheCLICKHOUSE_TEST_ENVIRONMENT=cloudguard was confirmed load-bearing by pointing it at a plaintext connection string and watching the fixture refuse it.One consequence of that guard: because it also refuses skip-verify and a pinned authority, cloud mode cannot be rehearsed locally — that would need a publicly trusted certificate for
localhost. The guard is CI-facing and the strictness is the point, so the skip path is verified in CI only.Known limits, deliberately left
Category("Cloud")is on 14 of the 37 integration fixtures — the ones that existed at B9, where this work was written. The other 23 arrived in epics between B9 and here and are not tagged, so the Cloud job does not run them yet. Tagging them is the obvious next step now that this branch sits at the top, but several want checking against the service first rather than assuming:ReadonlyUserIntegrationTests(CREATE USER/GRANT),QBitIntegrationTestsandGeometryIntegrationTests(version-gated),CompressionIntegrationTests,ClickHouseTcpTracingIntegrationTests, andPublicSurfaceIntegrationTests. Left out of this change deliberately, rather than tagged blind and discovered through a red job on a shared service.tests-tcp.ymland paying for the whole nine-job matrix, becausetests-cloud-tcp.ymlisworkflow_call-only and a workflow must exist on the default branch to be dispatchable. Giving it its ownworkflow_dispatchtrigger is worth doing once this lands.No CHANGELOG/RELEASENOTES entry, consistent with the rest of the
tcp/**stack: neither file mentionsClickHouse.Driver.Tcptoday, the assembly isIsPackable=falseand gated behind an experimental diagnostic, and epic R3 writes the client up as one piece when it ships.🤖 Generated with Claude Code