This is an automated email from the ASF dual-hosted git repository.
xiangfu0 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new a523b7beae2 Expose common segment metadata as built-in virtual columns
(#19179)
a523b7beae2 is described below
commit a523b7beae27a25c4e18f43ccac38921b3e894ce
Author: Xiang Fu <[email protected]>
AuthorDate: Mon Aug 10 01:08:58 2026 -0700
Expose common segment metadata as built-in virtual columns (#19179)
* Expose common segment metadata as built-in virtual columns
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.
* Use TIMESTAMP for the segment time columns, and address review feedback
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.
* Address review: suppress min/max for NULL columns, expose $crc as LONG
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.
---
.../pinot/common/config/provider/TableCache.java | 17 +-
.../PinotQueryResourceStaticValidationTest.java | 5 +-
.../pinot/controller/helix/TableCacheTest.java | 12 +-
.../tests/BaseClusterIntegrationTestSet.java | 40 ++
.../MultiStageEngineExplainIntegrationTest.java | 6 +-
.../tests/NullHandlingIntegrationTest.java | 8 +-
.../tests/OfflineClusterIntegrationTest.java | 82 ++++-
.../DefaultNullValueVirtualColumnProvider.java | 118 +-----
.../index/openstruct/OpenStructNullDataSource.java | 29 +-
.../index/readers/AllNullValueVectorReader.java | 60 +++
.../BaseConstantValueVirtualColumnProvider.java | 220 +++++++++++
.../BaseSegmentMetadataVirtualColumnProvider.java | 97 +++++
.../SegmentCrcVirtualColumnProvider.java | 49 +++
.../SegmentCreationTimeVirtualColumnProvider.java | 38 ++
.../SegmentEndTimeVirtualColumnProvider.java | 38 ++
.../SegmentStartTimeVirtualColumnProvider.java | 38 ++
.../SegmentTotalDocsVirtualColumnProvider.java | 35 ++
.../virtualcolumn/VirtualColumnIndexContainer.java | 18 +
.../virtualcolumn/VirtualColumnProvider.java | 14 +-
.../VirtualColumnProviderFactory.java | 86 ++++-
.../pinot/segment/local/utils/SchemaUtils.java | 7 +
.../mutable/MutableSegmentImplRawMVTest.java | 13 +-
.../mutable/MutableSegmentImplTest.java | 37 +-
.../local/segment/index/loader/LoaderTest.java | 38 +-
.../SegmentMetadataVirtualColumnProviderTest.java | 405 +++++++++++++++++++++
.../spi/data/BuiltInVirtualColumnDefinitions.java | 107 ++++++
.../java/org/apache/pinot/spi/data/SchemaInfo.java | 13 +-
.../apache/pinot/spi/utils/CommonConstants.java | 31 +-
.../data/BuiltInVirtualColumnDefinitionsTest.java | 99 +++++
.../org/apache/pinot/spi/data/SchemaInfoTest.java | 24 ++
30 files changed, 1590 insertions(+), 194 deletions(-)
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/config/provider/TableCache.java
b/pinot-common/src/main/java/org/apache/pinot/common/config/provider/TableCache.java
index 02c5e7cf313..2e62e80909d 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/config/provider/TableCache.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/config/provider/TableCache.java
@@ -31,11 +31,9 @@ import
org.apache.pinot.spi.config.provider.SchemaChangeListener;
import org.apache.pinot.spi.config.provider.TableConfigChangeListener;
import org.apache.pinot.spi.config.table.QueryConfig;
import org.apache.pinot.spi.config.table.TableConfig;
-import org.apache.pinot.spi.data.DimensionFieldSpec;
-import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.data.BuiltInVirtualColumnDefinitions;
import org.apache.pinot.spi.data.LogicalTableConfig;
import org.apache.pinot.spi.data.Schema;
-import org.apache.pinot.spi.utils.CommonConstants.Segment.BuiltInVirtualColumn;
import org.apache.pinot.spi.utils.TimestampIndexUtils;
import org.apache.pinot.sql.parsers.CalciteSqlParser;
import org.slf4j.Logger;
@@ -119,18 +117,7 @@ public interface TableCache extends PinotConfigProvider {
/// Adds the built-in virtual columns to the schema.
/// NOTE: The virtual column provider class is not added.
default void addBuiltInVirtualColumns(Schema schema) {
- if (!schema.hasColumn(BuiltInVirtualColumn.DOCID)) {
- schema.addField(new DimensionFieldSpec(BuiltInVirtualColumn.DOCID,
FieldSpec.DataType.INT, true));
- }
- if (!schema.hasColumn(BuiltInVirtualColumn.HOSTNAME)) {
- schema.addField(new DimensionFieldSpec(BuiltInVirtualColumn.HOSTNAME,
FieldSpec.DataType.STRING, true));
- }
- if (!schema.hasColumn(BuiltInVirtualColumn.SEGMENTNAME)) {
- schema.addField(new DimensionFieldSpec(BuiltInVirtualColumn.SEGMENTNAME,
FieldSpec.DataType.STRING, true));
- }
- if (!schema.hasColumn(BuiltInVirtualColumn.PARTITIONID)) {
- schema.addField(new DimensionFieldSpec(BuiltInVirtualColumn.PARTITIONID,
FieldSpec.DataType.STRING, false));
- }
+ BuiltInVirtualColumnDefinitions.addToSchema(schema);
}
static Map<Expression, Expression> createExpressionOverrideMap(String
physicalOrLogicalTableName,
diff --git
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/PinotQueryResourceStaticValidationTest.java
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/PinotQueryResourceStaticValidationTest.java
index 45dde807c67..0bd7222cb1c 100644
---
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/PinotQueryResourceStaticValidationTest.java
+++
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/PinotQueryResourceStaticValidationTest.java
@@ -26,6 +26,7 @@ import org.apache.pinot.spi.config.table.TableConfig;
import org.apache.pinot.spi.config.table.TableType;
import org.apache.pinot.spi.data.FieldSpec;
import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.utils.CommonConstants;
import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
import org.mockito.MockitoAnnotations;
import org.testng.Assert;
@@ -62,7 +63,9 @@ public class PinotQueryResourceStaticValidationTest {
Assert.assertNotNull(provider.getTableConfig("testTable_OFFLINE"));
Assert.assertNotNull(provider.getSchema("testTable"));
Assert.assertNotNull(provider.getColumnNameMap("testTable"));
- Assert.assertEquals(provider.getColumnNameMap("testTable").size(), 6); //
2 columns + 4 built-in virtual columns
+ // 2 columns + all built-in virtual columns
+ Assert.assertEquals(provider.getColumnNameMap("testTable").size(),
+ 2 +
CommonConstants.Segment.BuiltInVirtualColumn.BUILT_IN_VIRTUAL_COLUMNS.size());
Assert.assertTrue(provider.getTableNameMap().containsKey("testTable_OFFLINE"));
Assert.assertTrue(provider.getTableNameMap().containsKey("testTable"));
diff --git
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/TableCacheTest.java
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/TableCacheTest.java
index 9eeff07c69f..ca1e4861512 100644
---
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/TableCacheTest.java
+++
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/TableCacheTest.java
@@ -303,6 +303,11 @@ public class TableCacheTest {
expectedColumnMap.put(isCaseInsensitive ? "$hostname" : "$hostName",
"$hostName");
expectedColumnMap.put(isCaseInsensitive ? "$segmentname" : "$segmentName",
"$segmentName");
expectedColumnMap.put(isCaseInsensitive ? "$partitionid" : "$partitionId",
"$partitionId");
+ expectedColumnMap.put(isCaseInsensitive ? "$creationtime" :
"$creationTime", "$creationTime");
+ expectedColumnMap.put(isCaseInsensitive ? "$starttime" : "$startTime",
"$startTime");
+ expectedColumnMap.put(isCaseInsensitive ? "$endtime" : "$endTime",
"$endTime");
+ expectedColumnMap.put(isCaseInsensitive ? "$totaldocs" : "$totalDocs",
"$totalDocs");
+ expectedColumnMap.put("$crc", "$crc");
return expectedColumnMap;
}
@@ -311,7 +316,12 @@ public class TableCacheTest {
.addSingleValueDimension(BuiltInVirtualColumn.DOCID, DataType.INT)
.addSingleValueDimension(BuiltInVirtualColumn.HOSTNAME,
DataType.STRING)
.addSingleValueDimension(BuiltInVirtualColumn.SEGMENTNAME,
DataType.STRING)
- .addMultiValueDimension(BuiltInVirtualColumn.PARTITIONID,
DataType.STRING).build();
+ .addMultiValueDimension(BuiltInVirtualColumn.PARTITIONID,
DataType.STRING)
+ .addSingleValueDimension(BuiltInVirtualColumn.CREATIONTIME,
DataType.TIMESTAMP)
+ .addSingleValueDimension(BuiltInVirtualColumn.STARTTIME,
DataType.TIMESTAMP)
+ .addSingleValueDimension(BuiltInVirtualColumn.ENDTIME,
DataType.TIMESTAMP)
+ .addSingleValueDimension(BuiltInVirtualColumn.TOTALDOCS, DataType.INT)
+ .addSingleValueDimension(BuiltInVirtualColumn.CRC,
DataType.LONG).build();
}
@DataProvider(name = "testTableCacheDataProvider")
diff --git
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/BaseClusterIntegrationTestSet.java
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/BaseClusterIntegrationTestSet.java
index 8a52029a2ab..d13a9723214 100644
---
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/BaseClusterIntegrationTestSet.java
+++
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/BaseClusterIntegrationTestSet.java
@@ -22,6 +22,7 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.sql.Timestamp;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -465,6 +466,45 @@ public abstract class BaseClusterIntegrationTestSet
extends BaseClusterIntegrati
"select $docId, $segmentName, $hostName, $partitionId from mytable
where $docId = 5 limit 50");
getPinotConnection().execute(
"select $docId, $segmentName, $hostName, $partitionId from mytable
where $docId > 19998 limit 50");
+
+ // Segment metadata virtual columns. This method is the only place they
are exercised against a table that can
+ // have CONSUMING segments, so assert the results rather than just
checking that nothing throws.
+ long numTotalDocs = getCountStarResult();
+
+ // $totalDocs is the number of documents stored in the segment. On a
hybrid table the broker's time boundary can
+ // hide some of them, so a segment's $totalDocs is at least the number of
rows the query sees from it, never less.
+ ResultSet perSegment = getPinotConnection().execute(
+ "select $segmentName, max($totalDocs), count(*) from mytable group
by $segmentName limit 10000")
+ .getResultSet(0);
+ assertTrue(perSegment.getRowCount() > 0);
+ long visibleRows = 0;
+ for (int i = 0; i < perSegment.getRowCount(); i++) {
+ String segmentName = perSegment.getString(i, 0);
+ // MAX()/COUNT() render as floating point values in the single-stage
engine, so read them as doubles in both
+ long totalDocsInSegment = (long)
Double.parseDouble(perSegment.getString(i, 1));
+ long rowsFromSegment = (long) Double.parseDouble(perSegment.getString(i,
2));
+ assertTrue(totalDocsInSegment > 0, "Unexpected $totalDocs: " +
totalDocsInSegment + " for: " + segmentName);
+ assertTrue(totalDocsInSegment >= rowsFromSegment,
+ "$totalDocs: " + totalDocsInSegment + " is below the " +
rowsFromSegment + " rows returned by: "
+ + segmentName);
+ visibleRows += rowsFromSegment;
+ }
+ assertEquals(visibleRows, numTotalDocs, "Grouping by $segmentName should
cover every row exactly once");
+
+ // Every segment - CONSUMING included - is created with a creation time,
so none of them falls back to the epoch
+ // placeholder used when the metadata is unavailable
+ ResultSet creationTimes = getPinotConnection()
+ .execute("select $segmentName, $creationTime from mytable group by
$segmentName, $creationTime limit 10000")
+ .getResultSet(0);
+ assertTrue(creationTimes.getRowCount() > 0);
+ for (int i = 0; i < creationTimes.getRowCount(); i++) {
+ String creationTime = creationTimes.getString(i, 1);
+ assertTrue(Timestamp.valueOf(creationTime).getTime() > 0,
+ "Unexpected $creationTime: " + creationTime + " for segment: " +
creationTimes.getString(i, 0));
+ }
+
+ // Selecting them must not fail on any segment type
+ getPinotConnection().execute("select $creationTime, $startTime, $endTime,
$totalDocs, $crc from mytable limit 50");
}
/// Test queries from the query file.
diff --git
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/MultiStageEngineExplainIntegrationTest.java
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/MultiStageEngineExplainIntegrationTest.java
index 69d9861a5b4..1873fe8d1db 100644
---
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/MultiStageEngineExplainIntegrationTest.java
+++
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/MultiStageEngineExplainIntegrationTest.java
@@ -178,7 +178,7 @@ public class MultiStageEngineExplainIntegrationTest extends
BaseClusterIntegrati
"Execution Plan\n"
+ "PinotLogicalAggregate(group=[{}], agg#0=[SUM($0)],
aggType=[FINAL])\n"
+ " PinotLogicalExchange(distribution=[hash])\n"
- + " PinotLogicalAggregate(group=[{}], agg#0=[SUM($6)],
aggType=[LEAF])\n"
+ + " PinotLogicalAggregate(group=[{}], agg#0=[SUM($11)],
aggType=[LEAF])\n"
+ " PinotLogicalTableScan(table=[[default, mytable]])\n");
// Enable PinotAggregateFunctionRewriteRule through query option, ensure
it overrides the broker config
@@ -186,7 +186,7 @@ public class MultiStageEngineExplainIntegrationTest extends
BaseClusterIntegrati
"Execution Plan\n"
+ "PinotLogicalAggregate(group=[{}], agg#0=[SUMLONG($0)],
aggType=[FINAL])\n"
+ " PinotLogicalExchange(distribution=[hash])\n"
- + " PinotLogicalAggregate(group=[{}], agg#0=[SUMLONG($6)],
aggType=[LEAF])\n"
+ + " PinotLogicalAggregate(group=[{}], agg#0=[SUMLONG($11)],
aggType=[LEAF])\n"
+ " PinotLogicalTableScan(table=[[default, mytable]])\n",
Map.of("usePlannerRules",
CommonConstants.Broker.PlannerRuleNames.AGGREGATE_FUNCTION_REWRITE));
@@ -197,7 +197,7 @@ public class MultiStageEngineExplainIntegrationTest extends
BaseClusterIntegrati
+ "LogicalProject(EXPR$0=[CASE(=($1, 0), null:BIGINT, $0)])\n"
+ " PinotLogicalAggregate(group=[{}], agg#0=[SUMLONG($0)],
agg#1=[COUNT($1)], aggType=[FINAL])\n"
+ " PinotLogicalExchange(distribution=[hash])\n"
- + " PinotLogicalAggregate(group=[{}], agg#0=[SUMLONG($6)],
agg#1=[COUNT()], aggType=[LEAF])\n"
+ + " PinotLogicalAggregate(group=[{}], agg#0=[SUMLONG($11)],
agg#1=[COUNT()], aggType=[LEAF])\n"
+ " PinotLogicalTableScan(table=[[default, mytable]])\n",
Map.of("usePlannerRules",
CommonConstants.Broker.PlannerRuleNames.AGGREGATE_FUNCTION_REWRITE + ","
+
CommonConstants.Broker.PlannerRuleNames.AGGREGATE_REDUCE_FUNCTIONS));
diff --git
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/NullHandlingIntegrationTest.java
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/NullHandlingIntegrationTest.java
index 8388fc46722..c158d6913cf 100644
---
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/NullHandlingIntegrationTest.java
+++
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/NullHandlingIntegrationTest.java
@@ -434,7 +434,7 @@ public class NullHandlingIntegrationTest extends
BaseClusterIntegrationTestSet
explainLogical(query,
"Execution Plan\n"
+ "LogicalProject(EXPR$0=[1])\n"
- + " LogicalFilter(condition=[AND(IS NOT NULL($8), <>($8, 0))])\n"
+ + " LogicalFilter(condition=[AND(IS NOT NULL($13), <>($13,
0))])\n"
+ " PinotLogicalTableScan(table=[[default, mytable]])\n",
Map.of(CommonConstants.Broker.Request.QueryOptionKey.ENABLE_NULL_HANDLING,
"false"));
}
@@ -452,7 +452,7 @@ public class NullHandlingIntegrationTest extends
BaseClusterIntegrationTestSet
explainLogical(query,
"Execution Plan\n"
+ "LogicalProject(EXPR$0=[1])\n"
- + " LogicalFilter(condition=[<>($8, 0)])\n"
+ + " LogicalFilter(condition=[<>($13, 0)])\n"
+ " PinotLogicalTableScan(table=[[default, mytable]])\n",
Map.of(CommonConstants.Broker.Request.QueryOptionKey.ENABLE_NULL_HANDLING,
"true"));
}
@@ -470,7 +470,7 @@ public class NullHandlingIntegrationTest extends
BaseClusterIntegrationTestSet
explainLogical(query,
"Execution Plan\n"
+ "LogicalProject(EXPR$0=[1])\n"
- + " LogicalFilter(condition=[AND(IS NULL($8), <>($8, 0))])\n"
+ + " LogicalFilter(condition=[AND(IS NULL($13), <>($13, 0))])\n"
+ " PinotLogicalTableScan(table=[[default, mytable]])\n",
Map.of(CommonConstants.Broker.Request.QueryOptionKey.ENABLE_NULL_HANDLING,
"false"));
}
@@ -537,7 +537,7 @@ public class NullHandlingIntegrationTest extends
BaseClusterIntegrationTestSet
+ "PinotLogicalAggregate(group=[{0}], agg#0=[COUNT($1)],
agg#1=[COUNT($2)], aggType=[FINAL])\n"
+ " PinotLogicalExchange(distribution=[hash[0]])\n"
+ " PinotLogicalAggregate(group=[{0}], agg#0=[COUNT()],
agg#1=[COUNT() FILTER $1], aggType=[LEAF])\n"
- + " LogicalProject(city=[$5], $f1=[IS TRUE(=($7,
_UTF-8'unknown'))])\n"
+ + " LogicalProject(city=[$10], $f1=[IS TRUE(=($12,
_UTF-8'unknown'))])\n"
+ " PinotLogicalTableScan(table=[[default, mytable]])\n");
// IS_TRUE should be trimmed off, then the filter becomes always false
in the server execution plan
explainAskingServers(query,
diff --git
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/OfflineClusterIntegrationTest.java
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/OfflineClusterIntegrationTest.java
index 8984ca7027f..6872360a874 100644
---
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/OfflineClusterIntegrationTest.java
+++
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/OfflineClusterIntegrationTest.java
@@ -2449,6 +2449,78 @@ public class OfflineClusterIntegrationTest extends
BaseClusterIntegrationTestSet
assertEquals(response.get("numSegmentsMatched").asInt(),
numSegmentsToQuery);
}
+ @Test(dataProvider = "useBothQueryEngines")
+ public void testSegmentMetadataVirtualColumns(boolean
useMultiStageQueryEngine)
+ throws Exception {
+ setUseMultiStageQueryEngine(useMultiStageQueryEngine);
+
+ String query = "SELECT $creationTime, $startTime, $endTime, $totalDocs,
$crc FROM mytable LIMIT 10";
+ JsonNode response = postQuery(query);
+ JsonNode resultTable = response.get("resultTable");
+ JsonNode dataSchema = resultTable.get("dataSchema");
+ assertEquals(dataSchema.get("columnNames").toString(),
+
"[\"$creationTime\",\"$startTime\",\"$endTime\",\"$totalDocs\",\"$crc\"]");
+ // The three time columns are TIMESTAMP, so they render as formatted
timestamps rather than raw millis
+ assertEquals(dataSchema.get("columnDataTypes").toString(),
+ "[\"TIMESTAMP\",\"TIMESTAMP\",\"TIMESTAMP\",\"INT\",\"LONG\"]");
+ JsonNode rows = resultTable.get("rows");
+ assertEquals(rows.size(), 10);
+ for (int i = 0; i < 10; i++) {
+ JsonNode row = rows.get(i);
+ long creationTime = Timestamp.valueOf(row.get(0).asText()).getTime();
+ assertTrue(creationTime > 0, "Unexpected segment creation time: " +
row.get(0).asText());
+ long startTime = Timestamp.valueOf(row.get(1).asText()).getTime();
+ long endTime = Timestamp.valueOf(row.get(2).asText()).getTime();
+ assertTrue(startTime > 0, "Unexpected segment start time: " +
row.get(1).asText());
+ assertTrue(startTime <= endTime, "Segment start time: " + startTime + "
is after end time: " + endTime);
+ assertTrue(row.get(3).asInt() > 0, "Unexpected total docs: " +
row.get(3).asInt());
+ // A real CRC, not the placeholder the column falls back to when the
metadata is unavailable
+ long crc = row.get(4).asLong();
+ assertNotEquals(crc, (long)
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_LONG, "Segment CRC should be a real
CRC");
+ }
+
+ // $totalDocs is constant within a segment, so the per-segment values must
add up to the total number of rows
+ query = "SELECT $segmentName, MAX($totalDocs) FROM mytable GROUP BY
$segmentName LIMIT 10000";
+ rows = postQuery(query).get("resultTable").get("rows");
+ long totalDocs = 0;
+ for (JsonNode row : rows) {
+ totalDocs += row.get(1).asLong();
+ }
+ assertEquals(totalDocs, getCountStarResult());
+
+ // $startTime/$endTime are normalized from the time column's own unit.
mytable's time column is DaysSinceEpoch in
+ // DAYS, so each segment's values must be its own day boundaries expressed
as timestamps - this is what
+ // distinguishes a correct implementation from one leaking the raw DAYS
value.
+ query = "SELECT $segmentName, $startTime, $endTime, MIN(DaysSinceEpoch),
MAX(DaysSinceEpoch) FROM mytable "
+ + "GROUP BY $segmentName, $startTime, $endTime LIMIT 10000";
+ rows = postQuery(query).get("resultTable").get("rows");
+ assertFalse(rows.isEmpty());
+ long daysToMillis = TimeUnit.DAYS.toMillis(1);
+ for (JsonNode row : rows) {
+ assertEquals(Timestamp.valueOf(row.get(1).asText()).getTime(),
row.get(3).asLong() * daysToMillis,
+ "Unexpected $startTime for segment: " + row.get(0).asText());
+ assertEquals(Timestamp.valueOf(row.get(2).asText()).getTime(),
row.get(4).asLong() * daysToMillis,
+ "Unexpected $endTime for segment: " + row.get(0).asText());
+ }
+
+ // Filtering on a single segment's CRC must return exactly the documents
grouped under that CRC
+ query = "SELECT $crc, COUNT(*) FROM mytable GROUP BY $crc LIMIT 10000";
+ JsonNode crcRow = postQuery(query).get("resultTable").get("rows").get(0);
+ long segmentCrc = crcRow.get(0).asLong();
+ long docsForCrc = crcRow.get(1).asLong();
+ assertEquals(postQuery("SELECT COUNT(*) FROM mytable WHERE $crc = " +
segmentCrc).get("resultTable")
+ .get("rows").get(0).get(0).asLong(), docsForCrc);
+
+ // With null handling on, the engine must consult the null value vector.
Every segment of this table has a time
+ // range, a CRC and a creation time, so none of these columns may report a
null.
+ for (String column : List.of("$creationTime", "$startTime", "$endTime",
"$totalDocs", "$crc")) {
+ assertEquals(postQuery("SET enableNullHandling=true; SELECT COUNT(*)
FROM mytable WHERE " + column
+ + " IS NOT
NULL").get("resultTable").get("rows").get(0).get(0).asLong(),
getCountStarResult(), column);
+ assertEquals(postQuery("SET enableNullHandling=true; SELECT COUNT(*)
FROM mytable WHERE " + column
+ + " IS NULL").get("resultTable").get("rows").get(0).get(0).asLong(),
0, column);
+ }
+ }
+
@Test(dataProvider = "useBothQueryEngines")
public void testGroupByUDF(boolean useMultiStageQueryEngine)
throws Exception {
@@ -3581,7 +3653,7 @@ public class OfflineClusterIntegrationTest extends
BaseClusterIntegrationTestSet
+ " LogicalProject(count=[$1], name=[$0])\n"
+ " PinotLogicalAggregate(group=[{0}], agg#0=[COUNT($1)],
aggType=[FINAL])\n"
+ " PinotLogicalExchange(distribution=[hash[0]])\n"
- + " PinotLogicalAggregate(group=[{18}], agg#0=[COUNT()],
aggType=[LEAF])\n"
+ + " PinotLogicalAggregate(group=[{23}], agg#0=[COUNT()],
aggType=[LEAF])\n"
+ " PinotLogicalTableScan(table=[[default, mytable]])\n");
assertEquals(response1Json.get("rows").get(0).get(2).asText(), "Rule
Execution Times\n"
+ "Rule: SortRemove -> Time:*\n"
@@ -3870,22 +3942,22 @@ public class OfflineClusterIntegrationTest extends
BaseClusterIntegrationTestSet
JsonNode starColumnResponse = JsonUtils.objectToJsonNode(
getOrCreateAdminClient().getTableClient().getAggregateMetadata(
TableNameBuilder.OFFLINE.tableNameWithType(getTableName()), "*"));
- validateMetadataResponse(starColumnResponse, 83, 10);
+ validateMetadataResponse(starColumnResponse, 88, 10);
JsonNode starEncodedColumnResponse = JsonUtils.objectToJsonNode(
getOrCreateAdminClient().getTableClient().getAggregateMetadata(
TableNameBuilder.OFFLINE.tableNameWithType(getTableName()), "*"));
- validateMetadataResponse(starEncodedColumnResponse, 83, 10);
+ validateMetadataResponse(starEncodedColumnResponse, 88, 10);
JsonNode starWithExtraColumnResponse = JsonUtils.objectToJsonNode(
getOrCreateAdminClient().getTableClient().getAggregateMetadata(
TableNameBuilder.OFFLINE.tableNameWithType(getTableName()),
"CRSElapsedTime,*,OriginStateName"));
- validateMetadataResponse(starWithExtraColumnResponse, 83, 10);
+ validateMetadataResponse(starWithExtraColumnResponse, 88, 10);
JsonNode starWithExtraEncodedColumnResponse = JsonUtils.objectToJsonNode(
getOrCreateAdminClient().getTableClient().getAggregateMetadata(
TableNameBuilder.OFFLINE.tableNameWithType(getTableName()),
"CRSElapsedTime,*,OriginStateName"));
- validateMetadataResponse(starWithExtraEncodedColumnResponse, 83, 10);
+ validateMetadataResponse(starWithExtraEncodedColumnResponse, 88, 10);
}
private void validateMetadataResponse(JsonNode response, int numTotalColumn,
int numMVColumn) {
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/column/DefaultNullValueVirtualColumnProvider.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/column/DefaultNullValueVirtualColumnProvider.java
index f8db37712bc..05ee8c774d7 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/column/DefaultNullValueVirtualColumnProvider.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/column/DefaultNullValueVirtualColumnProvider.java
@@ -18,116 +18,22 @@
*/
package org.apache.pinot.segment.local.segment.index.column;
-import java.math.BigDecimal;
-import
org.apache.pinot.segment.local.segment.index.readers.ConstantValueBigDecimalDictionary;
-import
org.apache.pinot.segment.local.segment.index.readers.ConstantValueBytesDictionary;
-import
org.apache.pinot.segment.local.segment.index.readers.ConstantValueDoubleDictionary;
-import
org.apache.pinot.segment.local.segment.index.readers.ConstantValueFloatDictionary;
-import
org.apache.pinot.segment.local.segment.index.readers.ConstantValueIntDictionary;
-import
org.apache.pinot.segment.local.segment.index.readers.ConstantValueLongDictionary;
-import
org.apache.pinot.segment.local.segment.index.readers.ConstantValueStringDictionary;
-import
org.apache.pinot.segment.local.segment.index.readers.constant.ConstantMVForwardIndexReader;
-import
org.apache.pinot.segment.local.segment.index.readers.constant.ConstantMVInvertedIndexReader;
-import
org.apache.pinot.segment.local.segment.index.readers.constant.ConstantSortedIndexReader;
+import
org.apache.pinot.segment.local.segment.virtualcolumn.BaseConstantValueVirtualColumnProvider;
import
org.apache.pinot.segment.local.segment.virtualcolumn.VirtualColumnContext;
-import
org.apache.pinot.segment.local.segment.virtualcolumn.VirtualColumnProvider;
-import org.apache.pinot.segment.spi.index.metadata.ColumnMetadataImpl;
-import org.apache.pinot.segment.spi.index.reader.Dictionary;
-import org.apache.pinot.segment.spi.index.reader.ForwardIndexReader;
-import org.apache.pinot.segment.spi.index.reader.InvertedIndexReader;
-import org.apache.pinot.spi.data.FieldSpec;
-import org.apache.pinot.spi.utils.ByteArray;
-/// Provide the default null value.
-public class DefaultNullValueVirtualColumnProvider implements
VirtualColumnProvider {
+/// Provides the column's default null value for every document.
+///
+/// This is also how `$hostName` and `$segmentName` are served: their value is
constant for the segment and is carried
+/// as the field's default null value.
+///
+/// NOTE: The fully-qualified name of this class is stored in field specs (see
+/// `DimensionFieldSpec#DimensionFieldSpec(String, DataType, boolean, Class)`)
and resolved reflectively, so it must
+/// not be moved or renamed.
+public class DefaultNullValueVirtualColumnProvider extends
BaseConstantValueVirtualColumnProvider {
@Override
- public ForwardIndexReader<?> buildForwardIndex(VirtualColumnContext context)
{
- if (context.getFieldSpec().isSingleValueField()) {
- return new ConstantSortedIndexReader(context.getTotalDocCount());
- } else {
- return new ConstantMVForwardIndexReader();
- }
- }
-
- @Override
- public Dictionary buildDictionary(VirtualColumnContext context) {
- FieldSpec fieldSpec = context.getFieldSpec();
- switch (fieldSpec.getDataType().getStoredType()) {
- case INT:
- return new ConstantValueIntDictionary((int)
fieldSpec.getDefaultNullValue());
- case LONG:
- return new ConstantValueLongDictionary((long)
fieldSpec.getDefaultNullValue());
- case FLOAT:
- return new ConstantValueFloatDictionary((float)
fieldSpec.getDefaultNullValue());
- case DOUBLE:
- return new ConstantValueDoubleDictionary((double)
fieldSpec.getDefaultNullValue());
- case BIG_DECIMAL:
- return new ConstantValueBigDecimalDictionary((BigDecimal)
fieldSpec.getDefaultNullValue());
- case STRING:
- return new ConstantValueStringDictionary((String)
fieldSpec.getDefaultNullValue());
- case BYTES:
- case UUID:
- return new ConstantValueBytesDictionary((byte[])
fieldSpec.getDefaultNullValue());
- default:
- throw new IllegalStateException();
- }
- }
-
- @Override
- public InvertedIndexReader<?> buildInvertedIndex(VirtualColumnContext
context) {
- if (context.getFieldSpec().isSingleValueField()) {
- return new ConstantSortedIndexReader(context.getTotalDocCount());
- } else {
- return new ConstantMVInvertedIndexReader(context.getTotalDocCount());
- }
- }
-
- @Override
- public ColumnMetadataImpl buildMetadata(VirtualColumnContext context) {
- FieldSpec fieldSpec = context.getFieldSpec();
- ColumnMetadataImpl.Builder builder = new
ColumnMetadataImpl.Builder().setFieldSpec(fieldSpec)
- .setTotalDocs(context.getTotalDocCount())
- .setCardinality(1)
- .setHasDictionary(true);
- if (fieldSpec.isSingleValueField()) {
- builder.setSorted(true);
- } else {
- // When there is no value for a multi-value column, the
maxNumberOfMultiValues and cardinality should be
- // set as 1 because the MV column bitmap uses 1 to delimit the rows for
a MV column. Each MV column will have a
- // default null value based on column's data type
- builder.setMaxNumberOfMultiValues(1);
- }
-
- Object defaultNullValue = fieldSpec.getDefaultNullValue();
- switch (fieldSpec.getDataType().getStoredType()) {
- case INT:
- builder.setMinValue((int) defaultNullValue).setMaxValue((int)
defaultNullValue);
- break;
- case LONG:
- builder.setMinValue((long) defaultNullValue).setMaxValue((long)
defaultNullValue);
- break;
- case FLOAT:
- builder.setMinValue((float) defaultNullValue).setMaxValue((float)
defaultNullValue);
- break;
- case DOUBLE:
- builder.setMinValue((double) defaultNullValue).setMaxValue((double)
defaultNullValue);
- break;
- case BIG_DECIMAL:
- builder.setMinValue((BigDecimal)
defaultNullValue).setMaxValue((BigDecimal) defaultNullValue);
- break;
- case STRING:
- builder.setMinValue((String) defaultNullValue).setMaxValue((String)
defaultNullValue);
- break;
- case BYTES:
- builder.setMinValue(new ByteArray((byte[]) defaultNullValue))
- .setMaxValue(new ByteArray((byte[]) defaultNullValue));
- break;
- default:
- throw new IllegalStateException();
- }
-
- return builder.build();
+ protected Object getValue(VirtualColumnContext context) {
+ return context.getFieldSpec().getDefaultNullValue();
}
}
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/OpenStructNullDataSource.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/OpenStructNullDataSource.java
index be4ad585650..1b36dceff4d 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/OpenStructNullDataSource.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/OpenStructNullDataSource.java
@@ -23,19 +23,17 @@ import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import org.apache.pinot.segment.local.segment.index.datasource.BaseDataSource;
+import
org.apache.pinot.segment.local.segment.index.readers.AllNullValueVectorReader;
import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
import org.apache.pinot.segment.spi.datasource.OpenStructDataSource;
import org.apache.pinot.segment.spi.index.StandardIndexes;
import org.apache.pinot.segment.spi.index.column.ColumnIndexContainer;
import org.apache.pinot.segment.spi.index.reader.ForwardIndexReader;
import org.apache.pinot.segment.spi.index.reader.ForwardIndexReaderContext;
-import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
import org.apache.pinot.segment.spi.partition.PartitionFunction;
import org.apache.pinot.spi.data.DimensionFieldSpec;
import org.apache.pinot.spi.data.FieldSpec;
import org.apache.pinot.spi.data.FieldSpec.DataType;
-import org.roaringbitmap.buffer.ImmutableRoaringBitmap;
-import org.roaringbitmap.buffer.MutableRoaringBitmap;
/// Typed all-null {@link org.apache.pinot.segment.spi.datasource.DataSource}
for an OPEN_STRUCT key
@@ -54,7 +52,7 @@ public class OpenStructNullDataSource extends BaseDataSource {
public OpenStructNullDataSource(FieldSpec fieldSpec, int numDocs) {
super(new AllNullMetadata(fieldSpec, numDocs), new
ColumnIndexContainer.FromMap(Map.of(
StandardIndexes.forward(), new
TypedNullForwardIndex(fieldSpec.getDataType().getStoredType()),
- StandardIndexes.nullValueVector(), new AllNullValueVector(numDocs))));
+ StandardIndexes.nullValueVector(), new
AllNullValueVectorReader(numDocs))));
}
/// Creates an all-null DataSource for a key absent from this OPEN_STRUCT
segment.
@@ -131,29 +129,6 @@ public class OpenStructNullDataSource extends
BaseDataSource {
}
}
- /// {@link NullValueVectorReader} where every document is null.
- static class AllNullValueVector implements NullValueVectorReader {
- private final ImmutableRoaringBitmap _nullBitmap;
-
- AllNullValueVector(int numDocs) {
- MutableRoaringBitmap bm = new MutableRoaringBitmap();
- if (numDocs > 0) {
- bm.add(0L, numDocs);
- }
- _nullBitmap = bm.toImmutableRoaringBitmap();
- }
-
- @Override
- public boolean isNull(int docId) {
- return true;
- }
-
- @Override
- public ImmutableRoaringBitmap getNullBitmap() {
- return _nullBitmap;
- }
- }
-
private static class AllNullMetadata implements DataSourceMetadata {
private final FieldSpec _fieldSpec;
private final int _numDocs;
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/AllNullValueVectorReader.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/AllNullValueVectorReader.java
new file mode 100644
index 00000000000..5c8d6a50e06
--- /dev/null
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/AllNullValueVectorReader.java
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.segment.local.segment.index.readers;
+
+import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
+import org.roaringbitmap.buffer.ImmutableRoaringBitmap;
+import org.roaringbitmap.buffer.MutableRoaringBitmap;
+
+
+/// [NullValueVectorReader] where every document of the column is null.
+///
+/// Used by columns that carry no value at all: a materialized-but-absent
OPEN_STRUCT child, and the segment metadata
+/// virtual columns whose metadata is not available yet (e.g. the time range
of a CONSUMING segment).
+///
+/// The bitmap is materialized lazily, because null value vectors are only
consulted when null handling is enabled
+/// while the reader itself is created on every data source build for a
mutable segment.
+public class AllNullValueVectorReader implements NullValueVectorReader {
+ private final int _numDocs;
+ private volatile ImmutableRoaringBitmap _nullBitmap;
+
+ public AllNullValueVectorReader(int numDocs) {
+ _numDocs = numDocs;
+ }
+
+ @Override
+ public boolean isNull(int docId) {
+ return true;
+ }
+
+ @Override
+ public ImmutableRoaringBitmap getNullBitmap() {
+ ImmutableRoaringBitmap nullBitmap = _nullBitmap;
+ if (nullBitmap == null) {
+ // Benign race: concurrent callers may each build an equivalent bitmap,
and publication is safe because the
+ // volatile write happens-after the bitmap is fully built. NOTE:
toImmutableRoaringBitmap() returns `this`, so
+ // the returned instance is shared and must be treated as read-only by
callers.
+ MutableRoaringBitmap bitmap = _numDocs > 0 ?
MutableRoaringBitmap.bitmapOfRange(0, _numDocs)
+ : new MutableRoaringBitmap();
+ nullBitmap = bitmap.toImmutableRoaringBitmap();
+ _nullBitmap = nullBitmap;
+ }
+ return nullBitmap;
+ }
+}
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/BaseConstantValueVirtualColumnProvider.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/BaseConstantValueVirtualColumnProvider.java
new file mode 100644
index 00000000000..d4d7c9fcc10
--- /dev/null
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/BaseConstantValueVirtualColumnProvider.java
@@ -0,0 +1,220 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.segment.local.segment.virtualcolumn;
+
+import com.google.common.base.Preconditions;
+import java.math.BigDecimal;
+import javax.annotation.Nullable;
+import
org.apache.pinot.segment.local.segment.index.readers.ConstantValueBigDecimalDictionary;
+import
org.apache.pinot.segment.local.segment.index.readers.ConstantValueBytesDictionary;
+import
org.apache.pinot.segment.local.segment.index.readers.ConstantValueDoubleDictionary;
+import
org.apache.pinot.segment.local.segment.index.readers.ConstantValueFloatDictionary;
+import
org.apache.pinot.segment.local.segment.index.readers.ConstantValueIntDictionary;
+import
org.apache.pinot.segment.local.segment.index.readers.ConstantValueLongDictionary;
+import
org.apache.pinot.segment.local.segment.index.readers.ConstantValueStringDictionary;
+import
org.apache.pinot.segment.local.segment.index.readers.constant.ConstantMVForwardIndexReader;
+import
org.apache.pinot.segment.local.segment.index.readers.constant.ConstantMVInvertedIndexReader;
+import
org.apache.pinot.segment.local.segment.index.readers.constant.ConstantSortedIndexReader;
+import org.apache.pinot.segment.spi.index.column.ColumnIndexContainer;
+import org.apache.pinot.segment.spi.index.metadata.ColumnMetadataImpl;
+import org.apache.pinot.segment.spi.index.reader.Dictionary;
+import org.apache.pinot.segment.spi.index.reader.ForwardIndexReader;
+import org.apache.pinot.segment.spi.index.reader.InvertedIndexReader;
+import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
+import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.utils.ByteArray;
+
+
+/// Base for virtual columns holding a single constant value for every
document of a segment.
+///
+/// Subclasses supply the value by overriding
[#getValue(VirtualColumnContext)]; everything else - a constant sorted
+/// forward index, a single-entry dictionary and matching [ColumnMetadataImpl]
- is derived from it here.
+public abstract class BaseConstantValueVirtualColumnProvider implements
VirtualColumnProvider {
+
+ /// Returns the constant value to be stored for every document of the
column. The returned object must match the
+ /// stored type of the field spec (e.g. a `Long` for a LONG or TIMESTAMP
column).
+ protected abstract Object getValue(VirtualColumnContext context);
+
+ /// Reads the value via [#getValue(VirtualColumnContext)] and checks it
against the field's stored type, so that a
+ /// provider returning the wrong box type fails with the offending column
and provider named rather than with a bare
+ /// `ClassCastException` from deep inside segment loading.
+ private Object getCheckedValue(VirtualColumnContext context) {
+ return checkValue(context, getValue(context));
+ }
+
+ /// Checks an already-resolved value against the field's stored type. Every
path that turns a value into an index
+ /// goes through here, including the one taken by subclasses that resolve
the value themselves.
+ private Object checkValue(VirtualColumnContext context, Object value) {
+ FieldSpec fieldSpec = context.getFieldSpec();
+ Class<?> expectedClass = getValueClass(fieldSpec);
+ Preconditions.checkState(expectedClass.isInstance(value),
+ "Virtual column provider: %s returned value of type: %s for column:
%s, expecting: %s", getClass().getName(),
+ value != null ? value.getClass().getName() : "null",
fieldSpec.getName(), expectedClass.getName());
+ return value;
+ }
+
+ private static Class<?> getValueClass(FieldSpec fieldSpec) {
+ switch (fieldSpec.getDataType().getStoredType()) {
+ case INT:
+ return Integer.class;
+ case LONG:
+ return Long.class;
+ case FLOAT:
+ return Float.class;
+ case DOUBLE:
+ return Double.class;
+ case BIG_DECIMAL:
+ return BigDecimal.class;
+ case STRING:
+ return String.class;
+ case BYTES:
+ case UUID:
+ return byte[].class;
+ default:
+ throw new IllegalStateException(unsupportedStoredType(fieldSpec));
+ }
+ }
+
+ private static String unsupportedStoredType(FieldSpec fieldSpec) {
+ return "Unsupported stored type: " +
fieldSpec.getDataType().getStoredType() + " for virtual column: "
+ + fieldSpec.getName();
+ }
+
+ @Override
+ public ForwardIndexReader<?> buildForwardIndex(VirtualColumnContext context)
{
+ if (context.getFieldSpec().isSingleValueField()) {
+ return new ConstantSortedIndexReader(context.getTotalDocCount());
+ } else {
+ return new ConstantMVForwardIndexReader();
+ }
+ }
+
+ @Override
+ public Dictionary buildDictionary(VirtualColumnContext context) {
+ return buildDictionary(context, getCheckedValue(context));
+ }
+
+ /// Builds the dictionary from an already-resolved value, so that callers
which need the value more than once can
+ /// resolve it a single time.
+ private Dictionary buildDictionary(VirtualColumnContext context, Object
value) {
+ FieldSpec fieldSpec = context.getFieldSpec();
+ checkValue(context, value);
+ switch (fieldSpec.getDataType().getStoredType()) {
+ case INT:
+ return new ConstantValueIntDictionary((int) value);
+ case LONG:
+ return new ConstantValueLongDictionary((long) value);
+ case FLOAT:
+ return new ConstantValueFloatDictionary((float) value);
+ case DOUBLE:
+ return new ConstantValueDoubleDictionary((double) value);
+ case BIG_DECIMAL:
+ return new ConstantValueBigDecimalDictionary((BigDecimal) value);
+ case STRING:
+ return new ConstantValueStringDictionary((String) value);
+ case BYTES:
+ case UUID:
+ return new ConstantValueBytesDictionary((byte[]) value);
+ default:
+ throw new IllegalStateException(unsupportedStoredType(fieldSpec));
+ }
+ }
+
+ @Override
+ public InvertedIndexReader<?> buildInvertedIndex(VirtualColumnContext
context) {
+ if (context.getFieldSpec().isSingleValueField()) {
+ return new ConstantSortedIndexReader(context.getTotalDocCount());
+ } else {
+ return new ConstantMVInvertedIndexReader(context.getTotalDocCount());
+ }
+ }
+
+ @Override
+ public ColumnMetadataImpl buildMetadata(VirtualColumnContext context) {
+ return buildMetadata(context, getCheckedValue(context));
+ }
+
+ /// Builds the column metadata from an already-resolved value.
+ protected final ColumnMetadataImpl buildMetadata(VirtualColumnContext
context, Object value) {
+ return buildMetadata(context, value, true);
+ }
+
+ /// Builds the column metadata from an already-resolved value.
+ ///
+ /// @param hasValue `false` when `value` is only a placeholder standing in
for a value that is not available, in
+ /// which case the min/max are left unset. Segment pruners
read min/max without consulting the null
+ /// value vector, so publishing the placeholder there would
let an all-null column prune or reorder
+ /// segments as if it held a real extreme value.
+ protected final ColumnMetadataImpl buildMetadata(VirtualColumnContext
context, Object value, boolean hasValue) {
+ FieldSpec fieldSpec = context.getFieldSpec();
+ checkValue(context, value);
+ ColumnMetadataImpl.Builder builder = new
ColumnMetadataImpl.Builder().setFieldSpec(fieldSpec)
+ .setTotalDocs(context.getTotalDocCount())
+ .setCardinality(1)
+ .setHasDictionary(true);
+ if (fieldSpec.isSingleValueField()) {
+ builder.setSorted(true);
+ } else {
+ // When there is no value for a multi-value column, the
maxNumberOfMultiValues and cardinality should be
+ // set as 1 because the MV column bitmap uses 1 to delimit the rows for
a MV column. Each MV column will have a
+ // default null value based on column's data type
+ builder.setMaxNumberOfMultiValues(1);
+ }
+
+ if (!hasValue) {
+ return builder.build();
+ }
+
+ switch (fieldSpec.getDataType().getStoredType()) {
+ case INT:
+ builder.setMinValue((int) value).setMaxValue((int) value);
+ break;
+ case LONG:
+ builder.setMinValue((long) value).setMaxValue((long) value);
+ break;
+ case FLOAT:
+ builder.setMinValue((float) value).setMaxValue((float) value);
+ break;
+ case DOUBLE:
+ builder.setMinValue((double) value).setMaxValue((double) value);
+ break;
+ case BIG_DECIMAL:
+ builder.setMinValue((BigDecimal) value).setMaxValue((BigDecimal)
value);
+ break;
+ case STRING:
+ builder.setMinValue((String) value).setMaxValue((String) value);
+ break;
+ case BYTES:
+ builder.setMinValue(new ByteArray((byte[]) value)).setMaxValue(new
ByteArray((byte[]) value));
+ break;
+ default:
+ throw new IllegalStateException(unsupportedStoredType(fieldSpec));
+ }
+
+ return builder.build();
+ }
+
+ /// Builds the column index container from an already-resolved value, so
that a subclass whose value comes from
+ /// mutable state can resolve it a single time and keep every component
consistent.
+ protected final ColumnIndexContainer
buildColumnIndexContainer(VirtualColumnContext context, Object value,
+ @Nullable NullValueVectorReader nullValueVector) {
+ return new VirtualColumnIndexContainer(buildForwardIndex(context),
buildInvertedIndex(context),
+ buildDictionary(context, value), nullValueVector);
+ }
+}
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/BaseSegmentMetadataVirtualColumnProvider.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/BaseSegmentMetadataVirtualColumnProvider.java
new file mode 100644
index 00000000000..9e28ba5c836
--- /dev/null
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/BaseSegmentMetadataVirtualColumnProvider.java
@@ -0,0 +1,97 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.segment.local.segment.virtualcolumn;
+
+import javax.annotation.Nullable;
+import
org.apache.pinot.segment.local.segment.index.datasource.ImmutableDataSource;
+import
org.apache.pinot.segment.local.segment.index.readers.AllNullValueVectorReader;
+import org.apache.pinot.segment.spi.SegmentMetadata;
+import org.apache.pinot.segment.spi.datasource.DataSource;
+import org.apache.pinot.segment.spi.index.column.ColumnIndexContainer;
+import org.apache.pinot.segment.spi.index.metadata.ColumnMetadataImpl;
+import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
+
+
+/// Base class for the built-in virtual columns that expose a piece of the
segment metadata (creation time, time range,
+/// CRC, etc.) as a constant single-value column.
+///
+/// The value is read from [SegmentMetadata] when the column is built rather
than baked into the field spec, so that a
+/// mutable segment - which rebuilds its virtual data sources on every access
- picks up metadata that was not yet
+/// available when the segment was created. Within a single build the value is
resolved exactly once (see
+/// [BaseConstantValueVirtualColumnProvider#buildColumnIndexContainer]), so
the dictionary and the null value vector
+/// can never disagree about whether the column has a value.
+///
+/// When the metadata is not available at all, or the specific piece of
metadata is not set (e.g. the time range of a
+/// CONSUMING segment), the column stores the default null value of its data
type *and* reports every document as null,
+/// so that the placeholder is not mistaken for a real value once null
handling is enabled.
+public abstract class BaseSegmentMetadataVirtualColumnProvider extends
BaseConstantValueVirtualColumnProvider {
+
+ @Override
+ protected Object getValue(VirtualColumnContext context) {
+ return valueOrPlaceholder(context, extractValueOrNull(context));
+ }
+
+ @Nullable
+ @Override
+ public NullValueVectorReader buildNullValueVector(VirtualColumnContext
context) {
+ return extractValueOrNull(context) == null ? new
AllNullValueVectorReader(context.getTotalDocCount()) : null;
+ }
+
+ @Override
+ public ColumnIndexContainer buildColumnIndexContainer(VirtualColumnContext
context) {
+ return buildColumnIndexContainer(context, extractValueOrNull(context));
+ }
+
+ @Override
+ public ColumnMetadataImpl buildMetadata(VirtualColumnContext context) {
+ Object extracted = extractValueOrNull(context);
+ return buildMetadata(context, valueOrPlaceholder(context, extracted),
extracted != null);
+ }
+
+ @Override
+ public DataSource buildDataSource(VirtualColumnContext context) {
+ // Resolve the metadata exactly once for the whole data source, so the
dictionary, the column metadata's min/max
+ // and the null value vector all agree about whether this column has a
value. buildDataSource is the path a
+ // mutable segment takes, and it rebuilds its virtual data sources on
every access.
+ Object extracted = extractValueOrNull(context);
+ return new ImmutableDataSource(buildMetadata(context,
valueOrPlaceholder(context, extracted), extracted != null),
+ buildColumnIndexContainer(context, extracted));
+ }
+
+ private ColumnIndexContainer buildColumnIndexContainer(VirtualColumnContext
context, @Nullable Object extracted) {
+ return buildColumnIndexContainer(context, valueOrPlaceholder(context,
extracted),
+ extracted == null ? new
AllNullValueVectorReader(context.getTotalDocCount()) : null);
+ }
+
+ /// Falls back to the column's default null value, which is only ever a
placeholder: whenever it is used, the column
+ /// also reports every document as null.
+ private static Object valueOrPlaceholder(VirtualColumnContext context,
@Nullable Object extracted) {
+ return extracted != null ? extracted :
context.getFieldSpec().getDefaultNullValue();
+ }
+
+ @Nullable
+ private Object extractValueOrNull(VirtualColumnContext context) {
+ SegmentMetadata segmentMetadata = context.getSegmentMetadata();
+ return segmentMetadata != null ? extractValue(segmentMetadata) : null;
+ }
+
+ /// Extracts the value for this column from the given segment metadata, or
`null` when it is not available.
+ @Nullable
+ protected abstract Object extractValue(SegmentMetadata segmentMetadata);
+}
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentCrcVirtualColumnProvider.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentCrcVirtualColumnProvider.java
new file mode 100644
index 00000000000..d64d0683a61
--- /dev/null
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentCrcVirtualColumnProvider.java
@@ -0,0 +1,49 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.segment.local.segment.virtualcolumn;
+
+import javax.annotation.Nullable;
+import org.apache.pinot.segment.spi.SegmentMetadata;
+
+
+/// Virtual column provider for `$crc`, the CRC of the segment.
+///
+/// The CRC is exposed as a LONG, matching how it is stored everywhere else in
Pinot ([SegmentMetadata#getCrc()]
+/// renders the same `long` as a String). It reads as NULL for CONSUMING
segments, which have no CRC until they are
+/// committed. Grouping by `$segmentName` and `$crc` is a convenient way to
detect replicas of a segment that have
+/// diverged.
+public class SegmentCrcVirtualColumnProvider extends
BaseSegmentMetadataVirtualColumnProvider {
+ @Nullable
+ @Override
+ protected Object extractValue(SegmentMetadata segmentMetadata) {
+ String crc = segmentMetadata.getCrc();
+ if (crc == null) {
+ return null;
+ }
+ long crcValue;
+ try {
+ crcValue = Long.parseLong(crc);
+ } catch (NumberFormatException e) {
+ // The CRC is always rendered from a long, but never fail a segment load
over an unreadable one
+ return null;
+ }
+ // SegmentMetadataImpl renders an unset CRC as Long.MIN_VALUE
+ return crcValue != Long.MIN_VALUE ? crcValue : null;
+ }
+}
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentCreationTimeVirtualColumnProvider.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentCreationTimeVirtualColumnProvider.java
new file mode 100644
index 00000000000..3d564876ab3
--- /dev/null
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentCreationTimeVirtualColumnProvider.java
@@ -0,0 +1,38 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.segment.local.segment.virtualcolumn;
+
+import javax.annotation.Nullable;
+import org.apache.pinot.segment.spi.SegmentMetadata;
+
+
+/// Virtual column provider for `$creationTime`, the time the segment was
created.
+///
+/// For a CONSUMING segment this is the time the consuming segment was
created, not the time it was committed.
+public class SegmentCreationTimeVirtualColumnProvider extends
BaseSegmentMetadataVirtualColumnProvider {
+
+ @Nullable
+ @Override
+ protected Object extractValue(SegmentMetadata segmentMetadata) {
+ // An unset creation time is represented as Long.MIN_VALUE in the segment
metadata and as -1 in the segment ZK
+ // metadata a CONSUMING segment is created from, so treat any non-positive
value as unavailable.
+ long creationTime = segmentMetadata.getIndexCreationTime();
+ return creationTime > 0 ? creationTime : null;
+ }
+}
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentEndTimeVirtualColumnProvider.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentEndTimeVirtualColumnProvider.java
new file mode 100644
index 00000000000..ee9d79fe671
--- /dev/null
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentEndTimeVirtualColumnProvider.java
@@ -0,0 +1,38 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.segment.local.segment.virtualcolumn;
+
+import javax.annotation.Nullable;
+import org.apache.pinot.segment.spi.SegmentMetadata;
+import org.joda.time.Interval;
+
+
+/// Virtual column provider for `$endTime`, the end of the segment time range.
+///
+/// The value is normalized from the time unit of the segment's time column,
and exposed as a TIMESTAMP. It reads as
+/// NULL for segments without a time range, such as CONSUMING segments and
segments of tables without a time column.
+public class SegmentEndTimeVirtualColumnProvider extends
BaseSegmentMetadataVirtualColumnProvider {
+
+ @Nullable
+ @Override
+ protected Object extractValue(SegmentMetadata segmentMetadata) {
+ Interval timeInterval = segmentMetadata.getTimeInterval();
+ return timeInterval != null ? timeInterval.getEndMillis() : null;
+ }
+}
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentStartTimeVirtualColumnProvider.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentStartTimeVirtualColumnProvider.java
new file mode 100644
index 00000000000..50600c974a7
--- /dev/null
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentStartTimeVirtualColumnProvider.java
@@ -0,0 +1,38 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.segment.local.segment.virtualcolumn;
+
+import javax.annotation.Nullable;
+import org.apache.pinot.segment.spi.SegmentMetadata;
+import org.joda.time.Interval;
+
+
+/// Virtual column provider for `$startTime`, the start of the segment time
range.
+///
+/// The value is normalized from the time unit of the segment's time column,
and exposed as a TIMESTAMP. It reads as
+/// NULL for segments without a time range, such as CONSUMING segments and
segments of tables without a time column.
+public class SegmentStartTimeVirtualColumnProvider extends
BaseSegmentMetadataVirtualColumnProvider {
+
+ @Nullable
+ @Override
+ protected Object extractValue(SegmentMetadata segmentMetadata) {
+ Interval timeInterval = segmentMetadata.getTimeInterval();
+ return timeInterval != null ? timeInterval.getStartMillis() : null;
+ }
+}
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentTotalDocsVirtualColumnProvider.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentTotalDocsVirtualColumnProvider.java
new file mode 100644
index 00000000000..2b44503a2ba
--- /dev/null
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentTotalDocsVirtualColumnProvider.java
@@ -0,0 +1,35 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.segment.local.segment.virtualcolumn;
+
+
+/// Virtual column provider for `$totalDocs`, the number of documents in the
segment.
+///
+/// The count is taken from the virtual column context rather than from
`SegmentMetadata`,
+/// so that a CONSUMING segment reports the number of documents indexed so far
instead of `0`.
+///
+/// This is the number of documents physically stored in the segment, so for
an upsert table it also includes the
+/// documents that have been replaced and are no longer returned by queries.
+public class SegmentTotalDocsVirtualColumnProvider extends
BaseConstantValueVirtualColumnProvider {
+
+ @Override
+ protected Object getValue(VirtualColumnContext context) {
+ return context.getTotalDocCount();
+ }
+}
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/VirtualColumnIndexContainer.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/VirtualColumnIndexContainer.java
index d64f7291269..fc6d40beff0 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/VirtualColumnIndexContainer.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/VirtualColumnIndexContainer.java
@@ -27,6 +27,7 @@ import
org.apache.pinot.segment.spi.index.column.ColumnIndexContainer;
import org.apache.pinot.segment.spi.index.reader.Dictionary;
import org.apache.pinot.segment.spi.index.reader.ForwardIndexReader;
import org.apache.pinot.segment.spi.index.reader.InvertedIndexReader;
+import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
/// Column index container for virtual columns.
@@ -34,12 +35,23 @@ public class VirtualColumnIndexContainer implements
ColumnIndexContainer {
private final ForwardIndexReader<?> _forwardIndex;
private final InvertedIndexReader<?> _invertedIndex;
private final Dictionary _dictionary;
+ private final NullValueVectorReader _nullValueVector;
public VirtualColumnIndexContainer(ForwardIndexReader<?> forwardIndex,
InvertedIndexReader<?> invertedIndex,
Dictionary dictionary) {
+ this(forwardIndex, invertedIndex, dictionary, null);
+ }
+
+ /// @param nullValueVector marks which documents of the virtual column are
null, or `null` when the column has no
+ /// null value. A virtual column whose value is
genuinely unavailable (e.g. the time range of
+ /// a CONSUMING segment) must supply one, otherwise
the engine treats the placeholder value
+ /// stored in the forward index as a real value.
+ public VirtualColumnIndexContainer(ForwardIndexReader<?> forwardIndex,
InvertedIndexReader<?> invertedIndex,
+ Dictionary dictionary, @Nullable NullValueVectorReader nullValueVector) {
_forwardIndex = forwardIndex;
_invertedIndex = invertedIndex;
_dictionary = dictionary;
+ _nullValueVector = nullValueVector;
}
@Nullable
@@ -54,6 +66,9 @@ public class VirtualColumnIndexContainer implements
ColumnIndexContainer {
if (indexType.equals(StandardIndexes.dictionary())) {
return (I) _dictionary;
}
+ if (indexType.equals(StandardIndexes.nullValueVector())) {
+ return (I) _nullValueVector;
+ }
return null;
}
@@ -67,5 +82,8 @@ public class VirtualColumnIndexContainer implements
ColumnIndexContainer {
if (_dictionary != null) {
_dictionary.close();
}
+ if (_nullValueVector != null) {
+ _nullValueVector.close();
+ }
}
}
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/VirtualColumnProvider.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/VirtualColumnProvider.java
index 194773d0332..0079cdaa2b0 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/VirtualColumnProvider.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/VirtualColumnProvider.java
@@ -18,6 +18,7 @@
*/
package org.apache.pinot.segment.local.segment.virtualcolumn;
+import javax.annotation.Nullable;
import
org.apache.pinot.segment.local.segment.index.datasource.ImmutableDataSource;
import org.apache.pinot.segment.spi.ColumnMetadata;
import org.apache.pinot.segment.spi.datasource.DataSource;
@@ -25,6 +26,7 @@ import
org.apache.pinot.segment.spi.index.column.ColumnIndexContainer;
import org.apache.pinot.segment.spi.index.reader.Dictionary;
import org.apache.pinot.segment.spi.index.reader.ForwardIndexReader;
import org.apache.pinot.segment.spi.index.reader.InvertedIndexReader;
+import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
/// Virtual column provider interface, which is used to instantiate the
various components (dictionary, reader, etc)
@@ -39,9 +41,19 @@ public interface VirtualColumnProvider {
ColumnMetadata buildMetadata(VirtualColumnContext context);
+ /// Returns the null value vector of the virtual column, or `null` when the
column has no null value.
+ ///
+ /// Virtual columns almost always carry a real value for every document,
hence the default. A provider whose value
+ /// can be genuinely unavailable must override this, otherwise the
placeholder stored in the forward index is
+ /// indistinguishable from a real value once null handling is enabled.
+ @Nullable
+ default NullValueVectorReader buildNullValueVector(VirtualColumnContext
context) {
+ return null;
+ }
+
default ColumnIndexContainer buildColumnIndexContainer(VirtualColumnContext
context) {
return new VirtualColumnIndexContainer(buildForwardIndex(context),
buildInvertedIndex(context),
- buildDictionary(context));
+ buildDictionary(context), buildNullValueVector(context));
}
default DataSource buildDataSource(VirtualColumnContext context) {
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/VirtualColumnProviderFactory.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/VirtualColumnProviderFactory.java
index b60d9a3cf00..4d9f04908c3 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/VirtualColumnProviderFactory.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/virtualcolumn/VirtualColumnProviderFactory.java
@@ -18,9 +18,12 @@
*/
package org.apache.pinot.segment.local.segment.virtualcolumn;
+import com.google.common.base.Preconditions;
+import java.util.Map;
+import java.util.stream.Collectors;
import
org.apache.pinot.segment.local.segment.index.column.DefaultNullValueVirtualColumnProvider;
+import org.apache.pinot.spi.data.BuiltInVirtualColumnDefinitions;
import org.apache.pinot.spi.data.DimensionFieldSpec;
-import org.apache.pinot.spi.data.FieldSpec;
import org.apache.pinot.spi.data.Schema;
import org.apache.pinot.spi.plugin.PluginManager;
import org.apache.pinot.spi.utils.CommonConstants.Segment.BuiltInVirtualColumn;
@@ -29,11 +32,46 @@ import org.apache.pinot.spi.utils.NetUtils;
/// Factory for virtual column providers.
public class VirtualColumnProviderFactory {
+ /// Provider for each built-in virtual column. Its key set is asserted to
cover
+ /// [BuiltInVirtualColumnDefinitions#DEFINITIONS] by
`SegmentMetadataVirtualColumnProviderTest`, so a column added
+ /// there without a provider here is caught at build time; at runtime
`getProviderClass` names the offending column.
+ private static final Map<String, Class<? extends VirtualColumnProvider>>
PROVIDER_CLASSES =
+ Map.of(BuiltInVirtualColumn.DOCID, DocIdVirtualColumnProvider.class,
+ BuiltInVirtualColumn.HOSTNAME,
DefaultNullValueVirtualColumnProvider.class,
+ BuiltInVirtualColumn.SEGMENTNAME,
DefaultNullValueVirtualColumnProvider.class,
+ BuiltInVirtualColumn.PARTITIONID,
PartitionIdVirtualColumnProvider.class,
+ BuiltInVirtualColumn.CREATIONTIME,
SegmentCreationTimeVirtualColumnProvider.class,
+ BuiltInVirtualColumn.STARTTIME,
SegmentStartTimeVirtualColumnProvider.class,
+ BuiltInVirtualColumn.ENDTIME,
SegmentEndTimeVirtualColumnProvider.class,
+ BuiltInVirtualColumn.TOTALDOCS,
SegmentTotalDocsVirtualColumnProvider.class,
+ BuiltInVirtualColumn.CRC, SegmentCrcVirtualColumnProvider.class);
+
+ /// Shared instances of the built-in providers, keyed by the class name
stored in the field spec.
+ private static final Map<String, VirtualColumnProvider> BUILT_IN_PROVIDERS =
PROVIDER_CLASSES.values()
+ .stream()
+ .distinct()
+ .collect(Collectors.toUnmodifiableMap(Class::getName,
VirtualColumnProviderFactory::newInstance));
+
private VirtualColumnProviderFactory() {
}
+ private static VirtualColumnProvider newInstance(Class<? extends
VirtualColumnProvider> providerClass) {
+ try {
+ return providerClass.getDeclaredConstructor().newInstance();
+ } catch (Exception e) {
+ throw new IllegalStateException("Caught exception while creating
instance of: " + providerClass.getName(), e);
+ }
+ }
+
public static VirtualColumnProvider buildProvider(VirtualColumnContext
virtualColumnContext) {
String virtualColumnProvider =
virtualColumnContext.getFieldSpec().getVirtualColumnProvider();
+ // The built-in providers are stateless - everything they need comes from
the VirtualColumnContext - so a single
+ // shared instance serves every column of every segment. This matters at
segment load, which resolves a provider
+ // per virtual column per segment, and on the mutable path, which resolves
one per query.
+ VirtualColumnProvider builtInProvider =
BUILT_IN_PROVIDERS.get(virtualColumnProvider);
+ if (builtInProvider != null) {
+ return builtInProvider;
+ }
try {
return PluginManager.get().createInstance(virtualColumnProvider);
} catch (Exception e) {
@@ -41,25 +79,37 @@ public class VirtualColumnProviderFactory {
}
}
+ /// Adds the built-in virtual columns to the schema of a segment, together
with the provider that produces their
+ /// values.
+ ///
+ /// The shape of each column (name, data type, single-value vs multi-value)
comes from
+ /// [BuiltInVirtualColumnDefinitions#DEFINITIONS], which the broker side
uses as well, so the two can never
+ /// disagree on a type.
+ /// This method only layers on the provider class, and the constant value
for the columns whose value is already
+ /// known here.
public static void addBuiltInVirtualColumnsToSegmentSchema(Schema schema,
String segmentName) {
- if (!schema.hasColumn(BuiltInVirtualColumn.DOCID)) {
- schema.addField(new DimensionFieldSpec(BuiltInVirtualColumn.DOCID,
FieldSpec.DataType.INT, true,
- DocIdVirtualColumnProvider.class));
- }
-
- if (!schema.hasColumn(BuiltInVirtualColumn.HOSTNAME)) {
- schema.addField(new DimensionFieldSpec(BuiltInVirtualColumn.HOSTNAME,
FieldSpec.DataType.STRING, true,
- DefaultNullValueVirtualColumnProvider.class,
NetUtils.getHostnameOrAddress()));
- }
-
- if (!schema.hasColumn(BuiltInVirtualColumn.SEGMENTNAME)) {
- schema.addField(new DimensionFieldSpec(BuiltInVirtualColumn.SEGMENTNAME,
FieldSpec.DataType.STRING, true,
- DefaultNullValueVirtualColumnProvider.class, segmentName));
+ for (BuiltInVirtualColumnDefinitions.Definition definition :
BuiltInVirtualColumnDefinitions.DEFINITIONS) {
+ String column = definition.getName();
+ if (schema.hasColumn(column)) {
+ continue;
+ }
+ DimensionFieldSpec fieldSpec = definition.createFieldSpec();
+ fieldSpec.setVirtualColumnProvider(getProviderClass(column).getName());
+ // $hostName and $segmentName are constants known at schema construction
time, and are carried as the field's
+ // default null value, which DefaultNullValueVirtualColumnProvider reads
back.
+ if (BuiltInVirtualColumn.HOSTNAME.equals(column)) {
+ fieldSpec.setDefaultNullValue(NetUtils.getHostnameOrAddress());
+ } else if (BuiltInVirtualColumn.SEGMENTNAME.equals(column)) {
+ fieldSpec.setDefaultNullValue(segmentName);
+ }
+ schema.addField(fieldSpec);
}
+ }
- if (!schema.hasColumn(BuiltInVirtualColumn.PARTITIONID)) {
- schema.addField(new DimensionFieldSpec(BuiltInVirtualColumn.PARTITIONID,
FieldSpec.DataType.STRING, false,
- PartitionIdVirtualColumnProvider.class));
- }
+ private static Class<? extends VirtualColumnProvider>
getProviderClass(String column) {
+ Class<? extends VirtualColumnProvider> providerClass =
PROVIDER_CLASSES.get(column);
+ Preconditions.checkState(providerClass != null, "No virtual column
provider registered for built-in column: %s",
+ column);
+ return providerClass;
}
}
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SchemaUtils.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SchemaUtils.java
index 9c257b06eee..1481a5b26d8 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SchemaUtils.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SchemaUtils.java
@@ -31,6 +31,7 @@ import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.apache.pinot.common.evaluator.FunctionEvaluatorFactory;
import org.apache.pinot.spi.config.table.TableConfig;
+import org.apache.pinot.spi.data.BuiltInVirtualColumnDefinitions;
import org.apache.pinot.spi.data.DateTimeFieldSpec;
import org.apache.pinot.spi.data.DateTimeFormatSpec;
import org.apache.pinot.spi.data.DateTimeGranularitySpec;
@@ -162,6 +163,12 @@ public class SchemaUtils {
String column = fieldSpec.getName();
Preconditions.checkState(!StringUtils.containsWhitespace(column),
"The column name \"%s\" should not contain blank space.", column);
+ // A user column of the same name would be shadowed at query time: the
built-in virtual column is filtered out
+ // of the segment's physical columns when the segment 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.
+ Preconditions.checkState(
+ fieldSpec.isVirtualColumn() ||
!BuiltInVirtualColumnDefinitions.NAMES.contains(column),
+ "The column name \"%s\" is reserved for a built-in virtual column.",
column);
if (!fieldSpec.isVirtualColumn()) {
primaryKeyColumnCandidates.add(column);
}
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImplRawMVTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImplRawMVTest.java
index 448bfd242b9..82153f600e8 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImplRawMVTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImplRawMVTest.java
@@ -67,6 +67,14 @@ import static org.testng.Assert.assertNull;
public class MutableSegmentImplRawMVTest implements
PinotBuffersAfterClassCheckRule {
private static final String AVRO_FILE = "data/test_data-mv.avro";
private static final File TEMP_DIR = new File(FileUtils.getTempDirectory(),
"MutableSegmentImplRawMVTest");
+ /// Virtual columns describing the segment itself, which are expected to
differ between a mutable segment and an
+ /// immutable segment built from the same records.
+ private static final Set<String> SEGMENT_LEVEL_VIRTUAL_COLUMNS =
+ Set.of(CommonConstants.Segment.BuiltInVirtualColumn.SEGMENTNAME,
+ CommonConstants.Segment.BuiltInVirtualColumn.CREATIONTIME,
+ CommonConstants.Segment.BuiltInVirtualColumn.STARTTIME,
+ CommonConstants.Segment.BuiltInVirtualColumn.ENDTIME,
+ CommonConstants.Segment.BuiltInVirtualColumn.CRC);
private Schema _schema;
private MutableSegmentImpl _mutableSegmentImpl;
@@ -166,8 +174,9 @@ public class MutableSegmentImplRawMVTest implements
PinotBuffersAfterClassCheckR
Dictionary expectedDictionary = expectedDataSource.getDictionary();
assertEquals(actualDictionary.length(), expectedDictionary.length());
- // Allow the segment name to be different
- if
(column.equals(CommonConstants.Segment.BuiltInVirtualColumn.SEGMENTNAME)) {
+ // Allow the segment level metadata to be different between the
mutable segment and the immutable segment
+ // built from the same records
+ if (SEGMENT_LEVEL_VIRTUAL_COLUMNS.contains(column)) {
continue;
}
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImplTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImplTest.java
index c837922f05e..537704ed557 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImplTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImplTest.java
@@ -38,6 +38,7 @@ import
org.apache.pinot.segment.spi.index.creator.VectorIndexConfig;
import org.apache.pinot.segment.spi.index.reader.Dictionary;
import org.apache.pinot.segment.spi.index.reader.ForwardIndexReader;
import org.apache.pinot.segment.spi.index.reader.ForwardIndexReaderContext;
+import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
import org.apache.pinot.spi.config.instance.InstanceType;
import org.apache.pinot.spi.data.FieldSpec;
import org.apache.pinot.spi.data.Schema;
@@ -62,6 +63,14 @@ import static org.testng.Assert.assertEquals;
public class MutableSegmentImplTest {
private static final String AVRO_FILE = "data/test_data-mv.avro";
private static final File TEMP_DIR = new File(FileUtils.getTempDirectory(),
"MutableSegmentImplTest");
+ /// Virtual columns describing the segment itself, which are expected to
differ between a mutable segment and an
+ /// immutable segment built from the same records.
+ private static final Set<String> SEGMENT_LEVEL_VIRTUAL_COLUMNS =
+ Set.of(CommonConstants.Segment.BuiltInVirtualColumn.SEGMENTNAME,
+ CommonConstants.Segment.BuiltInVirtualColumn.CREATIONTIME,
+ CommonConstants.Segment.BuiltInVirtualColumn.STARTTIME,
+ CommonConstants.Segment.BuiltInVirtualColumn.ENDTIME,
+ CommonConstants.Segment.BuiltInVirtualColumn.CRC);
private Schema _schema;
private MutableSegmentImpl _mutableSegmentImpl;
@@ -132,6 +141,29 @@ public class MutableSegmentImplTest {
}
}
+ /// The segment metadata virtual columns are skipped in the
mutable-vs-immutable comparisons above because they
+ /// legitimately differ, so their values on a real mutable segment are
pinned here instead. This mutable segment is
+ /// built from ZK metadata without a creation time and never gets a time
range or a CRC, which is exactly the shape
+ /// of a CONSUMING segment.
+ @Test
+ public void testSegmentMetadataVirtualColumnsOnMutableSegment() {
+ for (String column :
Set.of(CommonConstants.Segment.BuiltInVirtualColumn.CREATIONTIME,
+ CommonConstants.Segment.BuiltInVirtualColumn.STARTTIME,
+ CommonConstants.Segment.BuiltInVirtualColumn.ENDTIME,
+ CommonConstants.Segment.BuiltInVirtualColumn.CRC)) {
+ DataSource dataSource = _mutableSegmentImpl.getDataSource(column);
+ NullValueVectorReader nullValueVector = dataSource.getNullValueVector();
+ Assert.assertNotNull(nullValueVector, "Expecting a null value vector for
virtual column: " + column);
+ assertEquals(nullValueVector.getNullBitmap().getCardinality(),
_mutableSegmentImpl.getNumDocsIndexed());
+ }
+
+ // $totalDocs tracks the documents indexed so far, and is never null
+ DataSource totalDocsDataSource =
+
_mutableSegmentImpl.getDataSource(CommonConstants.Segment.BuiltInVirtualColumn.TOTALDOCS);
+ assertEquals(totalDocsDataSource.getDictionary().getIntValue(0),
_mutableSegmentImpl.getNumDocsIndexed());
+ Assert.assertNull(totalDocsDataSource.getNullValueVector());
+ }
+
@Test
public void testDataSourceForSVColumns() {
for (FieldSpec fieldSpec : _schema.getAllFieldSpecs()) {
@@ -148,8 +180,9 @@ public class MutableSegmentImplTest {
Dictionary expectedDictionary = expectedDataSource.getDictionary();
assertEquals(actualDictionary.length(), expectedDictionary.length());
- // Allow the segment name to be different
- if
(column.equals(CommonConstants.Segment.BuiltInVirtualColumn.SEGMENTNAME)) {
+ // Allow the segment level metadata to be different between the
mutable segment and the immutable segment
+ // built from the same records
+ if (SEGMENT_LEVEL_VIRTUAL_COLUMNS.contains(column)) {
continue;
}
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/LoaderTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/LoaderTest.java
index cd50d0de804..ac22a9a3e56 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/LoaderTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/LoaderTest.java
@@ -20,7 +20,6 @@ package org.apache.pinot.segment.local.segment.index.loader;
import java.io.File;
import java.net.URL;
-import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
@@ -34,11 +33,14 @@ import
org.apache.pinot.segment.local.utils.SegmentOperationsThrottler;
import org.apache.pinot.segment.local.utils.SegmentOperationsThrottlerSet;
import org.apache.pinot.segment.spi.ImmutableSegment;
import org.apache.pinot.segment.spi.IndexSegment;
+import org.apache.pinot.segment.spi.SegmentMetadata;
import org.apache.pinot.segment.spi.V1Constants;
import org.apache.pinot.segment.spi.creator.SegmentGeneratorConfig;
import org.apache.pinot.segment.spi.creator.SegmentVersion;
+import org.apache.pinot.segment.spi.datasource.DataSource;
import org.apache.pinot.segment.spi.index.StandardIndexes;
import org.apache.pinot.segment.spi.index.metadata.SegmentMetadataImpl;
+import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
import org.apache.pinot.segment.spi.store.SegmentDirectory;
import org.apache.pinot.segment.spi.store.SegmentDirectoryPaths;
import org.apache.pinot.spi.config.table.FieldConfig;
@@ -217,11 +219,35 @@ public class LoaderTest {
}
private void testBuiltInVirtualColumns(IndexSegment indexSegment) {
- assertTrue(indexSegment.getColumnNames().containsAll(
- Arrays.asList(BuiltInVirtualColumn.DOCID,
BuiltInVirtualColumn.HOSTNAME, BuiltInVirtualColumn.SEGMENTNAME)));
- assertNotNull(indexSegment.getDataSource(BuiltInVirtualColumn.DOCID));
- assertNotNull(indexSegment.getDataSource(BuiltInVirtualColumn.HOSTNAME));
-
assertNotNull(indexSegment.getDataSource(BuiltInVirtualColumn.SEGMENTNAME));
+ // Iterate the full set so that a newly added built-in virtual column is
covered automatically
+
assertTrue(indexSegment.getColumnNames().containsAll(BuiltInVirtualColumn.BUILT_IN_VIRTUAL_COLUMNS));
+ for (String column : BuiltInVirtualColumn.BUILT_IN_VIRTUAL_COLUMNS) {
+ assertNotNull(indexSegment.getDataSource(column), "Missing data source
for virtual column: " + column);
+ }
+
+ // Segment metadata that this segment carries is exposed as a real value,
and is not marked null
+ SegmentMetadata segmentMetadata = indexSegment.getSegmentMetadata();
+
assertEquals(indexSegment.getDataSource(BuiltInVirtualColumn.TOTALDOCS).getDictionary().get(0),
+ segmentMetadata.getTotalDocs());
+
assertEquals(indexSegment.getDataSource(BuiltInVirtualColumn.CRC).getDictionary().get(0),
+ Long.parseLong(segmentMetadata.getCrc()));
+
assertEquals(indexSegment.getDataSource(BuiltInVirtualColumn.CREATIONTIME).getDictionary().get(0),
+ segmentMetadata.getIndexCreationTime());
+ for (String column : List.of(BuiltInVirtualColumn.TOTALDOCS,
BuiltInVirtualColumn.CRC,
+ BuiltInVirtualColumn.CREATIONTIME)) {
+ assertNull(indexSegment.getDataSource(column).getNullValueVector(),
+ "Unexpected null value vector for virtual column: " + column);
+ }
+
+ // This table has no time column, so the segment has no time range and the
two time columns read as NULL
+ assertNull(segmentMetadata.getTimeInterval());
+ for (String column : List.of(BuiltInVirtualColumn.STARTTIME,
BuiltInVirtualColumn.ENDTIME)) {
+ DataSource dataSource = indexSegment.getDataSource(column);
+ assertEquals(dataSource.getDictionary().get(0),
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_TIMESTAMP);
+ NullValueVectorReader nullValueVector = dataSource.getNullValueVector();
+ assertNotNull(nullValueVector, "Missing null value vector for virtual
column: " + column);
+ assertEquals(nullValueVector.getNullBitmap().getCardinality(),
segmentMetadata.getTotalDocs());
+ }
}
/// Tests loading default string column with empty ("") default null value.
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentMetadataVirtualColumnProviderTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentMetadataVirtualColumnProviderTest.java
new file mode 100644
index 00000000000..a336a2e831f
--- /dev/null
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/virtualcolumn/SegmentMetadataVirtualColumnProviderTest.java
@@ -0,0 +1,405 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.segment.local.segment.virtualcolumn;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.apache.pinot.segment.spi.ColumnMetadata;
+import org.apache.pinot.segment.spi.SegmentMetadata;
+import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
+import org.apache.pinot.segment.spi.index.StandardIndexes;
+import org.apache.pinot.segment.spi.index.column.ColumnIndexContainer;
+import org.apache.pinot.segment.spi.index.metadata.SegmentMetadataImpl;
+import org.apache.pinot.segment.spi.index.reader.Dictionary;
+import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
+import org.apache.pinot.spi.data.BuiltInVirtualColumnDefinitions;
+import org.apache.pinot.spi.data.DimensionFieldSpec;
+import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.utils.CommonConstants.Segment.BuiltInVirtualColumn;
+import org.joda.time.DateTimeZone;
+import org.joda.time.Interval;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+
+/// Tests for the built-in virtual columns exposing segment metadata
(`$creationTime`, `$startTime`, `$endTime`,
+/// `$totalDocs` and `$crc`).
+public class SegmentMetadataVirtualColumnProviderTest {
+ private static final int NUM_DOCS = 17;
+ private static final long CREATION_TIME_MS = 1_700_000_000_000L;
+ private static final long START_TIME_MS = 1_690_000_000_000L;
+ private static final long END_TIME_MS = 1_695_000_000_000L;
+ private static final long CRC = 1234567890L;
+
+ @DataProvider(name = "unsetCreationTimes")
+ public Object[][] unsetCreationTimes() {
+ return new Object[][]{{Long.MIN_VALUE}, {-1L}, {0L}};
+ }
+
+ /// Returns the schema a segment gets after the built-in virtual columns are
added to it.
+ private static Schema buildSegmentSchema() {
+ Schema schema = new Schema();
+
VirtualColumnProviderFactory.addBuiltInVirtualColumnsToSegmentSchema(schema,
"testSegment");
+ return schema;
+ }
+
+ private static SegmentMetadata mockSegmentMetadata() {
+ SegmentMetadata segmentMetadata = mock(SegmentMetadata.class);
+ when(segmentMetadata.getIndexCreationTime()).thenReturn(CREATION_TIME_MS);
+ when(segmentMetadata.getTimeInterval()).thenReturn(new
Interval(START_TIME_MS, END_TIME_MS, DateTimeZone.UTC));
+ when(segmentMetadata.getCrc()).thenReturn(String.valueOf(CRC));
+ when(segmentMetadata.getTotalDocs()).thenReturn(NUM_DOCS);
+ return segmentMetadata;
+ }
+
+ /// Returns the metadata a real CONSUMING segment is created with: no time
range and no CRC yet. Uses the real
+ /// `SegmentMetadataImpl` rather than a mock so that the unset-value
contract stays pinned to what
+ /// `pinot-segment-spi` actually produces.
+ private static SegmentMetadata consumingSegmentMetadata(long creationTime) {
+ return new SegmentMetadataImpl("testTable",
"testTable__0__0__20240101T0000Z", new Schema(), creationTime);
+ }
+
+ private static Dictionary buildDictionary(Schema schema, String column,
SegmentMetadata segmentMetadata) {
+ FieldSpec fieldSpec = schema.getFieldSpecFor(column);
+ assertNotNull(fieldSpec, column + " should be added to the segment
schema");
+ assertTrue(fieldSpec.isVirtualColumn(), column + " should be a virtual
column");
+ VirtualColumnContext context = new VirtualColumnContext(fieldSpec,
NUM_DOCS, segmentMetadata);
+ return
VirtualColumnProviderFactory.buildProvider(context).buildDictionary(context);
+ }
+
+ @Nullable
+ private static NullValueVectorReader buildNullValueVector(Schema schema,
String column,
+ SegmentMetadata segmentMetadata) {
+ FieldSpec fieldSpec = schema.getFieldSpecFor(column);
+ VirtualColumnContext context = new VirtualColumnContext(fieldSpec,
NUM_DOCS, segmentMetadata);
+ return
VirtualColumnProviderFactory.buildProvider(context).buildNullValueVector(context);
+ }
+
+ /// Asserts the column reads as SQL NULL for every document, and that the
data source exposes the null vector (which
+ /// is what the query engine consults once null handling is enabled).
+ private static void assertColumnIsNull(Schema schema, String column,
SegmentMetadata segmentMetadata) {
+ NullValueVectorReader nullValueVector = buildNullValueVector(schema,
column, segmentMetadata);
+ assertNotNull(nullValueVector, column + " should report a null value
vector when its metadata is unavailable");
+ assertEquals(nullValueVector.getNullBitmap().getCardinality(), NUM_DOCS);
+ for (int docId = 0; docId < NUM_DOCS; docId++) {
+ assertTrue(nullValueVector.isNull(docId));
+ }
+ FieldSpec fieldSpec = schema.getFieldSpecFor(column);
+ VirtualColumnContext context = new VirtualColumnContext(fieldSpec,
NUM_DOCS, segmentMetadata);
+
assertNotNull(VirtualColumnProviderFactory.buildProvider(context).buildDataSource(context).getNullValueVector(),
+ column + " data source should expose the null value vector");
+ }
+
+ private static ColumnMetadata buildColumnMetadata(Schema schema, String
column, SegmentMetadata segmentMetadata) {
+ FieldSpec fieldSpec = schema.getFieldSpecFor(column);
+ VirtualColumnContext context = new VirtualColumnContext(fieldSpec,
NUM_DOCS, segmentMetadata);
+ return
VirtualColumnProviderFactory.buildProvider(context).buildMetadata(context);
+ }
+
+ /// Replaces the class-load assertion that used to live in
VirtualColumnProviderFactory: a definition added without
+ /// a provider must be caught here at build time rather than at segment load.
+ @Test
+ public void testEveryDefinitionHasAProvider() {
+ Schema schema = buildSegmentSchema();
+ for (BuiltInVirtualColumnDefinitions.Definition definition :
BuiltInVirtualColumnDefinitions.DEFINITIONS) {
+ FieldSpec fieldSpec = schema.getFieldSpecFor(definition.getName());
+ assertNotNull(fieldSpec, "No field spec added for: " +
definition.getName());
+ assertTrue(fieldSpec.isVirtualColumn(), "No provider configured for: " +
definition.getName());
+ assertEquals(fieldSpec.getDataType(), definition.getDataType());
+ assertEquals(fieldSpec.isSingleValueField(),
definition.isSingleValueField());
+ }
+ }
+
+ @Test
+ public void testAllBuiltInVirtualColumnsAreAddedToSegmentSchema() {
+ Schema schema = buildSegmentSchema();
+ for (String column : BuiltInVirtualColumn.BUILT_IN_VIRTUAL_COLUMNS) {
+ FieldSpec fieldSpec = schema.getFieldSpecFor(column);
+ assertNotNull(fieldSpec, column + " should be added to the segment
schema");
+ assertTrue(fieldSpec.isVirtualColumn(), column + " should have a virtual
column provider configured");
+ // Every configured provider must be resolvable
+ assertNotNull(VirtualColumnProviderFactory.buildProvider(
+ new VirtualColumnContext(fieldSpec, NUM_DOCS,
mockSegmentMetadata())));
+ }
+ }
+
+ @Test
+ public void testSegmentMetadataColumnTypes() {
+ Schema schema = buildSegmentSchema();
+
assertEquals(schema.getFieldSpecFor(BuiltInVirtualColumn.CREATIONTIME).getDataType(),
+ FieldSpec.DataType.TIMESTAMP);
+
assertEquals(schema.getFieldSpecFor(BuiltInVirtualColumn.STARTTIME).getDataType(),
+ FieldSpec.DataType.TIMESTAMP);
+
assertEquals(schema.getFieldSpecFor(BuiltInVirtualColumn.ENDTIME).getDataType(),
+ FieldSpec.DataType.TIMESTAMP);
+
assertEquals(schema.getFieldSpecFor(BuiltInVirtualColumn.TOTALDOCS).getDataType(),
FieldSpec.DataType.INT);
+
assertEquals(schema.getFieldSpecFor(BuiltInVirtualColumn.CRC).getDataType(),
FieldSpec.DataType.LONG);
+ for (String column : List.of(BuiltInVirtualColumn.CREATIONTIME,
BuiltInVirtualColumn.STARTTIME,
+ BuiltInVirtualColumn.ENDTIME, BuiltInVirtualColumn.TOTALDOCS,
BuiltInVirtualColumn.CRC)) {
+ assertTrue(schema.getFieldSpecFor(column).isSingleValueField(), column +
" should be single-value");
+ }
+ }
+
+ @Test
+ public void testValuesFromSegmentMetadata() {
+ Schema schema = buildSegmentSchema();
+ SegmentMetadata segmentMetadata = mockSegmentMetadata();
+
+ assertEquals(buildDictionary(schema, BuiltInVirtualColumn.CREATIONTIME,
segmentMetadata).getLongValue(0),
+ CREATION_TIME_MS);
+ assertEquals(buildDictionary(schema, BuiltInVirtualColumn.STARTTIME,
segmentMetadata).getLongValue(0),
+ START_TIME_MS);
+ assertEquals(buildDictionary(schema, BuiltInVirtualColumn.ENDTIME,
segmentMetadata).getLongValue(0), END_TIME_MS);
+ assertEquals(buildDictionary(schema, BuiltInVirtualColumn.TOTALDOCS,
segmentMetadata).getIntValue(0), NUM_DOCS);
+ assertEquals(buildDictionary(schema, BuiltInVirtualColumn.CRC,
segmentMetadata).getLongValue(0), CRC);
+ }
+
+ @Test
+ public void testColumnMetadataMatchesValue() {
+ Schema schema = buildSegmentSchema();
+ SegmentMetadata segmentMetadata = mockSegmentMetadata();
+
+ ColumnMetadata creationTimeMetadata =
+ buildColumnMetadata(schema, BuiltInVirtualColumn.CREATIONTIME,
segmentMetadata);
+ assertEquals(creationTimeMetadata.getTotalDocs(), NUM_DOCS);
+ assertEquals(creationTimeMetadata.getCardinality(), 1);
+ assertTrue(creationTimeMetadata.isSorted());
+ assertTrue(creationTimeMetadata.hasDictionary());
+ assertEquals(creationTimeMetadata.getMinValue(), CREATION_TIME_MS);
+ assertEquals(creationTimeMetadata.getMaxValue(), CREATION_TIME_MS);
+
+ ColumnMetadata crcMetadata = buildColumnMetadata(schema,
BuiltInVirtualColumn.CRC, segmentMetadata);
+ assertEquals(crcMetadata.getMinValue(), CRC);
+ assertEquals(crcMetadata.getMaxValue(), CRC);
+
+ // When the metadata is available the column carries a real value, so
there is no null vector
+ for (String column : List.of(BuiltInVirtualColumn.CREATIONTIME,
BuiltInVirtualColumn.STARTTIME,
+ BuiltInVirtualColumn.ENDTIME, BuiltInVirtualColumn.TOTALDOCS,
BuiltInVirtualColumn.CRC)) {
+ assertNull(buildNullValueVector(schema, column, segmentMetadata), column
+ " should not be null");
+ }
+ }
+
+ /// A segment without a time range and without a CRC must fall back to the
default null value for those columns,
+ /// while `$creationTime` and `$totalDocs` remain meaningful. An unset
creation time is `Long.MIN_VALUE` in the
+ /// segment metadata of an immutable segment and `-1` in the ZK metadata a
CONSUMING segment is created from.
+ @Test(dataProvider = "unsetCreationTimes")
+ public void testUnavailableMetadataFallsBackToDefaultNullValue(long
unsetCreationTime) {
+ Schema schema = buildSegmentSchema();
+ SegmentMetadata segmentMetadata =
consumingSegmentMetadata(unsetCreationTime);
+
+ assertEquals(buildDictionary(schema, BuiltInVirtualColumn.CREATIONTIME,
segmentMetadata).getLongValue(0),
+ (long) FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_TIMESTAMP);
+ assertEquals(buildDictionary(schema, BuiltInVirtualColumn.STARTTIME,
segmentMetadata).getLongValue(0),
+ (long) FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_TIMESTAMP);
+ assertEquals(buildDictionary(schema, BuiltInVirtualColumn.ENDTIME,
segmentMetadata).getLongValue(0),
+ (long) FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_TIMESTAMP);
+ assertEquals(buildDictionary(schema, BuiltInVirtualColumn.CRC,
segmentMetadata).getLongValue(0),
+ (long) FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_LONG);
+ // The stored value is only a placeholder: the column must additionally
report every document as null
+ assertColumnIsNull(schema, BuiltInVirtualColumn.CREATIONTIME,
segmentMetadata);
+ assertColumnIsNull(schema, BuiltInVirtualColumn.STARTTIME,
segmentMetadata);
+ assertColumnIsNull(schema, BuiltInVirtualColumn.ENDTIME, segmentMetadata);
+ assertColumnIsNull(schema, BuiltInVirtualColumn.CRC, segmentMetadata);
+
+ // $totalDocs comes from the context, so it is always available and never
null
+ assertEquals(buildDictionary(schema, BuiltInVirtualColumn.TOTALDOCS,
segmentMetadata).getIntValue(0), NUM_DOCS);
+ assertNull(buildNullValueVector(schema, BuiltInVirtualColumn.TOTALDOCS,
segmentMetadata));
+ }
+
+ /// Segment pruners read `ColumnMetadata` min/max without consulting the
null value vector, so a column whose value
+ /// is unavailable must not publish the placeholder as its min/max.
Otherwise a CONSUMING segment reporting epoch 0
+ /// for `$creationTime` sorts first in `ORDER BY $creationTime ASC LIMIT n`
and prunes away the committed segments
+ /// that actually hold the answer.
+ @Test
+ public void testUnavailableMetadataPublishesNoMinMax() {
+ Schema schema = buildSegmentSchema();
+ SegmentMetadata segmentMetadata = consumingSegmentMetadata(-1L);
+ for (String column : List.of(BuiltInVirtualColumn.CREATIONTIME,
BuiltInVirtualColumn.STARTTIME,
+ BuiltInVirtualColumn.ENDTIME, BuiltInVirtualColumn.CRC)) {
+ ColumnMetadata columnMetadata = buildColumnMetadata(schema, column,
segmentMetadata);
+ assertNull(columnMetadata.getMinValue(), "Unavailable " + column + "
should not publish a min value");
+ assertNull(columnMetadata.getMaxValue(), "Unavailable " + column + "
should not publish a max value");
+
+ // The same has to hold through buildDataSource, which is the path a
mutable segment takes
+ FieldSpec fieldSpec = schema.getFieldSpecFor(column);
+ VirtualColumnContext context = new VirtualColumnContext(fieldSpec,
NUM_DOCS, segmentMetadata);
+ DataSourceMetadata dataSourceMetadata =
+
VirtualColumnProviderFactory.buildProvider(context).buildDataSource(context).getDataSourceMetadata();
+ assertNull(dataSourceMetadata.getMinValue(), column);
+ assertNull(dataSourceMetadata.getMaxValue(), column);
+ }
+
+ // A column whose value IS available still publishes min/max, so pruning
keeps working
+ ColumnMetadata available = buildColumnMetadata(schema,
BuiltInVirtualColumn.CREATIONTIME, mockSegmentMetadata());
+ assertEquals(available.getMinValue(), CREATION_TIME_MS);
+ assertEquals(available.getMaxValue(), CREATION_TIME_MS);
+ }
+
+ /// A segment that has not indexed anything yet still has to produce a
well-formed, empty null bitmap rather than
+ /// failing or reporting a stale document count.
+ @Test
+ public void testNullValueVectorOnAnEmptySegment() {
+ Schema schema = buildSegmentSchema();
+ FieldSpec fieldSpec =
schema.getFieldSpecFor(BuiltInVirtualColumn.STARTTIME);
+ VirtualColumnContext context = new VirtualColumnContext(fieldSpec, 0,
consumingSegmentMetadata(-1L));
+ NullValueVectorReader nullValueVector =
+
VirtualColumnProviderFactory.buildProvider(context).buildNullValueVector(context);
+ assertNotNull(nullValueVector);
+ assertTrue(nullValueVector.getNullBitmap().isEmpty());
+ }
+
+ /// The null value vector is built on every data source access for a mutable
segment, so the bitmap is materialized
+ /// lazily. Repeated reads must return an equal - and stable - bitmap.
+ @Test
+ public void testNullValueVectorBitmapIsStableAcrossReads() {
+ Schema schema = buildSegmentSchema();
+ FieldSpec fieldSpec = schema.getFieldSpecFor(BuiltInVirtualColumn.ENDTIME);
+ VirtualColumnContext context = new VirtualColumnContext(fieldSpec,
NUM_DOCS, consumingSegmentMetadata(-1L));
+ NullValueVectorReader nullValueVector =
+
VirtualColumnProviderFactory.buildProvider(context).buildNullValueVector(context);
+ assertNotNull(nullValueVector);
+ assertEquals(nullValueVector.getNullBitmap(),
nullValueVector.getNullBitmap());
+ assertEquals(nullValueVector.getNullBitmap().getCardinality(), NUM_DOCS);
+ }
+
+ /// The index container must hand the null value vector to the engine, and
must not hand one back for an index type
+ /// a virtual column does not have.
+ @Test
+ public void testIndexContainerExposesTheNullValueVector()
+ throws IOException {
+ Schema schema = buildSegmentSchema();
+ FieldSpec fieldSpec = schema.getFieldSpecFor(BuiltInVirtualColumn.CRC);
+ VirtualColumnContext context = new VirtualColumnContext(fieldSpec,
NUM_DOCS, consumingSegmentMetadata(-1L));
+ try (ColumnIndexContainer container =
+
VirtualColumnProviderFactory.buildProvider(context).buildColumnIndexContainer(context))
{
+ assertNotNull(container.getIndex(StandardIndexes.nullValueVector()));
+ assertNotNull(container.getIndex(StandardIndexes.forward()));
+ assertNotNull(container.getIndex(StandardIndexes.dictionary()));
+ assertNull(container.getIndex(StandardIndexes.range()));
+ }
+
+ // With the metadata available there is no null value vector at all
+ VirtualColumnContext availableContext = new
VirtualColumnContext(fieldSpec, NUM_DOCS, mockSegmentMetadata());
+ try (ColumnIndexContainer container =
+
VirtualColumnProviderFactory.buildProvider(availableContext).buildColumnIndexContainer(availableContext))
{
+ assertNull(container.getIndex(StandardIndexes.nullValueVector()));
+ }
+ }
+
+ /// Every stored type the constant-value base supports must round-trip
through the type check, so that a new virtual
+ /// column of any type is rejected loudly rather than silently mis-cast.
+ @Test(dataProvider = "storedTypeValues")
+ public void testValueTypeCheckAcceptsEveryStoredType(FieldSpec.DataType
dataType, Object value) {
+ FieldSpec fieldSpec = new DimensionFieldSpec("col", dataType, true);
+ VirtualColumnContext context = new VirtualColumnContext(fieldSpec,
NUM_DOCS, null);
+ BaseConstantValueVirtualColumnProvider provider = new
BaseConstantValueVirtualColumnProvider() {
+ @Override
+ protected Object getValue(VirtualColumnContext ctx) {
+ return value;
+ }
+ };
+ assertNotNull(provider.buildDictionary(context));
+ assertNotNull(provider.buildMetadata(context));
+
+ // ... and the same type check rejects a value of the wrong type
+ BaseConstantValueVirtualColumnProvider wrong = new
BaseConstantValueVirtualColumnProvider() {
+ @Override
+ protected Object getValue(VirtualColumnContext ctx) {
+ return new Object();
+ }
+ };
+ try {
+ wrong.buildDictionary(context);
+ fail("Expecting an IllegalStateException for data type: " + dataType);
+ } catch (IllegalStateException e) {
+ assertTrue(e.getMessage().contains("col"), e.getMessage());
+ }
+ }
+
+ @DataProvider(name = "storedTypeValues")
+ public Object[][] storedTypeValues() {
+ return new Object[][]{
+ {FieldSpec.DataType.INT, 1},
+ {FieldSpec.DataType.LONG, 1L},
+ {FieldSpec.DataType.FLOAT, 1.0f},
+ {FieldSpec.DataType.DOUBLE, 1.0d},
+ {FieldSpec.DataType.BIG_DECIMAL, BigDecimal.ONE},
+ {FieldSpec.DataType.STRING, "value"},
+ {FieldSpec.DataType.BYTES, new byte[]{1, 2}},
+ {FieldSpec.DataType.TIMESTAMP, 1L}
+ };
+ }
+
+ /// The type check exists so that a provider returning the wrong box type
names the column and the provider instead
+ /// of throwing a bare ClassCastException from deep inside segment loading.
+ @Test
+ public void testWrongValueTypeIsRejectedWithADiagnosticMessage() {
+ FieldSpec fieldSpec =
buildSegmentSchema().getFieldSpecFor(BuiltInVirtualColumn.CREATIONTIME);
+ VirtualColumnContext context = new VirtualColumnContext(fieldSpec,
NUM_DOCS, mockSegmentMetadata());
+ // A LONG-stored column handed an Integer
+ BaseConstantValueVirtualColumnProvider provider = new
BaseConstantValueVirtualColumnProvider() {
+ @Override
+ protected Object getValue(VirtualColumnContext ctx) {
+ return 1;
+ }
+ };
+ try {
+ provider.buildDictionary(context);
+ fail("Expecting an IllegalStateException for a wrongly typed virtual
column value");
+ } catch (IllegalStateException e) {
+ String message = e.getMessage();
+ assertTrue(message.contains(BuiltInVirtualColumn.CREATIONTIME), message);
+ assertTrue(message.contains("java.lang.Integer"), message);
+ assertTrue(message.contains("java.lang.Long"), message);
+ }
+ }
+
+ /// The segment metadata is not always available (e.g. when the virtual
column is built for a column that is missing
+ /// from the segment). The providers must not fail in that case.
+ @Test
+ public void testMissingSegmentMetadataFallsBackToDefaultNullValue() {
+ Schema schema = buildSegmentSchema();
+
+ assertEquals(buildDictionary(schema, BuiltInVirtualColumn.CREATIONTIME,
null).getLongValue(0),
+ (long) FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_TIMESTAMP);
+ assertEquals(buildDictionary(schema, BuiltInVirtualColumn.STARTTIME,
null).getLongValue(0),
+ (long) FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_TIMESTAMP);
+ assertEquals(buildDictionary(schema, BuiltInVirtualColumn.ENDTIME,
null).getLongValue(0),
+ (long) FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_TIMESTAMP);
+ assertEquals(buildDictionary(schema, BuiltInVirtualColumn.CRC,
null).getLongValue(0),
+ (long) FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_LONG);
+ assertEquals(buildDictionary(schema, BuiltInVirtualColumn.TOTALDOCS,
null).getIntValue(0), NUM_DOCS);
+
+ assertColumnIsNull(schema, BuiltInVirtualColumn.CREATIONTIME, null);
+ assertColumnIsNull(schema, BuiltInVirtualColumn.STARTTIME, null);
+ assertColumnIsNull(schema, BuiltInVirtualColumn.ENDTIME, null);
+ assertColumnIsNull(schema, BuiltInVirtualColumn.CRC, null);
+ }
+}
diff --git
a/pinot-spi/src/main/java/org/apache/pinot/spi/data/BuiltInVirtualColumnDefinitions.java
b/pinot-spi/src/main/java/org/apache/pinot/spi/data/BuiltInVirtualColumnDefinitions.java
new file mode 100644
index 00000000000..ab3903468bc
--- /dev/null
+++
b/pinot-spi/src/main/java/org/apache/pinot/spi/data/BuiltInVirtualColumnDefinitions.java
@@ -0,0 +1,107 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.spi.data;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.apache.pinot.spi.utils.CommonConstants.Segment.BuiltInVirtualColumn;
+
+
+/// Single source of truth for the shape (name, data type, single-value vs
multi-value) of the built-in virtual columns.
+///
+/// The columns are declared here once because they are materialized in two
independent places that must agree:
+///
+/// - the broker/controller side adds them to the table schema used for query
planning, without a provider class
+/// (see `TableCache#addBuiltInVirtualColumns`), and
+/// - the server side adds them to the segment schema together with the
provider that produces the values
+/// (see
`VirtualColumnProviderFactory#addBuiltInVirtualColumnsToSegmentSchema`).
+///
+/// If the two sides disagreed on a data type or on single-value vs
multi-value, the broker would declare one type
+/// while the server produced another. Both sides therefore build their field
specs from [#DEFINITIONS].
+///
+/// The column *names* additionally live in [BuiltInVirtualColumn], which is
where code that only needs to recognize a
+/// built-in virtual column by name looks them up. [#NAMES] exposes the names
derived from [#DEFINITIONS] so the two
+/// can be asserted equal in a test.
+public class BuiltInVirtualColumnDefinitions {
+ private BuiltInVirtualColumnDefinitions() {
+ }
+
+ /// Shape of a single built-in virtual column. The provider class is
intentionally not part of this definition: it
+ /// lives in `pinot-segment-local` and is only known to the server side.
+ public static class Definition {
+ private final String _name;
+ private final DataType _dataType;
+ private final boolean _singleValueField;
+
+ private Definition(String name, DataType dataType, boolean
singleValueField) {
+ _name = name;
+ _dataType = dataType;
+ _singleValueField = singleValueField;
+ }
+
+ public String getName() {
+ return _name;
+ }
+
+ public DataType getDataType() {
+ return _dataType;
+ }
+
+ public boolean isSingleValueField() {
+ return _singleValueField;
+ }
+
+ /// Creates a new field spec for this column, without a virtual column
provider. Callers that can resolve a
+ /// provider should set it on the returned spec.
+ ///
+ /// NOTE: Returns a fresh instance on every call. Field specs are mutable
and are stored by reference in the
+ /// schema they are added to, so they must never be shared across schemas.
+ public DimensionFieldSpec createFieldSpec() {
+ return new DimensionFieldSpec(_name, _dataType, _singleValueField);
+ }
+ }
+
+ public static final List<Definition> DEFINITIONS = List.of(
+ new Definition(BuiltInVirtualColumn.DOCID, DataType.INT, true),
+ new Definition(BuiltInVirtualColumn.HOSTNAME, DataType.STRING, true),
+ new Definition(BuiltInVirtualColumn.SEGMENTNAME, DataType.STRING, true),
+ new Definition(BuiltInVirtualColumn.PARTITIONID, DataType.STRING, false),
+ new Definition(BuiltInVirtualColumn.CREATIONTIME, DataType.TIMESTAMP,
true),
+ new Definition(BuiltInVirtualColumn.STARTTIME, DataType.TIMESTAMP, true),
+ new Definition(BuiltInVirtualColumn.ENDTIME, DataType.TIMESTAMP, true),
+ new Definition(BuiltInVirtualColumn.TOTALDOCS, DataType.INT, true),
+ new Definition(BuiltInVirtualColumn.CRC, DataType.LONG, true));
+
+ /// Names of all the built-in virtual columns, derived from [#DEFINITIONS].
+ public static final Set<String> NAMES =
+
DEFINITIONS.stream().map(Definition::getName).collect(Collectors.toUnmodifiableSet());
+
+ /// Adds the built-in virtual columns to the given schema, without a virtual
column provider.
+ ///
+ /// Existing columns are left untouched, so a user-defined column of the
same name always wins.
+ public static void addToSchema(Schema schema) {
+ for (Definition definition : DEFINITIONS) {
+ if (!schema.hasColumn(definition.getName())) {
+ schema.addField(definition.createFieldSpec());
+ }
+ }
+ }
+}
diff --git a/pinot-spi/src/main/java/org/apache/pinot/spi/data/SchemaInfo.java
b/pinot-spi/src/main/java/org/apache/pinot/spi/data/SchemaInfo.java
index a0364117b6a..fa7c2de0335 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/data/SchemaInfo.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/data/SchemaInfo.java
@@ -66,8 +66,17 @@ public class SchemaInfo {
public SchemaInfo(Schema schema) {
_schemaName = schema.getSchemaName();
- //Removed virtual columns($docId, $hostName, $segmentName) from dimension
fields count
- _numDimensionFields = schema.getDimensionFieldSpecs().size() - 3;
+ // The schema handed in here has usually been through
TableCache#addBuiltInVirtualColumns, which adds the built-in
+ // virtual columns as plain dimension fields (without a provider class, so
isVirtualColumn() does not identify
+ // them). Exclude them by name so that the reported count only covers the
user's own dimensions, and stays correct
+ // as built-in virtual columns are added.
+ int numDimensionFields = 0;
+ for (DimensionFieldSpec fieldSpec : schema.getDimensionFieldSpecs()) {
+ if
(!BuiltInVirtualColumnDefinitions.NAMES.contains(fieldSpec.getName())) {
+ numDimensionFields++;
+ }
+ }
+ _numDimensionFields = numDimensionFields;
_numDateTimeFields = schema.getDateTimeFieldSpecs().size();
_numMetricFields = schema.getMetricFieldSpecs().size();
_numComplexFields = schema.getComplexFieldSpecs().size();
diff --git
a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
index e00598b582e..c117713b8c8 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
@@ -2246,7 +2246,36 @@ public class CommonConstants {
public static final String HOSTNAME = "$hostName";
public static final String SEGMENTNAME = "$segmentName";
public static final String PARTITIONID = "$partitionId";
- public static final Set<String> BUILT_IN_VIRTUAL_COLUMNS = Set.of(DOCID,
HOSTNAME, SEGMENTNAME, PARTITIONID);
+
+ // Segment metadata virtual columns. Each of them is a constant
single-value column within a segment, exposing a
+ // piece of the segment metadata to queries. When the underlying
metadata is not available (e.g. on a CONSUMING
+ // segment, which has no time range and no CRC yet), the column reads as
NULL.
+ //
+ // The three time columns are 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.
+
+ /// Segment creation time (TIMESTAMP). This is the index creation time
recorded in the segment's creation
+ /// metadata; for a CONSUMING segment it is the time the consuming
segment was created.
+ public static final String CREATIONTIME = "$creationTime";
+ /// Start of the segment time range (TIMESTAMP), normalized from the
time column's own unit.
+ /// NULL for segments without a time range, such as CONSUMING segments
and tables without a time column.
+ public static final String STARTTIME = "$startTime";
+ /// End of the segment time range (TIMESTAMP), normalized from the time
column's own unit.
+ /// NULL for segments without a time range, such as CONSUMING segments
and tables without a time column.
+ public static final String ENDTIME = "$endTime";
+ /// Number of documents in the segment (INT). On a CONSUMING segment
this is the number of documents indexed so
+ /// far. NOTE: This counts all the documents physically stored in the
segment, so for an upsert table it also
+ /// includes the documents that have been replaced and are no longer
returned by queries.
+ public static final String TOTALDOCS = "$totalDocs";
+ /// Segment CRC (LONG). NULL on CONSUMING segments, which have no CRC
until they are committed.
+ /// NOTE: Do not confuse this constant with the enclosing [Segment#CRC],
which is the `segment.crc` metadata key.
+ public static final String CRC = "$crc";
+
+ /// NOTE: Kept in sync with
`BuiltInVirtualColumnDefinitions#DEFINITIONS`, which additionally carries the
data
+ /// type and single-value/multi-value shape of each column.
`BuiltInVirtualColumnDefinitionsTest` asserts the two
+ /// agree.
+ public static final Set<String> BUILT_IN_VIRTUAL_COLUMNS =
+ Set.of(DOCID, HOSTNAME, SEGMENTNAME, PARTITIONID, CREATIONTIME,
STARTTIME, ENDTIME, TOTALDOCS, CRC);
}
}
diff --git
a/pinot-spi/src/test/java/org/apache/pinot/spi/data/BuiltInVirtualColumnDefinitionsTest.java
b/pinot-spi/src/test/java/org/apache/pinot/spi/data/BuiltInVirtualColumnDefinitionsTest.java
new file mode 100644
index 00000000000..c8cc253d808
--- /dev/null
+++
b/pinot-spi/src/test/java/org/apache/pinot/spi/data/BuiltInVirtualColumnDefinitionsTest.java
@@ -0,0 +1,99 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.spi.data;
+
+import java.util.HashSet;
+import java.util.Set;
+import org.apache.pinot.spi.utils.CommonConstants.Segment.BuiltInVirtualColumn;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNotSame;
+import static org.testng.Assert.assertTrue;
+
+
+public class BuiltInVirtualColumnDefinitionsTest {
+
+ /// The names are declared twice - once as constants in
[BuiltInVirtualColumn] and once implicitly by
+ /// [BuiltInVirtualColumnDefinitions#DEFINITIONS]. A definition missing from
the name set would stop being filtered
+ /// out of a segment's physical columns and would be counted as a user
dimension by [SchemaInfo].
+ @Test
+ public void testDefinitionsMatchTheDeclaredNames() {
+ assertEquals(BuiltInVirtualColumnDefinitions.NAMES,
BuiltInVirtualColumn.BUILT_IN_VIRTUAL_COLUMNS);
+ assertEquals(BuiltInVirtualColumnDefinitions.DEFINITIONS.size(),
+ BuiltInVirtualColumn.BUILT_IN_VIRTUAL_COLUMNS.size(), "Duplicate name
in DEFINITIONS");
+ }
+
+ /// Every built-in virtual column name must start with `$`: that prefix is
what excludes them from `SELECT *` in
+ /// both query engines.
+ @Test
+ public void testNamesAreDollarPrefixed() {
+ for (String name : BuiltInVirtualColumnDefinitions.NAMES) {
+ assertTrue(name.startsWith("$"), "Built-in virtual column must be
$-prefixed: " + name);
+ }
+ }
+
+ /// Field specs are mutable and are stored by reference in the schema they
are added to, so each call must return a
+ /// fresh instance - the server side mutates them to attach the provider
class and, for `$segmentName`, a per-segment
+ /// value.
+ @Test
+ public void testCreateFieldSpecReturnsFreshInstances() {
+ for (BuiltInVirtualColumnDefinitions.Definition definition :
BuiltInVirtualColumnDefinitions.DEFINITIONS) {
+ DimensionFieldSpec first = definition.createFieldSpec();
+ DimensionFieldSpec second = definition.createFieldSpec();
+ assertNotSame(first, second, "Field spec must not be shared for: " +
definition.getName());
+ assertEquals(first, second);
+ assertEquals(first.getName(), definition.getName());
+ assertEquals(first.getDataType(), definition.getDataType());
+ assertEquals(first.isSingleValueField(),
definition.isSingleValueField());
+ }
+ }
+
+ @Test
+ public void testAddToSchemaIsIdempotentAndNeverOverwrites() {
+ Schema schema = new Schema.SchemaBuilder().setSchemaName("test")
+ .addSingleValueDimension("dim", FieldSpec.DataType.STRING)
+ .build();
+ BuiltInVirtualColumnDefinitions.addToSchema(schema);
+ Set<String> afterFirst = new HashSet<>(schema.getColumnNames());
+ assertTrue(afterFirst.containsAll(BuiltInVirtualColumnDefinitions.NAMES));
+
+ // A second call must not duplicate or replace anything
+ BuiltInVirtualColumnDefinitions.addToSchema(schema);
+ assertEquals(new HashSet<>(schema.getColumnNames()), afterFirst);
+
+ for (BuiltInVirtualColumnDefinitions.Definition definition :
BuiltInVirtualColumnDefinitions.DEFINITIONS) {
+ FieldSpec fieldSpec = schema.getFieldSpecFor(definition.getName());
+ assertNotNull(fieldSpec);
+ assertEquals(fieldSpec.getDataType(), definition.getDataType());
+ }
+ }
+
+ /// A user-defined column of the same name must win, so that adding a
built-in virtual column never silently
+ /// changes the type of an existing user column in the broker's schema.
+ @Test
+ public void testAddToSchemaDoesNotOverrideUserColumn() {
+ Schema schema = new Schema.SchemaBuilder().setSchemaName("test")
+ .addSingleValueDimension(BuiltInVirtualColumn.TOTALDOCS,
FieldSpec.DataType.STRING)
+ .build();
+ BuiltInVirtualColumnDefinitions.addToSchema(schema);
+
assertEquals(schema.getFieldSpecFor(BuiltInVirtualColumn.TOTALDOCS).getDataType(),
FieldSpec.DataType.STRING);
+ }
+}
diff --git
a/pinot-spi/src/test/java/org/apache/pinot/spi/data/SchemaInfoTest.java
b/pinot-spi/src/test/java/org/apache/pinot/spi/data/SchemaInfoTest.java
index d3ee0e31aeb..4266bbc2e5a 100644
--- a/pinot-spi/src/test/java/org/apache/pinot/spi/data/SchemaInfoTest.java
+++ b/pinot-spi/src/test/java/org/apache/pinot/spi/data/SchemaInfoTest.java
@@ -34,6 +34,30 @@ import static org.testng.Assert.assertEquals;
public class SchemaInfoTest {
+ /// The schema `SchemaInfo` is built from has usually been through the
broker's `addBuiltInVirtualColumns`, which adds
+ /// every built-in virtual column as a plain dimension field. None of them
may be reported as a user dimension,
+ /// whatever their number.
+ @Test
+ public void testBuiltInVirtualColumnsAreNotCounted() {
+ Schema schema = new Schema.SchemaBuilder().setSchemaName("TestSchema")
+ .addDimensionField("dim1", FieldSpec.DataType.STRING)
+ .addDimensionField("dim2", FieldSpec.DataType.INT)
+ .addDateTimeField("dt1", FieldSpec.DataType.LONG, "1:HOURS:EPOCH",
"1:HOURS")
+ .addMetricField("metric", INT)
+ .build();
+ assertEquals(new SchemaInfo(schema).getNumDimensionFields(), 2);
+
+ // Adding the built-in virtual columns must not change the reported
dimension count
+ BuiltInVirtualColumnDefinitions.addToSchema(schema);
+ assertEquals(schema.getDimensionFieldSpecs().size(),
+ 2 +
CommonConstants.Segment.BuiltInVirtualColumn.BUILT_IN_VIRTUAL_COLUMNS.size());
+ SchemaInfo schemaInfo = new SchemaInfo(schema);
+ assertEquals(schemaInfo.getNumDimensionFields(), 2);
+ assertEquals(schemaInfo.getNumDateTimeFields(), 1);
+ assertEquals(schemaInfo.getNumMetricFields(), 1);
+ assertEquals(schemaInfo.getNumComplexFields(), 0);
+ }
+
@Test
public void testSchemaInfoSerDeserWithVirtualColumns()
throws IOException {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]