This is an automated email from the ASF dual-hosted git repository. caicancai pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/calcite.git
commit 56668af6bfc9e0537dcb6389ecd78727185d1767 Author: Cancai Cai <[email protected]> AuthorDate: Fri Jun 19 15:32:39 2026 +0800 Revert "[CALCITE-7580] Remove Gandiva dependency from Arrow adapter" This reverts commit bf2b81b27d76ef3571ba676dbc7bce4ccb4a0413. --- arrow/build.gradle.kts | 1 + .../adapter/arrow/AbstractArrowEnumerator.java | 30 +-- .../adapter/arrow/ArrowDirectEnumerator.java | 31 ++- .../calcite/adapter/arrow/ArrowEnumerable.java | 25 ++- .../adapter/arrow/ArrowFilterEnumerator.java | 238 +++++---------------- .../adapter/arrow/ArrowProjectEnumerator.java | 80 +++++++ .../apache/calcite/adapter/arrow/ArrowRules.java | 2 +- .../apache/calcite/adapter/arrow/ArrowTable.java | 143 ++++++++++++- .../calcite/adapter/arrow/ArrowTranslator.java | 69 ++---- .../calcite/adapter/arrow/ConditionToken.java | 51 +---- .../calcite/adapter/arrow/ArrowAdapterTest.java | 50 ----- .../calcite/adapter/arrow/ArrowDataTest.java | 29 --- .../calcite/adapter/arrow/ArrowExtension.java | 23 +- bom/build.gradle.kts | 1 + .../java/org/apache/calcite/rex/RexSimplify.java | 26 ++- .../calcite/sql/type/SqlTypeFactoryImpl.java | 2 +- .../apache/calcite/sql2rel/SqlToRelConverter.java | 34 ++- .../org/apache/calcite/rex/RexProgramTest.java | 20 ++ .../calcite/sql/type/SqlTypeFactoryTest.java | 25 +++ .../apache/calcite/test/SqlToRelConverterTest.java | 18 ++ .../apache/calcite/test/SqlToRelConverterTest.xml | 24 +++ gradle.properties | 1 + site/_docs/history.md | 5 - .../org/apache/calcite/test/SqlToRelFixture.java | 5 + 24 files changed, 525 insertions(+), 408 deletions(-) diff --git a/arrow/build.gradle.kts b/arrow/build.gradle.kts index c75a8b5f67..598aa8a879 100644 --- a/arrow/build.gradle.kts +++ b/arrow/build.gradle.kts @@ -23,6 +23,7 @@ implementation("com.google.guava:guava") implementation("org.apache.arrow:arrow-memory-netty") implementation("org.apache.arrow:arrow-vector") + implementation("org.apache.arrow.gandiva:arrow-gandiva") annotationProcessor("org.immutables:value") compileOnly("org.immutables:value-annotations") diff --git a/arrow/src/main/java/org/apache/calcite/adapter/arrow/AbstractArrowEnumerator.java b/arrow/src/main/java/org/apache/calcite/adapter/arrow/AbstractArrowEnumerator.java index 486e3f60bd..e188757b0d 100644 --- a/arrow/src/main/java/org/apache/calcite/adapter/arrow/AbstractArrowEnumerator.java +++ b/arrow/src/main/java/org/apache/calcite/adapter/arrow/AbstractArrowEnumerator.java @@ -24,7 +24,9 @@ import org.apache.arrow.vector.TimeStampVector; import org.apache.arrow.vector.ValueVector; import org.apache.arrow.vector.VectorSchemaRoot; +import org.apache.arrow.vector.VectorUnloader; import org.apache.arrow.vector.ipc.ArrowFileReader; +import org.apache.arrow.vector.ipc.message.ArrowRecordBatch; import org.apache.arrow.vector.types.TimeUnit; import org.apache.arrow.vector.types.pojo.ArrowType; @@ -49,6 +51,8 @@ abstract class AbstractArrowEnumerator implements Enumerator<Object> { this.currRowIndex = -1; } + abstract void evaluateOperator(ArrowRecordBatch arrowRecordBatch); + protected void loadNextArrowBatch() { try { final VectorSchemaRoot vsr = arrowFileReader.getVectorSchemaRoot(); @@ -56,32 +60,14 @@ protected void loadNextArrowBatch() { this.valueVectors.add(vsr.getVector(i)); } this.rowCount = vsr.getRowCount(); + VectorUnloader vectorUnloader = new VectorUnloader(vsr); + ArrowRecordBatch arrowRecordBatch = vectorUnloader.getRecordBatch(); + evaluateOperator(arrowRecordBatch); } catch (IOException e) { throw Util.toUnchecked(e); } } - /** Loads the next non-empty Arrow batch. */ - protected boolean loadNextNonEmptyArrowBatch() { - while (true) { - final boolean hasNextBatch; - try { - hasNextBatch = arrowFileReader.loadNextBatch(); - } catch (IOException e) { - throw Util.toUnchecked(e); - } - if (!hasNextBatch) { - return false; - } - currRowIndex = -1; - valueVectors.clear(); - loadNextArrowBatch(); - if (rowCount > 0) { - return true; - } - } - } - @Override public Object current() { if (fields.size() == 1) { return getValue(this.valueVectors.get(0), currRowIndex); @@ -99,7 +85,7 @@ protected boolean loadNextNonEmptyArrowBatch() { * <p>For {@link TimeStampVector}, converts the raw value to * milliseconds since epoch, which is the representation used by * Calcite's Enumerable runtime for TIMESTAMP types. */ - protected static Object getValue(ValueVector vector, int index) { + private static Object getValue(ValueVector vector, int index) { if (vector instanceof TimeStampVector) { if (vector.isNull(index)) { return null; diff --git a/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowDirectEnumerator.java b/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowDirectEnumerator.java index 787ffd88d9..0cdec7baeb 100644 --- a/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowDirectEnumerator.java +++ b/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowDirectEnumerator.java @@ -17,11 +17,19 @@ package org.apache.calcite.adapter.arrow; import org.apache.calcite.util.ImmutableIntList; +import org.apache.calcite.util.Util; import org.apache.arrow.vector.ipc.ArrowFileReader; +import org.apache.arrow.vector.ipc.message.ArrowRecordBatch; + +import java.io.IOException; /** * Enumerator that reads projected Arrow value-vectors directly. + * + * <p>This path is used for identity projections that Gandiva cannot project + * through the existing {@code Projector} path, such as Arrow binary vectors. + * It is not a replacement for Gandiva expression evaluation. */ class ArrowDirectEnumerator extends AbstractArrowEnumerator { private final Runnable onClose; @@ -32,14 +40,27 @@ class ArrowDirectEnumerator extends AbstractArrowEnumerator { this.onClose = onClose; } + @Override protected void evaluateOperator(ArrowRecordBatch arrowRecordBatch) { + } + @Override public boolean moveNext() { - while (currRowIndex >= rowCount - 1) { - if (!loadNextNonEmptyArrowBatch()) { - return false; + if (currRowIndex >= rowCount - 1) { + final boolean hasNextBatch; + try { + hasNextBatch = arrowFileReader.loadNextBatch(); + } catch (IOException e) { + throw Util.toUnchecked(e); + } + if (hasNextBatch) { + currRowIndex = 0; + this.valueVectors.clear(); + loadNextArrowBatch(); } + return hasNextBatch; + } else { + currRowIndex++; + return true; } - currRowIndex++; - return true; } @Override public void close() { diff --git a/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowEnumerable.java b/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowEnumerable.java index b9c0c4171e..84ed5997aa 100644 --- a/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowEnumerable.java +++ b/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowEnumerable.java @@ -21,10 +21,11 @@ import org.apache.calcite.util.ImmutableIntList; import org.apache.calcite.util.Util; +import org.apache.arrow.gandiva.evaluator.Filter; +import org.apache.arrow.gandiva.evaluator.Projector; import org.apache.arrow.vector.ipc.ArrowFileReader; -import org.apache.arrow.vector.types.pojo.Schema; -import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Enumerable that reads from Arrow value-vectors. @@ -32,24 +33,28 @@ class ArrowEnumerable extends AbstractEnumerable<Object> { private final ArrowFileReader arrowFileReader; private final ImmutableIntList fields; - private final List<List<List<String>>> conditions; - private final Schema schema; + private final @Nullable Projector projector; + private final @Nullable Filter filter; private final Runnable onClose; ArrowEnumerable(ArrowFileReader arrowFileReader, ImmutableIntList fields, - List<List<List<String>>> conditions, Schema schema, Runnable onClose) { + @Nullable Projector projector, @Nullable Filter filter, + Runnable onClose) { this.arrowFileReader = arrowFileReader; - this.conditions = conditions; - this.schema = schema; + this.projector = projector; + this.filter = filter; this.fields = fields; this.onClose = onClose; } @Override public Enumerator<Object> enumerator() { try { - if (!conditions.isEmpty()) { - return new ArrowFilterEnumerator(arrowFileReader, fields, - conditions, schema, onClose); + if (projector != null) { + return new ArrowProjectEnumerator(arrowFileReader, fields, projector, + onClose); + } else if (filter != null) { + return new ArrowFilterEnumerator(arrowFileReader, fields, filter, + onClose); } // No projector and no filter means the query is an identity projection // that should read selected value-vectors directly. diff --git a/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowFilterEnumerator.java b/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowFilterEnumerator.java index 2f154cc6ef..5eddec2249 100644 --- a/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowFilterEnumerator.java +++ b/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowFilterEnumerator.java @@ -19,215 +19,91 @@ import org.apache.calcite.util.ImmutableIntList; import org.apache.calcite.util.Util; -import org.apache.arrow.vector.ValueVector; -import org.apache.arrow.vector.VectorSchemaRoot; +import org.apache.arrow.gandiva.evaluator.Filter; +import org.apache.arrow.gandiva.evaluator.SelectionVector; +import org.apache.arrow.gandiva.evaluator.SelectionVectorInt16; +import org.apache.arrow.gandiva.exceptions.GandivaException; +import org.apache.arrow.memory.ArrowBuf; +import org.apache.arrow.memory.BufferAllocator; +import org.apache.arrow.memory.RootAllocator; import org.apache.arrow.vector.ipc.ArrowFileReader; -import org.apache.arrow.vector.types.pojo.Field; -import org.apache.arrow.vector.types.pojo.Schema; +import org.apache.arrow.vector.ipc.message.ArrowRecordBatch; + +import org.checkerframework.checker.nullness.qual.Nullable; import java.io.IOException; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; import static java.util.Objects.requireNonNull; /** - * Enumerator that evaluates Arrow filter tokens in Java. + * Enumerator that reads from a filtered collection of Arrow value-vectors. */ class ArrowFilterEnumerator extends AbstractArrowEnumerator { - private final List<List<ConditionToken>> conditions; - private final Schema schema; + private final BufferAllocator allocator; + private final Filter filter; + private @Nullable ArrowBuf buf; + private @Nullable SelectionVector selectionVector; + private int selectionVectorIndex; + private final Runnable onClose; - private final List<ValueVector> filterVectors; - private final Map<String, Pattern> likePatterns; - ArrowFilterEnumerator(ArrowFileReader arrowFileReader, - ImmutableIntList fields, List<List<List<String>>> conditions, - Schema schema, Runnable onClose) { + ArrowFilterEnumerator(ArrowFileReader arrowFileReader, ImmutableIntList fields, + Filter filter, Runnable onClose) { super(arrowFileReader, fields); - this.conditions = toConditionTokens(conditions); - this.schema = schema; + this.allocator = new RootAllocator(Long.MAX_VALUE); + this.filter = filter; this.onClose = onClose; - this.filterVectors = new ArrayList<>(schema.getFields().size()); - this.likePatterns = new HashMap<>(); } - @Override protected void loadNextArrowBatch() { - super.loadNextArrowBatch(); - final VectorSchemaRoot root; + @Override void evaluateOperator(ArrowRecordBatch arrowRecordBatch) { try { - root = arrowFileReader.getVectorSchemaRoot(); - } catch (IOException e) { + this.buf = this.allocator.buffer((long) rowCount * 2); + this.selectionVector = new SelectionVectorInt16(buf); + filter.evaluate(arrowRecordBatch, selectionVector); + } catch (GandivaException e) { throw Util.toUnchecked(e); } - filterVectors.clear(); - for (int i = 0; i < schema.getFields().size(); i++) { - filterVectors.add(root.getVector(i)); - } } @Override public boolean moveNext() { - while (true) { - if (currRowIndex >= rowCount - 1) { - if (!loadNextNonEmptyArrowBatch()) { - return false; + if (selectionVector == null + || selectionVectorIndex >= selectionVector.getRecordCount()) { + boolean hasNextBatch; + while (true) { + try { + hasNextBatch = arrowFileReader.loadNextBatch(); + } catch (IOException e) { + throw Util.toUnchecked(e); } - } - currRowIndex++; - if (matches(currRowIndex)) { - return true; - } - } - } - - private boolean matches(int rowIndex) { - for (List<ConditionToken> orGroup : conditions) { - boolean any = false; - for (ConditionToken token : orGroup) { - if (matches(token, rowIndex)) { - any = true; - break; + if (hasNextBatch) { + selectionVectorIndex = 0; + this.valueVectors.clear(); + loadNextArrowBatch(); + requireNonNull(selectionVector, "selectionVector"); + if (selectionVectorIndex >= selectionVector.getRecordCount()) { + // the "filtered" batch is empty, but there may be more batches to fetch + continue; + } + currRowIndex = selectionVector.getIndex(selectionVectorIndex++); } + return hasNextBatch; } - if (!any) { - return false; - } - } - return true; - } - - private boolean matches(ConditionToken token, int rowIndex) { - final Object value = getValue(fieldVector(token.fieldName), rowIndex); - switch (token.operator) { - case IS_NULL: - return value == null; - case IS_NOT_NULL: - return value != null; - case IS_TRUE: - return Boolean.TRUE.equals(value); - case IS_FALSE: - return Boolean.FALSE.equals(value); - case IS_NOT_TRUE: - return !Boolean.TRUE.equals(value); - case IS_NOT_FALSE: - return !Boolean.FALSE.equals(value); - case EQUAL: - return value != null && compare(value, literal(token)) == 0; - case NOT_EQUAL: - return value != null && compare(value, literal(token)) != 0; - case LESS_THAN: - return value != null && compare(value, literal(token)) < 0; - case LESS_THAN_OR_EQUAL: - return value != null && compare(value, literal(token)) <= 0; - case GREATER_THAN: - return value != null && compare(value, literal(token)) > 0; - case GREATER_THAN_OR_EQUAL: - return value != null && compare(value, literal(token)) >= 0; - case LIKE: - return value != null - && like(value.toString(), requireNonNull(token.value, "value")); - default: - throw new AssertionError("Unhandled Arrow filter operator: " + token.operator); - } - } - - private ValueVector fieldVector(String fieldName) { - final Field field = schema.findField(fieldName); - final int index = schema.getFields().indexOf(field); - if (index < 0) { - throw new IllegalArgumentException("Unknown Arrow field: " + fieldName); - } - return filterVectors.get(index); - } - - private static Object literal(ConditionToken token) { - final String type = requireNonNull(token.valueType, "valueType"); - final String value = requireNonNull(token.value, "value"); - if (type.startsWith("decimal")) { - return new BigDecimal(value); - } else if (type.equals("integer")) { - return Integer.valueOf(value); - } else if (type.equals("long")) { - return Long.valueOf(value); - } else if (type.equals("float")) { - return Float.valueOf(value); - } else if (type.equals("double")) { - return Double.valueOf(value); - } else if (type.equals("string")) { - return unquote(value); - } - throw new UnsupportedOperationException("Unsupported literal type: " + type); - } - - private static int compare(Object left, Object right) { - if (left instanceof BigDecimal || right instanceof BigDecimal) { - return toBigDecimal(left).compareTo(toBigDecimal(right)); - } - if (left instanceof Number && right instanceof Number) { - return Double.compare(((Number) left).doubleValue(), - ((Number) right).doubleValue()); - } - return left.toString().compareTo(right.toString()); - } - - private static BigDecimal toBigDecimal(Object value) { - if (value instanceof BigDecimal) { - return (BigDecimal) value; - } - return new BigDecimal(value.toString()); - } - - private boolean like(String value, String pattern) { - final String unquotedPattern = unquote(pattern); - final Pattern compiledPattern = - likePatterns.computeIfAbsent(unquotedPattern, p -> { - return Pattern.compile(toRegex(p), Pattern.DOTALL); - }); - return compiledPattern.matcher(value).matches(); - } - - private static String toRegex(String pattern) { - final StringBuilder builder = new StringBuilder(); - for (int i = 0; i < pattern.length(); i++) { - final char c = pattern.charAt(i); - if (c == '%') { - builder.append(".*"); - } else if (c == '_') { - builder.append('.'); - } else { - builder.append(Pattern.quote(String.valueOf(c))); - } + } else { + currRowIndex = selectionVector.getIndex(selectionVectorIndex++); + return true; } - return builder.toString(); } - private static String unquote(String value) { - if (value.length() >= 2 && value.charAt(0) == '\'' - && value.charAt(value.length() - 1) == '\'') { - return value.substring(1, value.length() - 1).replace("''", "'"); - } - return value; - } - - private static List<List<ConditionToken>> toConditionTokens( - List<List<List<String>>> conditions) { - final List<List<ConditionToken>> result = - new ArrayList<>(conditions.size()); - for (List<List<String>> orGroup : conditions) { - final List<ConditionToken> tokens = new ArrayList<>(orGroup.size()); - for (List<String> token : orGroup) { - tokens.add(ConditionToken.fromTokenList(token)); + @Override public void close() { + try { + if (buf != null) { + buf.close(); } - result.add(tokens); + filter.close(); + } catch (GandivaException e) { + throw Util.toUnchecked(e); + } finally { + onClose.run(); } - return result; - } - - @Override public void close() { - onClose.run(); } } diff --git a/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowProjectEnumerator.java b/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowProjectEnumerator.java new file mode 100644 index 0000000000..0895f36cf1 --- /dev/null +++ b/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowProjectEnumerator.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.calcite.adapter.arrow; + +import org.apache.calcite.util.ImmutableIntList; +import org.apache.calcite.util.Util; + +import org.apache.arrow.gandiva.evaluator.Projector; +import org.apache.arrow.gandiva.exceptions.GandivaException; +import org.apache.arrow.vector.ipc.ArrowFileReader; +import org.apache.arrow.vector.ipc.message.ArrowRecordBatch; + +import java.io.IOException; + +/** + * Enumerator that reads from a projected collection of Arrow value-vectors. + */ +class ArrowProjectEnumerator extends AbstractArrowEnumerator { + private final Projector projector; + private final Runnable onClose; + + ArrowProjectEnumerator(ArrowFileReader arrowFileReader, ImmutableIntList fields, + Projector projector, Runnable onClose) { + super(arrowFileReader, fields); + this.projector = projector; + this.onClose = onClose; + } + + @Override protected void evaluateOperator(ArrowRecordBatch arrowRecordBatch) { + try { + projector.evaluate(arrowRecordBatch, valueVectors); + } catch (GandivaException e) { + throw Util.toUnchecked(e); + } + } + + @Override public boolean moveNext() { + if (currRowIndex >= rowCount - 1) { + final boolean hasNextBatch; + try { + hasNextBatch = arrowFileReader.loadNextBatch(); + } catch (IOException e) { + throw Util.toUnchecked(e); + } + if (hasNextBatch) { + currRowIndex = 0; + this.valueVectors.clear(); + loadNextArrowBatch(); + } + return hasNextBatch; + } else { + currRowIndex++; + return true; + } + } + + @Override public void close() { + try { + projector.close(); + } catch (GandivaException e) { + throw Util.toUnchecked(e); + } finally { + onClose.run(); + } + } +} diff --git a/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowRules.java b/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowRules.java index 3da1527f10..6e268d6469 100644 --- a/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowRules.java +++ b/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowRules.java @@ -100,7 +100,7 @@ RelNode convert(Filter filter) { final RelTraitSet traitSet = filter.getTraitSet().replace(ArrowRel.CONVENTION); // Expand SEARCH (e.g. IN, BETWEEN) before pushing to Arrow, - // since the Arrow adapter does not support SEARCH natively. + // since Gandiva does not support SEARCH natively. final RexNode condition = RexUtil.expandSearch(filter.getCluster().getRexBuilder(), null, filter.getCondition()); return new ArrowFilter(filter.getCluster(), traitSet, diff --git a/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowTable.java b/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowTable.java index 2585f5d156..74438efe2a 100644 --- a/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowTable.java +++ b/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowTable.java @@ -38,9 +38,17 @@ import org.apache.calcite.util.ImmutableIntList; import org.apache.calcite.util.Util; +import org.apache.arrow.gandiva.evaluator.Filter; +import org.apache.arrow.gandiva.evaluator.Projector; +import org.apache.arrow.gandiva.exceptions.GandivaException; +import org.apache.arrow.gandiva.expression.Condition; +import org.apache.arrow.gandiva.expression.ExpressionTree; +import org.apache.arrow.gandiva.expression.TreeBuilder; +import org.apache.arrow.gandiva.expression.TreeNode; import org.apache.arrow.memory.RootAllocator; import org.apache.arrow.vector.ipc.ArrowFileReader; import org.apache.arrow.vector.ipc.SeekableReadChannel; +import org.apache.arrow.vector.types.pojo.ArrowType; import org.apache.arrow.vector.types.pojo.Field; import org.apache.arrow.vector.types.pojo.Schema; @@ -50,15 +58,20 @@ import java.io.FileInputStream; import java.io.IOException; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.List; +import static java.lang.Double.parseDouble; +import static java.lang.Float.parseFloat; +import static java.lang.Integer.parseInt; +import static java.lang.Long.parseLong; import static java.util.Objects.requireNonNull; /** * Table backed by an Apache Arrow file. * - * <p>Reads data from an Arrow IPC file on disk. Projections and filters read - * directly from Arrow value-vectors. + * <p>Reads data from an Arrow IPC file on disk and supports projection + * and filter push-down via the Gandiva expression compiler. * * <p>Implements {@link TranslatableTable} so that it can be converted into * an {@link ArrowTableScan} for query planning, and {@link QueryableTable} @@ -103,6 +116,43 @@ public class ArrowTable extends AbstractTable public Enumerable<Object> query(DataContext root, ImmutableIntList fields, List<List<List<String>>> conditions) { requireNonNull(fields, "fields"); + final Projector projector; + final Filter filter; + + if (conditions.isEmpty()) { + filter = null; + projector = makeProjector(fields); + } else { + projector = null; + + final List<TreeNode> conjuncts = new ArrayList<>(conditions.size()); + for (List<List<String>> orGroup : conditions) { + final List<TreeNode> disjuncts = new ArrayList<>(orGroup.size()); + for (List<String> conditionParts : orGroup) { + disjuncts.add( + convertConditionToGandiva( + ConditionToken.fromTokenList(conditionParts))); + } + if (disjuncts.size() == 1) { + conjuncts.add(disjuncts.get(0)); + } else { + conjuncts.add(TreeBuilder.makeOr(disjuncts)); + } + } + final Condition filterCondition; + if (conjuncts.size() == 1) { + filterCondition = TreeBuilder.makeCondition(conjuncts.get(0)); + } else { + filterCondition = + TreeBuilder.makeCondition(TreeBuilder.makeAnd(conjuncts)); + } + + try { + filter = Filter.make(schema, filterCondition); + } catch (GandivaException e) { + throw Util.toUnchecked(e); + } + } FileInputStream fis = null; try { @@ -113,7 +163,7 @@ public Enumerable<Object> query(DataContext root, ImmutableIntList fields, final FileInputStream fisRef = fis; final Runnable onClose = () -> closeSilently(fisRef); fis = null; // ownership transferred to onClose - return new ArrowEnumerable(reader, fields, conditions, schema, onClose); + return new ArrowEnumerable(reader, fields, projector, filter, onClose); } catch (IOException e) { throw Util.toUnchecked(e); } finally { @@ -152,6 +202,70 @@ private static RelDataType deduceRowType(Schema schema, return builder.build(); } + private @Nullable Projector makeProjector(ImmutableIntList fields) { + if (requiresDirectVectorProjection(fields)) { + // Returning null selects ArrowEnumerable's direct vector-read path. + // Use that path because Gandiva does not support identity projection + // expressions over Arrow List and binary vectors. + return null; + } + + final List<ExpressionTree> expressionTrees = new ArrayList<>(); + for (int fieldOrdinal : fields) { + Field field = schema.getFields().get(fieldOrdinal); + TreeNode node = TreeBuilder.makeField(field); + expressionTrees.add(TreeBuilder.makeExpression(node, field)); + } + try { + return Projector.make(schema, expressionTrees); + } catch (GandivaException e) { + throw Util.toUnchecked(e); + } + } + + /** Returns whether selected fields should be projected by reading Arrow + * value-vectors directly rather than by creating a Gandiva projector. + * + * <p>CALCITE-7541 extends this direct projection path for Arrow binary vector + * families because Gandiva cannot project them through the existing identity + * projection path. Queries with filters still use Gandiva filters; this direct + * path only applies to no-filter projections. + */ + private boolean requiresDirectVectorProjection(ImmutableIntList fields) { + for (int fieldOrdinal : fields) { + switch (schema.getFields().get(fieldOrdinal).getType().getTypeID()) { + case List: + case Binary: + case LargeBinary: + case FixedSizeBinary: + return true; + default: + break; + } + } + return false; + } + + /** Converts a single {@link ConditionToken} into a Gandiva {@link TreeNode}. */ + private TreeNode convertConditionToGandiva(ConditionToken token) { + final List<TreeNode> treeNodes = new ArrayList<>(2); + treeNodes.add( + TreeBuilder.makeField(schema.getFields() + .get( + schema.getFields().indexOf( + schema.findField(token.fieldName))))); + + if (token.isBinary()) { + treeNodes.add( + makeLiteralNode( + requireNonNull(token.value, "value"), + requireNonNull(token.valueType, "valueType"))); + } + + return TreeBuilder.makeFunction( + token.operator, treeNodes, new ArrowType.Bool()); + } + /** Closes an {@link AutoCloseable} without throwing. */ private static void closeSilently(AutoCloseable closeable) { try { @@ -161,6 +275,29 @@ private static void closeSilently(AutoCloseable closeable) { } } + private static TreeNode makeLiteralNode(String literal, String type) { + if (type.startsWith("decimal")) { + String[] typeParts = + type.substring(type.indexOf('(') + 1, type.indexOf(')')).split(","); + int precision = parseInt(typeParts[0]); + int scale = parseInt(typeParts[1]); + return TreeBuilder.makeDecimalLiteral(literal, precision, scale); + } else if (type.equals("integer")) { + return TreeBuilder.makeLiteral(parseInt(literal)); + } else if (type.equals("long")) { + return TreeBuilder.makeLiteral(parseLong(literal)); + } else if (type.equals("float")) { + return TreeBuilder.makeLiteral(parseFloat(literal)); + } else if (type.equals("double")) { + return TreeBuilder.makeLiteral(parseDouble(literal)); + } else if (type.equals("string")) { + return TreeBuilder.makeStringLiteral(literal.substring(1, literal.length() - 1)); + } else { + throw new IllegalArgumentException("Invalid literal " + literal + + ", type " + type); + } + } + /** * Implementation of {@link Queryable} based on a {@link ArrowTable}. * diff --git a/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowTranslator.java b/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowTranslator.java index 2b42295981..0ec6804052 100644 --- a/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowTranslator.java +++ b/arrow/src/main/java/org/apache/calcite/adapter/arrow/ArrowTranslator.java @@ -35,26 +35,13 @@ import java.util.ArrayList; import java.util.List; -import static org.apache.calcite.adapter.arrow.ConditionToken.Operator.EQUAL; -import static org.apache.calcite.adapter.arrow.ConditionToken.Operator.GREATER_THAN; -import static org.apache.calcite.adapter.arrow.ConditionToken.Operator.GREATER_THAN_OR_EQUAL; -import static org.apache.calcite.adapter.arrow.ConditionToken.Operator.IS_FALSE; -import static org.apache.calcite.adapter.arrow.ConditionToken.Operator.IS_NOT_FALSE; -import static org.apache.calcite.adapter.arrow.ConditionToken.Operator.IS_NOT_NULL; -import static org.apache.calcite.adapter.arrow.ConditionToken.Operator.IS_NOT_TRUE; -import static org.apache.calcite.adapter.arrow.ConditionToken.Operator.IS_NULL; -import static org.apache.calcite.adapter.arrow.ConditionToken.Operator.IS_TRUE; -import static org.apache.calcite.adapter.arrow.ConditionToken.Operator.LESS_THAN; -import static org.apache.calcite.adapter.arrow.ConditionToken.Operator.LESS_THAN_OR_EQUAL; -import static org.apache.calcite.adapter.arrow.ConditionToken.Operator.LIKE; -import static org.apache.calcite.adapter.arrow.ConditionToken.Operator.NOT_EQUAL; import static org.apache.calcite.util.DateTimeStringUtils.ISO_DATETIME_FRACTIONAL_SECOND_FORMAT; import static org.apache.calcite.util.DateTimeStringUtils.getDateFormatter; import static java.util.Objects.requireNonNull; /** - * Translates a {@link RexNode} expression to Arrow predicate tokens. + * Translates a {@link RexNode} expression to Gandiva predicate tokens. */ class ArrowTranslator { final RexBuilder rexBuilder; @@ -78,7 +65,7 @@ public static ArrowTranslator create(RexBuilder rexBuilder, * * <p>If exceeded, {@link RexUtil#toCnf(RexBuilder, int, RexNode)} returns * the original expression unchanged, which may cause the subsequent - * translation to Arrow predicates to fail with an + * translation to Gandiva predicates to fail with an * {@link UnsupportedOperationException}. When invoked by the Arrow adapter * module, the exception is caught and the plan falls back to * an Enumerable convention. */ @@ -133,32 +120,32 @@ private static Object literalValue(RexLiteral literal) { private ConditionToken translateMatch2(RexNode node) { switch (node.getKind()) { case EQUALS: - return translateBinary(EQUAL, EQUAL, (RexCall) node); + return translateBinary("equal", "=", (RexCall) node); case NOT_EQUALS: - return translateBinary(NOT_EQUAL, NOT_EQUAL, (RexCall) node); + return translateBinary("not_equal", "<>", (RexCall) node); case LESS_THAN: - return translateBinary(LESS_THAN, GREATER_THAN, (RexCall) node); + return translateBinary("less_than", ">", (RexCall) node); case LESS_THAN_OR_EQUAL: - return translateBinary(LESS_THAN_OR_EQUAL, GREATER_THAN_OR_EQUAL, (RexCall) node); + return translateBinary("less_than_or_equal_to", ">=", (RexCall) node); case GREATER_THAN: - return translateBinary(GREATER_THAN, LESS_THAN, (RexCall) node); + return translateBinary("greater_than", "<", (RexCall) node); case GREATER_THAN_OR_EQUAL: - return translateBinary(GREATER_THAN_OR_EQUAL, LESS_THAN_OR_EQUAL, (RexCall) node); + return translateBinary("greater_than_or_equal_to", "<=", (RexCall) node); case IS_NULL: - return translateUnary(IS_NULL, (RexCall) node); + return translateUnary("isnull", (RexCall) node); case IS_NOT_NULL: - return translateUnary(IS_NOT_NULL, (RexCall) node); + return translateUnary("isnotnull", (RexCall) node); case IS_NOT_TRUE: - return translateUnary(IS_NOT_TRUE, (RexCall) node); + return translateUnary("isnottrue", (RexCall) node); case IS_NOT_FALSE: - return translateUnary(IS_NOT_FALSE, (RexCall) node); + return translateUnary("isnotfalse", (RexCall) node); case INPUT_REF: final RexInputRef inputRef = (RexInputRef) node; - return ConditionToken.unary(fieldNames.get(inputRef.getIndex()), IS_TRUE); + return ConditionToken.unary(fieldNames.get(inputRef.getIndex()), "istrue"); case NOT: - return translateUnary(IS_FALSE, (RexCall) node); + return translateUnary("isfalse", (RexCall) node); case LIKE: - return translateBinaryNoReverse(LIKE, (RexCall) node); + return translateBinary("like", null, (RexCall) node); default: throw new UnsupportedOperationException("Unsupported operator " + node); } @@ -168,8 +155,7 @@ private ConditionToken translateMatch2(RexNode node) { * Translates a call to a binary operator, reversing arguments if * necessary. */ - private ConditionToken translateBinary(ConditionToken.Operator op, - ConditionToken.Operator rop, RexCall call) { + private ConditionToken translateBinary(String op, String rop, RexCall call) { final RexNode left = call.operands.get(0); final RexNode right = call.operands.get(1); @Nullable ConditionToken expression = translateBinary2(op, left, right); @@ -183,21 +169,9 @@ private ConditionToken translateBinary(ConditionToken.Operator op, throw new UnsupportedOperationException("Unsupported binary operator " + call); } - /** Translates a call to a binary operator without reversing arguments. */ - private ConditionToken translateBinaryNoReverse(ConditionToken.Operator op, - RexCall call) { - final RexNode left = call.operands.get(0); - final RexNode right = call.operands.get(1); - @Nullable ConditionToken expression = translateBinary2(op, left, right); - if (expression != null) { - return expression; - } - throw new UnsupportedOperationException("Unsupported binary operator " + call); - } - /** Translates a call to a binary operator. Returns null on failure. */ - private @Nullable ConditionToken translateBinary2( - ConditionToken.Operator op, RexNode left, RexNode right) { + private @Nullable ConditionToken translateBinary2(String op, RexNode left, + RexNode right) { if (right.getKind() != SqlKind.LITERAL) { return null; } @@ -217,7 +191,7 @@ private ConditionToken translateBinaryNoReverse(ConditionToken.Operator op, /** Combines a field name, operator, and literal to produce a binary * condition token. */ - private ConditionToken translateOp2(ConditionToken.Operator op, String name, + private ConditionToken translateOp2(String op, String name, RexLiteral right) { Object value = literalValue(right); String valueString = value.toString(); @@ -235,7 +209,7 @@ private ConditionToken translateOp2(ConditionToken.Operator op, String name, } /** Translates a call to a unary operator. */ - private ConditionToken translateUnary(ConditionToken.Operator op, RexCall call) { + private ConditionToken translateUnary(String op, RexCall call) { final RexNode opNode = call.operands.get(0); @Nullable ConditionToken expression = translateUnary2(op, opNode); @@ -247,8 +221,7 @@ private ConditionToken translateUnary(ConditionToken.Operator op, RexCall call) } /** Translates a call to a unary operator. Returns null on failure. */ - private @Nullable ConditionToken translateUnary2(ConditionToken.Operator op, - RexNode opNode) { + private @Nullable ConditionToken translateUnary2(String op, RexNode opNode) { if (opNode.getKind() == SqlKind.INPUT_REF) { final RexInputRef inputRef = (RexInputRef) opNode; final String name = fieldNames.get(inputRef.getIndex()); diff --git a/arrow/src/main/java/org/apache/calcite/adapter/arrow/ConditionToken.java b/arrow/src/main/java/org/apache/calcite/adapter/arrow/ConditionToken.java index c5b690840a..44d3facea7 100644 --- a/arrow/src/main/java/org/apache/calcite/adapter/arrow/ConditionToken.java +++ b/arrow/src/main/java/org/apache/calcite/adapter/arrow/ConditionToken.java @@ -25,7 +25,7 @@ import static java.util.Objects.requireNonNull; /** - * A structured representation of a single Arrow predicate condition. + * A structured representation of a single Gandiva predicate condition. * * <p>A condition is either unary (e.g. {@code IS NULL}) or binary * (e.g. {@code =}, {@code <}). Unary conditions have a field name @@ -36,11 +36,11 @@ */ class ConditionToken { final String fieldName; - final Operator operator; + final String operator; final @Nullable String value; final @Nullable String valueType; - private ConditionToken(String fieldName, Operator operator, + private ConditionToken(String fieldName, String operator, @Nullable String value, @Nullable String valueType) { this.fieldName = requireNonNull(fieldName, "fieldName"); this.operator = requireNonNull(operator, "operator"); @@ -50,7 +50,7 @@ private ConditionToken(String fieldName, Operator operator, /** Creates a binary condition token * (e.g. {@code intField equal 12 integer}). */ - static ConditionToken binary(String fieldName, Operator operator, + static ConditionToken binary(String fieldName, String operator, String value, String valueType) { return new ConditionToken(fieldName, operator, requireNonNull(value, "value"), @@ -59,7 +59,7 @@ static ConditionToken binary(String fieldName, Operator operator, /** Creates a unary condition token * (e.g. {@code intField isnull}). */ - static ConditionToken unary(String fieldName, Operator operator) { + static ConditionToken unary(String fieldName, String operator) { return new ConditionToken(fieldName, operator, null, null); } @@ -76,55 +76,22 @@ boolean isBinary() { * binary conditions. */ List<String> toTokenList() { if (isBinary()) { - return ImmutableList.of(fieldName, operator.token, + return ImmutableList.of(fieldName, operator, requireNonNull(value, "value"), requireNonNull(valueType, "valueType")); } - return ImmutableList.of(fieldName, operator.token); + return ImmutableList.of(fieldName, operator); } /** Creates a {@code ConditionToken} from a serialized string list. */ static ConditionToken fromTokenList(List<String> tokens) { final int size = tokens.size(); if (size == 4) { - return binary(tokens.get(0), Operator.of(tokens.get(1)), + return binary(tokens.get(0), tokens.get(1), tokens.get(2), tokens.get(3)); } else if (size == 2) { - return unary(tokens.get(0), Operator.of(tokens.get(1))); + return unary(tokens.get(0), tokens.get(1)); } throw new IllegalArgumentException("Invalid condition tokens: " + tokens); } - - /** Operators supported by the Arrow adapter filter representation. */ - enum Operator { - IS_NULL("isnull"), - IS_NOT_NULL("isnotnull"), - IS_TRUE("istrue"), - IS_FALSE("isfalse"), - IS_NOT_TRUE("isnottrue"), - IS_NOT_FALSE("isnotfalse"), - EQUAL("equal"), - NOT_EQUAL("not_equal"), - LESS_THAN("less_than"), - LESS_THAN_OR_EQUAL("less_than_or_equal_to"), - GREATER_THAN("greater_than"), - GREATER_THAN_OR_EQUAL("greater_than_or_equal_to"), - LIKE("like"); - - final String token; - - Operator(String token) { - this.token = token; - } - - static Operator of(String token) { - for (Operator operator : values()) { - if (operator.token.equals(token)) { - return operator; - } - } - throw new UnsupportedOperationException( - "Unsupported Arrow filter operator: " + token); - } - } } diff --git a/arrow/src/test/java/org/apache/calcite/adapter/arrow/ArrowAdapterTest.java b/arrow/src/test/java/org/apache/calcite/adapter/arrow/ArrowAdapterTest.java index 8c9fda7f08..275bdd0be7 100644 --- a/arrow/src/test/java/org/apache/calcite/adapter/arrow/ArrowAdapterTest.java +++ b/arrow/src/test/java/org/apache/calcite/adapter/arrow/ArrowAdapterTest.java @@ -82,11 +82,6 @@ static void initializeArrowState(@TempDir Path sharedTempDir) arrowDataGenerator.writeArrowData(dataLocationFile); arrowDataGenerator.writeScottEmpData(arrowFilesDirectory); - File emptyBatchDataLocationFile = - arrowFilesDirectory.resolve("arrowemptybatch.arrow").toFile(); - ArrowDataTest emptyBatchDataGenerator = new ArrowDataTest(); - emptyBatchDataGenerator.writeArrowDataWithEmptyBatch(emptyBatchDataLocationFile); - File datatypeLocationFile = arrowFilesDirectory.resolve("arrowdatatype.arrow").toFile(); ArrowDataTest arrowtypeDataGenerator = new ArrowDataTest(); arrowtypeDataGenerator.writeArrowDataType(datatypeLocationFile); @@ -265,51 +260,6 @@ static void initializeArrowState(@TempDir Path sharedTempDir) .explainContains(plan); } - /** Test case for - * <a href="https://issues.apache.org/jira/browse/CALCITE-7580">[CALCITE-7580] - * Remove Gandiva dependency from Arrow adapter</a>. */ - @Test void testArrowProjectSkipsEmptyBatch() { - String sql = "select \"intField\", \"stringField\" from arrowemptybatch\n"; - String result = "intField=0; stringField=0\n" - + "intField=1; stringField=1\n" - + "intField=2; stringField=2\n"; - - CalciteAssert.that() - .with(arrow) - .query(sql) - .returns(result); - } - - /** Test case for - * <a href="https://issues.apache.org/jira/browse/CALCITE-7580">[CALCITE-7580] - * Remove Gandiva dependency from Arrow adapter</a>. */ - @Test void testArrowFilterSkipsEmptyBatch() { - String sql = "select \"intField\", \"stringField\"\n" - + "from arrowemptybatch\n" - + "where \"intField\" > 0"; - String result = "intField=1; stringField=1\n" - + "intField=2; stringField=2\n"; - - CalciteAssert.that() - .with(arrow) - .query(sql) - .returns(result); - } - - /** Test case for - * <a href="https://issues.apache.org/jira/browse/CALCITE-7580">[CALCITE-7580] - * Remove Gandiva dependency from Arrow adapter</a>. */ - @Test void testArrowFilterSkipsEmptyBatchWithNoMatches() { - String sql = "select \"intField\", \"stringField\"\n" - + "from arrowemptybatch\n" - + "where \"intField\" < 0"; - - CalciteAssert.that() - .with(arrow) - .query(sql) - .returns(""); - } - @Test void testArrowProjectFieldsWithIntegerFilter() { String sql = "select \"intField\", \"stringField\"\n" + "from arrowdata\n" diff --git a/arrow/src/test/java/org/apache/calcite/adapter/arrow/ArrowDataTest.java b/arrow/src/test/java/org/apache/calcite/adapter/arrow/ArrowDataTest.java index 4b9af441aa..a53cc1231a 100644 --- a/arrow/src/test/java/org/apache/calcite/adapter/arrow/ArrowDataTest.java +++ b/arrow/src/test/java/org/apache/calcite/adapter/arrow/ArrowDataTest.java @@ -301,35 +301,6 @@ public void writeArrowBinaryData(File file) throws IOException { fileOutputStream.close(); } - public void writeArrowDataWithEmptyBatch(File file) throws IOException { - Schema arrowSchema = makeArrowSchema(); - try (RootAllocator allocator = new RootAllocator(Integer.MAX_VALUE); - VectorSchemaRoot vectorSchemaRoot = - VectorSchemaRoot.create(arrowSchema, allocator); - FileOutputStream fileOutputStream = new FileOutputStream(file); - ArrowFileWriter arrowFileWriter = - new ArrowFileWriter(vectorSchemaRoot, null, - fileOutputStream.getChannel())) { - arrowFileWriter.start(); - - vectorSchemaRoot.setRowCount(0); - for (Field field : vectorSchemaRoot.getSchema().getFields()) { - vectorSchemaRoot.getVector(field.getName()).setValueCount(0); - } - arrowFileWriter.writeBatch(); - - int rowCount = 3; - vectorSchemaRoot.setRowCount(rowCount); - intField(vectorSchemaRoot.getVector("intField"), rowCount); - varCharField(vectorSchemaRoot.getVector("stringField"), rowCount); - floatField(vectorSchemaRoot.getVector("floatField"), rowCount); - longField(vectorSchemaRoot.getVector("longField"), rowCount); - arrowFileWriter.writeBatch(); - - arrowFileWriter.end(); - } - } - public void writeArrowDataType(File file) throws IOException { FileOutputStream fileOutputStream = new FileOutputStream(file); Schema arrowSchema = makeArrowDateTypeSchema(); diff --git a/arrow/src/test/java/org/apache/calcite/adapter/arrow/ArrowExtension.java b/arrow/src/test/java/org/apache/calcite/adapter/arrow/ArrowExtension.java index 4600dab100..ab1f5c2a88 100644 --- a/arrow/src/test/java/org/apache/calcite/adapter/arrow/ArrowExtension.java +++ b/arrow/src/test/java/org/apache/calcite/adapter/arrow/ArrowExtension.java @@ -18,12 +18,22 @@ import org.apache.calcite.config.CalciteSystemProperty; +import org.apache.arrow.gandiva.evaluator.Projector; +import org.apache.arrow.gandiva.exceptions.GandivaException; +import org.apache.arrow.gandiva.expression.ExpressionTree; +import org.apache.arrow.vector.types.pojo.Schema; + import org.junit.jupiter.api.extension.ConditionEvaluationResult; import org.junit.jupiter.api.extension.ExecutionCondition; import org.junit.jupiter.api.extension.ExtensionContext; +import java.util.ArrayList; +import java.util.List; + /** * JUnit5 extension to handle Arrow tests. + * + * <p>Tests will be skipped if the Gandiva library cannot be loaded on the given platform. */ class ArrowExtension implements ExecutionCondition { @@ -31,7 +41,8 @@ class ArrowExtension implements ExecutionCondition { * Whether to run this test. * * <p>Enabled by default, unless explicitly disabled from command line - * ({@code -Dcalcite.test.arrow=false}). + * ({@code -Dcalcite.test.arrow=false}) or if Gandiva library, used to implement arrow + * filtering/projection, cannot be loaded. * * @return {@code true} if the test is enabled and can run in the current environment, * {@code false} otherwise @@ -40,6 +51,16 @@ class ArrowExtension implements ExecutionCondition { final ExtensionContext context) { boolean enabled = CalciteSystemProperty.TEST_ARROW.value(); + try { + Schema emptySchema = new Schema(new ArrayList<>(), null); + List<ExpressionTree> expressions = new ArrayList<>(); + Projector.make(emptySchema, expressions); + } catch (GandivaException e) { + // this exception comes from using an empty expression, + // but the JNI library was loaded properly + } catch (UnsatisfiedLinkError e) { + enabled = false; + } if (enabled) { return ConditionEvaluationResult.enabled("Arrow tests enabled"); diff --git a/bom/build.gradle.kts b/bom/build.gradle.kts index 64cb532afd..f00ed7d8e5 100644 --- a/bom/build.gradle.kts +++ b/bom/build.gradle.kts @@ -101,6 +101,7 @@ fun DependencyConstraintHandlerScope.runtimev( apiv("org.apache.arrow:arrow-memory-netty", "arrow") apiv("org.apache.arrow:arrow-vector", "arrow") apiv("org.apache.arrow:arrow-jdbc", "arrow") + apiv("org.apache.arrow.gandiva:arrow-gandiva", "arrow-gandiva") apiv("org.apache.calcite.avatica:avatica-core", "calcite.avatica") apiv("org.apache.calcite.avatica:avatica-server", "calcite.avatica") apiv("org.apache.cassandra:cassandra-all") diff --git a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java index 4d7702295b..0ffb60454e 100644 --- a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java +++ b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java @@ -529,11 +529,14 @@ private RexNode simplifyLike(RexCall e, RexUnknownAs unknownAs) { } if (e.operands.size() == 3 && e.operands.get(2) instanceof RexLiteral) { final RexLiteral escapeLiteral = (RexLiteral) e.operands.get(2); - Character escape = requireNonNull(escapeLiteral.getValueAs(Character.class)); - e = (RexCall) rexBuilder - .makeCall(e.getParserPosition(), e.getOperator(), e.operands.get(0), - rexBuilder.makeLiteral(simplifyLikeString(likeStr, escape, '%'), - e.operands.get(1).getType(), true, true), escapeLiteral); + final String escapeStr = requireNonNull(escapeLiteral.getValueAs(String.class)); + if (escapeStr.length() == 1) { + char escape = escapeStr.charAt(0); + e = (RexCall) rexBuilder + .makeCall(e.getParserPosition(), e.getOperator(), e.operands.get(0), + rexBuilder.makeLiteral(simplifyLikeString(likeStr, escape, '%'), + e.operands.get(1).getType(), true, true), escapeLiteral); + } } } return simplifyGenericNode(e); @@ -543,7 +546,7 @@ private RexNode simplifyLike(RexCall e, RexUnknownAs unknownAs) { // string with even escapes 'AA\\\\%%__%%AA' simplify to 'AA\\__%AA' // string with odd escapes 'AA\\\\\\%%__%%AA' simplify to 'AA\\\\\\%__%AA' private String simplifyMixedWildcards(String str, char escape) { - Pattern pattern = Pattern.compile("[_%]+"); + Pattern pattern = getWildCardPattern(escape); Matcher matcher = pattern.matcher(str); StringBuilder builder = new StringBuilder(); int from = 0; @@ -567,6 +570,17 @@ && consecutiveSameCharCountBefore(str, start - 1, escape) % 2 == 1) { return builder.toString(); } + private static Pattern getWildCardPattern(char escape) { + switch (escape) { + case '%': + return Pattern.compile("_+"); + case '_': + return Pattern.compile("%+"); + default: + return Pattern.compile("[_%]+"); + } + } + // Tool method: count the number of consecutive identical characters before index private int consecutiveSameCharCountBefore(String str, int index, char escape) { int count = 0; diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java index 115b66fa21..d0ccab7dfd 100644 --- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java @@ -561,7 +561,7 @@ private static void assertBasic(SqlTypeName typeName) { } } - if (type.getSqlTypeName() == resultType.getSqlTypeName() + if (type.getSqlTypeName().getFamily() == resultType.getSqlTypeName().getFamily() && type.getSqlTypeName().allowsPrec() && type.getPrecision() != resultType.getPrecision()) { final int precision = diff --git a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java index 4d14e16937..882d423870 100644 --- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java +++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java @@ -1870,7 +1870,7 @@ public RelNode convertToSingleValueSubq( if (leftKeys.size() == 1) { SqlCall sqlCall = comparisonOp.createCall(rightVals.getParserPosition(), leftKeys.get(0), rightVals); - rexComparison = bb.convertExpression(sqlCall); + rexComparison = ensureComparisonTypes(bb.convertExpression(sqlCall)); } else { assert rightVals instanceof SqlCall; final SqlBasicCall call = (SqlBasicCall) rightVals; @@ -1880,9 +1880,10 @@ public RelNode convertToSingleValueSubq( RexUtil.composeConjunction(rexBuilder, transform( Pair.zip(leftKeys, call.getOperandList()), - pair -> bb.convertExpression( - comparisonOp.createCall(rightVals.getParserPosition(), - pair.left, pair.right)))); + pair -> ensureComparisonTypes( + bb.convertExpression( + comparisonOp.createCall(rightVals.getParserPosition(), + pair.left, pair.right))))); } comparisons.add(rexComparison); } @@ -1901,6 +1902,31 @@ public RelNode convertToSingleValueSubq( } } + /** + * Ensures that a comparison expression has matching operand types. If the + * operands have different type names, casts the right operand to match the + * left operand's type. This handles the case where type coercion is disabled + * and the IN-to-OR expansion produces comparisons with mismatched types + * (e.g., DATE = CHAR). + */ + private RexNode ensureComparisonTypes(RexNode node) { + if (validator != null && validator.config().typeCoercionEnabled()) { + return node; + } + if (node instanceof RexCall) { + final RexCall call = (RexCall) node; + if (call.operands.size() == 2) { + final RexNode left = call.operands.get(0); + final RexNode right = call.operands.get(1); + if (left.getType().getSqlTypeName() != right.getType().getSqlTypeName()) { + final RexNode castRight = rexBuilder.ensureType(left.getType(), right, true); + return rexBuilder.makeCall(call.getOperator(), left, castRight); + } + } + } + return node; + } + /** * Converts a {@link SqlNodeList} (for example an IN-list or VALUES list) * into a relational expression and produces a Rex-level sub-query that diff --git a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java index 2bdc7a1d56..ac7b5aebe6 100644 --- a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java +++ b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java @@ -4358,6 +4358,10 @@ private void checkSarg(String message, Sarg sarg, * Multiple consecutive '%' in the string matched by LIKE should simplify to a single '%'</a>, * <a href="https://issues.apache.org/jira/browse/CALCITE-7153">[CALCITE-7153] * Mixed wildcards of _ and % need to be simplified in LIKE operator</a>. + * <a href="https://issues.apache.org/jira/browse/CALCITE-7578">[CALCITE-7578] + * LIKE with empty ESCAPE might fail with StringIndexOutOfBoundsException</a>. + * <a href="https://issues.apache.org/jira/browse/CALCITE-7588">[CALCITE-7588] + * LIKE with ESCAPE symbols containing wildcards fails</a>. * */ @Test void testSimplifyLike() { final RexNode ref = input(tVarchar(true, 10), 0); @@ -4421,6 +4425,14 @@ private void checkSarg(String message, Sarg sarg, "LIKE($0, '###%%#%#%A#%%#%A%###%%', '#')"); checkSimplifyUnchanged(like(ref, literal("A"), literal("#"))); checkSimplifyUnchanged(like(ref, literal("%A"), literal("#"))); + checkSimplifyUnchanged(like(ref, literal("TE%_ST"), literal("%"))); + checkSimplifyUnchanged(like(ref, literal("TE%%ST"), literal("%"))); + checkSimplifyUnchanged(like(ref, literal("a%_b%%c"), literal("%"))); + checkSimplifyUnchanged(like(ref, literal("%_%%A%_"), literal("%"))); + // escape char equal to the '_' wildcard. '%E__S%' ESCAPE '_' is a literal '_'. + checkSimplifyUnchanged(like(ref, literal("%E__S%"), literal("_"))); + checkSimplifyUnchanged(like(ref, literal("TE_%ST"), literal("_"))); + checkSimplifyUnchanged(like(ref, literal("a_%b__c"), literal("_"))); // As above, but ref is NOT NULL final RexNode refMandatory = vVarcharNotNull(0); @@ -4451,6 +4463,14 @@ private void checkSarg(String message, Sarg sarg, // NOT(SIMILAR TO) is not optimized checkSimplifyUnchanged( not(rexBuilder.makeCall(SqlStdOperatorTable.SIMILAR_TO, ref, literal("%")))); + + try { + // Empty ESCAPE + checkSimplifyUnchanged(like(ref, literal("a"), literal(""))); + } catch (RuntimeException e) { + assertThat(e.getMessage(), + containsString("Invalid escape character ''")); + } } @Test void testSimplifyNullCheckInFilter() { diff --git a/core/src/test/java/org/apache/calcite/sql/type/SqlTypeFactoryTest.java b/core/src/test/java/org/apache/calcite/sql/type/SqlTypeFactoryTest.java index 8f5e5e4018..f0ff190d28 100644 --- a/core/src/test/java/org/apache/calcite/sql/type/SqlTypeFactoryTest.java +++ b/core/src/test/java/org/apache/calcite/sql/type/SqlTypeFactoryTest.java @@ -33,6 +33,7 @@ import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasToString; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -172,6 +173,30 @@ class SqlTypeFactoryTest { assertThat(leastRestrictive.getPrecision(), is(3)); } + /** + * Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-7567"> + * LeastRetrictiveSqlType for TIMESTAMP, TIMESTAMP_LTZ might ignore precision</a>. */ + @Test void testLeastRestrictiveForTimestampAndTimestampLtz() { + SqlTypeFixture f = new SqlTypeFixture(); + RelDataType ltz0 = + f.typeFactory.createSqlType(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE, 0); + RelDataType leastRestrictive = + f.typeFactory.leastRestrictive(Lists.newArrayList(ltz0, f.sqlTimestampPrec3)); + assertThat(leastRestrictive, is(notNullValue())); + assertThat(leastRestrictive.getPrecision(), is(3)); + } + + @Test void testLeastRestrictiveForTimestampLtzAndTimestamp() { + SqlTypeFixture f = new SqlTypeFixture(); + RelDataType ltz0 = + f.typeFactory.createSqlType(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE, 0); + RelDataType leastRestrictive = + f.typeFactory.leastRestrictive(Lists.newArrayList(f.sqlTimestampPrec3, ltz0)); + assertThat(leastRestrictive, is(notNullValue())); + assertThat(leastRestrictive.getPrecision(), is(3)); + } + @Test void testLeastRestrictiveForTimestampAndDate() { SqlTypeFixture f = new SqlTypeFixture(); RelDataType leastRestrictive = diff --git a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java index 694572683e..26000620af 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java @@ -2213,6 +2213,24 @@ void checkCorrelatedMapSubQuery(boolean expand) { sql(sql).withExpand(false).ok(); } + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-7562">[CALCITE-7562] + * SqlToRel misses CAST in case IN expression without type coercion</a>. */ + @Test void testInDateColumnWithoutTypeCoercion() { + final String sql = + "select * from emp_b where birthdate in ('2000-06-30', '2000-09-27')"; + sql(sql).withTypeCoercion(false).ok(); + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-7562">[CALCITE-7562] + * SqlToRel misses CAST in case IN expression without type coercion</a>. */ + @Test void testInDateColumnWithTypeCoercion() { + final String sql = + "select * from emp_b where birthdate in ('2000-06-30', '2000-09-27')"; + sql(sql).ok(); + } + @Test void testInValueListLong() { // Go over the default threshold of 20 to force a sub-query. final String sql = "select empno from emp where deptno in" diff --git a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml index 0bf02c9a73..71fe294d2f 100644 --- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml +++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml @@ -3305,6 +3305,30 @@ LogicalFilter(condition=[=($cor0.DEPTNO, $7)]) LogicalAggregate(group=[{0}], S=[SUM($1)], agg#1=[COUNT()]) LogicalProject(DEPTNO=[$7], SAL=[$5]) LogicalTableScan(table=[[CATALOG, SALES, EMP]]) +]]> + </Resource> + </TestCase> + <TestCase name="testInDateColumnWithTypeCoercion"> + <Resource name="sql"> + <![CDATA[select * from emp_b where birthdate in ('2000-06-30', '2000-09-27')]]> + </Resource> + <Resource name="plan"> + <![CDATA[ +LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], BIRTHDATE=[$9]) + LogicalFilter(condition=[OR(=($9, CAST('2000-06-30'):DATE NOT NULL), =($9, CAST('2000-09-27'):DATE NOT NULL))]) + LogicalTableScan(table=[[CATALOG, SALES, EMP_B]]) +]]> + </Resource> + </TestCase> + <TestCase name="testInDateColumnWithoutTypeCoercion"> + <Resource name="sql"> + <![CDATA[select * from emp_b where birthdate in ('2000-06-30', '2000-09-27')]]> + </Resource> + <Resource name="plan"> + <![CDATA[ +LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], BIRTHDATE=[$9]) + LogicalFilter(condition=[OR(=($9, CAST('2000-06-30'):DATE NOT NULL), =($9, CAST('2000-09-27'):DATE NOT NULL))]) + LogicalTableScan(table=[[CATALOG, SALES, EMP_B]]) ]]> </Resource> </TestCase> diff --git a/gradle.properties b/gradle.properties index fd82318264..eb0bd778ae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -81,6 +81,7 @@ jandex.version=3.5.3 # elasticsearch does not like asm:6.2.1+ aggdesigner-algorithm.version=6.1 apiguardian-api.version=1.1.2 +arrow-gandiva.version=15.0.0 arrow.version=15.0.0 asm.version=9.9.1 byte-buddy.version=1.18.8 diff --git a/site/_docs/history.md b/site/_docs/history.md index c72d6f78d9..15e2561d39 100644 --- a/site/_docs/history.md +++ b/site/_docs/history.md @@ -49,11 +49,6 @@ ## <a href="https://github.com/apache/calcite/releases/tag/calcite-1.43.0">1.43. #### Breaking Changes {: #breaking-1-43-0} -* [<a href="https://issues.apache.org/jira/browse/CALCITE-7580">CALCITE-7580</a>] - Remove Gandiva dependency from Arrow adapter. Arrow adapter projection and - filter evaluation now run in Java, and the `arrow-gandiva` dependency is no - longer included in the Arrow module or BOM. - #### New features {: #new-features-1-43-0} diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlToRelFixture.java b/testkit/src/main/java/org/apache/calcite/test/SqlToRelFixture.java index a6685cf952..784cb1eba1 100644 --- a/testkit/src/main/java/org/apache/calcite/test/SqlToRelFixture.java +++ b/testkit/src/main/java/org/apache/calcite/test/SqlToRelFixture.java @@ -178,6 +178,11 @@ public SqlToRelFixture withConformance(SqlConformance conformance) { .withValidatorConfig(c -> c.withConformance(conformance))); } + public SqlToRelFixture withTypeCoercion(boolean enabled) { + return withFactory(f -> + f.withValidatorConfig(c -> c.withTypeCoercionEnabled(enabled))); + } + public SqlToRelFixture withDiffRepos(DiffRepository diffRepos) { return new SqlToRelFixture(sql, decorrelate, tester, factory, trim, expression, diffRepos);
