diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java index ed04f327eb3ef..5e06556716ac5 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java @@ -768,7 +768,9 @@ else if (rel instanceof Intersect) RowFactory rowFactory = ctx.rowHandler().factory(ctx.getTypeFactory(), rowType); - return new ScanNode<>(ctx, rowType, new TableFunctionScan<>(rowType, dataSupplier, rowFactory)); + boolean emptyStrIsNull = IgniteSqlSemantics.emptyStringIsNull(ctx.unwrap(IgniteSqlSemantics.class)); + + return new ScanNode<>(ctx, rowType, new TableFunctionScan<>(rowType, dataSupplier, rowFactory, emptyStrIsNull)); } /** {@inheritDoc} */ diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/TableFunctionScan.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/TableFunctionScan.java index b29f91d6a7fe8..ac0eddbcba135 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/TableFunctionScan.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/TableFunctionScan.java @@ -17,13 +17,16 @@ package org.apache.ignite.internal.processors.query.calcite.exec; +import java.util.BitSet; import java.util.Collection; import java.util.Iterator; import java.util.function.Supplier; import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.sql.type.SqlTypeUtil; import org.apache.ignite.internal.processors.query.IgniteSQLException; import org.apache.ignite.internal.processors.query.calcite.exec.RowHandler.RowFactory; import org.apache.ignite.internal.util.typedef.F; +import org.jetbrains.annotations.Nullable; /** */ public class TableFunctionScan implements Iterable { @@ -36,15 +39,30 @@ public class TableFunctionScan implements Iterable { /** */ private final RowFactory rowFactory; + /** */ + private final @Nullable BitSet strCols; + /** */ public TableFunctionScan( RelDataType rowType, Supplier> dataSupplier, - RowFactory rowFactory + RowFactory rowFactory, + boolean emptyStringIsNull ) { this.rowType = rowType; this.dataSupplier = dataSupplier; this.rowFactory = rowFactory; + + if (emptyStringIsNull) { + strCols = new BitSet(rowType.getFieldCount()); + + for (int i = 0; i < rowType.getFieldCount(); i++) { + if (SqlTypeUtil.isCharacter(rowType.getFieldList().get(i).getType())) + strCols.set(i); + } + } + else + strCols = null; } /** {@inheritDoc} */ @@ -66,6 +84,27 @@ private Row convertToRow(Object rowContainer) { + "] doesn't match defined columns number [" + rowType.getFieldCount() + "]."); } - return rowFactory.create(rowArr); + return rowFactory.create(nullIfEmpty(rowArr)); + } + + /** Converts empty strings returned for string columns to {@code null}. */ + private Object[] nullIfEmpty(Object[] row) { + if (strCols == null) + return row; + + Object[] res = row; + + for (int i = strCols.nextSetBit(0); i >= 0; i = strCols.nextSetBit(i + 1)) { + Object val = row[i]; + + if (val instanceof String && ((String)val).isEmpty()) { + if (res == row) + res = row.clone(); + + res[i] = null; + } + } + + return res; } } diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactoryImpl.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactoryImpl.java index 4af8a6bda038f..55f45763f4b67 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactoryImpl.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactoryImpl.java @@ -72,6 +72,7 @@ import org.apache.ignite.internal.processors.query.calcite.exec.exp.agg.AccumulatorWrapper; import org.apache.ignite.internal.processors.query.calcite.exec.exp.agg.AccumulatorsFactory; import org.apache.ignite.internal.processors.query.calcite.exec.exp.agg.AggregateType; +import org.apache.ignite.internal.processors.query.calcite.prepare.IgniteSqlSemantics; import org.apache.ignite.internal.processors.query.calcite.prepare.bounds.ExactBounds; import org.apache.ignite.internal.processors.query.calcite.prepare.bounds.MultiBounds; import org.apache.ignite.internal.processors.query.calcite.prepare.bounds.RangeBounds; @@ -107,6 +108,9 @@ public class ExpressionFactoryImpl implements ExpressionFactory { /** */ private final RexBuilder rexBuilder; + /** */ + private final boolean emptyStrIsNull; + /** */ private static final RelDataType EMPTY_TYPE = new RelDataTypeFactory.Builder(Commons.typeFactory()).build(); @@ -130,6 +134,8 @@ public ExpressionFactoryImpl( this.typeFactory = typeFactory; this.conformance = conformance; this.rexBuilder = rexBuilder; + + emptyStrIsNull = IgniteSqlSemantics.emptyStringIsNull(ctx.unwrap(IgniteSqlSemantics.class)); } /** {@inheritDoc} */ @@ -549,7 +555,7 @@ private Scalar compile(List nodes, RelDataType type, boolean biInParams Function1 correlates = new CorrelatesBuilder(builder, ctx_, hnd_).build(nodes); List projects = RexToLixTranslator.translateProjects(program, typeFactory, conformance, - builder, null, ctx_, inputGetter, correlates); + builder, null, ctx_, inputGetter, correlates, emptyStrIsNull); assert nodes.size() == projects.size(); @@ -618,6 +624,7 @@ private String digest(List nodes, RelDataType type, boolean biParam) { } b.append(", biParam=").append(biParam); + b.append(", emptyStrIsNull=").append(emptyStrIsNull); b.append(']'); diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteRexBuilder.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteRexBuilder.java index d4e2debbcad6f..13023cd5fc5e3 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteRexBuilder.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteRexBuilder.java @@ -19,21 +19,40 @@ import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.List; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.rel.type.RelDataTypeFactory; import org.apache.calcite.rex.RexBuilder; import org.apache.calcite.rex.RexLiteral; +import org.apache.calcite.rex.RexNode; +import org.apache.calcite.sql.SqlKind; +import org.apache.calcite.sql.SqlOperator; +import org.apache.calcite.sql.SqlUtil; +import org.apache.calcite.sql.parser.SqlParserPos; import org.apache.calcite.sql.type.SqlTypeName; import org.apache.calcite.sql.type.SqlTypeUtil; +import org.apache.calcite.util.NlsString; import org.apache.ignite.internal.processors.query.IgniteSQLException; import org.apache.ignite.internal.processors.query.calcite.util.TypeUtils; import org.jetbrains.annotations.Nullable; +import static org.apache.ignite.internal.processors.query.calcite.sql.fun.IgniteOwnSqlOperatorTable.NULL_IF_EMPTY; + /** */ public class IgniteRexBuilder extends RexBuilder { + /** */ + private final boolean emptyStrIsNull; + /** */ public IgniteRexBuilder(RelDataTypeFactory typeFactory) { + this(typeFactory, false); + } + + /** */ + public IgniteRexBuilder(RelDataTypeFactory typeFactory, boolean emptyStrIsNull) { super(typeFactory); + + this.emptyStrIsNull = emptyStrIsNull; } /** {@inheritDoc} */ @@ -56,4 +75,37 @@ public IgniteRexBuilder(RelDataTypeFactory typeFactory) { return super.makeLiteral(o, type, typeName); } + + /** {@inheritDoc} */ + @Override public RexNode makeCall(SqlParserPos pos, RelDataType type, SqlOperator op, List exprs) { + return nullIfEmptyResult(pos, super.makeCall(pos, type, op, exprs), op); + } + + /** {@inheritDoc} */ + @Override public RexNode makeCall(SqlParserPos pos, SqlOperator op, List exprs) { + return nullIfEmptyResult(pos, super.makeCall(pos, op, exprs), op); + } + + /** {@inheritDoc} */ + @Override public RexLiteral makeCharLiteral(NlsString str) { + // VALUES conversion can retain the original character literal after validation. + if (emptyStrIsNull && str.getValue().isEmpty()) + return makeNullLiteral(SqlUtil.createNlsStringType(getTypeFactory(), str)); + + return super.makeCharLiteral(str); + } + + /** Wraps a string expression so an empty result is represented as {@code null}. */ + private RexNode nullIfEmptyResult(SqlParserPos pos, RexNode call, SqlOperator op) { + if (!emptyStrIsNull || op == NULL_IF_EMPTY || op.getKind() == SqlKind.AS || op.getKind() == SqlKind.CAST + || op.getKind() == SqlKind.DESCENDING || op.getKind() == SqlKind.NULLS_FIRST + || op.getKind() == SqlKind.NULLS_LAST + || !SqlTypeUtil.isCharacter(call.getType())) { + return call; + } + + RelDataType type = getTypeFactory().createTypeWithNullability(call.getType(), true); + + return super.makeCall(pos, type, NULL_IF_EMPTY, List.of(call)); + } } diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteSqlFunctions.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteSqlFunctions.java index be9dc99330df0..9ea898f563ab8 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteSqlFunctions.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteSqlFunctions.java @@ -51,6 +51,9 @@ public class IgniteSqlFunctions { /** */ private static final int DFLT_NUM_PRECISION = IgniteTypeSystem.INSTANCE.getDefaultPrecision(SqlTypeName.DECIMAL); + /** */ + private static final SqlFunctions.PosixRegexFunction POSIX_REGEX = new SqlFunctions.PosixRegexFunction(); + /** * Default constructor. */ @@ -342,4 +345,46 @@ public static boolean neAny(Object a, Object b) { return SqlFunctions.neAny(a, b); } + + /** Converts an empty string value to {@code null}. */ + public static @Nullable String nullIfEmpty(@Nullable String s) { + return s == null || s.isEmpty() ? null : s; + } + + /** Case-sensitive POSIX regular expression match. */ + public static @Nullable Boolean posixRegexCaseSensitive(@Nullable String s, @Nullable String regex) { + return posixRegex(s, regex, true, false); + } + + /** Case-insensitive POSIX regular expression match. */ + public static @Nullable Boolean posixRegexCaseInsensitive(@Nullable String s, @Nullable String regex) { + return posixRegex(s, regex, false, false); + } + + /** Negated case-sensitive POSIX regular expression match. */ + public static @Nullable Boolean negatedPosixRegexCaseSensitive(@Nullable String s, @Nullable String regex) { + return posixRegex(s, regex, true, true); + } + + /** Negated case-insensitive POSIX regular expression match. */ + public static @Nullable Boolean negatedPosixRegexCaseInsensitive(@Nullable String s, @Nullable String regex) { + return posixRegex(s, regex, false, true); + } + + /** + * POSIX regular expression match. + * + *

The pattern is evaluated even when the source is {@code null}. This preserves an invalid-pattern error while + * the result of a valid match with a null operand remains {@code null}.

+ */ + private static @Nullable Boolean posixRegex(@Nullable String s, @Nullable String regex, boolean caseSensitive, boolean negate) { + if (regex == null) + return null; + + boolean matches = caseSensitive + ? POSIX_REGEX.posixRegexSensitive(s == null ? "" : s, regex) + : POSIX_REGEX.posixRegexInsensitive(s == null ? "" : s, regex); + + return s == null ? null : matches != negate; + } } diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/RexExecutorImpl.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/RexExecutorImpl.java index eb1297bcc592d..3cf8f81abdad5 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/RexExecutorImpl.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/RexExecutorImpl.java @@ -103,7 +103,7 @@ private static String compile( final RexProgram program = programBuilder.getProgram(); final List expressions = RexToLixTranslator.translateProjects(program, javaTypeFactory, - conformance, blockBuilder, null, root_, getter, null); + conformance, blockBuilder, null, root_, getter, null, false); blockBuilder.add( Expressions.return_(null, Expressions.newArrayInit(Object[].class, expressions))); diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/RexImpTable.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/RexImpTable.java index e3e3ce5be27cd..b0aec4fa822ef 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/RexImpTable.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/RexImpTable.java @@ -266,6 +266,7 @@ import static org.apache.ignite.internal.processors.query.calcite.sql.fun.IgniteOwnSqlOperatorTable.GREATEST2; import static org.apache.ignite.internal.processors.query.calcite.sql.fun.IgniteOwnSqlOperatorTable.LEAST2; import static org.apache.ignite.internal.processors.query.calcite.sql.fun.IgniteOwnSqlOperatorTable.NULL_BOUND; +import static org.apache.ignite.internal.processors.query.calcite.sql.fun.IgniteOwnSqlOperatorTable.NULL_IF_EMPTY; import static org.apache.ignite.internal.processors.query.calcite.sql.fun.IgniteOwnSqlOperatorTable.QUERY_ENGINE; import static org.apache.ignite.internal.processors.query.calcite.sql.fun.IgniteOwnSqlOperatorTable.SYSTEM_RANGE; import static org.apache.ignite.internal.processors.query.calcite.sql.fun.IgniteOwnSqlOperatorTable.TYPEOF; @@ -324,6 +325,7 @@ public class RexImpTable { defineMethod(SOUNDEX, BuiltInMethod.SOUNDEX.method, NullPolicy.STRICT); defineMethod(DIFFERENCE, BuiltInMethod.DIFFERENCE.method, NullPolicy.STRICT); defineMethod(REVERSE, BuiltInMethod.REVERSE.method, NullPolicy.STRICT); + defineMethod(NULL_IF_EMPTY, IgniteMethod.NULL_IF_EMPTY.method(), NullPolicy.NONE); map.put(TRIM, new TrimImplementor()); @@ -455,16 +457,21 @@ public class RexImpTable { BuiltInMethod.SIMILAR_ESCAPE.method); // POSIX REGEX - ReflectiveImplementor insensitiveImplementor = - defineReflective(POSIX_REGEX_CASE_INSENSITIVE, - BuiltInMethod.POSIX_REGEX_INSENSITIVE.method); - ReflectiveImplementor sensitiveImplementor = - defineReflective(POSIX_REGEX_CASE_SENSITIVE, - BuiltInMethod.POSIX_REGEX_SENSITIVE.method); + AbstractRexCallImplementor insensitiveImplementor = + new ReflectiveImplementor(ImmutableList.of(BuiltInMethod.POSIX_REGEX_INSENSITIVE.method)); + AbstractRexCallImplementor sensitiveImplementor = + new ReflectiveImplementor(ImmutableList.of(BuiltInMethod.POSIX_REGEX_SENSITIVE.method)); + + map.put(POSIX_REGEX_CASE_INSENSITIVE, new EmptyStringSemanticsImplementor(insensitiveImplementor, + new MethodImplementor(IgniteMethod.POSIX_REGEX_CASE_INSENSITIVE.method(), NullPolicy.NONE, false))); + map.put(POSIX_REGEX_CASE_SENSITIVE, new EmptyStringSemanticsImplementor(sensitiveImplementor, + new MethodImplementor(IgniteMethod.POSIX_REGEX_CASE_SENSITIVE.method(), NullPolicy.NONE, false))); map.put(NEGATED_POSIX_REGEX_CASE_INSENSITIVE, - NotImplementor.of(insensitiveImplementor)); + new EmptyStringSemanticsImplementor(NotImplementor.of(insensitiveImplementor), + new MethodImplementor(IgniteMethod.NEGATED_POSIX_REGEX_CASE_INSENSITIVE.method(), NullPolicy.NONE, false))); map.put(NEGATED_POSIX_REGEX_CASE_SENSITIVE, - NotImplementor.of(sensitiveImplementor)); + new EmptyStringSemanticsImplementor(NotImplementor.of(sensitiveImplementor), + new MethodImplementor(IgniteMethod.NEGATED_POSIX_REGEX_CASE_SENSITIVE.method(), NullPolicy.NONE, false))); defineReflective(REGEXP_REPLACE_3, BuiltInMethod.REGEXP_REPLACE3.method, BuiltInMethod.REGEXP_REPLACE4.method, @@ -2588,4 +2595,31 @@ public static RexCallImplementor createRexCallImplementor( } }; } + + /** Selects an expression implementation according to the empty string SQL semantics. */ + private static class EmptyStringSemanticsImplementor implements RexCallImplementor { + /** */ + private final RexCallImplementor dfltImplementor; + + /** */ + private final RexCallImplementor emptyStrIsNullImplementor; + + /** */ + private EmptyStringSemanticsImplementor(RexCallImplementor dfltImplementor, RexCallImplementor emptyStrIsNullImplementor) { + this.dfltImplementor = dfltImplementor; + this.emptyStrIsNullImplementor = emptyStrIsNullImplementor; + } + + /** {@inheritDoc} */ + @Override public RexToLixTranslator.Result implement( + RexToLixTranslator translator, + RexCall call, + List arguments + ) { + RexCallImplementor implementor = translator.emptyStringIsNull() ? emptyStrIsNullImplementor : dfltImplementor; + + return implementor.implement(translator, call, arguments); + } + } + } diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/RexToLixTranslator.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/RexToLixTranslator.java index f11b432d1f010..40336eaad5440 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/RexToLixTranslator.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/RexToLixTranslator.java @@ -76,6 +76,7 @@ import static org.apache.calcite.sql.fun.SqlStdOperatorTable.CASE; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.SEARCH; +import static org.apache.ignite.internal.processors.query.calcite.sql.fun.IgniteOwnSqlOperatorTable.NULL_IF_EMPTY; /** * Translates {@link RexNode REX expressions} to {@link Expression linq4j expressions}. @@ -105,6 +106,9 @@ public class RexToLixTranslator implements RexVisitor /** */ private final Function1 correlates; + /** */ + private final boolean emptyStrIsNull; + /** * Map from RexLiteral's variable name to its literal, which is often a ({@link ConstantExpression})) It is used in * the some {@code RexCall}'s implementors, such as {@code ExtractImplementor}. @@ -142,7 +146,9 @@ private RexToLixTranslator(RexProgram program, BlockBuilder list, RexBuilder builder, SqlConformance conformance, - Function1 correlates) { + Function1 correlates, + boolean emptyStrIsNull + ) { this.program = program; // may be null this.typeFactory = Objects.requireNonNull(typeFactory); this.conformance = Objects.requireNonNull(conformance); @@ -151,6 +157,7 @@ private RexToLixTranslator(RexProgram program, this.list = Objects.requireNonNull(list); this.builder = Objects.requireNonNull(builder); this.correlates = correlates; // may be null + this.emptyStrIsNull = emptyStrIsNull; } /** @@ -164,12 +171,14 @@ private RexToLixTranslator(RexProgram program, * @param root Root expression * @param inputGetter Generates expressions for inputs * @param correlates Provider of references to the values of correlated variables + * @param emptyStringIsNull Whether empty string is represented as {@code null} * @return Sequence of expressions, optional condition */ public static List translateProjects(RexProgram program, JavaTypeFactory typeFactory, SqlConformance conformance, BlockBuilder list, PhysType outputPhysType, Expression root, - InputGetter inputGetter, Function1 correlates) { + InputGetter inputGetter, Function1 correlates, + boolean emptyStringIsNull) { List storageTypes = null; if (outputPhysType != null) { final RelDataType rowType = outputPhysType.getRowType(); @@ -178,7 +187,7 @@ public static List translateProjects(RexProgram program, storageTypes.add(outputPhysType.getJavaFieldType(i)); } return new RexToLixTranslator(program, typeFactory, root, inputGetter, - list, new IgniteRexBuilder(typeFactory), conformance, null) + list, new IgniteRexBuilder(typeFactory, emptyStringIsNull), conformance, null, emptyStringIsNull) .setCorrelates(correlates) .translateList(program.getProjectList(), storageTypes); } @@ -206,7 +215,7 @@ Expression translate(RexNode expr, Type storageType) { Expression translate(RexNode expr, RexImpTable.NullAs nullAs, Type storageType) { currentStorageType = storageType; - final Result result = expr.accept(this); + final Result result = normalizeStringResult(expr, expr.accept(this)); final Expression translated = ConverterUtils.toInternal(result.valueVariable, storageType); assert translated != null; @@ -831,7 +840,39 @@ public List translateList(List operandList, * @return Whether expression is nullable */ public boolean isNullable(RexNode e) { - return e.getType().isNullable(); + return (emptyStrIsNull && SqlTypeUtil.isCharacter(e.getType())) || e.getType().isNullable(); + } + + /** Returns whether empty string is represented as {@code null}. */ + boolean emptyStringIsNull() { + return emptyStrIsNull; + } + + /** Converts an empty result of a string expression to {@code null}. */ + private Result normalizeStringResult(RexNode node, Result result) { + if (!emptyStrIsNull || isNullIfEmpty(node) || !SqlTypeUtil.isCharacter(node.getType()) + || result.valueVariable.getType() != String.class) { + return result; + } + + ParameterExpression valVariable = Expressions.parameter( + String.class, list.newName(result.valueVariable.name + "_null_if_empty")); + list.add(Expressions.declare(Modifier.FINAL, valVariable, + Expressions.call(IgniteMethod.NULL_IF_EMPTY.method(), result.valueVariable))); + + ParameterExpression isNullVariable = Expressions.parameter( + Boolean.TYPE, list.newName(result.isNullVariable.name + "_null_if_empty")); + list.add(Expressions.declare(Modifier.FINAL, isNullVariable, checkNull(valVariable))); + + return new Result(isNullVariable, valVariable); + } + + /** Returns whether the node explicitly converts an empty string to {@code null}. */ + private boolean isNullIfEmpty(RexNode node) { + while (node instanceof RexLocalRef) + node = deref(node); + + return node instanceof RexCall && ((RexCall)node).getOperator() == NULL_IF_EMPTY; } /** */ @@ -840,7 +881,7 @@ public RexToLixTranslator setBlock(BlockBuilder block) { return this; return new RexToLixTranslator(program, typeFactory, root, inputGetter, - block, builder, conformance, correlates); + block, builder, conformance, correlates, emptyStrIsNull); } /** */ @@ -850,7 +891,7 @@ public RexToLixTranslator setCorrelates( return this; return new RexToLixTranslator(program, typeFactory, root, inputGetter, list, - builder, conformance, correlates); + builder, conformance, correlates, emptyStrIsNull); } /** */ @@ -1050,7 +1091,7 @@ private ConstantExpression getTypedNullLiteral(RexLiteral literal) { final List operandResults = new ArrayList<>(); for (int i = 0; i < operandList.size(); i++) { final Result operandResult = - implementCallOperand(operandList.get(i), storageTypes.get(i), this); + implementCallOperand(operandList.get(i), storageTypes.get(i), this, operator != NULL_IF_EMPTY); operandResults.add(operandResult); } callOperandResultMap.put(call, operandResults); @@ -1062,9 +1103,19 @@ private ConstantExpression getTypedNullLiteral(RexLiteral literal) { /** */ private static Result implementCallOperand(final RexNode operand, final Type storageType, final RexToLixTranslator translator) { + return implementCallOperand(operand, storageType, translator, true); + } + + /** */ + private static Result implementCallOperand(final RexNode operand, final Type storageType, + final RexToLixTranslator translator, boolean normalizeStringResult) { final Type originalStorageType = translator.currentStorageType; translator.currentStorageType = storageType; Result operandResult = operand.accept(translator); + + if (normalizeStringResult) + operandResult = translator.normalizeStringResult(operand, operandResult); + if (storageType != null) operandResult = translator.toInnerStorageType(operandResult, storageType); translator.currentStorageType = originalStorageType; diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/BaseQueryContext.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/BaseQueryContext.java index 90cd1cd56f445..8520c6f82e171 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/BaseQueryContext.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/BaseQueryContext.java @@ -79,6 +79,9 @@ public final class BaseQueryContext extends AbstractQueryContext { /** */ private static final RexBuilder REX_BUILDER; + /** */ + private static final RexBuilder EMPTY_STR_IS_NULL_REX_BUILDER; + /** */ public static final RelOptCluster CLUSTER; @@ -117,6 +120,7 @@ public final class BaseQueryContext extends AbstractQueryContext { TYPE_FACTORY = new IgniteTypeFactory(typeSys); REX_BUILDER = new IgniteRexBuilder(TYPE_FACTORY); + EMPTY_STR_IS_NULL_REX_BUILDER = new IgniteRexBuilder(TYPE_FACTORY, true); CLUSTER = RelOptCluster.create(EMPTY_PLANNER, REX_BUILDER); @@ -204,7 +208,9 @@ private BaseQueryContext( typeFactory = TYPE_FACTORY; - rexBuilder = REX_BUILDER; + IgniteSqlSemantics sqlSem = unwrap(IgniteSqlSemantics.class); + + rexBuilder = IgniteSqlSemantics.emptyStringIsNull(sqlSem) ? EMPTY_STR_IS_NULL_REX_BUILDER : REX_BUILDER; } /** diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlSemantics.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlSemantics.java index 52db5be64fe13..4a03dfc17f942 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlSemantics.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlSemantics.java @@ -26,9 +26,13 @@ public final class IgniteSqlSemantics { /** */ private final RoundingMode paginationRoundingMode; + /** */ + private final boolean emptyStrIsNull; + /** */ private IgniteSqlSemantics(Builder builder) { paginationRoundingMode = builder.paginationRoundingMode; + emptyStrIsNull = builder.emptyStrIsNull; } /** Returns a new builder initialized with default settings. */ @@ -41,11 +45,26 @@ public RoundingMode paginationRoundingMode() { return paginationRoundingMode; } + /** + * Returns whether empty string in literals, parameters, SQL writes, expression results, and UDF/UDTF + * inputs and outputs are treated as {@code null}. + * + *

The setting must be identical on all cluster nodes and should only be enabled on a new cluster. Existing + * empty strings and indexes built for them may otherwise produce inconsistent query results. The setting affects + * SQL only; values written through key-value APIs must be normalized by the user. + */ + public boolean emptyStringIsNull() { + return emptyStrIsNull; + } + /** */ public static final class Builder { /** */ private RoundingMode paginationRoundingMode = IgniteMath.NUMERIC_ROUNDING_MODE; + /** */ + private boolean emptyStrIsNull; + /** */ private Builder() { // No-op. @@ -58,6 +77,20 @@ public Builder paginationRoundingMode(RoundingMode paginationRoundingMode) { return this; } + /** + * Sets whether empty string in literals, parameters, SQL writes, expression results, and UDF/UDTF + * inputs and outputs should be treated as {@code null}. + * + *

The value must be identical on all cluster nodes and should only be enabled on a new cluster. Existing + * empty strings and indexes built for them may otherwise produce inconsistent query results. The setting + * affects SQL only; values written through key-value APIs must be normalized by the user. + */ + public Builder emptyStringIsNull(boolean emptyStrIsNull) { + this.emptyStrIsNull = emptyStrIsNull; + + return this; + } + /** */ public IgniteSqlSemantics build() { return new IgniteSqlSemantics(this); @@ -68,4 +101,9 @@ public IgniteSqlSemantics build() { public static long convertPaginationValueToLong(Number value, @Nullable IgniteSqlSemantics sem) { return sem == null ? IgniteMath.convertToLongExact(value) : IgniteMath.convertToLongExact(value, sem.paginationRoundingMode()); } + + /** Returns whether empty string is treated as {@code null} by the specified SQL semantics. */ + public static boolean emptyStringIsNull(@Nullable IgniteSqlSemantics sem) { + return sem != null && sem.emptyStrIsNull; + } } diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlValidator.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlValidator.java index d7af46290f4c9..38123cfcd68c9 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlValidator.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlValidator.java @@ -61,6 +61,7 @@ import org.apache.calcite.sql.type.SqlTypeCoercionRule; import org.apache.calcite.sql.type.SqlTypeFamily; import org.apache.calcite.sql.type.SqlTypeName; +import org.apache.calcite.sql.type.SqlTypeUtil; import org.apache.calcite.sql.validate.SelectScope; import org.apache.calcite.sql.validate.SqlQualified; import org.apache.calcite.sql.validate.SqlValidator; @@ -722,7 +723,17 @@ private IgniteTypeFactory typeFactory() { return type; } - return super.deriveType(scope, expr); + RelDataType type = super.deriveType(scope, expr); + + if (IgniteSqlSemantics.emptyStringIsNull(sqlSem) + && expr instanceof SqlCall && !((SqlCall)expr).getOperator().isAggregator() + && expr.getKind() != SqlKind.AS && expr.getKind() != SqlKind.CAST + && SqlTypeUtil.isCharacter(type) && !type.isNullable()) { + type = typeFactory.createTypeWithNullability(type, true); + setValidatedNodeType(expr, type); + } + + return type; } /** */ @@ -814,6 +825,13 @@ else if (operandTypeChecker instanceof FamilyOperandTypeChecker) { /** {@inheritDoc} */ @Override public SqlLiteral resolveLiteral(SqlLiteral literal) { + // Replace it before type inference so an empty string literal has a nullable SQL type. + if (IgniteSqlSemantics.emptyStringIsNull(sqlSem) + && literal.getTypeName().getFamily() == SqlTypeFamily.CHARACTER + && literal.getValueAs(String.class).isEmpty()) { + return SqlLiteral.createNull(literal.getParserPosition()); + } + if (literal instanceof SqlNumericLiteral && literal.createSqlType(typeFactory).getSqlTypeName() == SqlTypeName.BIGINT) { BigDecimal bd = literal.getValueAs(BigDecimal.class); diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/fun/IgniteOwnSqlOperatorTable.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/fun/IgniteOwnSqlOperatorTable.java index d5a7dd434e332..34893afd2a463 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/fun/IgniteOwnSqlOperatorTable.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/fun/IgniteOwnSqlOperatorTable.java @@ -16,6 +16,8 @@ */ package org.apache.ignite.internal.processors.query.calcite.sql.fun; +import java.util.function.Supplier; +import org.apache.calcite.plan.Strong; import org.apache.calcite.sql.SqlAggFunction; import org.apache.calcite.sql.SqlFunction; import org.apache.calcite.sql.SqlFunctionCategory; @@ -96,6 +98,21 @@ public class IgniteOwnSqlOperatorTable extends ReflectiveSqlOperatorTable { OperandTypes.NILADIC, SqlFunctionCategory.SYSTEM); + /** Converts an empty string expression result to {@code null}. */ + public static final SqlFunction NULL_IF_EMPTY = new SqlFunction( + "$NULL_IF_EMPTY", + SqlKind.OTHER_FUNCTION, + ReturnTypes.ARG0_FORCE_NULLABLE, + null, + OperandTypes.CHARACTER, + SqlFunctionCategory.SYSTEM + ) { + /** {@inheritDoc} */ + @Override public Supplier getStrongPolicyInference() { + return () -> Strong.Policy.AS_IS; + } + }; + /** * Least of two arguments. Unlike LEAST, which is converted to CASE WHEN THEN END clause, this function * is natively implemented. diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/IgniteMethod.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/IgniteMethod.java index f4773275fe6cb..5970ea93f769e 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/IgniteMethod.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/IgniteMethod.java @@ -89,6 +89,21 @@ public enum IgniteMethod { /** See {@link IgniteSqlFunctions#toByteString(String)} */ STRING_TO_BYTESTRING(IgniteSqlFunctions.class, "toByteString", String.class), + /** See {@link IgniteSqlFunctions#nullIfEmpty(String)} */ + NULL_IF_EMPTY(IgniteSqlFunctions.class, "nullIfEmpty", String.class), + + /** See {@link IgniteSqlFunctions#posixRegexCaseSensitive(String, String)} */ + POSIX_REGEX_CASE_SENSITIVE(IgniteSqlFunctions.class, "posixRegexCaseSensitive", String.class, String.class), + + /** See {@link IgniteSqlFunctions#posixRegexCaseInsensitive(String, String)} */ + POSIX_REGEX_CASE_INSENSITIVE(IgniteSqlFunctions.class, "posixRegexCaseInsensitive", String.class, String.class), + + /** See {@link IgniteSqlFunctions#negatedPosixRegexCaseSensitive(String, String)} */ + NEGATED_POSIX_REGEX_CASE_SENSITIVE(IgniteSqlFunctions.class, "negatedPosixRegexCaseSensitive", String.class, String.class), + + /** See {@link IgniteSqlFunctions#negatedPosixRegexCaseInsensitive(String, String)} */ + NEGATED_POSIX_REGEX_CASE_INSENSITIVE(IgniteSqlFunctions.class, "negatedPosixRegexCaseInsensitive", String.class, String.class), + /** See {@link IgniteSqlFunctions#least2(Object, Object)} */ LEAST2(IgniteSqlFunctions.class, "least2", Object.class, Object.class), diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/RexToLixTranslatorTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/RexToLixTranslatorTest.java new file mode 100644 index 0000000000000..3ca2992654b5c --- /dev/null +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/RexToLixTranslatorTest.java @@ -0,0 +1,89 @@ +/* + * 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.ignite.internal.processors.query.calcite.exec.exp; + +import java.util.List; +import org.apache.calcite.DataContext; +import org.apache.calcite.linq4j.tree.BlockBuilder; +import org.apache.calcite.linq4j.tree.Expression; +import org.apache.calcite.linq4j.tree.Expressions; +import org.apache.calcite.linq4j.tree.ParameterExpression; +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rex.RexNode; +import org.apache.calcite.rex.RexProgram; +import org.apache.calcite.rex.RexProgramBuilder; +import org.apache.calcite.sql.fun.SqlStdOperatorTable; +import org.apache.calcite.sql.type.SqlTypeName; +import org.apache.calcite.sql.validate.SqlConformanceEnum; +import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** Tests for {@link RexToLixTranslator}. */ +public class RexToLixTranslatorTest { + /** */ + @Test + public void testEmptyStringResultIsNormalizedOnce() { + IgniteTypeFactory typeFactory = new IgniteTypeFactory(); + IgniteRexBuilder rexBuilder = new IgniteRexBuilder(typeFactory, true); + + RelDataType strType = typeFactory.createTypeWithNullability(typeFactory.createSqlType(SqlTypeName.VARCHAR), true); + RelDataType rowType = typeFactory.builder().add("VAL", strType).build(); + + RexNode input = rexBuilder.makeInputRef(strType, 0); + RexNode upper = rexBuilder.makeCall(SqlStdOperatorTable.UPPER, input); + RexNode lower = rexBuilder.makeCall(SqlStdOperatorTable.LOWER, upper); + + RexProgramBuilder programBuilder = new RexProgramBuilder(rowType, rexBuilder); + programBuilder.addProject(lower, "RES"); + RexProgram program = programBuilder.getProgram(); + + BlockBuilder block = new BlockBuilder(); + ParameterExpression inputVal = Expressions.parameter(String.class, "input"); + + List projects = RexToLixTranslator.translateProjects( + program, + typeFactory, + SqlConformanceEnum.DEFAULT, + block, + null, + DataContext.ROOT, + (builder, idx, storageType) -> inputVal, + null, + true + ); + + block.add(Expressions.return_(null, projects.get(0))); + + String code = block.toBlock().toString(); + + // One normalization for the input and one for each string function result. + assertEquals(code, 3, occurrences(code, "nullIfEmpty(")); + } + + /** Counts non-overlapping occurrences of the specified substring. */ + private static int occurrences(String str, String substr) { + int cnt = 0; + + for (int pos = 0; (pos = str.indexOf(substr, pos)) >= 0; pos += substr.length()) + cnt++; + + return cnt; + } +} diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/EmptyStringIsNullIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/EmptyStringIsNullIntegrationTest.java new file mode 100644 index 0000000000000..f9df72a1b2c64 --- /dev/null +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/EmptyStringIsNullIntegrationTest.java @@ -0,0 +1,171 @@ +/* + * 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.ignite.internal.processors.query.calcite.integration; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import org.apache.calcite.plan.Contexts; +import org.apache.calcite.tools.FrameworkConfig; +import org.apache.calcite.tools.Frameworks; +import org.apache.ignite.cache.query.annotations.QuerySqlFunction; +import org.apache.ignite.cache.query.annotations.QuerySqlTableFunction; +import org.apache.ignite.configuration.CacheConfiguration; +import org.apache.ignite.configuration.IgniteConfiguration; +import org.apache.ignite.internal.processors.query.IgniteSQLException; +import org.apache.ignite.internal.processors.query.calcite.CalciteQueryProcessor; +import org.apache.ignite.internal.processors.query.calcite.prepare.IgniteSqlSemantics; +import org.apache.ignite.plugin.AbstractTestPluginProvider; +import org.apache.ignite.plugin.PluginContext; +import org.jetbrains.annotations.Nullable; +import org.junit.Test; + +/** Tests SQL semantics that treats empty string as {@code null}. */ +public class EmptyStringIsNullIntegrationTest extends AbstractBasicIntegrationTest { + /** {@inheritDoc} */ + @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception { + return super.getConfiguration(igniteInstanceName) + .setPluginProviders(new AbstractTestPluginProvider() { + /** {@inheritDoc} */ + @Override public String name() { + return "Empty string is null semantics"; + } + + /** {@inheritDoc} */ + @Override public @Nullable T createComponent(PluginContext ctx, Class cls) { + if (FrameworkConfig.class.equals(cls)) { + FrameworkConfig cfg = Frameworks.newConfigBuilder(CalciteQueryProcessor.FRAMEWORK_CONFIG) + .context(Contexts.chain( + CalciteQueryProcessor.FRAMEWORK_CONFIG.getContext(), + Contexts.of(IgniteSqlSemantics.builder() + .emptyStringIsNull(true) + .build()))) + .build(); + + return (T)cfg; + } + + return super.createComponent(ctx, cls); + } + }); + } + + /** */ + @Test + public void testLiteralsAndComparisons() { + assertQuery("SELECT '', '' IS NULL, '' IS NOT NULL, COALESCE('', 'fallback')") + .returns(null, true, false, "fallback") + .check(); + + assertQuery("SELECT '' = '', 'value' = '', 'value' <> ''") + .returns(null, null, null) + .check(); + + assertQuery("SELECT CAST(? AS VARCHAR) IS NULL") + .withParams("") + .returns(true) + .check(); + } + + /** */ + @Test + public void testStorage() { + sql("CREATE TABLE empty_string_test(id INT PRIMARY KEY, val VARCHAR)"); + + sql("INSERT INTO empty_string_test VALUES (1, ''), (2, 'value'), (3, ?)", ""); + + assertQuery("SELECT id, val, val IS NULL FROM empty_string_test ORDER BY id") + .returns(1, null, true) + .returns(2, "value", false) + .returns(3, null, true) + .check(); + + assertQuery("SELECT id FROM empty_string_test WHERE val = '' OR val <> ''") + .resultSize(0) + .check(); + } + + /** */ + @Test + public void testNotNullConstraint() { + sql("CREATE TABLE empty_string_not_null_test(id INT PRIMARY KEY, val VARCHAR NOT NULL)"); + + assertThrows("INSERT INTO empty_string_not_null_test VALUES (1, '')", IgniteSQLException.class, + "Null value is not allowed"); + assertThrows("INSERT INTO empty_string_not_null_test VALUES (2, ?)", IgniteSQLException.class, + "Null value is not allowed", ""); + } + + /** */ + @Test + public void testExpressionAndAggregateResults() { + assertQuery("SELECT LTRIM(' '), RTRIM(' '), TRIM(' '), REPEAT('value', -1)") + .returns(null, null, null, null) + .check(); + + assertQuery("SELECT REPLACE('11', '1', ''), STRING_AGG('', '')") + .returns(null, null) + .check(); + + assertQuery("SELECT '' ~ '.*', '' ~* '.*', '' !~ '.*', '' !~* '.*', 'value' ~ ''") + .returns(null, null, null, null, null) + .check(); + + assertThrows("SELECT '' ~ '[a-z'", IgniteSQLException.class, null); + } + + /** */ + @Test + public void testUdfs() { + client.getOrCreateCache(new CacheConfiguration(DEFAULT_CACHE_NAME) + .setSqlSchema("PUBLIC") + .setSqlFunctionClasses(Functions.class)); + + assertQuery("SELECT STRINGISNULL(''), STRINGISNULL(?), STRINGISNULL(CAST(? AS VARCHAR)), EMPTYSTRING()") + .withParams("", "") + .returns(true, true, true, null) + .check(); + + assertQuery("SELECT * FROM STRINGNULLS('')") + .returns(true, null) + .check(); + } + + /** */ + public static class Functions { + /** */ + @QuerySqlFunction + public static boolean stringIsNull(String val) { + return val == null; + } + + /** */ + @QuerySqlFunction + public static String emptyString() { + return ""; + } + + /** */ + @QuerySqlTableFunction( + columnTypes = {boolean.class, String.class}, + columnNames = {"INPUT_IS_NULL", "EMPTY_RESULT"} + ) + public static Collection> stringNulls(String val) { + return List.of(Arrays.asList(val == null, "")); + } + } +} diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/FunctionsTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/FunctionsTest.java index 2e52c5a34797a..20c4906f9cd22 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/FunctionsTest.java +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/FunctionsTest.java @@ -263,6 +263,12 @@ public void testReplace() { assertQuery("SELECT REPLACE('aA', 'A', 'b')").returns("ab").check(); } + /** */ + @Test + public void testEmptyStringIsNotNullByDefault() { + assertQuery("SELECT '', '' IS NULL, LTRIM(' ')").returns("", false, "").check(); + } + /** */ @Test public void testRange() { @@ -429,6 +435,7 @@ public void testRegex() { assertQuery("SELECT 'abcd' !~* null").returns(NULL_RESULT).check(); assertQuery("SELECT null !~* null").returns(NULL_RESULT).check(); assertThrows("SELECT 'abcd' ~ '[a-z'", IgniteSQLException.class, null); + assertQuery("SELECT CAST(NULL AS VARCHAR) ~ '[a-z'").returns(NULL_RESULT).check(); } /** */ diff --git a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java index 2a2cb856731a3..21ec5915b3058 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java +++ b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java @@ -39,6 +39,7 @@ import org.apache.ignite.internal.processors.query.calcite.integration.DateTimeTest; import org.apache.ignite.internal.processors.query.calcite.integration.DistributedJoinIntegrationTest; import org.apache.ignite.internal.processors.query.calcite.integration.DynamicParametersIntegrationTest; +import org.apache.ignite.internal.processors.query.calcite.integration.EmptyStringIsNullIntegrationTest; import org.apache.ignite.internal.processors.query.calcite.integration.ExpiredEntriesIntegrationTest; import org.apache.ignite.internal.processors.query.calcite.integration.FunctionsTest; import org.apache.ignite.internal.processors.query.calcite.integration.HashSpoolIntegrationTest; @@ -197,6 +198,7 @@ SystemColumnsScanTest.class, BulkOperationDeadlockIntegrationTest.class, SelectForUpdateIntegrationTest.class, + EmptyStringIsNullIntegrationTest.class, }) public class IntegrationTestSuite { } diff --git a/modules/calcite/src/test/java/org/apache/ignite/testsuites/UtilTestSuite.java b/modules/calcite/src/test/java/org/apache/ignite/testsuites/UtilTestSuite.java index 527f0240060d6..4bc0d2786792f 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/testsuites/UtilTestSuite.java +++ b/modules/calcite/src/test/java/org/apache/ignite/testsuites/UtilTestSuite.java @@ -21,6 +21,7 @@ import org.apache.ignite.internal.processors.query.calcite.exec.ClosableIteratorsHolderTest; import org.apache.ignite.internal.processors.query.calcite.exec.KeyFilteringCursorTest; import org.apache.ignite.internal.processors.query.calcite.exec.exp.IgniteSqlFunctionsTest; +import org.apache.ignite.internal.processors.query.calcite.exec.exp.RexToLixTranslatorTest; import org.apache.ignite.internal.processors.query.calcite.exec.task.QueryBlockingTaskExecutorTest; import org.apache.ignite.internal.processors.query.calcite.exec.task.QueryTasksQueueTest; import org.apache.ignite.internal.processors.query.calcite.exec.tracker.MemoryTrackerTest; @@ -39,6 +40,7 @@ KeyFilteringCursorTest.class, QueryBlockingTaskExecutorTest.class, QueryTasksQueueTest.class, + RexToLixTranslatorTest.class, }) public class UtilTestSuite { }