Repository navigation
Conversation
f56f2f7 to
851e1c7
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #19035 +/- ##
============================================
+ Coverage 48.29% 48.44% +0.15%
- Complexity 625 635 +10
============================================
Files 5572 5572
Lines 411719 412036 +317
Branches 54229 54314 +85
============================================
+ Hits 198820 199631 +811
+ Misses 212899 212405 -494 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
35286ac to
077029a
Compare
JackieTien97
left a comment
There was a problem hiding this comment.
Thanks for the detailed root-cause analysis and the regression tests. I left a few inline comments, mostly about the cost the new sort adds to INSERT INTO ... SELECT and about how much of the problem the storage-layer guard actually covers. The numbers below were measured locally on the PR head (7f12bc27af; JDK 21, warmed-up JIT, 10,000-row batches).
|
I would like to keep this PR scoped to the The invariant violation originates because the internal I do not think this PR should add defensive behavior to the general storage write path or to the query read path. Please remove the Once |
80cd9d2 to
00022a2
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The sorting changes leave critical watermark, RPC redirection, and pipe data-integrity defects unresolved, while the regression test lacks the multi-page setup needed to reliably expose the original failure.
4 open issues
What changed in this PR
This PR addresses #19020 by sorting tablet rows before storage routing, preventing out-of-order writes from creating overlapping sequence TsFiles. It also adjusts pipe time filtering for device-grouped tablets.
Changes:
- Adds coordinator-side sorting and supplies column categories for table-model
INSERT INTO ... SELECT. - Moves reusable tablet sorters into a shared utility package and updates their callers.
- Adds sorting, pipe-filtering, and SQL regression coverage; documents the remaining TTL limitation.
| File | Description |
|---|---|
iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/tagfilter/TabletTagFilterTest.java |
Uses the relocated table sorter. |
iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/operator/process/InsertTabletStatementGeneratorTest.java |
Adds sorting, null, stability, and generator-reuse tests. |
iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/sink/PipeTabletEventSorterTest.java |
Updates sorter references. |
iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/sink/PipeStatementEventSorterTest.java |
Updates statement-sorter references. |
iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/event/PipeTabletInsertionEventTest.java |
Tests time filtering of device-grouped rows. |
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/sorter/TreeModelTabletEventSorter.java |
Relocates tree sorting and shares timestamp logic. |
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/sorter/TabletAdapter.java |
Relocates the tablet adapter. |
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/sorter/TableModelTabletEventSorter.java |
Adds stable device-and-time sorting without deduplication. |
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/sorter/InsertTabletStatementAdapter.java |
Relocates the statement adapter. |
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/sorter/InsertEventSorter.java |
Provides shared timestamp sorting and bitmap handling. |
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/sorter/InsertEventDataAdapter.java |
Relocates the shared adapter interface. |
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessor.java |
Documents the TTL write-path limitation; no behavior change. |
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertTabletStatement.java |
Adds explicit device-ID cache invalidation. |
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/write/InsertTabletNode.java |
Documents non-prefix expired rows; no behavior change. |
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/Coordinator.java |
Sorts tablet statements before planning and routing. |
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/TableInsertTabletStatementGenerator.java |
Attaches column categories to generated statements. |
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/protocol/opcda/OpcDaServerHandle.java |
Uses the relocated tree sorter. |
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletRawReqV2.java |
Updates table and tree sorter references. |
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletRawReq.java |
Updates tree sorter references. |
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/batch/PipeTabletEventTsFileBatch.java |
Uses shared sorters when buffering tablets. |
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tablet/PipeInsertNodeTabletInsertionEvent.java |
Avoids timestamp-endpoint filtering for multi-device tablets. |
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tablet/parser/TabletInsertionEventParser.java |
Computes actual timestamp bounds before filtering. |
integration-test/src/test/java/org/apache/iotdb/relational/it/insertquery/IoTDBInsertQueryIT.java |
Adds SQL regression assertions for ordering and deduplication. |
Review effort: Balanced
|
Reviewed commit I do not recommend merging this revision yet. The following existing comments remain applicable after tracing the affected call paths:
No additional distinct actionable findings were confirmed in this review, so I have not duplicated the existing inline comments. Validation: static review of the complete PR diff and relevant callers, schema-validation paths, sorters, and tests. No build, unit tests, or integration tests were run in this review. |
|
The SonarCloud quality gate is failing on code this PR does not touch:
Both files are identical to Nothing in this PR can clear those two findings — they belong to a separate change (or disappear once master is analysed again). The rest of the gate (duplications, security, maintainability, coverage) is green, and the |
The sortedness check, the stable sort and the typed reorder of an insert tablet are needed by the write path too, so the sorter moves to org.apache.iotdb.db.utils.sorter and drops the Pipe prefix. It also used to reach into the pipe package for compactBitMaps; it now calls org.apache.iotdb.db.utils.BitMapUtils directly, so the query engine can use it without depending on pipe. The time-only sort moves from TableModelTabletEventSorter to its base class, because a statement sorter of either model can use it, and the base class now exposes the row permutation a sort applied, so a caller can keep the order the rows were submitted in. No behavior change otherwise: the pipe classes and tests only follow the new package.
`INSERT INTO ... SELECT` copies the rows of the child operator verbatim into the insert tablet, while the storage layer documents that it assumes the batch has been sorted by the client (InsertTabletNode#checkTTLInternal). The rows of a SELECT are not ordered: a join, an ORDER BY DESC or a scan over several devices can emit them in any order, so a tablet whose rows go backwards in time could write a row older than the device's last flush time into the sequence space. The resulting sequence TsFile overlapped the existing one, and queries returned duplicated rows and broke the time order, silently. Sort the tablet by time in TableInsertTabletStatementGenerator, i.e. only on the table-model INSERT INTO path, before the statement is submitted. The sort is stable, so rows that share a timestamp keep their relative order and the last one still wins, and it reuses the sorter that the pipe already had instead of a second implementation. A batch that is already ordered only costs one pass over its timestamps. Sorting moves rows, but an attribute is device metadata: it is the last non-null value of the rows as the SELECT emitted them. The statement records the permutation the sort applied and InsertTablet uses it, so the attribute still comes from the last submitted row. Other ingresses (the tree model, REST, MQTT, the session API) are out of scope: they are expected to submit ordered batches, as the storage layer documents.
Reproduce the overlap end to end through SQL: build a device whose sequence file is closed, insert the rows [100, 15] through INSERT INTO ... SELECT, and read them back. Without the sort the older row is written into a new sequence TsFile that overlaps the previous one, and the query returns a duplicated, unordered result; the test asserts the exact row sequence in both scan directions.
1ceb434 to
0c03539
Compare
|
Reworked the scope after the review discussion — the description is updated.
The tree-model INTO path, REST, MQTT and the session API are explicitly out of scope: they still rely on the documented contract that the client submits an ordered batch. If we want to cover them later, the same sort can be invoked at their entry points. |
The sort computes the row permutation anyway, so the timestamps can be rearranged with it in O(N) instead of being sorted a second time in O(N log N).
|






Problem
Fixes #19020.
A tablet whose rows are not in ascending time order could write a row older than the device's last flush time into the sequence space. The resulting sequence TsFile overlapped the existing sequence TsFile of the same device, and plain
SELECTqueries then returned duplicated rows and broke the time order, silently — no exception, no log entry.A pure-SQL reproduction (create the layout, insert an out-of-order tablet through
INSERT INTO ... SELECT, then observe the wrong order) is in the issue.Root cause
INSERT INTO ... SELECTcopies the rows of the child operator verbatim into the insert tablet, while the storage layer documents that it assumes the batch has been sorted by the client (InsertTabletNode.checkTTLInternal). The rows of aSELECTare not ordered — a join, anORDER BY time DESCor a scan over several devices can emit them in any order — so the storage-side steps below see rows that go backwards in time:falseDataRegion.split()TsFileProcessorupdates the tsfile resource boundariesInsertTabletNode.collectSplitRanges()InsertTabletNode.composeLastTimeValuePair()For the tablet
[100, 15]of a device whose last flushed time is39,100flips the sequence flag and15is classified as sequence as well, so it is written into a new sequence TsFile that overlaps the existing one.Fix
Scope. Only the table-model
INSERT INTO ... SELECTpath.TableInsertTabletStatementGenerator#constructInsertTabletStatement()sorts the tablet before the statement leaves the INTO path, so it is ordered by the time it reaches the coordinator. (Earlier revisions of this PR sorted every tablet statement in the coordinator; this revision narrows the change to the INTO path.) The other ingresses — the tree model, REST, MQTT and the session API — build the statement from the request as received and are expected to submit ordered batches, as the storage layer documents; they are untouched here.Sort. A stable sort by time: rows that share a timestamp keep their relative order, so the last one still wins when the storage layer resolves the duplicate. A batch that is already ordered only costs one pass over its timestamps and is not copied. It reuses the sorter the pipe already had — sortedness check, typed reorder of the value columns, bitmap handling, DATE handling — moved to
org.apache.iotdb.db.utils.sorter, so there is a single implementation to keep correct and the query engine does not depend on the pipe package.Attributes. Moving rows changes which row supplies the last non-null value of a device, and an attribute is device metadata rather than a timestamped field. The statement records the permutation the sort applied, and
InsertTabletaggregates attributes in the submitted order, so the client's last row still wins.Trade-off. A table-model tablet usually holds several devices, and sorting by time interleaves them:
splitByDevicethen yields one segment per contiguous run instead of one per device, and the write cost grows with the number of segments (10k-row tablets measured at ~1.2–2.4 ms grouped versus ~5.6–7.9 ms when most rows become their own segment). Sorting by device and then by time would keep the grouping, but it reorders rows across devices, which changes the device order that the storage layer, the session redirect metadata and the device attributes see; this change keeps to the time order only.Known gaps
The TTL check still treats expired rows as a prefix, which a tablet holding several devices does not guarantee: a row marked out-of-ttl after the first alive row can be written. The scan applies the ttl lower bound, so such a row is filtered out at query time and never read back — the cost is storage, not correctness. A comment in
InsertTabletNode#checkTTLInternalandTsFileProcessor#insertTabletrecords it for a follow-up.Testing
InsertTabletStatementGeneratorTest, all against the table-model generator:[100, 15]come out as[15, 100]with the values following their rows, and the permutation is recorded;rowCount == rowLimit, where the generator hands its own arrays to the statement) of mixed types is sorted;reset().InsertTabletTest#testAttributeValueFollowsSubmittedRowOrderAfterSortchecks that an attribute still comes from the last submitted row after the sort.IoTDBInsertQueryIT#testInsertIntoQueryWithOutOfOrderTabletDoesNotOverlapSequenceFilesreproduces the situation end-to-end through SQL (INSERT INTO ... SELECTwith an unsorted tablet, thenSELECT) and asserts the rows come back in time order, in both scan directions.The pipe sorter tests (
PipeStatementEventSorterTest,PipeTabletEventSorterTest) are unchanged apart from the new package. All of the above were run locally on this branch.Out of scope
START REPAIR DATAmoves an overlapping sequence file to the unsequence space.🤖 Generated with Claude Code