3030import org .apache .iceberg .Metrics ;
3131import org .apache .iceberg .MetricsConfig ;
3232import org .apache .iceberg .Schema ;
33+ import org .apache .iceberg .TableProperties ;
3334import org .apache .iceberg .data .GenericRecord ;
3435import org .apache .iceberg .data .Record ;
3536import org .apache .iceberg .data .parquet .InternalWriter ;
@@ -72,6 +73,16 @@ public class TestVariantMetrics {
7273
7374 private static final String ROOT_FIELD = "$" ;
7475
76+ private static final byte [] BINARY_20_BYTES = new byte [20 ];
77+ private static final byte [] BINARY_20_BYTES_ALL_FF = new byte [20 ];
78+
79+ static {
80+ for (int i = 0 ; i < 20 ; i += 1 ) {
81+ BINARY_20_BYTES [i ] = (byte ) (i + 1 );
82+ BINARY_20_BYTES_ALL_FF [i ] = (byte ) 0xFF ;
83+ }
84+ }
85+
7586 private static final VariantValue [] PRIMITIVES =
7687 new VariantValue [] {
7788 Variants .of (true ),
@@ -226,11 +237,7 @@ public void testShreddedPrimitiveTypeMismatch(VariantValue value) throws IOExcep
226237 @ Test
227238 public void testShreddedBinaryBoundsTruncation () throws IOException {
228239 // binary longer than the 16-byte truncation length so the bounds are truncated
229- byte [] bytes = new byte [20 ];
230- for (int i = 0 ; i < bytes .length ; i += 1 ) {
231- bytes [i ] = (byte ) (i + 1 );
232- }
233- VariantValue value = Variants .of (ByteBuffer .wrap (bytes ));
240+ VariantValue value = Variants .of (ByteBuffer .wrap (BINARY_20_BYTES ));
234241
235242 Metrics metrics =
236243 writeParquet (
@@ -241,21 +248,17 @@ public void testShreddedBinaryBoundsTruncation() throws IOException {
241248
242249 assertThat (metrics .lowerBounds ().get (2 ))
243250 .extracting (b -> Variant .from (b ).value ().asObject ().get (ROOT_FIELD ))
244- .isEqualTo (Variants .of (BinaryUtil .truncateBinaryMin (ByteBuffer .wrap (bytes ), 16 )));
251+ .isEqualTo (Variants .of (BinaryUtil .truncateBinaryMin (ByteBuffer .wrap (BINARY_20_BYTES ), 16 )));
245252
246253 assertThat (metrics .upperBounds ().get (2 ))
247254 .extracting (b -> Variant .from (b ).value ().asObject ().get (ROOT_FIELD ))
248- .isEqualTo (Variants .of (BinaryUtil .truncateBinaryMax (ByteBuffer .wrap (bytes ), 16 )));
255+ .isEqualTo (Variants .of (BinaryUtil .truncateBinaryMax (ByteBuffer .wrap (BINARY_20_BYTES ), 16 )));
249256 }
250257
251258 @ Test
252259 public void testShreddedBinaryUpperBoundOverflow () throws IOException {
253260 // an all-0xFF binary cannot be truncated up so the upper bound is omitted
254- byte [] bytes = new byte [20 ];
255- for (int i = 0 ; i < bytes .length ; i += 1 ) {
256- bytes [i ] = (byte ) 0xFF ;
257- }
258- VariantValue value = Variants .of (ByteBuffer .wrap (bytes ));
261+ VariantValue value = Variants .of (ByteBuffer .wrap (BINARY_20_BYTES_ALL_FF ));
259262
260263 Metrics metrics =
261264 writeParquet (
@@ -266,13 +269,174 @@ public void testShreddedBinaryUpperBoundOverflow() throws IOException {
266269
267270 assertThat (metrics .lowerBounds ().get (2 ))
268271 .extracting (b -> Variant .from (b ).value ().asObject ().get (ROOT_FIELD ))
269- .isEqualTo (Variants .of (BinaryUtil .truncateBinaryMin (ByteBuffer .wrap (bytes ), 16 )));
272+ .isEqualTo (
273+ Variants .of (BinaryUtil .truncateBinaryMin (ByteBuffer .wrap (BINARY_20_BYTES_ALL_FF ), 16 )));
270274
271275 assertThat (metrics .upperBounds ().get (2 ))
272276 .extracting (b -> Variant .from (b ).value ().asObject ().get (ROOT_FIELD ))
273277 .isNull ();
274278 }
275279
280+ @ Test
281+ public void testShreddedBinaryBoundsTruncateLength () throws IOException {
282+ // a per-column truncate(8) overrides the default 16-byte truncation
283+ VariantValue value = Variants .of (ByteBuffer .wrap (BINARY_20_BYTES ));
284+
285+ MetricsConfig metricsConfig =
286+ MetricsConfig .from (
287+ ImmutableMap .of (TableProperties .METRICS_MODE_COLUMN_CONF_PREFIX + "var" , "truncate(8)" ),
288+ SCHEMA ,
289+ null );
290+
291+ Metrics metrics =
292+ writeParquetWithMetricsConfig (
293+ (id , name ) -> ParquetVariantUtil .toParquetSchema (value ),
294+ metricsConfig ,
295+ Variant .of (EMPTY , value ),
296+ Variant .of (EMPTY , Variants .ofNull ()),
297+ null );
298+
299+ assertThat (metrics .lowerBounds ().get (2 ))
300+ .extracting (b -> Variant .from (b ).value ().asObject ().get (ROOT_FIELD ))
301+ .isEqualTo (Variants .of (BinaryUtil .truncateBinaryMin (ByteBuffer .wrap (BINARY_20_BYTES ), 8 )));
302+
303+ assertThat (metrics .upperBounds ().get (2 ))
304+ .extracting (b -> Variant .from (b ).value ().asObject ().get (ROOT_FIELD ))
305+ .isEqualTo (Variants .of (BinaryUtil .truncateBinaryMax (ByteBuffer .wrap (BINARY_20_BYTES ), 8 )));
306+ }
307+
308+ @ Test
309+ public void testShreddedBinaryBoundsFull () throws IOException {
310+ // full mode leaves the bounds untruncated
311+ VariantValue value = Variants .of (ByteBuffer .wrap (BINARY_20_BYTES ));
312+
313+ MetricsConfig metricsConfig =
314+ MetricsConfig .from (
315+ ImmutableMap .of (TableProperties .METRICS_MODE_COLUMN_CONF_PREFIX + "var" , "full" ),
316+ SCHEMA ,
317+ null );
318+
319+ Metrics metrics =
320+ writeParquetWithMetricsConfig (
321+ (id , name ) -> ParquetVariantUtil .toParquetSchema (value ),
322+ metricsConfig ,
323+ Variant .of (EMPTY , value ),
324+ Variant .of (EMPTY , Variants .ofNull ()),
325+ null );
326+
327+ assertThat (metrics .lowerBounds ().get (2 ))
328+ .extracting (b -> Variant .from (b ).value ().asObject ().get (ROOT_FIELD ))
329+ .isEqualTo (Variants .of (ByteBuffer .wrap (BINARY_20_BYTES )));
330+
331+ assertThat (metrics .upperBounds ().get (2 ))
332+ .extracting (b -> Variant .from (b ).value ().asObject ().get (ROOT_FIELD ))
333+ .isEqualTo (Variants .of (ByteBuffer .wrap (BINARY_20_BYTES )));
334+ }
335+
336+ @ Test
337+ public void testShreddedBinaryBoundsCounts () throws IOException {
338+ // counts mode drops shredded bounds
339+ VariantValue value = Variants .of (ByteBuffer .wrap (BINARY_20_BYTES ));
340+
341+ MetricsConfig metricsConfig =
342+ MetricsConfig .from (
343+ ImmutableMap .of (TableProperties .METRICS_MODE_COLUMN_CONF_PREFIX + "var" , "counts" ),
344+ SCHEMA ,
345+ null );
346+
347+ Metrics metrics =
348+ writeParquetWithMetricsConfig (
349+ (id , name ) -> ParquetVariantUtil .toParquetSchema (value ),
350+ metricsConfig ,
351+ Variant .of (EMPTY , value ),
352+ Variant .of (EMPTY , Variants .ofNull ()),
353+ null );
354+
355+ assertThat (metrics .valueCounts ()).containsKey (2 );
356+ assertThat (metrics .lowerBounds ()).doesNotContainKey (2 );
357+ assertThat (metrics .upperBounds ()).doesNotContainKey (2 );
358+ }
359+
360+ @ Test
361+ public void testShreddedStringBoundsTruncateLength () throws IOException {
362+ // a per-column truncate(8) overrides the default 16-char truncation
363+ VariantValue value = Variants .of ("iceberg_variant" );
364+
365+ MetricsConfig metricsConfig =
366+ MetricsConfig .from (
367+ ImmutableMap .of (TableProperties .METRICS_MODE_COLUMN_CONF_PREFIX + "var" , "truncate(8)" ),
368+ SCHEMA ,
369+ null );
370+
371+ Metrics metrics =
372+ writeParquetWithMetricsConfig (
373+ (id , name ) -> ParquetVariantUtil .toParquetSchema (value ),
374+ metricsConfig ,
375+ Variant .of (EMPTY , value ),
376+ Variant .of (EMPTY , Variants .ofNull ()),
377+ null );
378+
379+ assertThat (metrics .lowerBounds ().get (2 ))
380+ .extracting (b -> Variant .from (b ).value ().asObject ().get (ROOT_FIELD ))
381+ .isEqualTo (Variants .of (UnicodeUtil .truncateStringMin ("iceberg_variant" , 8 )));
382+
383+ assertThat (metrics .upperBounds ().get (2 ))
384+ .extracting (b -> Variant .from (b ).value ().asObject ().get (ROOT_FIELD ))
385+ .isEqualTo (Variants .of (UnicodeUtil .truncateStringMax ("iceberg_variant" , 8 )));
386+ }
387+
388+ @ Test
389+ public void testShreddedStringBoundsFull () throws IOException {
390+ // full mode leaves the string bound untruncated
391+ VariantValue value = Variants .of ("iceberg_variant_full" );
392+
393+ MetricsConfig metricsConfig =
394+ MetricsConfig .from (
395+ ImmutableMap .of (TableProperties .METRICS_MODE_COLUMN_CONF_PREFIX + "var" , "full" ),
396+ SCHEMA ,
397+ null );
398+
399+ Metrics metrics =
400+ writeParquetWithMetricsConfig (
401+ (id , name ) -> ParquetVariantUtil .toParquetSchema (value ),
402+ metricsConfig ,
403+ Variant .of (EMPTY , value ),
404+ Variant .of (EMPTY , Variants .ofNull ()),
405+ null );
406+
407+ assertThat (metrics .lowerBounds ().get (2 ))
408+ .extracting (b -> Variant .from (b ).value ().asObject ().get (ROOT_FIELD ))
409+ .isEqualTo (Variants .of ("iceberg_variant_full" ));
410+
411+ assertThat (metrics .upperBounds ().get (2 ))
412+ .extracting (b -> Variant .from (b ).value ().asObject ().get (ROOT_FIELD ))
413+ .isEqualTo (Variants .of ("iceberg_variant_full" ));
414+ }
415+
416+ @ Test
417+ public void testShreddedStringBoundsCounts () throws IOException {
418+ // counts mode must not truncate the shredded string bound: truncate length 0 would throw
419+ VariantValue value = Variants .of ("iceberg_variant" );
420+
421+ MetricsConfig metricsConfig =
422+ MetricsConfig .from (
423+ ImmutableMap .of (TableProperties .METRICS_MODE_COLUMN_CONF_PREFIX + "var" , "counts" ),
424+ SCHEMA ,
425+ null );
426+
427+ Metrics metrics =
428+ writeParquetWithMetricsConfig (
429+ (id , name ) -> ParquetVariantUtil .toParquetSchema (value ),
430+ metricsConfig ,
431+ Variant .of (EMPTY , value ),
432+ Variant .of (EMPTY , Variants .ofNull ()),
433+ null );
434+
435+ assertThat (metrics .valueCounts ()).containsKey (2 );
436+ assertThat (metrics .lowerBounds ()).doesNotContainKey (2 );
437+ assertThat (metrics .upperBounds ()).doesNotContainKey (2 );
438+ }
439+
276440 @ Test
277441 public void testVariantFloatNaN () throws IOException {
278442 // NaN values are not counted because there is no ID for FieldMetrics
@@ -578,13 +742,20 @@ public void testShreddedValueColumnWithEmptyStats() throws IOException {
578742
579743 private Metrics writeParquet (VariantShreddingFunction shredding , Variant ... variants )
580744 throws IOException {
745+ return writeParquetWithMetricsConfig (shredding , MetricsConfig .getDefault (), variants );
746+ }
747+
748+ private Metrics writeParquetWithMetricsConfig (
749+ VariantShreddingFunction shredding , MetricsConfig metricsConfig , Variant ... variants )
750+ throws IOException {
581751 OutputFile out = new InMemoryOutputFile ();
582752 GenericRecord record = GenericRecord .create (SCHEMA );
583753
584754 FileAppender <Record > writer =
585755 Parquet .write (out )
586756 .schema (SCHEMA )
587757 .variantShreddingFunc (shredding )
758+ .metricsConfig (metricsConfig )
588759 .createWriterFunc (fileSchema -> InternalWriter .create (SCHEMA .asStruct (), fileSchema ))
589760 .build ();
590761
0 commit comments