Skip to content

Add codec spec DSL and configuration plumbing - #19284

Open
xiangfu0 wants to merge 5 commits into
masterfrom
xiangfu0/codex/codec-stack/01-config
Open

Add codec spec DSL and configuration plumbing#19284
xiangfu0 wants to merge 5 commits into
masterfrom
xiangfu0/codex/codec-stack/01-config

Conversation

@xiangfu0

@xiangfu0 xiangfu0 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Context

This is the bottom layer extracted from #18229 after the request to split the codec-pipeline work and land the DSL plus configuration wiring first.

Native GitHub stack:

What changed

  • Replaces the unreleased outer CODEC(...) form with an ordered invocation list such as DELTA,ZSTD(3).
  • Adds immutable, bounded structural parsing and canonicalization for codec invocations.
  • Adds nullable indexes.forward.codecSpec support to ForwardIndexConfig, including JSON, builder, copy, equality, RAW-only validation, and mutual exclusion with legacy compressionCodec.
  • Reconciles legacy noDictionary and top-level compression signals with the nested forward-index config.
  • Fails closed anywhere config validation can be bypassed: table validation, immutable creator selection, realtime mutable indexes, preprocessing, and config-aware reader creation.
  • Rejects codec specs for OPEN_STRUCT materialized children during table validation and again before the splitter replaces a child forward config.

Safety and compatibility

This layer does not activate codecSpec and does not change any on-disk format. Every non-null spec is deliberately rejected before a reader, writer, or mutable index can silently ignore it. Activation will be a later stacked PR after the runtime and reader/writer lifecycle are reviewable.

The unreleased CODEC(...) syntax is intentionally not retained as an alias.

Why a dedicated parser rather than the SQL expression parser

The DSL is function-call shaped, so reusing Pinot's expression parsing is a fair question. Two separate answers:

CalciteSqlParser is unreachable from pinot-segment-spi. It lives in pinot-common, and pinot-common already declares a dependency on pinot-segment-spi, so calling into it would close a module cycle.

Calcite's own SqlParser is on the classpath — this module already uses Calcite for type inference — but is deliberately not used either:

  • The canonical form is frozen into segment headers. It is embedded in the V7 header (Add V7 raw forward index format for codec pipelines #19307) and compared by string equality to detect rewrites. Delegating canonicalization would tie on-disk segment compatibility to Calcite's identifier-casing, quoting, and literal-formatting rules across Calcite upgrades.
  • Narrowing it back down would cost more than the parser. DELTA,ZSTD(3) is not a valid statement, so it would have to be wrapped and then walked to reject arithmetic, nested calls, string literals, aliases, and qualified names — larger than the parser here, and failing open as new node types appear in later Calcite versions, in a path that otherwise fails closed.
  • It runs on every table-config deserialization, from the ForwardIndexConfig constructor, on controller, server, and minion.

The cost of that choice, stated plainly: arguments are unsigned integers only, so neither negative arguments (ZSTD fast-mode levels) nor keyword arguments (LZ4(HIGH)) are expressible today. Widening the grammar later is additive and contained; removing a Calcite dependency from segment headers would not be. The rationale is recorded in the CodecSpecParser javadoc.

Tests

  • 31 codec AST/parser and ForwardIndexConfig tests
  • 171 targeted forward-index/config validation tests
  • 33 focused OPEN_STRUCT validation and splitter tests
  • focused post-review parser and reader-gate regressions
  • canonical-form equality/hashCode pinning for CodecInvocation and CodecPipeline, plus accept-boundary coverage for every documented structural limit through both the parser and the AST constructors
  • Spotless, Checkstyle, license format, and license check for pinot-segment-spi and pinot-segment-local

@xiangfu0 xiangfu0 added feature New functionality index Related to indexing (general) release-notes Referenced by PRs that need attention when compiling the next release notes labels Aug 18, 2026
@xiangfu0
xiangfu0 requested a balanced review from Copilot August 18, 2026 00:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the codec-spec DSL and configuration plumbing while keeping codec execution disabled.

Changes:

  • Adds bounded parsing and immutable codec AST types.
  • Adds codecSpec to forward-index configuration.
  • Adds fail-closed validation across creation, loading, preprocessing, and realtime paths.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
ForwardIndexConfig.java Adds codec-spec configuration support.
CodecSpecParser.java Parses and canonicalizes the DSL.
CodecPipeline.java Represents ordered codec stages.
CodecInvocation.java Represents individual codec calls.
ForwardIndexType.java Reconciles configuration and rejects activation.
ForwardIndexReaderFactory.java Adds reader-side rejection.
ForwardIndexCreatorFactory.java Adds creator-side rejection.
ForwardIndexHandler.java Rejects preprocessing with codec specs.
ForwardIndexConfigTest.java Tests configuration behavior.
CodecSpecParserTest.java Tests parsing and limits.
CodecPipelineTest.java Tests pipeline immutability.
CodecInvocationTest.java Tests invocation validation.
TableConfigUtilsTest.java Tests table validation.
ForwardIndexHandlerTest.java Tests preprocessing rejection.
ForwardIndexTypeTest.java Tests reconciliation and mutable indexes.
ForwardIndexReaderFactoryTest.java Tests reader rejection.
ForwardIndexCreatorFactoryTest.java Tests creator rejection.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@codecov-commenter

codecov-commenter commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.99034% with 29 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.13%. Comparing base (84becf5) to head (6dac83b).
⚠️ Report is 5 commits behind head on master.

Files with missing lines Patch % Lines
...pache/pinot/segment/spi/codec/CodecSpecParser.java 88.42% 4 Missing and 7 partials ⚠️
...pache/pinot/segment/spi/codec/CodecInvocation.java 83.72% 2 Missing and 5 partials ⚠️
.../apache/pinot/segment/spi/codec/CodecPipeline.java 78.94% 2 Missing and 2 partials ⚠️
...he/pinot/segment/spi/index/ForwardIndexConfig.java 82.60% 2 Missing and 2 partials ⚠️
.../local/segment/index/forward/ForwardIndexType.java 80.00% 0 Missing and 2 partials ⚠️
.../segment/index/openstruct/OpenStructIndexType.java 66.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19284      +/-   ##
============================================
- Coverage     67.18%   67.13%   -0.06%     
  Complexity     1424     1424              
============================================
  Files          3462     3465       +3     
  Lines        220361   220870     +509     
  Branches      35147    35307     +160     
============================================
+ Hits         148045   148274     +229     
- Misses        60506    60749     +243     
- Partials      11810    11847      +37     
Flag Coverage Δ
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 0.00% <ø> (ø)
java-25 67.13% <85.99%> (-0.06%) ⬇️
lane-a 100.00% <ø> (ø)
lane-b 0.00% <ø> (ø)
temurin 67.13% <85.99%> (-0.06%) ⬇️
unittests 67.12% <85.99%> (-0.06%) ⬇️
unittests1 57.85% <82.60%> (+0.01%) ⬆️
unittests2 39.08% <11.59%> (-0.14%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@xiangfu0
xiangfu0 force-pushed the xiangfu0/codex/codec-stack/01-config branch from 1c5df46 to ad52533 Compare August 18, 2026 07:21
@xiangfu0
xiangfu0 marked this pull request as ready for review August 18, 2026 07:23
@xiangfu0
xiangfu0 requested review from Jackie-Jiang and a balanced review from Copilot August 18, 2026 07:23
@xiangfu0

Copy link
Copy Markdown
Contributor Author

Reviewed against the split plan. One thing worth fixing before this merges, since this PR freezes the codecSpec grammar: CodecSpecParser accepts leading zeros in numeric args and canonicalization preserves them, so ZSTD(03) and ZSTD(3) canonicalize to different strings. The canonical form later gets embedded in V7 segment headers and compared for rewrite detection (upcoming PRs in this stack), so two semantically identical specs would compare as different and could trigger spurious rewrites. Suggest rejecting or normalizing leading-zero args in parseArg.

Minor: MAX_SPEC_LENGTH = 64KB is generous for a string destined for a segment header; a 1–4KB cap would bound header bloat.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.

The canonical codec spec is later frozen into V7 segment headers, so every
argument value must have exactly one spelling: ZSTD(03) and ZSTD(3) must not
produce different canonical strings. Reject leading zeros in both the parser
and the CodecInvocation constructor (programmatic construction bypasses the
parser). Also tighten MAX_SPEC_LENGTH from 64KB to 4KB to bound V7 header
bloat; the cap still admits any realistic spec, and where it intersects the
combinatorial maximum of the per-stage limits, the length cap wins. Widen
REMOVED_WRAPPER_NAME to public so every layer enforcing the reservation
shares one definition.
@xiangfu0

Copy link
Copy Markdown
Contributor Author

Pushed `e00e837b5c` addressing review findings on the DSL surface.

Leading-zero arguments are now rejected. `ZSTD(03)` and `ZSTD(3)` previously canonicalized to two different strings. Since the canonical spec is frozen into V7 segment headers and compared by equality on reload, two spellings of the same value would eventually mean spurious rewrites and a parser that has to accept both forever. The grammar is now `arg ::= "0" | [1-9][0-9]*`, enforced in both `CodecSpecParser.parseArg` and the `CodecInvocation` constructor (programmatic construction bypasses the parser). A bare `0` remains valid.

`MAX_SPEC_LENGTH` tightened 64KB to 4KB to bound header bloat. Note this now wins over the combinatorial maximum of the per-stage limits (32 stages x 128-char names is ~4.1KB); any realistic spec is far below it. Tests pin both boundaries — exactly `MAX_SPEC_LENGTH` parses, one over is rejected.

`REMOVED_WRAPPER_NAME` widened to public so every layer enforcing the `CODEC` reservation shares one definition instead of re-spelling the literal (the registry in #19285 now references it).

All of these tighten acceptance before first release, which is the reversible direction — accepting now and rejecting later would not be.

Verification: `CodecSpecParserTest`, `CodecInvocationTest`, `CodecPipelineTest`, `ForwardIndexConfigTest` (pinot-segment-spi) plus `ForwardIndexTypeTest`, `TableConfigUtilsTest`, `ForwardIndexCreatorFactoryTest`, `OpenStructIndexTypeTest` (pinot-segment-local) all green; spotless/checkstyle/license clean on both modules.

@xiangfu0

Copy link
Copy Markdown
Contributor Author

Note: pushed a master sync to this branch (merge commit, no code changes of its own) so the downstream stack includes the #19282 chunk-caching fix that the reload tests in #19308 depend on.

The full stack continuing this PR: #19305 (DELTA/DELTADELTA) → #19306 (T64/GORILLA) → #19307 (V7 format) → #19308 (reload + enable) → #19309 (integration tests + docs). Each is based on its predecessor's branch.

The canonical spec produced here is frozen into segment headers and compared
for rewrite detection by later layers in this stack, but CodecInvocation and
CodecPipeline had no equals/hashCode coverage, and the documented structural
limits were only exercised on the reject side.

Add tests that pin the frozen contract:
- a parsed invocation/pipeline equals the programmatically built equivalent
  and agrees on hashCode, argument-less invocations canonicalize alike, and
  stage order is significant
- every documented limit (identifier length, argument length, arguments per
  invocation, pipeline stages) is checked at its accept boundary as well as
  one over, through both the parser and the AST constructors, which are
  independent entry points

Test-only; no production behavior changes.
The DSL is function-call shaped, so reviewers reasonably ask why it does not
reuse Pinot's expression parsing. Record the answer where it is discoverable
rather than leaving it to be re-derived.

CalciteSqlParser is unreachable from pinot-segment-spi: it lives in
pinot-common, which already depends on this module, so calling into it would
close a module cycle. Calcite's own SqlParser is on the classpath but is
deliberately unused, chiefly because the canonical form produced here is
frozen into segment headers and compared by string equality, which would tie
on-disk segment compatibility to Calcite's casing, quoting, and literal
formatting across upgrades.

Also records the cost of that choice: unsigned integer arguments only, so
negative and keyword arguments are not expressible today.

Javadoc-only; no behavior change.
@xiangfu0 xiangfu0 added the configuration Config changes (addition/deletion/change in behavior) label Aug 19, 2026
@xiangfu0 xiangfu0 added the index-spi Related to index SPI interfaces label Aug 19, 2026
@Jackie-Jiang
Jackie-Jiang requested a balanced review from Copilot August 20, 2026 01:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

configuration Config changes (addition/deletion/change in behavior) feature New functionality index Related to indexing (general) index-spi Related to index SPI interfaces release-notes Referenced by PRs that need attention when compiling the next release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants