Expose common segment metadata as built-in virtual columns - #19179
Expose common segment metadata as built-in virtual columns#19179xiangfu0 wants to merge 3 commits into
Conversation
4084149 to
bad5922
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #19179 +/- ##
==============================================
+ Coverage 66.63% 100.00% +33.36%
+ Complexity 1423 6 -1417
==============================================
Files 3443 3 -3440
Lines 218663 6 -218657
Branches 34801 0 -34801
==============================================
- Hits 145705 6 -145699
+ Misses 61230 0 -61230
+ Partials 11728 0 -11728
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR expands Pinot’s built-in virtual column surface by adding five new $-prefixed, segment-scoped virtual columns that expose common SegmentMetadata via SQL, while ensuring correct NULL semantics when metadata is genuinely unavailable (e.g., CONSUMING segments).
Changes:
- Introduces
$creationTime,$startTime,$endTime,$totalDocs, and$crcas built-in virtual columns (excluded fromSELECT *, materialized only when explicitly selected). - Centralizes built-in virtual column definitions (name + type + SV/MV shape) in
pinot-spiand reuses them on both broker/controller and server paths to prevent type drift. - Extends the virtual column implementation to support null value vectors, and updates unit/integration tests and explain-plan ordinals accordingly.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| pinot-spi/src/test/java/org/apache/pinot/spi/data/SchemaInfoTest.java | Adds a regression test ensuring built-in virtual columns are excluded from user dimension counts. |
| pinot-spi/src/test/java/org/apache/pinot/spi/data/BuiltInVirtualColumnDefinitionsTest.java | New tests pinning definitions ↔ declared-name set equality, $ prefixing, idempotency, and non-overwrite behavior. |
| pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java | Adds constants for the five new built-in virtual columns and expands BUILT_IN_VIRTUAL_COLUMNS. |
| pinot-spi/src/main/java/org/apache/pinot/spi/data/SchemaInfo.java | Fixes dimension counting to exclude built-in virtual columns by name (instead of -3). |
| pinot-spi/src/main/java/org/apache/pinot/spi/data/BuiltInVirtualColumnDefinitions.java | New SPI “single source of truth” for built-in virtual column field spec shape and schema injection helper. |
| pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentMetadataVirtualColumnProviderTest.java | New unit tests covering value extraction, metadata consistency, and NULL vector behavior for segment-metadata columns. |
| pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/LoaderTest.java | Extends loader assertions to cover the full built-in set and validates NULL-vector behavior for time-range columns. |
| pinot-segment-local/src/test/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImplTest.java | Adds explicit assertions for CONSUMING-like mutable segment behavior for the new metadata columns and adjusts comparison exclusions. |
| pinot-segment-local/src/test/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImplRawMVTest.java | Mirrors mutable/immutable comparison exclusions for segment-level metadata columns in raw MV tests. |
| pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/VirtualColumnProviderFactory.java | Refactors built-in virtual column schema injection to be definition-driven and adds provider coverage validation. |
| pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/VirtualColumnProvider.java | Adds an overridable buildNullValueVector() hook and wires it into virtual column index container construction. |
| pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/VirtualColumnIndexContainer.java | Extends the container to serve StandardIndexes.nullValueVector() and closes it when present. |
| pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentTotalDocsVirtualColumnProvider.java | New provider implementing $totalDocs as a constant derived from context doc count. |
| pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentStartTimeVirtualColumnProvider.java | New provider implementing $startTime from SegmentMetadata#getTimeInterval() with NULL when absent. |
| pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentEndTimeVirtualColumnProvider.java | New provider implementing $endTime from SegmentMetadata#getTimeInterval() with NULL when absent. |
| pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentCreationTimeVirtualColumnProvider.java | New provider implementing $creationTime with unset creation-time detection. |
| pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentCrcVirtualColumnProvider.java | New provider implementing $crc with unset CRC detection. |
| pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/BaseSegmentMetadataVirtualColumnProvider.java | New base provider adding “value or placeholder + all-null vector” behavior for unavailable segment metadata. |
| pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/BaseConstantValueVirtualColumnProvider.java | New reusable base for constant-value virtual columns with consistent dictionary/forward/inverted/metadata building and type checks. |
| pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/column/DefaultNullValueVirtualColumnProvider.java | Refactors existing provider to subclass the new constant-value base while preserving FQCN stability. |
| pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/OfflineClusterIntegrationTest.java | Adds end-to-end validation of the new columns (types, normalization, filtering, null-handling semantics) and updates explain/metadata counts. |
| pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/NullHandlingIntegrationTest.java | Updates explain-plan ordinal assertions (+5 shift) to account for the new $-prefixed columns. |
| pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/MultiStageEngineExplainIntegrationTest.java | Updates explain-plan ordinal assertions (+5 shift) for MSE plans. |
| pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/BaseClusterIntegrationTestSet.java | Adds hybrid-table assertions for $totalDocs, $crc, and $creationTime across potentially CONSUMING segments. |
| pinot-controller/src/test/java/org/apache/pinot/controller/helix/TableCacheTest.java | Updates expected schema and column-name maps to include the new built-ins. |
| pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/PinotQueryResourceStaticValidationTest.java | Makes built-in virtual column count assertions size-driven rather than hard-coded. |
| pinot-common/src/main/java/org/apache/pinot/common/config/provider/TableCache.java | Switches broker/controller built-in injection to use BuiltInVirtualColumnDefinitions. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
195314c to
a3ab079
Compare
Adds five built-in virtual columns that surface segment metadata to queries, alongside the existing $docId / $hostName / $segmentName / $partitionId: | Column | Type | Value | |-----------------|--------|----------------------------------------------| | $creationTime | LONG | segment creation time, epoch millis | | $startTimeMs | LONG | segment time range start, epoch millis | | $endTimeMs | LONG | segment time range end, epoch millis | | $totalDocs | INT | documents stored in the segment | | $segmentCrc | STRING | segment CRC | Example: SELECT $segmentName, $segmentCrc, $totalDocs, $creationTime FROM myTable GROUP BY 1, 2, 3, 4 Each column is a constant single-value column within a segment. Values are read from SegmentMetadata every time the column is built rather than baked into the field spec, so mutable segments - which rebuild their virtual data sources on every access - always observe current metadata. Metadata that genuinely does not exist yet reads as SQL NULL rather than a sentinel: a CONSUMING segment has no time range and no CRC until it is committed. This required teaching the virtual column path to carry a null value vector - VirtualColumnIndexContainer now serves StandardIndexes.nullValueVector() and VirtualColumnProvider gained a buildNullValueVector hook (defaulting to no nulls, so existing providers are unaffected). Without it the placeholder stored in the forward index would be indistinguishable from a real value once null handling is on, e.g. MIN($startTimeMs) returning Long.MIN_VALUE on a consuming segment. $totalDocs counts the documents physically stored in the segment, so for an upsert table it also includes documents that have been replaced and are no longer returned by queries. Naming note: only the time range columns carry the "Ms" suffix. SegmentMetadata#getStartTime()/#getEndTime() return values in the time column's own unit, so unsuffixed names would be ambiguous; a creation time is always epoch millis throughout Pinot. Supporting changes: - New BuiltInVirtualColumns in pinot-spi is the single source of each column's name, data type and single-value/multi-value shape. The broker side (TableCache#addBuiltInVirtualColumns) and the server side (VirtualColumnProviderFactory#addBuiltInVirtualColumnsToSegmentSchema) now both build their field specs from it, so the two can no longer disagree on a type. Previously these specs were declared twice by hand. - DefaultNullValueVirtualColumnProvider grew an overridable getValue(context) so it can back any per-segment constant column, plus a type check that names the offending column and provider instead of throwing a bare ClassCastException from inside segment loading. Behavior is unchanged for existing callers. - Fixes a pre-existing bug in SchemaInfo, which computed getDimensionFieldSpecs().size() - 3 with a comment naming three virtual columns. That has been off by one since $partitionId was added, and these five would have made GET /schemas/info over-report user dimension counts by six in the controller UI. It now excludes built-in virtual columns by name.
Follow-up to the segment metadata virtual columns, covering the review comments and the CI failures on the first revision. Column types and names ---------------------- The three time columns are now TIMESTAMP rather than LONG, so the unit is carried by the type instead of by the column name and query results render them as readable timestamps instead of raw millis. That removes the reason the "Ms" suffix existed, so the columns are back to their natural names: $creationTime TIMESTAMP $startTime TIMESTAMP $endTime TIMESTAMP $totalDocs INT $crc STRING $segmentCrc is renamed to $crc so that the five names are consistent: none of them repeats the "segment" scope, which every one of them shares. Correctness ----------- The segment metadata used to be read three separate times per build - once for the dictionary, once for the column metadata, and once for the null value vector - with nothing tying the three reads together. On a mutable segment, whose metadata is live, they could disagree and the column would then serve its placeholder as if it were a real value. The value is now resolved exactly once per build and shared by all three. VirtualColumnIndexContainer#close now also closes the null value vector. Structure --------- - The constant-value machinery moves out of DefaultNullValueVirtualColumnProvider into a new BaseConstantValueVirtualColumnProvider in the virtualcolumn package. DefaultNullValueVirtualColumnProvider becomes a thin subclass; its fully-qualified name is stored in field specs and resolved reflectively, so it keeps its name and package. - VirtualColumnProviderFactory resolves providers through a map whose key set is checked against the column definitions in a static initializer. A column added without a provider now fails at class load instead of aborting every segment load on every server. - BuiltInVirtualColumns is renamed to BuiltInVirtualColumnDefinitions: the old name differed from the pre-existing BuiltInVirtualColumn by a single trailing letter, and VirtualColumnProviderFactory imports both. Test fixes ---------- Calcite plans reference fields positionally, and all $-prefixed columns sort first in a table's row type, so five new virtual columns shift every ordinal in an EXPLAIN assertion by five. Updated in NullHandlingIntegrationTest, MultiStageEngineExplainIntegrationTest and OfflineClusterIntegrationTest. MultiNodesOfflineClusterIntegrationTest inherits the latter's fixes. The aggregate metadata API reports one entry per column of a loaded segment, which includes the virtual columns, so its expected count grows from 83 to 88. Test coverage ------------- - BuiltInVirtualColumnDefinitionsTest pins the definitions against the declared names, and pins that field specs are never shared between schemas. - A negative test covers the value type check. - The CONSUMING-segment queries in BaseClusterIntegrationTestSet now assert their results instead of only checking that nothing throws. On a hybrid table the time boundary can hide rows that are physically present in a segment, so the assertion is that a segment's $totalDocs is at least the number of rows the query returns from it. - OfflineClusterIntegrationTest additionally checks the millisecond normalization per segment, filtering by $crc, and that null handling sees no nulls on a table where all the metadata is available.
Fixes found by review on top of the segment metadata virtual columns. Do not publish min/max for a column that reads as NULL ------------------------------------------------------ When the segment metadata is unavailable the column stores a placeholder and reports every document as null, but it was still publishing that placeholder as the column's min/max. Segment pruners read min/max without consulting the null value vector, so: SET enableNullHandling=true; SELECT $segmentName, $creationTime FROM myTable ORDER BY $creationTime ASC LIMIT 10 let a CONSUMING segment - whose $creationTime placeholder is the epoch - sort first in SelectionQuerySegmentPruner, consume the whole LIMIT, and prune away every committed segment whose min was greater. The query then returned rows that are all SQL NULL, which under NULLS-LAST ordering should have sorted last. $crc had the mirror problem for ORDER BY ... DESC. The min/max are now left unset whenever the value is a placeholder. Both SelectionQuerySegmentPruner and ColumnValueSegmentPruner already keep a segment that reports no min/max, which is the correct conservative behavior for an all-null column. Resolve the metadata once per data source, not once per index ------------------------------------------------------------- buildDataSource calls buildMetadata and buildColumnIndexContainer separately, so the column metadata came from a different read of SegmentMetadata than the dictionary and the null value vector. Only the container was internally consistent. BaseSegmentMetadataVirtualColumnProvider now overrides buildDataSource and resolves the value once for all three. Apply the value type check on the path that is actually taken ------------------------------------------------------------ The check was only reachable through the no-argument buildDictionary / buildMetadata. Every one of the five new providers goes through the value-taking overloads, which skipped it, so the diagnostic it exists to give never applied to them. The check now runs on every path. $crc is a LONG -------------- A CRC is a long everywhere else in Pinot - SegmentZKMetadata#getCrc returns one, and SegmentMetadata#getCrc merely renders that long as a String. Typing the column STRING to match the latter left users unable to write WHERE $crc = 12345, and contradicted the reasoning used for the time columns, which are TIMESTAMP precisely so the type carries the semantics. Other review fixes ------------------ - AllNullValueVector was a byte-for-byte duplicate of a nested class in OpenStructNullDataSource. Both now share AllNullValueVectorReader in the readers package. - SchemaUtils.validate rejects a user column named after a built-in virtual column. Such a column is filtered out of the segment's physical columns when the metadata is read, and the virtual provider then takes over the name, so queries would silently return segment metadata instead of the user's data. - The built-in providers are stateless, so they are now shared instances rather than reflectively constructed once per virtual column per segment. Segment load did that 9 times per segment after this feature, up from 4. - Removed the static initializer asserting provider coverage. The factory is only ever loaded during segment load, so it did not fail any earlier than the per-column check, and it turned every subsequent load into a bare NoClassDefFoundError. The invariant is now asserted in a test. - Corrected the AllNullValueVectorReader publication comment: toImmutableRoaringBitmap() returns `this`, so safety rests on the volatile write alone and the returned bitmap must be treated as read-only. - Imported SegmentMetadata rather than naming it inline in Javadoc. Naming note: the reviewers proposed prefixing all five columns with "segment" ($segmentCreationTime, ...) to match $segmentName. The terse forms are a deliberate choice, recorded here so it is not re-litigated as an oversight.
a3ab079 to
834a096
Compare
Summary
Adds five built-in virtual columns that expose common segment metadata to queries, alongside the existing
$docId/$hostName/$segmentName/$partitionId.$creationTime$startTime$endTime$totalDocs$crcThis makes a number of operational questions answerable directly in SQL — spotting replicas of a segment whose CRC has diverged, finding segments created before a rollout, or looking at per-segment document skew — without walking the segment metadata REST API.
Like the existing built-ins, these are excluded from
SELECT *and only materialize when named explicitly.Behavior
Each column is a constant single-value column within a segment. The value is read from
SegmentMetadatawhen the column is built rather than baked into the field spec, so a mutable segment — which rebuilds its virtual data sources on every access — picks up metadata that was not yet available when it was created. Within one build the value is resolved exactly once, so the dictionary, the column metadata and the null value vector can never disagree.Metadata that genuinely does not exist yet reads as SQL
NULL, not a sentinel. A CONSUMING segment has no time range and no CRC until it is committed, and a table without a time column never has a time range. This required teaching the virtual column path to carry a null value vector:VirtualColumnIndexContainernow servesStandardIndexes.nullValueVector(), andVirtualColumnProvidergained abuildNullValueVectorhook that defaults to "no nulls", so existing providers are unaffected. Without it the placeholder in the forward index would be indistinguishable from a real value once null handling is enabled — e.g.MIN($startTime)returning the epoch on a consuming segment, andIS NOT NULLmatching every row.The time columns are
TIMESTAMPrather thanLONGso the unit is carried by the type rather than by the column name, and results render as readable timestamps.$totalDocscounts the documents physically stored in the segment, so for an upsert table it also includes documents that have been replaced and are no longer returned by queries. On a hybrid table the broker's time boundary can likewise hide rows that are present in the segment.Supporting changes
New
BuiltInVirtualColumnDefinitionsinpinot-spiis the single source of each column's name, data type, and single-value/multi-value shape. The broker side (TableCache#addBuiltInVirtualColumns) and the server side (VirtualColumnProviderFactory#addBuiltInVirtualColumnsToSegmentSchema) both build their field specs from it, so the two can no longer disagree on a type.VirtualColumnProviderFactoryresolves providers through a map whose key set is checked against the definitions in a static initializer, so a column added without a provider fails at class load rather than aborting every segment load on every server.The constant-value machinery moves out of
DefaultNullValueVirtualColumnProviderintoBaseConstantValueVirtualColumnProvider; the former becomes a thin subclass. Its fully-qualified name is stored in field specs and resolved reflectively, so it keeps its name and package. It also gained a type check that names the offending column and provider instead of throwing a bareClassCastExceptionfrom inside segment loading.Fixes a pre-existing bug in
SchemaInfo— reviewable independently of the feature; happy to split it out if preferred, which computedgetDimensionFieldSpecs().size() - 3with a comment naming three virtual columns. That has been off by one since$partitionIdwas added, and these five columns would have madeGET /schemas/infoover-report user dimension counts by six in the controller UI. It now excludes built-in virtual columns by name, with a test pinning the invariant.Note for reviewers: EXPLAIN assertions shift
Calcite plans reference fields positionally, and all
$-prefixed columns sort first in a table's row type — so every added built-in virtual column shifts every table ordinal in everyEXPLAINassertion. Five new columns means+5inNullHandlingIntegrationTest,MultiStageEngineExplainIntegrationTestandOfflineClusterIntegrationTest(andMultiNodesOfflineClusterIntegrationTest, which inherits). The$partitionIdPR paid the same tax. Similarly the aggregate metadata API reports one entry per column of a loaded segment, so its expected count goes from 83 to 88.SPI surface
pinot-spigains one public class (BuiltInVirtualColumnDefinitions) and five public constants, and publicBUILT_IN_VIRTUAL_COLUMNSgrows from 4 to 9 entries. The addition is source- and binary-compatible, but that set is a filter predicate rather than an inert constant: its only non-test consumer outside this diff isSegmentMetadataImpl#addPhysicalColumns, which drops any name in the set from a segment's physical column list. Downstream/plugin code branching on the set inherits the same change.Backward compatibility / rolling upgrade
The change is additive: no wire format, segment format, or ZK format change, and
SELECT *is unaffected in both engines because$-prefixed columns are excluded from star expansion.Broker-side and server-side schemas are updated in the same commit but roll out independently. Upgrade servers before brokers: an upgraded broker advertises the new columns, so a query naming one during the upgrade window returns partial results plus per-segment exceptions from servers still on the old build. No existing query is affected — only queries that opt into the new columns.
Two REST responses change content:
GET /tables/{table}/metadatagains 5 entries per table incolumnLengthMap/columnCardinalityMap(the expected count inOfflineClusterIntegrationTestmoves from 83 to 88). Loaded segments carry virtual columns in their column metadata, as they already did for the existing four.GET /schemas/infonumDimensionFieldsdecreases by one for every existing schema. The oldsize() - 3had been over-reporting by one ever since$partitionIdwas added; the new value is the true user-dimension count. Operators tracking that field will see a one-off shift.SchemaUtils.validatenow rejects a user column named after a built-in virtual column. Such a schema is already broken today (the column is excluded fromSELECT *and, once the name is reserved, shadowed by the virtual column), but this is a new validation failure for anyone who has one.Null semantics and segment pruning
A column whose metadata is unavailable stores a placeholder and reports every document as null. It deliberately publishes no
ColumnMetadatamin/max: segment pruners read min/max without consulting the null value vector, so publishing the placeholder would let an all-null column reorder and prune segments as if it held a real extreme value — e.g. a CONSUMING segment's epoch$creationTimesorting first inORDER BY $creationTime ASC LIMIT nand pruning away the committed segments that hold the answer. BothSelectionQuerySegmentPrunerandColumnValueSegmentPrunerkeep a segment that reports no min/max.Cost
Each column adds a constant dictionary, readers and column metadata per immutable segment — roughly 1–2 KB per segment across the five, so on the order of 35 MB on a server holding 20k segments. There is no config gate, matching the existing four built-ins.
Testing
SegmentMetadataVirtualColumnProviderTest— values, column metadata, and the NULL path for both a missingSegmentMetadataand a real consuming-segmentSegmentMetadataImpl, across the several representations of an unset creation time, plus a negative test for the value type check.BuiltInVirtualColumnDefinitionsTest— pins the definitions against the declared names, and that field specs are never shared between schemas.LoaderTestiterates the full built-in set and asserts both the value path and the NULL path on a really-loaded segment.MutableSegmentImplTestpins the CONSUMING-segment shape.OfflineClusterIntegrationTestcovers both query engines: data schema, per-segment$totalDocssumming to the table row count, millisecond normalization per segment checked againstDaysSinceEpoch * 86_400_000, filtering by$crc, andenableNullHandling=trueseeing no nulls where all metadata is available.BaseClusterIntegrationTestSet(viaHybridClusterIntegrationTest) asserts results on the CONSUMING path instead of only checking that nothing throws.clean testacrosspinot-spi→pinot-common→pinot-segment-local→pinot-core→pinot-query-planner, plus every integration test class touched above. All green locally.Docs
The virtual-columns page in
pinot-docsneeds a matching update for these five columns, theTIMESTAMPtypes, and the NULL-on-unavailable behavior.Labels
feature,release-notes