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.*;
020
021import java.io.*;
022
023/**
024 * A Parquet schema element representing a field in the Parquet schema.
025 *
026 * <p>
027 * Maps to the SchemaElement struct in the
028 * <a class="doclink" href="https://github.com/apache/parquet-format/blob/master/src/main/thrift/parquet.thrift">parquet.thrift</a>
029 * specification. Used for both primitive (leaf) and group (non-leaf) nodes.
030 *
031 * <h5 class='section'>Parquet Type Enum (Type):</h5>
032 * <ul>
033 *    <li>BOOLEAN = 0</li>
034 *    <li>INT32 = 1</li>
035 *    <li>INT64 = 2</li>
036 *    <li>FLOAT = 4</li>
037 *    <li>DOUBLE = 5</li>
038 *    <li>BYTE_ARRAY = 6</li>
039 *    <li>FIXED_LEN_BYTE_ARRAY = 7</li>
040 * </ul>
041 *
042 * <h5 class='section'>Field Repetition Type:</h5>
043 * <ul>
044 *    <li>REQUIRED = 0</li>
045 *    <li>OPTIONAL = 1</li>
046 *    <li>REPEATED = 2</li>
047 * </ul>
048 *
049 * <h5 class='section'>Converted Type (common values):</h5>
050 * <ul>
051 *    <li>UTF8 = 0, ENUM = 4, DECIMAL = 5, TIMESTAMP_MILLIS = 9, INT_8 = 15, INT_16 = 16, INT_32 = 17, INT_64 = 18, MAP = 1, LIST = 3</li>
052 * </ul>
053 */
054public final class ParquetSchemaElement {
055
056   /** Parquet physical type: BOOLEAN. */
057   public static final int TYPE_BOOLEAN = 0;
058   /** Parquet physical type: INT32. */
059   public static final int TYPE_INT32 = 1;
060   /** Parquet physical type: INT64. */
061   public static final int TYPE_INT64 = 2;
062   /** Parquet physical type: INT96 (legacy 12-byte timestamp; read-only). */
063   public static final int TYPE_INT96 = 3;
064   /** Parquet physical type: FLOAT. */
065   public static final int TYPE_FLOAT = 4;
066   /** Parquet physical type: DOUBLE. */
067   public static final int TYPE_DOUBLE = 5;
068   /** Parquet physical type: BYTE_ARRAY. */
069   public static final int TYPE_BYTE_ARRAY = 6;
070   /** Parquet physical type: FIXED_LEN_BYTE_ARRAY. */
071   public static final int TYPE_FIXED_LEN_BYTE_ARRAY = 7;
072
073   /** Field repetition type: REQUIRED (parquet.thrift FieldRepetitionType). */
074   public static final int REQUIRED = 0;
075   /** Field repetition type: OPTIONAL (parquet.thrift FieldRepetitionType). */
076   public static final int OPTIONAL = 1;
077   /** Field repetition type: REPEATED (parquet.thrift FieldRepetitionType). */
078   public static final int REPEATED = 2;
079
080   /** Converted type: UTF8 (parquet.thrift ConvertedType). */
081   public static final int CONVERTED_UTF8 = 0;
082   /** Converted type: MAP. */
083   public static final int CONVERTED_MAP = 1;
084   /** Converted type: LIST. */
085   public static final int CONVERTED_LIST = 3;
086   /** Converted type: ENUM. */
087   public static final int CONVERTED_ENUM = 4;
088   /** Converted type: DECIMAL. */
089   public static final int CONVERTED_DECIMAL = 5;
090   /** Converted type: TIMESTAMP_MILLIS. */
091   public static final int CONVERTED_TIMESTAMP_MILLIS = 9;
092   /** Converted type: INT_8. */
093   public static final int CONVERTED_INT_8 = 15;
094   /** Converted type: INT_16. */
095   public static final int CONVERTED_INT_16 = 16;
096   /** Converted type: INT_32. */
097   public static final int CONVERTED_INT_32 = 17;
098   /** Converted type: INT_64. */
099   public static final int CONVERTED_INT_64 = 18;
100
101   /** LogicalType union discriminant for STRING (StringType). */
102   static final int LOGICAL_TYPE_STRING = 1;
103   /** LogicalType union discriminant for UUID (UUIDType). */
104   static final int LOGICAL_TYPE_UUID = 14;
105   /** LogicalType union discriminant for TIMESTAMP (TimestampType). */
106   static final int LOGICAL_TYPE_TIMESTAMP = 8;
107   /** LogicalType union discriminant for DECIMAL (DecimalType). */
108   static final int LOGICAL_TYPE_DECIMAL = 5;
109   /** LogicalType union discriminant for DATE (DateType). */
110   static final int LOGICAL_TYPE_DATE = 6;
111   /** LogicalType union discriminant for TIME (TimeType). */
112   static final int LOGICAL_TYPE_TIME = 7;
113
114   /** TimeUnit union discriminant: MILLIS. */
115   static final int TIME_UNIT_MILLIS = 1;
116   /** TimeUnit union discriminant: MICROS. */
117   static final int TIME_UNIT_MICROS = 2;
118   /** TimeUnit union discriminant: NANOS. */
119   static final int TIME_UNIT_NANOS = 3;
120
121   /** Converted type: DATE (days since epoch). */
122   public static final int CONVERTED_DATE = 6;
123   /** Converted type: TIME_MILLIS. */
124   public static final int CONVERTED_TIME_MILLIS = 7;
125   /** Converted type: TIMESTAMP_MICROS. */
126   public static final int CONVERTED_TIMESTAMP_MICROS = 10;
127   /** Converted type: TIME_MICROS. */
128   public static final int CONVERTED_TIME_MICROS = 8;
129
130   /** Field name. Required. */
131   public final String name;
132   /** Parquet physical type. Null for group nodes. */
133   public final Integer type;
134   /** Byte length for FIXED_LEN_BYTE_ARRAY. */
135   public final Integer typeLength;
136   /** Repetition type. Null for root. */
137   public final Integer repetitionType;
138   /** Number of child elements. Non-null for group nodes. */
139   public final Integer numChildren;
140   /** Deprecated converted type. Used for forward compatibility. */
141   public final Integer convertedType;
142   /** Logical type discriminant (1=STRING, 5=DECIMAL, 8=TIMESTAMP, 14=UUID, etc.). Null if none. */
143   public final Integer logicalType;
144   /** Scale for DECIMAL. */
145   public final Integer scale;
146   /** Precision for DECIMAL. */
147   public final Integer precision;
148   /** Path from root (e.g. "host" or "db.port") for leaf columns. */
149   public final String path;
150
151   /**
152    * Creates a schema element.
153    *
154    * @param name Field name (required).
155    * @param type Parquet physical type (null for groups).
156    * @param typeLength Byte length for FIXED_LEN_BYTE_ARRAY (null otherwise).
157    * @param repetitionType REQUIRED/OPTIONAL/REPEATED (null for root).
158    * @param numChildren Child count for groups (null for primitives).
159    * @param convertedType ConvertedType enum value (null if none).
160    * @param logicalType LogicalType union discriminant (null if none).
161    * @param scale Scale for DECIMAL (null otherwise).
162    * @param precision Precision for DECIMAL (null otherwise).
163    * @param path Dotted path for leaf columns (e.g. "db.port").
164    */
165   @SuppressWarnings({
166      "java:S107" // Constructor mirrors Parquet schema fields; refactor would obscure mapping
167   })
168   public ParquetSchemaElement(String name, Integer type, Integer typeLength, Integer repetitionType,
169         Integer numChildren, Integer convertedType, Integer logicalType, Integer scale, Integer precision,
170         String path) {
171      this.name = name;
172      this.type = type;
173      this.typeLength = typeLength;
174      this.repetitionType = repetitionType;
175      this.numChildren = numChildren;
176      this.convertedType = convertedType;
177      this.logicalType = logicalType;
178      this.scale = scale;
179      this.precision = precision;
180      this.path = or(path, name);
181   }
182
183   /**
184    * Writes this schema element to the Thrift Compact Protocol encoder.
185    *
186    * <p>
187    * When {@code emitLogicalTypes} is <jk>false</jk> (the default) only the physical type and the legacy
188    * {@code convertedType} discriminant are written, matching the historical wire shape. When
189    * <jk>true</jk>, an explicit {@code LogicalType} union (parquet.thrift field 10) is additionally
190    * emitted for ambiguous physical representations. The first-pass scope is UUID only: the one type whose
191    * round-trip currently relies on the {@code FIXED_LEN_BYTE_ARRAY} physical-type signal alone. The
192    * {@code convertedType} field is still written alongside the union so that readers that consume only
193    * the legacy field (including older Juneau versions) continue to work unchanged.
194    *
195    * @param enc The encoder.
196    * @param emitLogicalTypes If <jk>true</jk>, additionally emit the {@code LogicalType} union for UUID columns.
197    * @throws IOException If an I/O error occurs.
198    */
199   public void writeTo(ThriftCompactEncoder enc, boolean emitLogicalTypes) throws IOException {
200      enc.writeStructBegin();
201      if (type != null) {
202         enc.writeFieldBegin(ThriftCompactEncoder.I32, 1);
203         enc.writeI32(type);
204      }
205      if (typeLength != null) {
206         enc.writeFieldBegin(ThriftCompactEncoder.I32, 2);
207         enc.writeI32(typeLength);
208      }
209      if (repetitionType != null) {
210         enc.writeFieldBegin(ThriftCompactEncoder.I32, 3);
211         enc.writeI32(repetitionType);
212      }
213      enc.writeFieldBegin(ThriftCompactEncoder.BINARY, 4);
214      enc.writeString(name);
215      if (numChildren != null) {
216         enc.writeFieldBegin(ThriftCompactEncoder.I32, 5);
217         enc.writeI32(numChildren);
218      }
219      if (convertedType != null) {
220         enc.writeFieldBegin(ThriftCompactEncoder.I32, 6);
221         enc.writeI32(convertedType);
222      }
223      if (scale != null) {
224         enc.writeFieldBegin(ThriftCompactEncoder.I32, 7);
225         enc.writeI32(scale);
226      }
227      if (precision != null) {
228         enc.writeFieldBegin(ThriftCompactEncoder.I32, 8);
229         enc.writeI32(precision);
230      }
231      // Opt-in only, UUID-first scope: emit the LogicalType union (field 10) so readers can route
232      // FIXED_LEN_BYTE_ARRAY deterministically instead of relying on the "FLBA == UUID" physical-type
233      // assumption. The union member for UUID is field 14 (UUIDType, an empty struct). Other logical
234      // types (STRING/TIMESTAMP/DECIMAL) are intentionally not emitted yet — they round-trip fine via
235      // convertedType + physical type today.
236      if (emitLogicalTypes && logicalType != null && logicalType == LOGICAL_TYPE_UUID) {
237         enc.writeFieldBegin(ThriftCompactEncoder.STRUCT, 10);
238         enc.writeStructBegin();
239         enc.writeFieldBegin(ThriftCompactEncoder.STRUCT, LOGICAL_TYPE_UUID);
240         enc.writeStructBegin();
241         enc.writeStructEnd();
242         enc.writeStructEnd();
243      }
244      enc.writeStructEnd();
245   }
246
247   /**
248    * Returns true if this is a leaf (primitive) column.
249    *
250    * @return <jk>true</jk> if this element has a type and no children.
251    */
252   public boolean isLeaf() {
253      return type != null && (numChildren == null || numChildren == 0);
254   }
255}