001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.marshall.parquet; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020import static org.apache.juneau.marshall.parquet.ParquetSchemaElement.*; 021 022import java.time.*; 023import java.util.*; 024import java.util.stream.*; 025 026import org.apache.juneau.commons.bean.*; 027import org.apache.juneau.commons.reflect.*; 028import org.apache.juneau.marshall.*; 029import org.apache.juneau.marshall.serializer.*; 030 031/** 032 * Builds Parquet schema (list of SchemaElement) from Juneau bean ClassMeta. 033 * 034 * <p> 035 * Maps Java types to Parquet physical and logical types per the 036 * <a class="doclink" href="https://parquet.apache.org/docs/file-format/">Parquet specification</a>. 037 */ 038@SuppressWarnings({ 039 "java:S1192", // Duplicated "value" is Parquet Optional key; constant would obscure schema mapping 040 "java:S3776" // addLeafSchema() and addBeanSchema() map every Java type to a Parquet physical/logical type in one pass; splitting them would obscure the mapping 041}) 042public final class ParquetSchemaBuilder { 043 044 private final MarshallingContext marshallingContext; 045 private final boolean writeDatesAsTimestamp; 046 private final ParquetCycleHandling cycleHandling; 047 private final int maxRecursionDepth; 048 private final boolean nativeLogicalTypes; 049 050 /** Fixed scale/precision for INT64-backed native DECIMAL emission (GAP-9). 9 fractional digits, 18 total. */ 051 static final int DECIMAL_SCALE = 9; 052 static final int DECIMAL_PRECISION = 18; 053 054 /** 055 * Constructor. 056 * 057 * @param marshallingContext The bean context for type resolution. 058 * @param writeDatesAsTimestamp If true, Date/Calendar/Temporal map to INT64+TIMESTAMP_MILLIS; else to BYTE_ARRAY+STRING. 059 * @param cycleHandling How to handle structural cycles when building schema. 060 */ 061 public ParquetSchemaBuilder(MarshallingContext marshallingContext, boolean writeDatesAsTimestamp, ParquetCycleHandling cycleHandling) { 062 this(marshallingContext, writeDatesAsTimestamp, cycleHandling, 5, false); 063 } 064 065 /** 066 * Constructor. 067 * 068 * @param marshallingContext The bean context for type resolution. 069 * @param writeDatesAsTimestamp If true, Date/Calendar/Temporal map to INT64+TIMESTAMP_MILLIS; else to BYTE_ARRAY+STRING. 070 * @param cycleHandling How to handle structural cycles when building schema. 071 * @param maxRecursionDepth Maximum expansion depth for self-referential bean types (GAP-13). 072 */ 073 public ParquetSchemaBuilder(MarshallingContext marshallingContext, boolean writeDatesAsTimestamp, ParquetCycleHandling cycleHandling, int maxRecursionDepth) { 074 this(marshallingContext, writeDatesAsTimestamp, cycleHandling, maxRecursionDepth, false); 075 } 076 077 /** 078 * Constructor. 079 * 080 * @param marshallingContext The bean context for type resolution. 081 * @param writeDatesAsTimestamp If true, Date/Calendar/Temporal map to INT64+TIMESTAMP_MILLIS; else to BYTE_ARRAY+STRING. 082 * @param cycleHandling How to handle structural cycles when building schema. 083 * @param maxRecursionDepth Maximum expansion depth for self-referential bean types (GAP-13). 084 * @param nativeLogicalTypes If true, emit binary-native DECIMAL/DATE/TIME/TIMESTAMP-micros logical types 085 * instead of the default string / TIMESTAMP-millis normalization (GAP-9/10). 086 */ 087 public ParquetSchemaBuilder(MarshallingContext marshallingContext, boolean writeDatesAsTimestamp, ParquetCycleHandling cycleHandling, int maxRecursionDepth, boolean nativeLogicalTypes) { 088 this.marshallingContext = marshallingContext; 089 this.writeDatesAsTimestamp = writeDatesAsTimestamp; 090 this.cycleHandling = or(cycleHandling, ParquetCycleHandling.NULL); 091 this.maxRecursionDepth = Math.max(1, maxRecursionDepth); 092 this.nativeLogicalTypes = nativeLogicalTypes; 093 } 094 095 /** 096 * Builds the Parquet schema for the given bean class. 097 * 098 * @param cm The bean class metadata. 099 * @return The depth-first list of schema elements (root first). 100 */ 101 public List<ParquetSchemaElement> buildSchema(ClassMeta<?> cm) { 102 return buildSchema(cm, null); 103 } 104 105 /** 106 * Builds the Parquet schema, using a sample bean to resolve collection element types when generics are erased. 107 * 108 * @param cm The bean class metadata. 109 * @param sampleBean Optional sample instance; when provided, collection element types are resolved from actual values. 110 * @return The depth-first list of schema elements (root first). 111 */ 112 public List<ParquetSchemaElement> buildSchema(ClassMeta<?> cm, Object sampleBean) { 113 var elements = new ArrayList<ParquetSchemaElement>(); 114 var typesInProgress = new HashMap<Class<?>,Integer>(); 115 addSchemaElements(elements, cm, "root", null, true, sampleBean, typesInProgress); 116 return elements; 117 } 118 119 /** 120 * Builds the Parquet schema for a Map root (single row, columns from map keys). 121 * 122 * @param map The map instance (keys become column names). 123 * @return The depth-first list of schema elements (root first). 124 */ 125 public List<ParquetSchemaElement> buildSchemaFromMap(Map<?,?> map) { 126 var elements = new ArrayList<ParquetSchemaElement>(); 127 var keys = map.keySet().stream() 128 .filter(String.class::isInstance) 129 .map(k -> (String) k) 130 .sorted() 131 .toList(); 132 elements.add(new ParquetSchemaElement( 133 "root", 134 null, 135 null, 136 null, 137 keys.size(), 138 null, 139 null, 140 null, 141 null, 142 null)); 143 var typesInProgress = new HashMap<Class<?>,Integer>(); 144 for (var key : keys) { 145 var val = map.get(key); 146 Class<?> valClass = val != null ? val.getClass() : Object.class; 147 var cm = marshallingContext.getClassMeta(valClass); 148 addSchemaElements(elements, cm, key, "root", false, null, typesInProgress); 149 } 150 return elements; 151 } 152 153 /** 154 * Builds the Parquet schema for a Map root with non-String keys (flat list-of-pairs: root.key, root.value). 155 * 156 * <p> 157 * Each map entry becomes one Parquet row. Keys and values use native Parquet types. 158 * 159 * @param keySample Sample key for type inference. 160 * @param valueSample Sample value for type inference. 161 * @return The depth-first list of schema elements (root first). 162 */ 163 public List<ParquetSchemaElement> buildSchemaForKeyValuePairs(Object keySample, Object valueSample) { 164 var elements = new ArrayList<ParquetSchemaElement>(); 165 var typesInProgress = new HashMap<Class<?>,Integer>(); 166 Class<?> keyClass = keySample != null ? keySample.getClass() : Object.class; 167 Class<?> valueClass = valueSample != null ? valueSample.getClass() : Object.class; 168 var keyCm = marshallingContext.getClassMeta(keyClass); 169 var valueCm = marshallingContext.getClassMeta(valueClass); 170 elements.add(new ParquetSchemaElement("root", null, null, null, 2, null, null, null, null, null)); 171 addSchemaElements(elements, keyCm, "key", "root", false, null, typesInProgress); 172 addSchemaElements(elements, valueCm, "value", "root", false, null, typesInProgress); 173 return elements; 174 } 175 176 private void addSchemaElements(List<ParquetSchemaElement> elements, ClassMeta<?> cm, String name, String parentPath, boolean isRoot, Object sampleBean, Map<Class<?>,Integer> typesInProgress) { 177 if (cm.isOptional()) { 178 addOptionalSchema(elements, cm, name, parentPath, isRoot, sampleBean, typesInProgress); 179 } else if (cm.isBean()) { 180 addBeanSchema(elements, cm, name, parentPath, isRoot, sampleBean, typesInProgress); 181 } else if (cm.isByteArray()) { 182 addLeafSchema(elements, cm, name, parentPath, isRoot); 183 } else if (cm.isCollection() || cm.isArray()) { 184 addListSchema(elements, cm, name, parentPath, isRoot, sampleBean, typesInProgress); 185 } else if (cm.isMap()) { 186 addMapSchema(elements, cm, name, parentPath, isRoot, typesInProgress); 187 } else { 188 addLeafSchema(elements, cm, name, parentPath, isRoot); 189 } 190 } 191 192 /** Optional<X> → optional group with "value" child of type X (2.2). */ 193 private void addOptionalSchema(List<ParquetSchemaElement> elements, ClassMeta<?> cm, String name, String parentPath, boolean isRoot, Object sampleBean, Map<Class<?>,Integer> typesInProgress) { 194 var et = cm.getElementType(); 195 Object innerSample = sampleBean instanceof Optional<?> sampleBean2 ? sampleBean2.orElse(null) : sampleBean; 196 var optPath = parentPath != null ? parentPath + "." + name : name; 197 elements.add(new ParquetSchemaElement(name, null, null, isRoot ? null : OPTIONAL, 1, null, null, null, null, optPath)); 198 addSchemaElements(elements, et, "value", optPath, false, innerSample, typesInProgress); 199 } 200 201 private void addBeanSchema(List<ParquetSchemaElement> elements, ClassMeta<?> cm, String name, String parentPath, boolean isRoot, Object sampleBean, Map<Class<?>,Integer> typesInProgress) { 202 var bm = cm.getBeanMeta(); 203 var beanClass = cm.inner(); 204 // Recursion reached here indirectly (e.g. through Optional/collection of the same type) at the depth 205 // limit: keep the legacy String back-reference placeholder so the enclosing group's child count stays 206 // consistent. Direct bean-property recursion is handled by the per-property filter below (truncated 207 // by omission, which round-trips as null). 208 if (typesInProgress.getOrDefault(beanClass, 0) >= maxRecursionDepth) { 209 if (cycleHandling == ParquetCycleHandling.THROW) { 210 var path = parentPath + "." + name; 211 throw new SerializeException("Cyclic type reference at '%s' (type '%s'). Use @ParentProperty to exclude back-references or set cycleHandling(NULL).", path, beanClass.getName()); 212 } 213 addLeafSchema(elements, marshallingContext.getClassMeta(String.class), name, parentPath, isRoot); 214 return; 215 } 216 typesInProgress.merge(beanClass, 1, Integer::sum); 217 try { 218 var ap = marshallingContext.getAnnotationProvider(); 219 var props = bm.getProperties().values().stream() 220 .filter(BeanPropertyMeta::canRead) 221 .filter(p -> !isParentProperty(ap, p)) 222 .toList(); 223 var childParentPath = parentPath != null ? parentPath + "." + name : name; 224 // Resolve each property (incl. Object->sample inference) and drop direct recursive bean properties 225 // that would exceed the depth limit (GAP-13). Omitting them keeps the schema acyclic; on read the 226 // missing column reconstructs as null (documented-lossy beyond the limit) instead of a toString. 227 var entries = new ArrayList<ResolvedProp>(); 228 for (var p : props) { 229 Object childSample = null; 230 if (sampleBean instanceof Map<?,?> sampleBean2) 231 childSample = sampleBean2.get(p.getName()); 232 ClassMeta<?> propCm = (ClassMeta<?>) p.getBeanInfo(); 233 // Native logical types (GAP-9/10): a java.time temporal property carries a swap, so its 234 // resolved ClassMeta is an Object/String surrogate that hides the concrete type. Recover the 235 // declared field/getter type so the DATE/TIME/TIMESTAMP-micros leaf branches can fire. Only 236 // overrides when the declared type is one of the native logical types we emit. 237 if (nativeLogicalTypes) { 238 var declared = declaredNativeType(p); 239 if (declared != null) 240 propCm = marshallingContext.getClassMeta(declared); 241 } 242 // When property type is Object, infer from sample so Map/Collection get proper schema (2.2) 243 if ((propCm == null || propCm.isObject()) && childSample != null) // HTT: propCm is non-null (getBeanInfo always non-null for bean properties) 244 propCm = marshallingContext.getClassMeta(childSample.getClass()); 245 // When property type is the abstract Number, re-type from a concrete numeric sample so 246 // the column uses the actual physical width (INT32/INT64/DOUBLE) and fractional values survive 247 // (GAP-12). Only fires when the sample is itself a Number — a swapped property whose raw sample 248 // is non-numeric stays on the lossless INT64 default. 249 else if (propCm != null && propCm.is(Number.class) && childSample instanceof Number) // HTT: propCm is non-null (same invariant as above) 250 propCm = marshallingContext.getClassMeta(childSample.getClass()); 251 if (propCm != null && propCm.isBean() && typesInProgress.getOrDefault(propCm.inner(), 0) >= maxRecursionDepth) { // HTT: propCm is non-null 252 if (cycleHandling == ParquetCycleHandling.THROW) 253 throw new SerializeException("Cyclic type reference at '%s.%s' (type '%s'). Use @ParentProperty to exclude back-references or set cycleHandling(NULL).", childParentPath, p.getName(), propCm.inner().getName()); 254 continue; 255 } 256 entries.add(new ResolvedProp(p.getName(), propCm, childSample)); 257 } 258 elements.add(new ParquetSchemaElement( 259 name, 260 null, 261 null, 262 isRoot ? null : OPTIONAL, 263 entries.size(), 264 null, 265 null, 266 null, 267 null, 268 childParentPath)); 269 for (var e : entries) 270 addSchemaElements(elements, e.cm, e.name, childParentPath, false, e.sample, typesInProgress); 271 } finally { 272 typesInProgress.merge(beanClass, -1, (a, b) -> a + b == 0 ? null : a + b); 273 } 274 } 275 276 private record ResolvedProp(String name, ClassMeta<?> cm, Object sample) {} 277 278 /** 279 * Returns the property's declared field/getter class when it is a binary-native logical type 280 * (java.time temporal, {@link java.math.BigDecimal}, or {@link java.math.BigInteger}); otherwise 281 * <jk>null</jk>. Used to look past a registered swap whose resolved ClassMeta hides the concrete 282 * type, so the native logical-type leaf branches in {@link #addLeafSchema} can fire (GAP-9/10). 283 */ 284 private static Class<?> declaredNativeType(BeanPropertyMeta p) { 285 // Resolve the declared class from the field (preferred) or, for getter-only properties, the getter. 286 var f = p.getField(); 287 Class<?> c = f != null ? f.getFieldType().inner() : p.getGetter().getReturnType().inner(); 288 if (c == LocalDate.class || c == LocalTime.class || c == OffsetTime.class 289 || c == java.math.BigDecimal.class || c == java.math.BigInteger.class 290 || Date.class.isAssignableFrom(c) || Calendar.class.isAssignableFrom(c) 291 || java.time.temporal.Temporal.class.isAssignableFrom(c)) 292 return c; 293 return null; 294 } 295 296 private static boolean isParentProperty(AnnotationProvider ap, BeanPropertyMeta p) { 297 var g = p.getGetter(); 298 if (g != null && ap.has(ParentProperty.class, g)) 299 return true; 300 var s = p.getSetter(); 301 if (s != null && ap.has(ParentProperty.class, s)) 302 return true; 303 var f = p.getField(); 304 return f != null && ap.has(ParentProperty.class, f); 305 } 306 307 private void addListSchema(List<ParquetSchemaElement> elements, ClassMeta<?> cm, String name, String parentPath, boolean isRoot, Object sampleBean, Map<Class<?>,Integer> typesInProgress) { 308 var et = cm.getElementType(); 309 // Resolve element type from sample when generics are erased (et is Object) for proper list-of-bean 310 // expansion into leaf columns (e.g. members.list.element.name, members.list.element.age) 311 var sampleCollection = extractSampleCollection(sampleBean); 312 if (ine(sampleCollection)) { 313 var first = sampleCollection.iterator().next(); 314 if (first != null) 315 et = marshallingContext.getClassMeta(first.getClass()); 316 } 317 var listPath = parentPath != null ? parentPath + "." + name : name; 318 elements.add(new ParquetSchemaElement(name, null, null, isRoot ? null : OPTIONAL, 1, CONVERTED_LIST, null, null, null, null)); 319 elements.add(new ParquetSchemaElement("list", null, null, REPEATED, 1, null, null, null, null, null)); 320 Object elementSample = null; 321 if (ine(sampleCollection)) 322 elementSample = sampleCollection.iterator().next(); 323 addSchemaElements(elements, et, "element", listPath + ".list", false, elementSample, typesInProgress); 324 } 325 326 private static Collection<?> extractSampleCollection(Object sampleBean) { 327 if (sampleBean instanceof Collection<?> sampleBean2) 328 return sampleBean2; 329 return Collections.emptyList(); 330 } 331 332 private void addMapSchema(List<ParquetSchemaElement> elements, ClassMeta<?> cm, String name, String parentPath, boolean isRoot, Map<Class<?>,Integer> typesInProgress) { 333 var vt = cm.getValueType(); 334 // Parquet MAP stores keys as STRING; non-String keys (e.g. Enum) are serialized via toString/name() 335 // vt is null for BeanMap-category classes (cat has both MAP and BEANMAP set, which suppresses the 336 // key/value type resolution in ClassMeta.findKeyValueTypes()) -- genuinely reachable, not dead code. 337 if (vt == null) 338 vt = marshallingContext.getClassMeta(Object.class); 339 var mapPath = parentPath != null ? parentPath + "." + name : name; 340 elements.add(new ParquetSchemaElement(name, null, null, isRoot ? null : OPTIONAL, 1, CONVERTED_MAP, null, null, null, null)); 341 elements.add(new ParquetSchemaElement("key_value", null, null, REPEATED, 2, null, null, null, null, null)); 342 elements.add(new ParquetSchemaElement("key", TYPE_BYTE_ARRAY, null, REQUIRED, null, CONVERTED_UTF8, LOGICAL_TYPE_STRING, null, null, mapPath + ".key_value.key")); 343 addSchemaElements(elements, vt, "value", mapPath + ".key_value", false, null, typesInProgress); 344 } 345 346 private void addLeafSchema(List<ParquetSchemaElement> elements, ClassMeta<?> cm, String name, String parentPath, boolean isRoot) { 347 int repetition = isRoot ? REQUIRED : OPTIONAL; 348 String path = parentPath != null ? parentPath + "." + name : name; 349 350 if (cm.isBoolean()) { 351 elements.add(new ParquetSchemaElement(name, TYPE_BOOLEAN, null, repetition, null, null, null, null, null, path)); 352 } else if (cm.is(byte.class) || cm.is(Byte.class) || cm.is(short.class) || cm.is(Short.class) || cm.is(int.class) || cm.is(Integer.class)) { 353 Integer ct; 354 if (cm.is(byte.class) || cm.is(Byte.class)) 355 ct = CONVERTED_INT_8; 356 else if (cm.is(short.class) || cm.is(Short.class)) 357 ct = CONVERTED_INT_16; 358 else // int/Integer — the only remaining type the enclosing guard admits. 359 ct = CONVERTED_INT_32; 360 elements.add(new ParquetSchemaElement(name, TYPE_INT32, null, repetition, null, ct, null, null, null, path)); 361 } else if (cm.is(Number.class)) { 362 // A statically-typed Number leaf has no fixed physical width. Mapping it to INT32 silently 363 // truncates values above 2^31 (e.g. 5_000_000_000L -> 705032704) — GAP-12. Default to INT64, which 364 // is lossless for every integral Number and keeps the wire value integral. Integral matters for 365 // round-trip correctness: an ObjectSwap whose swap type is exactly Number (e.g. 366 // enum-as-ordinal) round-trips through String.valueOf(...), and "0" parses where "0.0" would not. 367 // When a concrete runtime sample is available (addBeanSchema), the column is re-typed to the actual 368 // subtype (INT32/INT64/DOUBLE) so fractional values are preserved too. BigInteger/BigDecimal have 369 // their own lossless string mappings and never reach this branch. 370 elements.add(new ParquetSchemaElement(name, TYPE_INT64, null, repetition, null, CONVERTED_INT_64, null, null, null, path)); 371 } else if (cm.is(long.class) || cm.is(Long.class)) { 372 elements.add(new ParquetSchemaElement(name, TYPE_INT64, null, repetition, null, CONVERTED_INT_64, null, null, null, path)); 373 } else if (cm.is(float.class) || cm.is(Float.class)) { 374 elements.add(new ParquetSchemaElement(name, TYPE_FLOAT, null, repetition, null, null, null, null, null, path)); 375 } else if (cm.is(double.class) || cm.is(Double.class)) { 376 elements.add(new ParquetSchemaElement(name, TYPE_DOUBLE, null, repetition, null, null, null, null, null, path)); 377 } else if (cm.isAssignableTo(CharSequence.class)) { // String is a CharSequence, so this covers both. 378 elements.add(new ParquetSchemaElement(name, TYPE_BYTE_ARRAY, null, repetition, null, CONVERTED_UTF8, LOGICAL_TYPE_STRING, null, null, path)); 379 } else if (cm.isEnum()) { 380 elements.add(new ParquetSchemaElement(name, TYPE_BYTE_ARRAY, null, repetition, null, CONVERTED_ENUM, null, null, null, path)); 381 } else if (cm.isByteArray()) { 382 elements.add(new ParquetSchemaElement(name, TYPE_BYTE_ARRAY, null, repetition, null, null, null, null, null, path)); 383 } else if (nativeLogicalTypes && cm.inner() == LocalDate.class) { 384 // DATE (GAP-10): days since epoch as INT32. Match on inner() (the concrete class resolved from a 385 // runtime sample) since a swapped property ClassMeta reports neither is(LocalDate) nor isDateOrCalendarOrTemporal. 386 elements.add(new ParquetSchemaElement(name, TYPE_INT32, null, repetition, null, CONVERTED_DATE, LOGICAL_TYPE_DATE, null, null, path)); 387 } else if (nativeLogicalTypes && (cm.inner() == LocalTime.class || cm.inner() == OffsetTime.class)) { 388 // TIME (GAP-10): micros since midnight as INT64 (TIME_MICROS). 389 elements.add(new ParquetSchemaElement(name, TYPE_INT64, null, repetition, null, CONVERTED_TIME_MICROS, LOGICAL_TYPE_TIME, null, null, path)); 390 } else if (nativeLogicalTypes && cm.isDateOrCalendarOrTemporal()) { 391 // TIMESTAMP (GAP-10): micros since epoch as INT64 (TIMESTAMP_MICROS) — sub-millisecond precise. 392 elements.add(new ParquetSchemaElement(name, TYPE_INT64, null, repetition, null, CONVERTED_TIMESTAMP_MICROS, LOGICAL_TYPE_TIMESTAMP, null, null, path)); 393 } else if (cm.isDateOrCalendarOrTemporal() && writeDatesAsTimestamp) { 394 elements.add(new ParquetSchemaElement(name, TYPE_INT64, null, repetition, null, CONVERTED_TIMESTAMP_MILLIS, LOGICAL_TYPE_TIMESTAMP, null, null, path)); 395 } else if (cm.isDateOrCalendarOrTemporal()) { 396 elements.add(new ParquetSchemaElement(name, TYPE_BYTE_ARRAY, null, repetition, null, CONVERTED_UTF8, LOGICAL_TYPE_STRING, null, null, path)); 397 } else if (cm.isDuration()) { 398 elements.add(new ParquetSchemaElement(name, TYPE_BYTE_ARRAY, null, repetition, null, CONVERTED_UTF8, LOGICAL_TYPE_STRING, null, null, path)); 399 } else if (nativeLogicalTypes && (cm.is(java.math.BigDecimal.class) || cm.is(java.math.BigInteger.class))) { 400 // DECIMAL (GAP-9): unscaled value as INT64 with scale/precision in the schema. Scale is fixed at 401 // DECIMAL_SCALE; values requiring more fractional digits are rounded (HALF_UP) on write. 402 elements.add(new ParquetSchemaElement(name, TYPE_INT64, null, repetition, null, CONVERTED_DECIMAL, LOGICAL_TYPE_DECIMAL, DECIMAL_SCALE, DECIMAL_PRECISION, path)); 403 } else if (cm.isAssignableTo(UUID.class)) { 404 elements.add(new ParquetSchemaElement(name, TYPE_FIXED_LEN_BYTE_ARRAY, 16, repetition, null, null, LOGICAL_TYPE_UUID, null, null, path)); 405 } else { 406 elements.add(new ParquetSchemaElement(name, TYPE_BYTE_ARRAY, null, repetition, null, CONVERTED_UTF8, LOGICAL_TYPE_STRING, null, null, path)); 407 } 408 } 409 410 /** 411 * Returns leaf (primitive) schema elements suitable for column chunk writing. 412 * 413 * @param schema The full schema from buildSchema. 414 * @return List of leaf elements with non-null paths. 415 */ 416 public static List<ParquetSchemaElement> getLeafColumns(List<ParquetSchemaElement> schema) { 417 return schema.stream().filter(ParquetSchemaElement::isLeaf).collect(Collectors.toCollection(ArrayList::new)); 418 } 419}