Repository navigation
fix(cpp): keep aligned value columns row-aligned when a record omits measurements - #968
Merged
ColinLeeo merged 4 commits intoOct 10, 2026
Merged
Conversation
…surements Every value column of an aligned chunk group has to consume exactly one row per time column row. write_record_aligned() / write_tablet_aligned() only advanced the columns that the incoming record/tablet carried, so a measurement missing from a row left its column one row short: its not-null bitmap started at the wrong row index and the values were paired with the earliest timestamps of the page on read, and the statistics recomputed after recovery described the shifted rows. - create the value chunk writer when the measurement is registered on an aligned device, so every registered measurement takes part from row 0 - write NULL rows for the measurements a record/tablet does not carry (Java: AlignedChunkGroupWriterImpl#write -> writeEmptyDataInOneRow) - reject registering a new measurement once rows have been written, which would need backfilled rows/pages; Java does not allow expanding an aligned device either - ValuePageWriter::write_null_rows() / ValueChunkWriter::write_null_batch() advance the column by NULL rows and keep page boundaries in step with the time column - a record repeating a measurement now advances that column once (last point wins) instead of running ahead of the time column Tests: TsFileWriterTest.AlignedRecordMissingMeasurementsStayRowAligned, AlignedRecordMissingMeasurementsAcrossPages, AlignedTabletMissingColumnStaysRowAligned, AlignedRecordDuplicateMeasurementWritesOneRow, AlignedRegisterAfterWriteIsRejected and RestorableTsFileIOWriterTest.AlignedTimeseriesRecoverAndWriteNullValue.
The writer only takes ownership of a MeasurementSchema when the registration succeeds, so the two schemas used to provoke E_INVALID_ARG / E_ALREADY_EXIST have to be released by the test. LeakSanitizer flagged them in the ASan jobs of PR apache#968 (208 bytes in 2 allocations).
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Mixed tablet/record page boundaries and duplicate tablet columns can still corrupt aligned column row counts.
3 open findings
What changed in this PR
Fixes sparse aligned C++ writes so value columns remain synchronized with timestamps.
Changes:
- Adds NULL padding for omitted record/tablet measurements.
- Initializes aligned value writers during registration.
- Adds alignment and recovery regression tests.
| File | Description |
|---|---|
cpp/src/writer/tsfile_writer.cc |
Implements aligned-column padding and registration checks. |
cpp/src/writer/tsfile_writer.h |
Declares aligned writer initialization helper. |
cpp/src/writer/value_chunk_writer.h |
Adds batched NULL-row writing. |
cpp/src/writer/value_page_writer.h |
Adds page-level NULL advancement. |
cpp/test/writer/tsfile_writer_test.cc |
Tests sparse records, tablets, pages, duplicates, and registration. |
cpp/test/file/restorable_tsfile_io_writer_test.cc |
Tests sparse aligned recovery and continued writes. |
🧠 Review effort: Balanced
Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.
Comment on lines
+1081
to
+1083
| for (size_t c = 0; c < tablet.get_column_count(); c++) { | ||
| tablet_measurements.insert(tablet.schema_vec_->at(c).measurement_name_); | ||
| } |
Comment on lines
+352
to
+354
| if (device_schema->is_aligned_ && | ||
| device_schema->time_chunk_writer_ != nullptr && | ||
| device_schema->time_chunk_writer_->hasData()) { |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Bug
For an aligned (tree-model) device, every value column has to consume exactly one row per time-column row. The C++ writer only advanced the columns that the incoming row actually carried:
TsFileWriter::write_record_aligned()built its writer list fromrecord.points_, while the time chunk was written for every record;TsFileWriter::write_tablet_aligned()only advanced the tablet's own columns.So a measurement that was missing from a row left its column one row short. Its not-null bitmap then started at the wrong row index, and on read the values were paired with the earliest timestamps of the page while the tail rows came back as
NULL; the statistics thatRestorableTsFileIOWriterrecomputes after a crash described those shifted rows too (so time-filter pruning could drop or return the wrong range).Java does not have this problem —
AlignedChunkGroupWriterImpl#writefills the measurements a row does not carry:Reproducer (same data, same C++ reader; Java-written file on the left, C++-written file on the right) — a device with
s1/s2where even rows only writes1and odd rows only writes2:The tablet path was broken the same way: a column that first showed up in the second tablet got its values shifted to the beginning of the page.
Fix
Make the aligned invariant hold: every registered measurement of an aligned device advances exactly one row per row written, with a
NULLbit when the row carries no value for it.TsFileWriter::register_aligned_timeseries()now creates theValueChunkWriterof an aligned measurement at registration time (ensure_aligned_value_chunk_writer()), so the column takes part from row 0.write_record_aligned()iterates the device schema instead of only the record's points and writesNULLrows for the measurements the record does not carry. Values are still written in record order; the mapping is by measurement name, so the order of the points inside a record (and the order of the columns) does not matter.write_tablet_aligned()writesNULLrows for the registered measurements a tablet does not carry, and both paths now include those columns in the page-seal lockstep.ValuePageWriter::write_null_rows()/ValueChunkWriter::write_null_batch()implement theNULLwalk: one zero bit per row, no value bytes, no statistic update, page boundaries split onpage_writer_max_point_num_likewrite_batch()so the page lists of the time column and of every value column stay in step.E_INVALID_ARG, even before the first row, after an empty or nonempty flush, and after recovery. The non-aligned registration entry point cannot bypass this restriction. The device schema persists across flushes and is rebuilt from recovered chunk metadata, so no separate write-history flag is needed. Ordinary non-aligned devices can still register additional measurements.Behaviour notes / limits
count == 0) instead of being absent from the metadata. This matches what the aligned tablet path already did for a column whose values are allNULL.Nvalues insidestart_time..end_timethere is generally more than one candidate). Those files should be regenerated; the stored chunk statistic contradicting the bitmap-derived range can at least be used to detect them.Tests
cpp/test/writer/tsfile_writer_test.ccAlignedRecordMissingMeasurementsStayRowAligned— sparse records, per-row values/timestamps and per-measurement statistics.AlignedRecordMissingMeasurementsAcrossPages— same withpage_writer_max_point_num_ = 7so the NULL padding has to seal pages in lockstep.AlignedTabletMissingColumnStaysRowAligned— tablet path, column introduced by the second tablet.AlignedRecordDuplicateMeasurementWritesOneRowAlignedRecordPointOrderDoesNotMatter— rotatedadd_pointorder per row.RegistrationStages/AlignedRegistrationTest.MeasurementsAreFixedAtRegistration— 8 combinations of single-schema/batch first registration and registration before writing / after empty flush / after buffered writes / after data flush. Checks both registration entry points, duplicate measurements, rejected schema-reference copies, and exact timestamp/value/NULL read-back.InvalidBatches/AlignedRegistrationFailureTest.FailedBatchCanBeRetried— empty, null, duplicate-name, and unsupported-encoding batches fail without publishing a partial device; corrected batches can reuse caller-owned schemas.NonAlignedRegistrationRemainsIncremental— ordinary measurements remain extensible, and aligned registration cannot convert an existing device.PageBoundaries/AlignedTabletRecordBoundaryTest.MissingMeasurementsStayAligned— 24 combinations of tablet sizes 6/7/8/14 with a page capacity of 7, present/absent tablet columns, and no flush / flush after tablet / flush after sparse record. Checks page alignment and reads back every timestamp, value and NULL. Six boundary combinations fail before the follow-up fix; all 24 pass afterward.cpp/test/file/restorable_tsfile_io_writer_test.ccRecoveredAlignedDeviceRejectsMeasurementRegistration— rejects extra and duplicate columns immediately after recovery, including attempts through the non-aligned entry point. Appends to existing columns, checks an originally all-NULL column’s row positions, and permits a new device.AlignedTimeseriesRecoverAndWriteNullValue— 10 sparse rows, corrupt tail,RestorableTsFileIOWriterrecovery, 10 more sparse rows; assertscount == 10,start/endper measurement and the row positions after recovery.The original sparse-row reproducers fail against unpatched
develop. The 9 registration/recovery policy cases also fail against the previous PR revision and pass with this change.Default C++ Release build using the repository’s feature defaults (Snappy, LZ4, lzokay, zlib, Zstandard, ANTLR4 and SIMD enabled): 990 tests, 987 passed, 3 skipped, 0 failed. The skipped cases require external fixture configuration. This PR does not change any build feature defaults.
macOS
leaksreports 0 leaked bytes across the registration stages, failed-batch retries, non-aligned registrations, and recovered-device regression. Scoped C++ Spotless formatting passes.