Repository navigation
TCP converters 6b/6: write through the converter layer everywhere and delete the old write members - #677
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
0046f41 to
0346937
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 cbc1227. Configure here.
There was a problem hiding this comment.
🔵 Needs a closer look
The broad wire-format rewrite and extensive removal of reference paths warrant final human review despite comprehensive tests and successful CI.
0 open findings
What changed in this PR
This TCP-client PR completes migration of writes to the converter layer, restricts codecs to storage-level serialization, and fixes conversions between related decoded column types.
Changes:
- Routes all insert forms through derived writers and removes legacy codec write machinery.
- Preserves semantic values, raw string bytes, and hidden nullable values when reinserting decoded columns.
- Simplifies failure ordering and differential tests while expanding integration, slicing, and byte-level coverage.
| File | Description |
|---|---|
docs/tcp.mdx |
Documents revised read/write behavior. |
ClickHouse.Driver.Tcp/Types/TupleFieldColumn.cs |
Removes legacy tuple projection. |
ClickHouse.Driver.Tcp/Types/SubstituteColumn.cs |
Removes nullable substitution views. |
ClickHouse.Driver.Tcp/Types/ProjectedColumn.cs |
Removes projected write columns. |
ClickHouse.Driver.Tcp/Types/ISpanWritableCodec.cs |
Removes legacy span-write interface. |
ClickHouse.Driver.Tcp/Types/EnumMemberTable.cs |
Adds enum-definition comparison. |
ClickHouse.Driver.Tcp/Types/EnumColumn.cs |
Exposes internal member metadata. |
ClickHouse.Driver.Tcp/Types/Converters/MapWriter.cs |
Clarifies refusal diagnostics. |
ClickHouse.Driver.Tcp/Types/Converters/MapReader.cs |
Removes failure-order replay. |
ClickHouse.Driver.Tcp/Types/Converters/LeafTable.cs |
Uses typed enum placeholders. |
ClickHouse.Driver.Tcp/Types/Converters/FixedLeafWriter.cs |
Exposes equality-conversion property. |
ClickHouse.Driver.Tcp/Types/Converters/FixedInterner.cs |
Optimizes 128-bit keys. |
ClickHouse.Driver.Tcp/Types/Converters/EmptyTupleWriter.cs |
Adds converter-layer empty-tuple writing. |
ClickHouse.Driver.Tcp/Types/Converters/DictionaryWriters.cs |
Adds CLR-keyed fixed interning. |
ClickHouse.Driver.Tcp/Types/Converters/ConverterDerivation.cs |
Adds nullability caching and simplifies derivation. |
ClickHouse.Driver.Tcp/Types/Converters/CodecWriter.cs |
Removes codec-backed converter writer. |
ClickHouse.Driver.Tcp/Types/Converters/BytesLeafWriter.cs |
Clarifies refusal diagnostics. |
ClickHouse.Driver.Tcp/Types/Converters/ArrayWriter.cs |
Exposes derived element writer. |
ClickHouse.Driver.Tcp/Types/ConcatColumn.cs |
Removes flattening write view. |
ClickHouse.Driver.Tcp/Types/CompositeElementProjections.cs |
Removes legacy projection helper. |
ClickHouse.Driver.Tcp/Types/ColumnCodecExtensions.cs |
Removes obsolete codec write helpers. |
ClickHouse.Driver.Tcp/Types/Codecs/ValueNullableShape.cs |
Removes nullable write behavior. |
ClickHouse.Driver.Tcp/Types/Codecs/UuidColumnCodec.cs |
Restricts writes to decoded storage. |
ClickHouse.Driver.Tcp/Types/Codecs/StringColumnCodec.cs |
Writes only stored raw bytes. |
ClickHouse.Driver.Tcp/Types/Codecs/ReferenceNullableShape.cs |
Removes reference nullable writes. |
ClickHouse.Driver.Tcp/Types/Codecs/NothingColumnCodec.cs |
Removes obsolete write metadata. |
ClickHouse.Driver.Tcp/Types/Codecs/LowCardinalityValueWriter.cs |
Removes legacy dictionary writer. |
ClickHouse.Driver.Tcp/Types/Codecs/LowCardinalityShape.cs |
Restricts writes to dense storage. |
ClickHouse.Driver.Tcp/Types/Codecs/LowCardinalityKeys.cs |
Removes legacy key strategies. |
ClickHouse.Driver.Tcp/Types/Codecs/JsonStringColumnCodec.cs |
Restricts JSON codec writes. |
ClickHouse.Driver.Tcp/Types/Codecs/INullableShape.cs |
Reduces interface to reading. |
ClickHouse.Driver.Tcp/Types/Codecs/ILowCardinalityShape.cs |
Reduces interface to storage checks. |
ClickHouse.Driver.Tcp/Types/Codecs/IArrayWriteShape.cs |
Removes legacy array writing. |
ClickHouse.Driver.Tcp/Types/Codecs/GeoColumnCodecs.cs |
Exposes geo structures to derivation. |
ClickHouse.Driver.Tcp/Types/Codecs/FixedWidthColumnCodec.cs |
Restricts writes to primitive storage. |
ClickHouse.Driver.Tcp/Types/Codecs/EnumColumnCodec.cs |
Validates matching enum definitions. |
ClickHouse.Driver.Tcp/Types/Codecs/EmptyTupleColumnCodec.cs |
Restricts storage writes and shares placeholder. |
ClickHouse.Driver.Tcp/Types/Codecs/DynamicTypeInference.cs |
Updates converter-layer documentation. |
ClickHouse.Driver.Tcp/Types/Codecs/DecimalColumnCodec.cs |
Restricts writes to decoded storage. |
ClickHouse.Driver.Tcp/Types/Codecs/BFloat16ColumnCodec.cs |
Adds chunked stored-value encoding. |
ClickHouse.Driver.Tcp/Types/ArrayColumn.cs |
Removes non-owning buffer wrapper. |
ClickHouse.Driver.Tcp/Protocol/ClickHouseTcpConnection.cs |
Plans converter and decoded-part writes. |
ClickHouse.Driver.Tcp/Poco/UntypedRowColumns.cs |
Uses derivation nullability. |
ClickHouse.Driver.Tcp/Poco/PocoWritePlan.cs |
Removes codec dependency from planning. |
ClickHouse.Driver.Tcp/Poco/PocoWriteConversion.cs |
Removes legacy nullability helper. |
ClickHouse.Driver.Tcp/Poco/PocoScatterTier.cs |
Documents relaxed failure order. |
ClickHouse.Driver.Tcp/Poco/PocoGatherTier.cs |
Documents relaxed failure order. |
ClickHouse.Driver.Tcp/Poco/PocoColumnScatterFactory.cs |
Removes failed-read replay. |
ClickHouse.Driver.Tcp/Poco/PocoColumnBuilder.cs |
Uses derivation nullability. |
ClickHouse.Driver.Tcp/Format/InsertColumn.cs |
Removes implicit codec write construction. |
ClickHouse.Driver.Tcp/Format/BlockWriter.cs |
Plans all writes through insert routing. |
ClickHouse.Driver.Tcp.Tests/Utilities/NestedArrayShape.cs |
Updates segment-write documentation. |
ClickHouse.Driver.Tcp.Tests/Utilities/DecodedColumns.cs |
Adds decoded-column fixture helper. |
ClickHouse.Driver.Tcp.Tests/Types/UuidColumnCodecTests.cs |
Tests storage UUID encoding. |
ClickHouse.Driver.Tcp.Tests/Types/NothingColumnCodecTests.cs |
Tests complete write refusal. |
ClickHouse.Driver.Tcp.Tests/Types/IpColumnCodecTests.cs |
Uses storage-write harness. |
ClickHouse.Driver.Tcp.Tests/Types/FixedWidthColumnCodecTests.cs |
Tests storage-only fixed writes. |
ClickHouse.Driver.Tcp.Tests/Types/EnumColumnCodecTests.cs |
Removes obsolete codec-write tests. |
ClickHouse.Driver.Tcp.Tests/Types/DerivedColumnTests.cs |
Stabilizes concurrency tests. |
ClickHouse.Driver.Tcp.Tests/Types/DateColumnCodecTests.cs |
Uses storage-write harness. |
ClickHouse.Driver.Tcp.Tests/Types/Converters/WriteConverterRegistration.cs |
Removes obsolete differential plug-in. |
ClickHouse.Driver.Tcp.Tests/Types/Converters/RefusalMessageTests.cs |
Updates explicit insert descriptors. |
ClickHouse.Driver.Tcp.Tests/Types/Converters/LeafSamples.cs |
Updates write terminology. |
ClickHouse.Driver.Tcp.Tests/Types/Converters/LeafReaderTests.cs |
Updates differential-test references. |
ClickHouse.Driver.Tcp.Tests/Types/Converters/LeafConverterRegistrationTests.cs |
Directly checks leaf coverage. |
ClickHouse.Driver.Tcp.Tests/Types/Converters/DictionaryEntryCacheTests.cs |
Updates readers and concurrency setup. |
ClickHouse.Driver.Tcp.Tests/Types/Converters/ConverterDerivationTests.cs |
Tests nullability derivation and caching. |
ClickHouse.Driver.Tcp.Tests/Types/ColumnReadScenario.cs |
Updates scenario documentation. |
ClickHouse.Driver.Tcp.Tests/Types/BFloat16ColumnCodecTests.cs |
Tests storage-shape discrimination. |
ClickHouse.Driver.Tcp.Tests/Types/ArrayColumnTests.cs |
Removes deleted buffer-wrapper tests. |
ClickHouse.Driver.Tcp.Tests/Poco/RowWriteRegistration.cs |
Removes obsolete differential registration. |
ClickHouse.Driver.Tcp.Tests/Poco/PocoReadRegistration.cs |
Removes obsolete differential registration. |
ClickHouse.Driver.Tcp.Tests/Poco/PocoReadPlanTests.cs |
Updates decoded-column terminology. |
ClickHouse.Driver.Tcp.Tests/Poco/PocoColumnScatterFactoryTests.cs |
Tests relaxed failure ordering. |
ClickHouse.Driver.Tcp.Tests/Integration/PocoReadTierIntegrationTests.cs |
Expands multi-window tier coverage. |
ClickHouse.Driver.Tcp.Tests/Integration/PocoReadFillIntegrationTests.cs |
Removes duplicate integration fixture. |
ClickHouse.Driver.Tcp.Tests/Integration/NestedArrayInsertIntegrationTests.cs |
Updates segmented-write coverage. |
ClickHouse.Driver.Tcp.Tests/Integration/GeometryIntegrationTests.cs |
Builds decoded geo alternatives. |
ClickHouse.Driver.Tcp.Tests/Integration/ColumnarReadSurfaceIntegrationTests.cs |
Uses decoded dictionaries. |
ClickHouse.Driver.Tcp.Tests/Integration/ClickHouseTcpConnectionInsertIntegrationTests.cs |
Verifies dense storage routing. |
ClickHouse.Driver.Tcp.Tests/Integration/ClickHouseTcpClientIntegrationTests.cs |
Clarifies Dynamic JSON behavior. |
ClickHouse.Driver.Tcp.Tests/Format/BlockWriterTests.cs |
Tests explicitly planned writes. |
ClickHouse.Driver.Tcp.Tests/Differential/Expectation.cs |
Removes old expectation framework. |
ClickHouse.Driver.Benchmark/TcpConverterRead.cs |
Benchmarks insert-plan routing. |
changelog.d/converter-6b-write-reroute.improvements.md |
Records write-routing changes. |
changelog.d/converter-6b-failure-order.improvements.md |
Records failure-order changes. |
changelog.d/converter-6b-decoded-string-bytes.fixes.md |
Records raw-byte preservation fix. |
changelog.d/converter-6b-decoded-hidden-values.fixes.md |
Records hidden-value fix. |
changelog.d/converter-6b-decoded-column-parameters.fixes.md |
Records parameter-conversion fixes. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Summary Final verdict✅ Approve Automated review by Polyglot AI (gpt-6-astra) of |
… types and dense arrays The pins are the outcome of one insert column write for the whole column and for the rows from a later row: the bytes, a refusal, or the exception. They cover the writes that ClickHouse/integrations#801 moves to the converter layer, the columns of no single CLR type, and a null reference in a row insert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e converter layer A geo type derives the writers of its structure (Tuple, Array, Variant), from its canonical CLR type only. Dynamic gets a writer that infers the type of each value and writes the values of each type through the derived tree of that type. QBit transposes its vectors in a writer of its own, and Tuple() writes one placeholder byte for each value. CodecWriter, which gave these types to their codecs, has no user and goes. The pins give the same bytes, refusals and exceptions. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each leaf codec names its decoded column as the column that it writes from its storage, and each composite codec asks the codecs of its children, so a dense composite whose children a caller built goes to the converter derivation. A decoded column stays on the codec: a new fixture test checks that for every case, and that the column has the element type of the codec. The dense test sources of Nested and Geometry now hold decoded children, the only children that such a column can have in a query result. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dense array that its codec does not write from storage, as ClickHouseTcpColumn.CreateArray builds over a caller's column, is written as its offsets and then its inner column through the element writer of the derived Array tree, so its rows are not copied into arrays. A column of no single CLR type is written as the first suggested CLR type of the target type that it implements. That writes two pinned cases that the codecs refused or threw on: Nullable(Int32) and FixedString(1) from a column that is also IColumn<string>. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The row inserts asked the null placeholder of the codec whether a target column holds NULL. ConverterDerivation.HoldsNull answers it from the type: Nullable, LowCardinality(Nullable), Variant, Dynamic and Geometry, also as the value type of a SimpleAggregateFunction. PocoWriteConversion has no member left and goes. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he codec Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A decoded column of a leaf type now goes to its codec, also at the top level of an insert. DateTime, DateTime64, Time and Time64 copy their stored wire values in one write, and Date, BFloat16, IPv4 and IPv6 convert their values a chunk at a time, as the leaf writers of the converter layer do. Without this, a decoded DateTime column wrote 14 times slower than through the converter tree. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ClrKeyedByteInterner found each value with CollectionsMarshal.GetValueRefOrAddDefault, which costs more than TryGetValue for a key that is already there, and in a LowCardinality column almost every value is. Over 100,000 strings with 100 distinct values, the Intern loop was 1396 us and is 1235 us (TryGetValue alone: 1149 us), and LowCardinality(String) writes 0.97 to 0.99 times the time of the codec path, at the top level and inside a dense Array (was 1.11 to 1.19). A new value costs a second lookup, which adds it. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lated type The pins hold the bytes and the read-back values of Nullable(Int8) into Nullable(Enum8), Nullable(Enum8) into Nullable(Int8), and Nullable(FixedString(2)) into Nullable(String): the value under each NULL is the placeholder of the target. The two pinned columns of no single CLR type now say the rule that writes them. docs/tcp.mdx gives the rule, and the two routes of a CreateArray column. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TcpConverterWriteRoutes inserts one block of a Dynamic column of eight kinds of value, and of two dense arrays that ClickHouseTcpColumn.CreateArray builds over a caller's column: Array(LowCardinality(String)) with 100 distinct values and Array(Nullable(Int32)). It has the attributes of TcpConverterWrite, so the gate of ClickHouse/integrations#801 runs it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pinned The 26 refused leaf values and the 28 refused composite values got their error from the codec write of the same values. Their exception type, parameter name and message are now literals, checked against the derived writer under the invariant culture. WireBytePinTests holds the literal bytes of 12 writes that a read cannot show: LowCardinality entries of the DateTime family, BFloat16, Time, Time64 and an IPv4-mapped IPv6 address, duplicate Map keys, the discriminators of a decoded Variant of another order, and the NULL of a DateTime in a zone that TimeZoneInfo cannot hold. All were the bytes of the codec write when captured. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… values The default value shares slot 0 and a repeated value its entry, for LowCardinality of DateTime, DateTime64, Time and Time64 written from their stored values. The bytes are the ones of the codec write when captured. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ionaries at the codec cost LowCardinality(UUID) wrote 2.24 times as slowly as the codec path for 100 distinct values and 1.37 times for distinct ones. A profile of the write (perf, 100k values) put 59% of the time in the lookup of the UInt128 canonical value (Dictionary.GetValueRefOrAddDefault: the hash of a UInt128 is a chain of multiplications) and 30% in the conversion of each Guid, which stalled on a 16-byte load after two 8-byte stores. The codec looked up the Guid itself and converted only the dictionary entries. - ClrKeyedFixedInterner looks a value up by its CLR value first, as ClrKeyedByteInterner does for strings, and converts and interns it only when that lookup fails. The FixedInterner stays the source of the keys and the entries, and the probe stops the CLR lookup for values that do not repeat. Each write conversion states whether equal values convert to equal canonical values (IWriteConversion.ClrEqualityImpliesCanonicalEquality): not for the floats, not for a DateTime (its equality does not compare the Kind), and not for an IPAddress or a ClickHouseTcpDecimal with an Int128 mantissa, whose lookup costs more than their conversion. - FixedInterner hashes a 16-byte canonical value (UInt128, Int128) as two mixed 64-bit halves (WideKey). - A Guid converts to its wire bytes with integer operations on its halves. Release, 100,000 values, against the codec path, 100 distinct / all distinct: UUID 0.96 / 0.90 (was 2.22 / 1.37), and every fixed-width leaf at or below 1.05. The fixture, the pins and the wire literals give the same bytes. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TcpConverterWriteRoutes gets three shapes: LowCardinality(UUID) from Guid with 100 distinct values and with every value distinct, and LowCardinality(UInt32) from uint with 100 distinct values. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cleanup of the test audit, section 5.1, items 1 to 7 (ClickHouse/integrations#801): - The read and answer arms of the differential fixture go, with ReadConverterRegistration.cs and LeafConverterRegistration.cs. The two write arms become one arm, "Insert write through the indexer", over every write facet except the decoded input. - The declared outcomes go (DeclareOutcomes, DeclaredOutcome, CompareWithDeclared, Expectation.ForRows) with their self-tests. Two self-tests check a stated failure on the tail. - PocoReadFillIntegrationTests goes. PocoReadTierIntegrationTests reads one result of 600 rows (three windows; Nullable, LowCardinality(Nullable(String)), Array, Map and DateTime64 columns) through each scatter tier. PocoReadIntegrationTestBase goes back into PocoReadIntegrationTests. - ReadRulesTests.Derive_EachTarget_GivesTheOutcomeOfFillThroughEmitAndPocoMapping goes: PocoReadRulesTests reads the same matrix through both tiers. - The 19 scenario methods of ColumnReadProjectionTests go. DifferentialCases lists their 39 scenarios, which it already ran. - The barrier tests start their workers on dedicated threads. Net -940 lines; 6497 to 6116 tests. The line coverage of ClickHouse.Driver.Tcp (coverlet.collector, full net9.0 suite) does not change: 11135 of 11622 lines, no line lost or gained. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ripCase
Twelve InsertRoundTripCase cases: DateTime from a DateTime of Kind Local,
DateTime64 from DateTimeOffset at the scales 0, 6, 7 and 8, a DateTime64
wall clock in an hour that occurs twice, Decimal values at the declared
precision (Decimal(1, 0), Decimal32(0), Decimal64(0), Decimal(19, 2),
Decimal128(0)), and a float that BFloat16 cannot hold. The codec tests of
these values go with the codec write paths in a later commit.
The lift-matrix case Nullable(Tuple(DateTime('UTC'), String)) is the one
case of WritePathEquivalenceTests that the differential tests did not
have, so that file goes: the differential tests write every decoded
column back and compare the bytes.
Two refusals join the leaf refusal table: a DateTime above the range from
a DateTime, and a Time64 above the range. They agree with the codec write
here.
The case list reaches the Decimal32 and Decimal128 leaves, and the pinned
counts of the differential tests grow with the cases.
Part of ClickHouse/integrations#801.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The converter tests compared each derived write with the codec's write of the same values. The codec write of a caller's column goes in the next commit, so the comparisons become pins. Each literal is the output of that codec write, and this commit passes with the codec write in place: - LeafWriterTests: the bytes of the leaf pairs that no differential case reaches, of each kind of leaf writer under marks, of the canonical values and of UUIDs; the refusals in segments; bytes computed from the plain wire form (Unix seconds, padded FixedString) for the long inputs. - WriteCombinatorTests: the 29 column types that no differential case has are read back through the codec and compared with the values, for a whole column and a slice; the 28 that the differential tests write go. Zero rows pin the state prefixes. The LowCardinality key widths are pinned and read back. A type given a row shape that it does not have is refused by ClickHouseTcpTypes.CanWrite. - LeafTableTests: a literal list of the write pairs of each leaf, as for the read pairs. The comparison of the derivation with CanWriteElementType goes here and in WriteDerivationTests: the lists, SuggestedTypesTests and the WriteRulesTests table pin the same acceptance. - InternerTests, VariantWriterTests: literal bytes; the two large dictionaries against the order of first use; the wide Variant read back. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mbers IColumnCodec is the wire layer: TypeName, ElementType, ReadStatePrefixAsync, ReadColumnAsync, CanWrite, BeginWrite, WriteStatePrefix, WriteColumn and ClaimsValue. CanWrite is the storage check (WritesFromStorage before): true for the column that a query of the type reads, or for a dense composite column whose child columns the child codecs write. A codec writes such a column only; the converter layer writes every other column (InsertColumnWrite.For). Deleted: WritableElementTypes, CanWriteElementType, NullPlaceholder, NullPlaceholderAs, LowCardinalityKeyWriter, the WriteStatePrefix and WriteColumn overloads with no state, the write shapes of Array, Nullable, LowCardinality and Map, LowCardinalityValueWriter, LowCardinalityKeys, SubstituteColumn, ConcatColumn, TupleFieldColumn, ProjectedColumn, ISpanWritableCodec, CompositeElementProjections, ColumnCodecExtensions, ArrayColumn.OverBuffer, the scattered writes of Variant and Dynamic, the transposition of QBit, and the conversions of every leaf codec write. EnumColumnCodec keeps its placeholder for the leaf writers. The Dynamic codec writes a decoded column only when the codec of each type writes its type column, as the Variant codec does. The block writer of self-described columns (tests only) plans each column through InsertColumnWrite.For, an insert plan of zero rows holds no writes, and PocoColumnBuilderFactory.Create takes no codec. The TcpConverterRead benchmark encodes its blocks through InsertColumnWrite.For. Tests, by the test policy of the stack: a test of a deleted member, a value test that InsertRoundTripCase or the differential tests cover, and a wrong-column test of the deleted path go; the CanWrite rejections test the storage check; the dense writes go through WriteStoredAsync, which fails when the codec does not write the column; insert-path tests of bytes or refusals that nothing else pins stay. CodecTestHarness writes through the insert plan. ColumnWriteAcceptanceTests and ArrayColumnTests go; the row shapes that a type refuses are ClickHouseTcpTypes.CanWrite tests. The dense Variant and Dynamic integration tests take decoded children and assert that the codec writes the column. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ClickHouse.Driver.Tcp/AGENTS.md: "Types" names the two layers (a codec reads, and writes a column from its storage; the converter layer gives every other reading and write), where a leaf pair goes, how a combinator is built from the trees of its children, the read and write rules, and the Fill/Emit rule. "Codec test layering" names the differential tests and what a codec test, a converter test and a literal pin assert, and the dense tests write through CodecTestHarness.WriteStoredAsync. The slice guidance names the converter write path. Changelog: a column of no single CLR type in InsertAsync, and the row of a refused QBit vector. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s match A column that a query read as DateTime64(3) and an insert gave to a DateTime64(6) column was written as its stored counts, so the server stored instants 1000 times too early; Time64 and Enum (an ordinal of another definition means another label) were the same. main writes such a column in the same way. The storage checks compare what gives a stored value its meaning: the scale of DateTime64 and Time64, the members and the width of an Enum. A timezone gives no other meaning to a count, so a DateTime64 of the same scale into another timezone stays a storage write. Through the converter tree of its CLR type, such a column would give the same counts and ordinals, so InsertColumnWrite.For converts a column that a query read as another type through the meaning of its values: the reader of the source type gives DateTimeOffset, TimeSpan or the label, also inside Nullable, LowCardinality, Array, Map and Tuple (and an array that CreateArray builds over such a column), and the writer of the target type writes them, with its own rules (a label that the target does not declare and an instant finer than its scale are refused). A column of a scale above 7, and a Variant or Nested of these types, have no CLR type that holds their values, so the insert refuses them with the reason. Decimal (CLR values), FixedString (the width is compared, and a value of another width is refused), QBit (the layout is compared), Date/Date32 and DateTime (instants) were not affected; tests pin each one. Tests: the storage checks and the comparison of meanings offline, the conversion through the insert plan, and a server test that reads a column from one table and inserts it into a table of another scale, timezone, enum definition or precision, then reads back the values. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A row insert asks the converter derivation whether each target type holds
NULL (the untyped rows ask twice for each column). The answer parsed the
type string on each call, about 0.5 us for Nullable(DateTime('UTC')). The
derivation now keeps the answers by type string, with the limit of its
cache of trees. The bytes of the inserts do not change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…values A column that a query read as a composite with a DateTime64, Time64 or Enum of other parameters was converted as a whole, through one CLR value per row. A String part next to the converted part went through the text of its bytes, so bytes that are not UTF-8 were stored as U+FFFD (EFBFBD for FF), and a DateTime64(9) part refused the whole column. The insert now writes such a column part by part (ConverterDerivation.PlanDecodedPart): a Tuple element by element, an Array and a Map as their stored offsets and then their parts, a Nullable of a composite as its null map and then the composite. Only a leaf whose values mean other values (alone or in Nullable, LowCardinality or SimpleAggregateFunction) is converted through the meaning of its values; every other part goes to its codec when the codec writes it from its storage, else to the converter tree of its CLR type. Under a Nullable Tuple, the null map marks the rows with no value, so a converted part writes its placeholder there. Tests: 14 byte pins (all rows and from row 1) of parts that keep their bytes (String and LowCardinality(String) that are not UTF-8, FixedString, NaN payloads and -0, DateTime64 of the same scale, Decimal, UUID, IPv6, Enum of the same definition) in Tuple, Map keys and values, Array, Nullable Tuple and SimpleAggregateFunction; CreateArray over a decoded Tuple; Tuple into Nullable Tuple and back; refusals of a part; 5 server cases with bytes that are not UTF-8. With the whole conversion, 18 of them fail. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cenarios The read scenarios of the differential tests are cases in DifferentialCases; ColumnReadProjectionTests keeps the projection tests only. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The comment blocks that this part changed keep no em dash, and say what a test checks rather than which defect it would catch. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A String column that a query read, written into a LowCardinality(String),
LowCardinality(Nullable(String)), Nullable(String) or FixedString(N)
column, went through the converter tree of its CLR type string, so bytes
that are not UTF-8 were stored as U+FFFD (EFBFBD for FF). main stores
EFBFBD for the same inserts and refuses the FixedString targets.
Such a column, and such a part of a read composite, is now written part
by part: a String or FixedString part whose target is another type of a
string (another width, Nullable or LowCardinality wrappers) is read as
byte[] and written through the byte[] tree of the target, also inside
Array, Map and Tuple and next to a part of another meaning. A
FixedString(N) takes a value of exactly N bytes, as for any byte[]. A
String into String stays a storage write, and a column of string values
that a caller built keeps the text rules (a shorter FixedString value is
padded).
Tests: 15 byte pins (all rows and from row 1) for String, Nullable(String),
LowCardinality(String), LowCardinality(Nullable(String)) and FixedString
sources into the other string types, in Array, Map and Tuple; the read
bytes against the padded text of a caller for FixedString; 12 server cases
with unhex('FF'). Without the rule, 26 of the 28 fail.
Part of ClickHouse/integrations#801.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A column that a query read was written part by part only for a part of another meaning or a string part of another type, and only the converted parts marked the rows that an enclosing NULL hides. A part of another type that keeps its meaning (a Decimal(18, 2) into Decimal(9, 2)) still converted and checked the hidden value, so a NULL row that hid a value out of the range of the target failed the insert with an OverflowException; the elements of an Array or a Map in such a row were not marked at all. main fails the same way for a Nullable(Decimal(18, 2)) column into Nullable(Decimal(9, 2)) after nullIf. The rows that a NULL hides are marked for every part: the null map of a Nullable (with the marks of an enclosing one) for its inner part, and for the elements of an Array or a Map through the offsets of the rows. A part of another type under marks goes through the converter tree with the rows marked, so it writes the placeholder of its target there with no conversion and no check; a converted part does not read a hidden row; a part of the same type keeps its stored bytes. A composite of another type under marks is taken apart too, and a read column of another type that holds a Nullable is written part by part when every part can be (else as before). A target that is not Nullable refuses a NULL row only when no enclosing NULL hides it. Tests: 10 byte pins (all rows and from row 1) with a hidden Decimal out of the range of the target, enum ordinals that the target or the source does not declare, a DateTime64 finer than the target scale and strings of another FixedString width, in Nullable, Nullable(Tuple), the Array and Map elements of a NULL row, LowCardinality(Nullable) and Array(Nullable(Tuple)); 2 server cases with nullIf. With the code before, 8 of the 12 fail; each mark is mutation-checked on its own. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing miss The writers of LowCardinality(String) and of the fixed-width dictionaries look a value up by its CLR value during a probe of 1024 values, and stop that lookup when more than half of the probe misses. A miss costs a read-only lookup and an add, so a block of distinct values paid two hash lookups for each of its first 1024 values (about 25 us of a 1 ms block of 10,000 distinct strings). Misses only grow during the probe, so the probe stops the lookup at the miss that passes the half, with the same decision: a block of distinct values pays the CLR lookup for 513 values. A value that the lookup finds takes the same path as before, so the write loop of the dictionary writer has the same machine code. The keys come from the canonical interner only, so the bytes do not change. Interleaved runs, 10 blocks of 10,000 rows, against 0006c365 (part 6a with the route benchmarks) and the tip before (63ad85c), tiered compilation off: LowCardinality(String) all distinct 1033.7 / 1076.4 / 945.0 us; 100 distinct 283.9 / 281.6 / 281.3 us; LowCardinality(UUID) all distinct 787.4 / 500.7 / 493.8 us. Tiered compilation on: 615.8 / 633.1 / 608.0, 174.0 / 146.8 / 144.5, 727.0 / 481.1 / 441.1 us. Tests: the distinct-value tests pin the miss at which the lookup stops; a value refused after the probe leaves no CLR key (both interners). Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hidden-value pins hid row 0, so a write that marked the hidden rows only for a slice from row 0 passed them. Three cases hide row 1, so the write of the rows from row 1 starts at a hidden row: a Nullable(Decimal(18, 2)) into Nullable(Decimal(9, 2)), and the Array elements and the Map values of a NULL row of a Nullable(Tuple), each with a Decimal out of the range of the target. With the marks applied only when the slice starts at row 0, the three cases fail. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…one of them
The read and write paths no longer replay a failed bulk read to throw the
first failure in row order:
- The dictionary failure order (DictionaryOrder) is gone. A dictionary
entry that the child cannot convert throws only at a row that uses it,
in every tier ("row" policy). Block.ReadAs no longer fails on an entry
that no row uses; POCO mapping already behaved so.
- The tuple readers (arity 1 to 7), the map reader and the Fill tier of the
POCO scatter let the failure of the bulk read propagate.
Kept: the per-entry failures that the row policy needs, the NULL slot and
the Nullable rules (a value under a NULL is never converted), the NULL row
translation of the POCO tiers, and every message text and row number. The
write side had no order-only code: its catch blocks return pooled buffers.
Tests that pinned the first failure of several now accept the failure of
any of the failing values in every path (Fill, Emit, Block.ReadAs, both
POCO scatter tiers; the columnar, POCO and untyped inserts). Single-failure
tests stay exact. New: dictionary entries that no row uses, a failing entry
that one row uses, and a write of two refused values in every tier.
Part of ClickHouse/integrations#801.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/tcp.mdx says that a read or an insert of a column with several failing values throws the failure of one of them, which can differ between the tiers, and that a LowCardinality dictionary entry fails a read only when a row uses it. ClickHouse.Driver.Tcp/AGENTS.md says the same of Fill and Emit. A changelog fragment describes the visible change. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The engine, the registry, the arms with facet counts, the expectations and their self-tests are gone. In their place, four tests run each case of DifferentialCases through the tiers of the client, for all rows and for a tail that starts above row 0, and one test pins the case list: - reads: Block.ReadAs, POCO mapping and its Fill tier read the same values, CanRead agrees, and each tier reads its tail as the same rows of the read of all rows; - writes: the insert against an independent write that gathers the slice through the indexer (the slicing oracle), the POCO inserts in both gather tiers and the untyped insert against the insert, CanWrite against each insert, and a column read back as another type against the source bytes; - the decoded column: written from its storage, it gives back its bytes, and every insert of it decodes to the source rows; - the scenario cases give their stated values or exception and message; - the number of cases of each source and the CanRead and CanWrite answers over the case list are pinned. A failure of several rows is compared by the failure contract: it must be the failure of one of its rows read or written alone. The tier runners (ClientArms, RowWriteArms) and the Outcome comparer are kept, smaller, under the names that the other test files use. Line coverage of ClickHouse.Driver.Tcp is the same (no line or branch lost). Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 4, F1: the differential tests compared a row insert with the insert only when both gave bytes, so a row insert that failed for every case passed them. Now, for every input of every case and both ranges: - where the insert writes bytes and CanWrite is true, the POCO insert in both gather tiers and the untyped insert must write the same bytes; - where the insert fails, each row insert must fail, with the exception type of the failure of one of its rows written alone (a row insert can name the row of its block, so the message is not compared); where the insert refuses or CanWrite is false, each row insert must refuse; - each insert of the decoded column must give bytes that decode to the source rows, or a refusal from a row insert where CanWrite is false. The one case where the row inserts legitimately differ is pinned in a literal list: the Geometry case, whose column the insert writes from its storage and whose ring and line string values a row insert cannot place. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 5, F1: a Block.ReadAs failure was compared only by its kind, and a tail could fail or give values whenever the read of all rows failed. So a ReadAs that threw an unrelated failure, or gave default values for a failing tail, passed the 416 read cases. Every family (reads, writes, the decoded column) now checks one rule (CheckTiers, Outcome.RangeDifference), for every tier and both ranges: - each row is also read or written alone by the same tier; a range is refused with its rows, fails exactly when one of the rows that it reads or writes fails alone, with the failure of one of them (the exception type and message; the numbers of a row insert's message are masked, because it names the row of its block), and else gives a result; a range of values has the values of its rows alone; - the two tiers of each pair give, for each row alone, the same kind of outcome (a row insert refuses what CanWrite refuses), the same values or bytes, and the same failure (by type between tiers that word it in their own terms); for each range that both read or write, the same kind and the same values or bytes. Block.ReadAs converts every row of the column, so its rule takes every row, also for the tail; a row alone is read with the tree of its view. The pinned Geometry row inserts are not compared with the insert, but follow the rule of their own rows. Five failure scenarios add writes that fail at two rows or only in the first half (a NULL from int?, FixedString values of another length, undeclared enum labels), and a read whose only failing row is row 0, so every failing path is exercised. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…text The differential tests compared a failure of a row insert with the failure of the insert by type only. A row insert refuses a null value itself when it gathers the row, in its own words; every other failure comes from the converter layer, with the text of the insert up to its numbers (a row insert names the row of its block). The pair of the insert and each row insert now compares those failures by the text with its numbers masked, and a null row by type only. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mber
Review round 6, F1: a failure of a row insert was compared with every
number of its message masked, so a message that named a row that does not
fail, or another width or length, still passed. A row insert writes each
block from row 0, so Outcome.InSourceRows maps the row ("row N") and the
position ("index N") of the block to the row of the source; the row of the
insert that the gather names ("row N of the insert", "row N holds", "Row
N") stays; only the element number of an Array or a Map ("element N"),
which has no row, is masked. Every other number stays, and the failure must
then equal the failure of a failing row alone. A test rejects a message
that names a row that does not fail, another length and another width.
Part of ClickHouse/integrations#801.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…imit The GitHub review of the converter core (ClickHouse/integrations#801) asked about the limit of the derivation cache, whose count check does not lock. The cache of the Dynamic writer's type writers and the cache of HoldsNull have the same check. The Dynamic writer's comment now gives the bound: each call adds one entry at most after its check, so the cache passes the limit by fewer entries than the number of calls that run at the same time. The comment of the HoldsNull cache says that it has the count check of the cache of the trees, whose comment gives the same bound. The code does not change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…single CLR type The block writer of self-described columns (tests only) composed its refusal from IColumn.ElementType. For a column that implements IColumn<> more than once, and that InsertColumnWrite.For cannot write, that getter throws InvalidOperationException, so the caller got that error and not the ArgumentException that names the column and the target type. The refusal now names the class of such a column, with the words that InsertColumnWrite gives for it. The insert plan of the connection already guards the getter (DescribeUnwritableColumn). Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The differential samples of Array(Array(...)) types must hold scalar values and an empty array at each depth, so that the deep cases convert leaf values. DifferentialTests checks this for depths 1 to 4 and for Array(Array(Array(DateTime64(3, 'UTC')))). Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IColumnCodec.CanWrite(IColumn) and ILowCardinalityShape.CanWrite(IColumn) answer whether the codec writes a column from its storage, with no conversion of its values. Part 4a gave this check the name WritesFromStorage; this part keeps that name for it, so that it is not confused with ClickHouseTcpTypes.CanWrite, which answers whether any insert writes a CLR type into a column type. The codec tests that test the check take the same name. No behavior change. Part of ClickHouse/integrations#801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cbc1227 to
f83c7ce
Compare

This is part 6b of the stack of pull requests for ClickHouse/integrations#801, a converter layer for the native TCP client.
It is stacked on #674 (part 6a), which is on #673, #672, #671, #670, #668, #667, #666 and #665. Part 6a deleted the old read
members. This part moves the last production users of the codecs' write paths to the converter derivation, with the
same bytes, and then deletes the write members, so that
IColumnCodecis the wire layer only. It also fixes the insertof a column that a query read into a column of another
DateTime64orTime64scale orEnumdefinition, whichmainwrites as raw counts or ordinals. Last, it trims the stack: code that kept the old failure order of a column with several
failing rows, and the plug-in machinery of the differential tests, go (see "Trim").
A codec reads a column, and writes a column from its storage: the column class that a query of the type reads, or a
dense composite column whose child columns the child codecs write from their storage, with the same parameters where a
parameter gives a stored value its meaning. Such a write keeps the bytes that the server sent. The converter layer
writes every other column, in every insert.
What moves to the converter layer
Dynamic: the inference of a type for each value, then one derived writer for each inferred type.Point,Ring,LineString,Polygon,MultiLineString,MultiPolygonandGeometry: the derived writers oftheir structures (
Tuple,Array,Variant).QBitandTuple(): their own writers.ClickHouseTcpColumn.CreateArrayover a column of your own: the offsets, then the inner column through the elementwriter of the array's tree, with no copy of the rows into arrays.
IColumn<T>for more than oneT: the first of those types in the list of CLR types thatthe insert's refusal message names for the target type.
NULLchecks of the row inserts ask the type whether it holdsNULL.Before the move, a new test file pinned the bytes of every route: 97 column types and CLR types, for a write from row 0
and from a later row, and 21 row inserts with a null value, through the insert plan. The pins pass after the move,
except the two declared changes below.
What changes for a user
Fix.
InsertAsyncwith a column that a query read, into a column of another type: aDateTime64(3)column into aDateTime64(6)column was written as its counts, so the server stored other instants (milliseconds read asmicroseconds).
Time64of another scale andEnum8/Enum16of another definition (an ordinal means another label)were the same. The bug is on
maintoo, for the re-insert of a read column. Now:DateTime64orTime64and the members ofan
Enumare the same (a timezone does not count: a count is an instant);DateTimeOffset,TimeSpanor thelabel, also inside
Nullable,LowCardinality,Array,MapandTuple, and writes them with the rules of thetarget type (a label that the target does not declare, and an instant finer than its scale, are refused). In a
composite only those parts are converted: the offsets, the null map and every other part are written as they were
read, so a
Stringnext to them keeps bytes that are not UTF-8;VariantorNestedof such types into another type, arerefused before the insert sends data, with the reason: no CLR type holds those values without a loss.
The other parameters were checked:
Decimalstores CLR values, which the codec encodes at the scale of the target;FixedStringandQBitcompare their width and layout and refuse a value of another width or dimension;DateTimeand
Date/Date32store instants and dates. A server test reads a column from one table and inserts it into a tableof another scale, timezone, enum definition or precision, then reads back the values.
Fix.
InsertAsyncwith aStringcolumn that a query read, into aLowCardinality(String),LowCardinality(Nullable(String))orNullable(String)column: the insert wrote the text of the values, so bytes thatare not valid UTF-8 were stored as U+FFFD (
maindoes the same). Such a column, and such a part of anArray,Mapor
Tuple, is written from its bytes. AFixedString(N)column, whichmainrefuses for such a column, takes it wheneach value is exactly N bytes, as for any
byte[]. A column ofstringvalues that you build keeps the text rules(a shorter
FixedStringvalue is padded), and aStringinto aStringkeeps the bytes that the server sent.Fix.
InsertAsyncwith a column that a query read, whose NULL row hides a value that the target type cannot hold(for example a
Nullable(Decimal(18, 2))column afternullIf, into aNullable(Decimal(9, 2))column): the insertconverted the hidden value and failed with an
OverflowException(maindoes the same). The row gets the placeholderof the target, with no conversion and no check, also inside a
Nullable(Tuple(...))and in theArrayandMapelements of such a row. A part of the same type keeps the bytes that the server sent.
InsertAsyncwith a column that implementsIColumn<T>for more than oneT: aNullable(Int32)column that alsoimplemented
IColumn<string>threwInvalidOperationException, and aFixedString(N)column ofstringvalues thatalso implemented
IColumn<int>was refused. Both are written now, asint?and asstring. The rule is in the docs("Columnar inserts").
A decoded
Nullablecolumn inserted into a column of a related type (for exampleNullable(Enum8('a' = 1, 'b' = 2))into
Nullable(Int8)) has the placeholder of the target type under eachNULL, not the value that the source columnheld there. The values that the server stores are the same.
An insert that refuses a
QBitvector names its row in the column, also in a block after the first. The refusals of aGeometryvalue come from theVariantwriter, with the type nameGeometry; the exception types are the same.Writes of
Dynamic, of the geo types and ofCreateArraycolumns over a column of your own are faster (measuredbelow).
What is deleted
IColumnCodecand the codecs:WritableElementTypes,CanWriteElementType,NullPlaceholder,NullPlaceholderAs,LowCardinalityKeyWriter, theWriteStatePrefixandWriteColumnoverloads with no state, andthe old
CanWrite("does the codec take a column of this CLR type"). The storage check keeps its name,WritesFromStorage.Array,Nullable,LowCardinalityandMap,LowCardinalityValueWriter,LowCardinalityKeys,the substitute, concatenated, tuple-field and projected columns,
ISpanWritableCodec, the write half of the compositeelement projections, the scattered writes of
VariantandDynamic, the transposition ofQBit, and the conversionsof every leaf codec write (with the helpers that only they used).
ColumnCodecExtensionsandArrayColumn.OverBuffer, which only tests used after the move.The
Dynamiccodec writes a decoded column only when the codec of each type writes its type column from its storage,as the
VariantandNestedcodecs do.Trim
Two cleanups at the end of this part, with no change to values, bytes, accepted types or the error text of a single
failing row:
tuple reader, the map reader and the
Filltier of POCO mapping read the rows again one at a time after a bulk readfailed, and a
LowCardinalityreader had two failure orders for its dictionary. These replays ran only on failureand existed only for that order, so they are gone (about 180 lines of production code). The write side had no such
code. The visible change: a column with two or more failing values can report another one of them than
main, andBlock.ReadAs<T>no longer fails on aLowCardinalitydictionary entry that no row uses (asQueryAsync<T>onmain). A changelog fragment anddocs/tcp.mdxsay so.path with the new one, which part 6 removed. Four direct tests over the same case list now check what they still
checked: the read tiers agree, the inserts agree (with an independent write that gathers each slice through the
indexer, which checks the slicing of the insert), a decoded column written again decodes to its rows, and the
scenario cases give their stated outcome; a fifth test pins the case list. Every tier and range follows one rule: each
row is also read or written alone, and a read or a write fails exactly when one of its rows fails alone, with the
failure of one of them, and else gives the values of its rows. 3,443 lines become 1,993, with the same line
coverage (no line or branch lost) and with every hand mutation of the insert slicing, the
FillandEmitrowhandling, the POCO row numbers and the decoded-column checks still failing the suite.
The failure contract, in every tier:
message and row number. The tests pin these exactly.
Block.ReadAs<T>, the two tiers of POCO mapping, a slice and a full write. A message that names a row names the row ofthat value. The tests accept the failure of any one of the failing rows.
needs fails.
LowCardinalitydictionary entry that has no reading fails a read only when a row of the read uses it.Tests
The test layers follow one policy, written for this part: a test of a deleted member goes; a value test goes when an
InsertRoundTripCasecase (a server round trip of the same type and CLR type) or the differential tests cover it; thevalues that nothing covered become
InsertRoundTripCasecases; bytes that no round trip can observe are pinned asliterals; and the tests of the codecs' dense writes stay.
InsertRoundTripCasecases for the values that onlycodec tests held: a
DateTimeofDateTimeKind.Local,DateTime64fromDateTimeOffsetat the scales 0, 6, 7 and 8,a
DateTime64wall clock in an hour that occurs twice,Decimalvalues at the declared precision (Decimal(1, 0),Decimal32(0),Decimal64(0),Decimal(19, 2),Decimal128(0)), and afloatthatBFloat16cannot hold. One liftcase,
Nullable(Tuple(DateTime('UTC'), String)), for the differential tests. The converter tests that compared aderived write with the codec write compare with literal bytes and texts, with bytes computed from the plain wire form,
or with the values that the codec reads back. These two commits pass with the codec write paths still in place, so
every literal is the output of the write that it replaces.
WritesFromStorage(rejections:a child that a caller built, another arity, another order of
Variantalternatives, aNothingchild, aLowCardinalitycolumn without theNULLslot) and the dense writes, which go through a helper that fails when thecodec does not write the column from its storage. A few tests of bytes or refusals that nothing else pins stay in the
codec test files and write through the insert plan.
answer arms of the converters, the declared outcomes, the second run of the POCO read server suite through the
Filltier, and a duplicate rule matrix (−381 tests; measured, no line or branch of coverage lost).
after removing the duplicate layers; 5610 after the deletions (the same on net8.0 and net10.0); 5747 with the three
fixes; 5749 after the failure-order trim; 6603 after the fixture trim; 6614 at the tip, with the changes after review, 0 failed, 0 skipped. The last step adds tests although
it removes lines: each of the three per-case differential tests is one test for each of the 416 cases. Of the codec unit tests, 475 cases go to 334; of the converter tests that used the codec write as the
reference, 673 cases go to 376. The suite takes 2 m 25 s here (2 m 16 s on
mainfor 4186 tests); the sum of the testdurations is the same as before the deletions (132 s).
(whole columns and slices, with the refusals), byte pins of the parts that keep their bytes (strings that are not
UTF-8, NaN payloads, -0, a
DateTime64(9),Decimal,UUID,IPv6,Enum,LowCardinalitydictionaries, inTuple,Map,Array,Nullable(Tuple)), and 20 server cases. The fix of theStringbytes adds 28: byte pins ofeach source and target string type, also in
Array,MapandTuple, the read bytes against the text of a columnthat you build, and 12 server cases with
unhex('FF'). The fix of the hidden values adds 15: byte pins with a hiddenDecimalout of range (also at a row where a slice starts), hidden enum ordinals, a hiddenDateTime64finer than thetarget and hidden strings of another width, and 2 server cases with
nullIf.ClickHouse.Driver.Tcpwith the whole suite: 95.9%, the same before and after the trim of thedifferential tests.
part of another scale was converted as a whole, so a
Stringpart that was not UTF-8 was stored as U+FFFD; only theparts whose values mean other values are converted, and a read
Stringinto another type of a string is written fromits bytes. Round 2: a value that a NULL hides was still converted by a part of another type; it gets the placeholder
of the target. Round 3: approved; one more test pins a hidden value at the row where a slice starts. A fourth round on
the trim: a row insert that failed counted as agreeing with the insert; the differential tests now require each row
insert to give the outcome of the insert, and pin the one case where it legitimately differs. A fifth round: a
Block.ReadAsfailure was compared by its kind only; every tier and range now follows one rule against its rows reador written alone, and a mutation of each kind in each tier fails the differential tests. A sixth round approved; a
failure of a row insert is now compared with the row of the source that it names, not with its numbers masked.
ClickHouse.Driver.Tcp/AGENTS.mddescribes the two layers (where a leaf pair goes, how a combinator is built, theFill/Emitrule, the differential tests) and what each test layer asserts.Lines
ClickHouse.Driver.Tcp)mainNet, the stack from
main: +16,344 lines, of which +6,436 production and +8,622 tests. Before the trim: +17,855, ofwhich +6,614 production and +9,971 tests.
Benchmarks
The route benchmarks that this part adds, against the code before this part (the first gate below, ratio new / old):
CreateArrayofNullable(Int32)0.38,Dynamicof mixed kinds 0.82,LowCardinality(UUID)0.68 (100 distinct values)and 0.76 (all distinct),
CreateArrayofLowCardinality(String)0.95,LowCardinality(UInt32)1.03 (100 distinct).Measured outside BenchmarkDotNet, 100,000 rows:
Point0.07,Ring0.11,Geometry0.46,QBit(Float32, 16)0.82; everyfixed-width
LowCardinalityleaf at 0.31 to 1.05 of the codec write that it replaces.Two performance gates, measured on
b15ba584(the later commit changes tests only), 4 rounds each, each run alternatelyon base and head on the same machine. A shape fails when its ratio head/base is above 1.05 in every pair;
checkmarks ashape above 1.05 in some pairs only.
Against the code before this part:
0006c365is a local commit (not pushed) of part 6a with the benchmark file of this part(
TcpConverterWriteRoutes.cs) added, so that the new route benchmarks have a base. PASS.The gate of the stack: the converter benchmarks of part 0 on part 0 (base) and on this part (head). One
FAIL.The shapes that are not
okare not a cost of this part. TheFAIL,Columnar(LowCardinalityNullableStringAsString)at1.15x against part 0 (1.04x against part 6a), is the code-placement artifact that parts 3 and 6a found on this shape and
that is accepted for the stack: the method that reads the column has the same disassembly as on part 0 apart from
embedded addresses, it starts at offset 0 instead of 32 (mod 64) in the benchmark's processes, and with tiered
compilation on the two builds read at the same speed. Of the
checkshapes against part 6a,Poco(NullableDateTimePropertyIntoDateTime)is placement: its hot methods have the same disassembly as on part 6a, start32 bytes apart (mod 64), and that offset alone makes it 1.07x slower in one build.
Poco(NullableIntPropertyIntoInt32)(23 µs, above 1.05 in two pairs of four, a spread of 7% on each side) is a row write of the same POCO gather, which this
part does not change after its earlier gates (0.986 on an earlier commit of this part).
Columnar(ArrayStringAsBytes)(above 1.05 in three pairs, 0.955 in the fourth, a spread of 14% on the head side) is a read path that this part does not
change (
ArrayReaderover theStringleaf read as bytes). The benchmarks do not change.Changes after review
DynamicandHoldsNullcaches give their bound past the limit.DifferentialTestschecks that the nestedArraysamples hold values at every depth (the check of TCP converters 0/6: differential oracle and converter benchmarks #665, after the trim).WritesFromStoragethat TCP converters 4a/6: write combinators for the converter layer #671 gives it.🤖 Generated with Claude Code