3131import java .util .Map ;
3232import java .util .Set ;
3333import javax .annotation .Nullable ;
34+ import org .apache .pinot .segment .local .io .codec .CodecPipelineExecutor ;
35+ import org .apache .pinot .segment .local .io .codec .CodecSpecUtils ;
3436import org .apache .pinot .segment .local .realtime .impl .forward .CLPMutableForwardIndexV2 ;
3537import org .apache .pinot .segment .local .realtime .impl .forward .FixedByteMVMutableForwardIndex ;
3638import org .apache .pinot .segment .local .realtime .impl .forward .FixedByteSVMutableForwardIndex ;
@@ -104,24 +106,21 @@ public ForwardIndexConfig getDefaultConfig() {
104106 @ Override
105107 public void validate (FieldIndexConfigs indexConfigs , FieldSpec fieldSpec , TableConfig tableConfig ) {
106108 ForwardIndexConfig forwardIndexConfig = indexConfigs .getConfig (StandardIndexes .forward ());
107- rejectUnsupportedCodecSpec (forwardIndexConfig , fieldSpec .getName ());
108109 if (forwardIndexConfig .isEnabled ()) {
109110 validateForwardIndexEnabled (forwardIndexConfig , indexConfigs , fieldSpec );
110111 } else {
111112 validateForwardIndexDisabled (indexConfigs , fieldSpec , tableConfig );
112113 }
113114 }
114115
115- static void rejectUnsupportedCodecSpec (ForwardIndexConfig config , String column ) {
116- Preconditions .checkState (!config .hasCodecSpec (),
117- "codecSpec is not supported yet for column: %s" , column );
118- }
119-
120116 private void validateForwardIndexEnabled (ForwardIndexConfig forwardIndexConfig , FieldIndexConfigs indexConfigs ,
121117 FieldSpec fieldSpec ) {
122118 String column = fieldSpec .getName ();
123119 CompressionCodec compressionCodec = forwardIndexConfig .getCompressionCodec ();
124120 DictionaryIndexConfig dictionaryConfig = indexConfigs .getConfig (StandardIndexes .dictionary ());
121+ if (forwardIndexConfig .hasCodecSpec ()) {
122+ validateCodecSpec (forwardIndexConfig , fieldSpec );
123+ }
125124 // Dictionary-encoded forward index requires a dictionary to translate dict ids back to values.
126125 if (forwardIndexConfig .getEncodingType () == FieldConfig .EncodingType .DICTIONARY ) {
127126 Preconditions .checkState (dictionaryConfig .isEnabled (),
@@ -143,6 +142,51 @@ private void validateForwardIndexEnabled(ForwardIndexConfig forwardIndexConfig,
143142 }
144143 }
145144
145+ /// Semantic validation for `codecSpec` at table-config time: parses and validates the pipeline
146+ /// (unknown codecs, stage ordering, per-codec type compatibility) against the column's stored
147+ /// type, then enforces the V7 codec-pipeline writer's shape constraints — single-value INT/LONG —
148+ /// for specs that cannot be served by the legacy raw forward-index formats.
149+ private void validateCodecSpec (ForwardIndexConfig forwardIndexConfig , FieldSpec fieldSpec ) {
150+ String column = fieldSpec .getName ();
151+ Preconditions .checkState (forwardIndexConfig .getEncodingType () == FieldConfig .EncodingType .RAW ,
152+ "codecSpec requires RAW forward-index encoding for column: %s" , column );
153+ FieldSpec .DataType storedType = fieldSpec .getDataType ().getStoredType ();
154+
155+ String codecSpec = forwardIndexConfig .getCodecSpec ();
156+ try {
157+ // CodecPipelineExecutor.create parses the spec and runs full pipeline validation.
158+ CodecPipelineExecutor .create (codecSpec , storedType );
159+ } catch (IllegalArgumentException e ) {
160+ throw new IllegalStateException (
161+ "Codec pipeline validation failed for column '" + column + "' (codecSpec='" + codecSpec + "'): "
162+ + e .getMessage (), e );
163+ }
164+ // Transforms, chains, and non-default options all fail the legacy ChunkCompressionType mapping, so a
165+ // null mapping is exactly "needs the V7 writer".
166+ if (CodecSpecUtils .toLegacyChunkCompressionType (codecSpec ) == null ) {
167+ validateV7WriterShape (codecSpec , fieldSpec );
168+ }
169+ }
170+
171+ /// Shared V7 codec-pipeline writer shape constraints: specs that cannot be represented by a legacy
172+ /// [org.apache.pinot.segment.spi.compression.ChunkCompressionType] (transform, chain, or non-default
173+ /// options) require the V7 writer, which only supports single-value INT/LONG columns. Called at
174+ /// table-config validation time, and again from [ForwardIndexCreatorFactory] as defense-in-depth for
175+ /// direct factory calls that bypass validation.
176+ static void validateV7WriterShape (String codecSpec , FieldSpec fieldSpec ) {
177+ String column = fieldSpec .getName ();
178+ Preconditions .checkArgument (fieldSpec .isSingleValueField (),
179+ "codecSpec '%s' requires the V7 codec-pipeline writer (transform, chain, or non-default options), "
180+ + "which only supports single-value columns. Column '%s' is multi-value; use a compression-only "
181+ + "spec representable by ChunkCompressionType (LZ4, SNAPPY, GZIP, ZSTD/ZSTD(3))." ,
182+ codecSpec , column );
183+ FieldSpec .DataType storedType = fieldSpec .getDataType ().getStoredType ();
184+ Preconditions .checkArgument (storedType == FieldSpec .DataType .INT || storedType == FieldSpec .DataType .LONG ,
185+ "codecSpec '%s' requires the V7 codec-pipeline writer (transform, chain, or non-default options), "
186+ + "which only supports INT and LONG columns. Column '%s' has type: %s." ,
187+ codecSpec , column , storedType );
188+ }
189+
146190 private void validateForwardIndexDisabled (FieldIndexConfigs indexConfigs , FieldSpec fieldSpec ,
147191 TableConfig tableConfig ) {
148192 String column = fieldSpec .getName ();
@@ -314,7 +358,6 @@ public static ChunkCompressionType getDefaultCompressionType(FieldSpec.FieldType
314358
315359 @ Override
316360 public boolean shouldCreateIndex (IndexCreationContext context , ForwardIndexConfig indexConfig ) {
317- rejectUnsupportedCodecSpec (indexConfig , context .getFieldSpec ().getName ());
318361 return context .getFieldSpec ().getDataType () != FieldSpec .DataType .OPEN_STRUCT ;
319362 }
320363
@@ -377,7 +420,9 @@ public List<String> getFileExtensions(@Nullable ColumnMetadata columnMetadata) {
377420 @ Nullable
378421 @ Override
379422 public MutableIndex createMutableIndex (MutableIndexContext context , ForwardIndexConfig config ) {
380- rejectUnsupportedCodecSpec (config , context .getFieldSpec ().getName ());
423+ // Note: codecSpec applies only when an immutable segment is created (initial creation or
424+ // conversion/commit of a consuming segment). The mutable (consuming) forward index always uses
425+ // the standard in-memory formats below, so a configured codecSpec is intentionally ignored here.
381426 if (config .isDisabled ()) {
382427 return null ;
383428 }
0 commit comments