Add end-to-end Parquet VARIANT support - #19101
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #19101 +/- ##
=============================================
- Coverage 66.65% 39.03% -27.63%
- Complexity 1423 1494 +71
=============================================
Files 3443 3457 +14
Lines 218632 220691 +2059
Branches 34793 35163 +370
=============================================
- Hits 145726 86136 -59590
- Misses 61192 126615 +65423
+ Partials 11714 7940 -3774
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
3e4dc18 to
acfba94
Compare
ca98faf to
b1a2872
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds end-to-end support for the VARIANT logical type across Pinot ingestion (notably Parquet VARIANT(1)), query planning/execution (single-stage + multi-stage), response encoders, and Java/JDBC client surfaces, while explicitly rejecting unsupported raw-VARIANT operations (e.g., ordering/grouping/join keys) with actionable errors.
Changes:
- Introduces
VARIANTas a first-class Pinot data type, including schema/DDL handling, wire/proto support, and null-sentinel semantics. - Adds Parquet
VARIANT(1)ingestion support via the native Parquet reader and includes a packaged “variant batch” quickstart with resources and CI coverage. - Adds query-time VARIANT functions (parse/get/existence/type/json rendering) and enforces operator-level restrictions where raw VARIANT lacks equality/hash/ordering semantics.
Reviewed changes
Copilot reviewed 174 out of 175 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| pom.xml | Adds managed dependency for org.apache.parquet:parquet-variant. |
| pinot-tools/src/test/java/org/apache/pinot/tools/admin/command/QuickStartTest.java | Maps new quickstart command strings to VariantQuickStart. |
| pinot-tools/src/main/resources/examples/batch/variantEvents/* | Adds VARIANT batch quickstart schema/table config, ingestion spec, and documentation. |
| pinot-tools/pom.xml | Adds packaged quick-start-variant-batch program entry. |
| pinot-sql-ddl/src/main/, src/test/ | Adds DDL compile/reverse/roundtrip support for VARIANT and validates default emission rules. |
| pinot-spi/src/main/, src/test/ | Adds PinotDataType.VARIANT conversion/ingestion mapping and schema validation updates. |
| pinot-segment-spi/src/main/** | Handles VARIANT default-null sentinel persisted as empty string in segment metadata. |
| pinot-segment-local/src/main/, src/test/ | Adds VARIANT validation/sanitization rules; disables min/max, ordering, dictionary creation where unsupported; adds targeted tests. |
| pinot-query-planner/src/main/, src/test/ | Adds VARIANT type mapping (Calcite/plan nodes), plan validation, constant-folding guardrails, and serde coverage. |
| pinot-query-runtime/src/main/, src/test/ | Enforces VARIANT restrictions at runtime operators (sort/window/set/group-by/etc.) and adds operator tests. |
| pinot-core/src/main/, src/test/ | Enforces VARIANT restrictions in single-stage operators/aggregations/distinct/order-by and adds tests. |
| pinot-common/src/main/, src/test/ | Adds VARIANT to encoders, datablock equality, proto enum, function canonicalization, and introduces scalar VariantFunctions. |
| pinot-clients/pinot-jdbc-client/src/main/, src/test/ | Treats VARIANT as canonical JSON text (VARCHAR) while preserving encoded-variant-null vs SQL-null behavior. |
| pinot-clients/pinot-java-client/src/main/, src/test/ | Preserves legacy getString() null behavior while distinguishing VARIANT nulls. |
| pinot-plugins/pinot-input-format/pinot-parquet/src/main/** | Enhances Parquet reader selection/metadata helpers and initializes native reader with schema-bound converters for VARIANT. |
| pinot-controller/src/main/, src/test/ | Ensures stored-vs-compiled schema default comparisons handle VARIANT byte[] defaults by content. |
| pinot-compatibility-verifier/** | Adds file-contains operation and VARIANT mixed-version suite coverage including old-broker/new-servers phase. |
| .github/workflows/scripts/.pinot_quickstart.sh | Adds CI validation for the packaged VARIANT batch quickstart. |
| for (int i = 0; i < groupKeyIds.length; i++) { | ||
| ColumnDataType dataType = resultSchema.getColumnDataType(i); | ||
| if (!dataType.supportsEquality() || !dataType.supportsHashing()) { | ||
| throw new IllegalArgumentException( | ||
| "Raw VARIANT values do not support GROUP BY; extract a typed path with variantGet first"); | ||
| } | ||
| } |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 174 out of 175 changed files in this pull request and generated no new comments.
Suppressed comments (1)
pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/SortOperator.java:81
- The validation uses
supportsOrdering()but the error text always mentions raw VARIANT.supportsOrdering()is alsofalsefor other non-orderable logical types (e.g. OBJECT / array types), so this can surface a misleading VARIANT-specific message when the real unsupported type is something else.
Consider tailoring the message based on the actual column type (keep the VARIANT guidance when type == VARIANT, otherwise emit a generic "ORDER BY does not support values of type X" message).
ac0f4d0 to
cdcfc2c
Compare
Introduce the VARIANT logical type and envelope, Parquet reconstruction, scalar and transform functions, and segment and query validation across the single-stage and multi-stage engines. Propagate VARIANT through schemas, wire formats, responses, clients, DDL, quickstart, and integration coverage while rejecting raw operations that require ordering, equality, or hashing.
Allow SQL NULL literals to participate in comparison validation without being classified as raw VARIANT values. Keep equality and DISTINCT FROM behavior aligned in the single-stage transform layer and the multi-stage filter operand, with regression coverage for both engines.
Exercise new-broker/old-server, mixed-server, and old-broker/new-server compatibility, including deterministic handling of the VARIANT wire type before server upgrade and rejection of partial mixed-fleet results. Centralize join-key validation, clarify reusable extraction ownership, and strengthen scalar, null-placeholder, window, and end-to-end coverage.
cdcfc2c to
ae86bc2
Compare
xiangfu0
left a comment
There was a problem hiding this comment.
Reviewed the full VARIANT PR (checked out the branch; ran domain-focused passes over SPI/envelope, Parquet ingestion, the VariantUtils codec, single-stage + multi-stage enforcement, schema/table validation, and wire/client/compat). Overall this is very carefully engineered — the PVAR envelope, the four null-state handling, and the defense-in-depth opacity guards all check out, and I found no data-corruption or crash bugs in the core paths.
Two items I'd suggest fixing before merge (inline: an OBJECT-comparison regression and a partial-upsert validation gap), plus a few low-severity polish notes.
Verified-correct highlights: proto appends VARIANT=24 with no renumbering and ColumnDataType is name-serialized (no ordinal()-indexed tables exist repo-wide, so the mid-enum insertion is safe); Parquet buffer ownership / shredded-vs-unshredded selection / reader precedence / reinit safety; codec bounds + per-row cursor reset + thread-confinement; and the full set of schema/table restrictions (dictionary / secondary indexes / PK / partition / sorted / star-tree (default and explicit) / metrics-agg / upsert-comparison all rejected, min/max never published, never sorted).
| if (lhsType == rhsType) { | ||
| Preconditions.checkArgument((lhsType == ColumnDataType.UNKNOWN || lhsType.supportsOrdering()) | ||
| && (rhsType == ColumnDataType.UNKNOWN || rhsType.supportsOrdering()), | ||
| "Raw VARIANT values do not support comparison; extract a typed path with variantGet first"); |
There was a problem hiding this comment.
Behavior regression + misleading message (Medium). This guard requires both operands to be UNKNOWN || supportsOrdering(), but ColumnDataType.supportsOrdering() is false for OBJECT, arrays, and MAP — not just VARIANT. The class Javadoc just above ("if either side is null or OBJECT, we best-effort cast data into the other side's data type") documents OBJECT as an intentionally-supported case, and OBJECT = OBJECT previously took the equal-types branch and ran. After this change such a comparison throws "Raw VARIANT values do not support comparison" — both a narrowing of the existing contract and a wrong error for non-VARIANT types.
Suggest gating specifically on the VARIANT case (or restoring the OBJECT/UNKNOWN best-effort path) and making the message name the actual offending type. Same pattern applies to the In guard at line 120 (supportsEquality(), also false for OBJECT/arrays).
There was a problem hiding this comment.
Fixed in xiangfu0#236. The comparison/IN/DISTINCT-FROM guards now reject only raw VARIANT (!= ColumnDataType.VARIANT) instead of any non-orderable/non-equatable type, so OBJECT/array/MAP comparisons keep their prior behavior. Applied to both engines: FilterOperand (Predicate, In), TransformOperandFactory (distinct-from), and the single-stage BinaryOperatorTransformFunction / InTransformFunction, which had the same guard. Added FilterOperandTest.testNonVariantOpaqueTypesAreNotRejectedAsVariant.
| Preconditions.checkState(fieldSpec != null, "Merger cannot be applied to non-existing column: %s", column); | ||
| if (fieldSpec.getDataType() == DataType.VARIANT) { | ||
| Preconditions.checkState(columnStrategy == UpsertConfig.Strategy.OVERWRITE, | ||
| "VARIANT column supports only OVERWRITE partial-upsert strategy: %s", column); |
There was a problem hiding this comment.
Validation gap (Medium). This VARIANT OVERWRITE-only check only runs for columns explicitly present in partialUpsertStrategies. A VARIANT column not listed here falls back to upsertConfig.getDefaultPartialUpsertStrategy() at merge time — see PartialUpsertColumnarMerger#merge (_column2Mergers.getOrDefault(column, _defaultColumnValueMerger)), where the default merger is built from getDefaultPartialUpsertStrategy() and is user-settable to INCREMENT/APPEND/UNION/IGNORE.
Failing scenario: a partial-upsert table with an unlisted VARIANT dimension and defaultPartialUpsertStrategy: INCREMENT. Validation passes, then at runtime IncrementMerger is applied to the byte envelope → failure/corruption; IGNORE would silently retain stale variant values. This violates the documented "partial upsert only OVERWRITE" contract.
Suggest: when a VARIANT column exists under active partial upsert, also require the effective default strategy to be OVERWRITE. (Also note the custom partialUpsertMergerClass path applies no VARIANT guard — likely acceptable as user code, but worth a doc note.)
There was a problem hiding this comment.
Fixed in xiangfu0#236. validatePartialUpsertStrategies now validates the effective strategy for every VARIANT column up front: it uses the explicit entry if listed, otherwise defaultPartialUpsertStrategy, and requires OVERWRITE. It also rejects a custom partialUpsertMergerClass on VARIANT columns (which cannot be validated against the OVERWRITE-only contract). Added 3 tests covering unlisted-column default, valid default, and custom-merger rejection.
| for (RelFieldCollation collation : collations) { | ||
| Preconditions.checkArgument( | ||
| _dataSchema.getColumnDataType(collation.getFieldIndex()).supportsOrdering(), | ||
| "ORDER BY does not support raw VARIANT values; extract a typed path with variantGet first"); |
There was a problem hiding this comment.
Misleading message (Low). supportsOrdering() is false for VARIANT and arrays/OBJECT/MAP/STRUCT/LIST, so this rejects e.g. ORDER BY array_col with a VARIANT-specific message. The rejection is correct; only the text is wrong for non-VARIANT types. Same hard-coded-VARIANT-message-on-a-generic-supportsOrdering()-gate appears in SortedMailboxReceiveOperator, OrderByComparatorFactory, and VariantTypeValidationVisitor#visitSort/visitWindow. Consider naming the actual unsupported type.
There was a problem hiding this comment.
Fixed in xiangfu0#236. The ORDER BY / sort / window / set-op / aggregate rejections now name the actual unsupported type (e.g. "ORDER BY does not support OBJECT values") and keep the raw-VARIANT wording plus variantGet guidance only for VARIANT. Applied to SortOperator, SortedMailboxReceiveOperator, OrderByComparatorFactory, and VariantTypeValidationVisitor.
| /// | ||
| /// <p>This method is also invoked by the runtime as a defensive check for plans that did not pass through the | ||
| /// current broker planner. | ||
| public static void validateAggregateInputs(AggregateNode node, DataSchema inputSchema) { |
There was a problem hiding this comment.
Consistency (Low, not exploitable). validateAggregateInputs iterates only the agg-call operands; it never checks AggregateNode.getGroupKeys(), so the planner gate doesn't reject GROUP BY raw_variant. This is not a hole — MultistageGroupByExecutor and the single-stage GroupBy operators reject it at runtime via supportsEquality()/supportsHashing(). But since every other operation (sort, set-op, join, window partition) is validated in this visitor, adding the group-key check here would make the planner the consistent authoritative gate.
There was a problem hiding this comment.
Fixed in xiangfu0#236. validateAggregateInputs(AggregateNode, ...) now also validates AggregateNode.getGroupKeys() (equality+hashing), so the planner gate rejects GROUP BY raw_variant consistently with sort/join/set-op/window (runtime already rejected it). Added testRejectsRawVariantGroupByKey.
…eys in planner Addresses the two Low review findings on apache#19101: - ORDER BY / sort / set-op / window / aggregate rejections previously emitted a raw-VARIANT-specific message for any non-orderable/non-hashable type (OBJECT, arrays, MAP). The messages now name the actual unsupported type and keep the raw-VARIANT wording plus variantGet guidance only for VARIANT. Applies to VariantTypeValidationVisitor, SortOperator, SortedMailboxReceiveOperator, and OrderByComparatorFactory. - VariantTypeValidationVisitor.validateAggregateInputs now also validates the AggregateNode GROUP BY keys, so the planner gate rejects GROUP BY on a raw VARIANT key consistently with sort/join/set-op/window (runtime already rejected it). Added a planner test.
…ert validation, and type-accurate error messages Addresses the four review findings on apache#19101 (2 Medium: OBJECT-comparison regression, partial-upsert default-strategy gap; 2 Low: type-accurate opacity messages, planner GROUP BY key validation). Tests added across pinot-core, pinot-query-runtime, pinot-query-planner, pinot-segment-local.
|
Follow-up on my review: all four findings are now fixed and merged into this PR (head is now
Regression tests were added for each. spotless/checkstyle/license are clean on all four affected modules, and every affected unit-test class passes locally on JDK 25 ( On the current CI run, the Linter and all VARIANT-exercising tests pass. The three red checks are pre-existing flakes unrelated to these changes and to VARIANT: |
Summary
Add an end-to-end
VARIANTuser journey for Apache Pinot:VARIANTdimension retained in Pinot's raw forward indexVARIANT(1)values from unshredded and shredded filesRaw Variant values are deliberately opaque to operations that require equality, hashing, or ordering. Queries must extract a typed scalar before comparing, grouping, joining, sorting, using set operations, or applying general aggregates.
COUNT(raw_variant)remains supported.The persisted format, null semantics, ownership boundaries, and rollout contract are documented in
pinot-spi/VARIANT_DESIGN.md.End-to-end flow
Define a table column with
"dataType": "VARIANT", storage null handling enabled, dictionary disabled, and a raw forward index. The native Parquet reader reconstructsVARIANT(1)metadata/value pairs—including supported shredded layouts—into Pinot's versionedPVARenvelope.Frequently queried fields can be materialized with ingestion transforms while the full Variant remains available:
Supported scope
VARIANT(1)columnsVARIANTdimensions without dictionariesSET enableNullHandling=trueNested/repeated Variant columns, streaming ingestion, and quoted path keys are outside the initial scope.
A JSON index is intentionally not accepted on a
VARIANTcolumn. The existing JSON index expects Pinot JSON text and its query rewrite semantics; treating the binaryPVARenvelope as JSON would be incorrect. Indexing Variant paths should be introduced separately with an explicit physical/index contract. Materialized scalar columns are the supported indexed path for this PR.The automatic Parquet reader selection preserves existing Avro-metadata precedence. Files containing Avro metadata must explicitly select the native Parquet reader to ingest Variant.
Production safety
PVARframing and validation inVariantEnvelopeand freezes version-1 bytes with golden tests24to Variant while preserving UUID/UUID_ARRAY at22/23"null", missing paths, and conversion failures across query and client response formatsRolling upgrades
Variant remains an explicit full-fleet activation feature: upgrade controllers, brokers, servers, clients, and external ingestion jobs before registering a Variant schema, and do not roll back while Variant tables are active.
Compatibility coverage verifies:
Quickstart
The quickstart registers
variantEvents, ingests the committed five-row Parquet fixture, uploads the segment, and runs materialized-field, nested-extraction, JSON-rendering, and null-semantics queries. Its resources live underpinot-tools/src/main/resources/examples/batch/variantEvents.Verification
VariantTypeTestscenarios across both query engines in the full integration-test reactorupstream/mastergit diff --checkPerformance benchmark
A 2,000,000-row workstation benchmark compared identical logical events represented as JSON text and Parquet
VARIANT(1), then ingested into raw Pinot columns with ZSTD compression. A third JSON scenario added Pinot's JSON index and is reported separately because it is an index-assisted comparison, not a format-only comparison. The reproducible harness is preserved in the benchmark snapshot.Ingestion and storage
Compared with raw JSON, the VARIANT source was 2.22% smaller and its Pinot segment was 1.61% larger. Adding the JSON index made the JSON segment 4.26× the size of the raw JSON segment.
Query latency
Each value is the median of four round-level p50 values. Every round used two warmups and 10 measured sequential executions.
eventTypecount (control)The raw comparison used four Parquet files/Pinot segments and four ingestion rounds with alternating JSON/VARIANT build order. Source generation was excluded from ingestion timing, and every query result was checked against independently generated ground truth.
These are paired, sequential, warm-cache, single-stage measurements from an Apple M2 Max workstation with 32 GB RAM, macOS 26.5.2, and Temurin 25 on AC power. They are directional workstation evidence, not distributed production-cluster throughput: network transfer, multi-stage execution, concurrent clients, distributed scheduling, and production hardware were outside the run.
Review areas
This PR changes public SPI and query-wire behavior. The main ownership reviews are: