Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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;
Expand All @@ -36,6 +37,9 @@ public class TableFunctionScan<Row> implements Iterable<Row> {
/** */
private final RowFactory<Row> rowFactory;

/** Character columns that require Oracle-compatible empty string handling. */
private final boolean[] characterColumns;

/** */
public TableFunctionScan(
RelDataType rowType,
Expand All @@ -45,6 +49,11 @@ public TableFunctionScan(
this.rowType = rowType;
this.dataSupplier = dataSupplier;
this.rowFactory = rowFactory;

characterColumns = new boolean[rowType.getFieldCount()];

for (int i = 0; i < characterColumns.length; i++)
characterColumns[i] = SqlTypeUtil.isCharacter(rowType.getFieldList().get(i).getType());
}

/** {@inheritDoc} */
Expand All @@ -66,6 +75,22 @@ 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 character columns to {@code null}. */
private Object[] nullIfEmpty(Object[] row) {
Object[] normalizedRow = row;

for (int i = 0; i < characterColumns.length; i++) {
if (characterColumns[i] && "".equals(row[i])) {
if (normalizedRow == row)
normalizedRow = row.clone();

normalizedRow[i] = null;
}
}

return normalizedRow;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,64 @@

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 {
/** */
public IgniteRexBuilder(RelDataTypeFactory typeFactory) {
super(typeFactory);
}

/** {@inheritDoc} */
@Override public RexNode makeCall(SqlParserPos pos, RelDataType type, SqlOperator op, List<RexNode> exprs) {
return nullIfEmptyResult(pos, super.makeCall(pos, type, op, exprs), op);
}

/** {@inheritDoc} */
@Override public RexNode makeCall(SqlParserPos pos, SqlOperator op, List<? extends RexNode> exprs) {
return nullIfEmptyResult(pos, super.makeCall(pos, op, exprs), op);
}

/** Wraps a character expression so an empty result is represented as {@code null}. */
private RexNode nullIfEmptyResult(SqlParserPos pos, RexNode call, SqlOperator op) {
if (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));
}

/** {@inheritDoc} */
@Override public RexLiteral makeCharLiteral(NlsString str) {
// VALUES conversion can retain the original character literal after validation.
if (str.getValue().isEmpty())
return makeNullLiteral(SqlUtil.createNlsStringType(getTypeFactory(), str));

return super.makeCharLiteral(str);
}

/** {@inheritDoc} */
@Override protected RexLiteral makeLiteral(@Nullable Comparable o, RelDataType type, SqlTypeName typeName) {
if (o != null && typeName == SqlTypeName.DECIMAL) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ public class IgniteSqlFunctions {
/** */
private static final int DFLT_NUM_PRECISION = IgniteTypeSystem.INSTANCE.getDefaultPrecision(SqlTypeName.DECIMAL);

/** POSIX regular expression implementation. */
private static final SqlFunctions.PosixRegexFunction POSIX_REGEX = new SqlFunctions.PosixRegexFunction();

/**
* Default constructor.
*/
Expand All @@ -73,6 +76,11 @@ public static String toString(BigDecimal x) {
return x == null ? null : x.toPlainString();
}

/** Converts an empty character value to {@code null}. */
public static @Nullable String nullIfEmpty(@Nullable String val) {
return val == null || val.isEmpty() ? null : val;
}

/** CAST(DOUBLE AS DECIMAL). */
public static BigDecimal toBigDecimal(double val, int precision, int scale) {
return removeDefaultScale(precision, scale, toBigDecimal(BigDecimal.valueOf(val), precision, scale));
Expand Down Expand Up @@ -167,6 +175,48 @@ public static String toString(ByteString b) {
return b == null ? null : new String(b.getBytes(), Commons.typeFactory().getDefaultCharset());
}

/** 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.
*
* <p>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;
}

/** LEAST2. */
public static Object least2(Object arg0, Object arg1) {
return leastOrGreatest(true, arg0, arg1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());

Expand Down Expand Up @@ -455,16 +457,12 @@ 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);
map.put(NEGATED_POSIX_REGEX_CASE_INSENSITIVE,
NotImplementor.of(insensitiveImplementor));
map.put(NEGATED_POSIX_REGEX_CASE_SENSITIVE,
NotImplementor.of(sensitiveImplementor));
defineMethod(POSIX_REGEX_CASE_INSENSITIVE, IgniteMethod.POSIX_REGEX_CASE_INSENSITIVE.method(), NullPolicy.NONE);
defineMethod(POSIX_REGEX_CASE_SENSITIVE, IgniteMethod.POSIX_REGEX_CASE_SENSITIVE.method(), NullPolicy.NONE);
defineMethod(NEGATED_POSIX_REGEX_CASE_INSENSITIVE,
IgniteMethod.NEGATED_POSIX_REGEX_CASE_INSENSITIVE.method(), NullPolicy.NONE);
defineMethod(NEGATED_POSIX_REGEX_CASE_SENSITIVE,
IgniteMethod.NEGATED_POSIX_REGEX_CASE_SENSITIVE.method(), NullPolicy.NONE);
defineReflective(REGEXP_REPLACE_3,
BuiltInMethod.REGEXP_REPLACE3.method,
BuiltInMethod.REGEXP_REPLACE4.method,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,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;
Expand Down Expand Up @@ -831,7 +831,24 @@ public List<Expression> translateList(List<? extends RexNode> operandList,
* @return Whether expression is nullable
*/
public boolean isNullable(RexNode e) {
return e.getType().isNullable();
return SqlTypeUtil.isCharacter(e.getType()) || e.getType().isNullable();
}

/** Converts an empty result of a character expression to {@code null}. */
private Result normalizeStringResult(RexNode node, Result result) {
if (!SqlTypeUtil.isCharacter(node.getType()) || result.valueVariable.getType() != String.class)
return result;

final 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)));

final 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);
}

/** */
Expand Down Expand Up @@ -1064,7 +1081,7 @@ private static Result implementCallOperand(final RexNode operand,
final Type storageType, final RexToLixTranslator translator) {
final Type originalStorageType = translator.currentStorageType;
translator.currentStorageType = storageType;
Result operandResult = operand.accept(translator);
Result operandResult = translator.normalizeStringResult(operand, operand.accept(translator));
if (storageType != null)
operandResult = translator.toInnerStorageType(operandResult, storageType);
translator.currentStorageType = originalStorageType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -721,7 +722,16 @@ private IgniteTypeFactory typeFactory() {
return type;
}

return super.deriveType(scope, expr);
RelDataType type = super.deriveType(scope, expr);

if (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;
}

/** */
Expand Down Expand Up @@ -813,6 +823,10 @@ else if (operandTypeChecker instanceof FamilyOperandTypeChecker) {

/** {@inheritDoc} */
@Override public SqlLiteral resolveLiteral(SqlLiteral literal) {
// Replace before type inference so an empty character literal has a nullable SQL type.
if (literal.getTypeName() == SqlTypeName.CHAR && 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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -96,6 +98,22 @@ public class IgniteOwnSqlOperatorTable extends ReflectiveSqlOperatorTable {
OperandTypes.NILADIC,
SqlFunctionCategory.SYSTEM);

/** Converts an empty character 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<Strong.Policy> 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,31 @@ 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),

Expand Down
Loading
Loading