Skip to content
Merged
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 @@ -183,6 +183,12 @@ else if ((SqlTypeUtil.isApproximateNumeric(sourceType) || SqlTypeUtil.isDecimal(
// NUMBER_TO_STRING uses java's built-in method to get the string representation of a number
return makeCall(type, PPLBuiltinOperators.NUMBER_TO_STRING, List.of(exp));
}
// VARCHAR → VARBINARY for ip/binary fields. Emit BINARY(varchar) as a placeholder
// RexCall the analytics backend adapter rewrites into a VARBINARY literal.
else if (sqlType == SqlTypeName.VARBINARY
&& sourceType.getSqlTypeName() == SqlTypeName.VARCHAR) {
return makeCall(type, PPLBuiltinOperators.BINARY, List.of(exp));
}
return super.makeCast(pos, type, exp, matchNullability, safe, format);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ public static RelDataType convertExprTypeToRelDataType(ExprType fieldType, boole
return TYPE_FACTORY.createUDT(ExprUDT.EXPR_TIME, nullable);
case TIMESTAMP:
return TYPE_FACTORY.createUDT(ExprUDT.EXPR_TIMESTAMP, nullable);
case BINARY:
return TYPE_FACTORY.createSqlType(SqlTypeName.VARBINARY, nullable);
case ARRAY:
return TYPE_FACTORY.createArrayType(
TYPE_FACTORY.createSqlType(SqlTypeName.ANY, nullable), -1);
Expand Down Expand Up @@ -225,6 +227,7 @@ public static ExprType convertSqlTypeNameToExprType(SqlTypeName sqlTypeName) {
case FLOAT, REAL -> FLOAT;
case DOUBLE, DECIMAL -> DOUBLE; // TODO the decimal is only used for literal
case CHAR, VARCHAR, MULTISET -> STRING; // call toString() for MULTISET
case VARBINARY, BINARY -> BINARY;
case BOOLEAN -> BOOLEAN;
case DATE -> DATE;
case TIME, TIME_TZ, TIME_WITH_LOCAL_TIME_ZONE -> TIME;
Expand Down Expand Up @@ -411,10 +414,34 @@ public static boolean isNumericType(RelDataType fieldType) {
}
return first;
}
// When the list has a VARBINARY column plus VARCHAR literals, treat VARBINARY
// as the common type so IN / BETWEEN can insert casts.
RelDataType varbinaryResult = leastRestrictiveVarbinaryVarchar(types);
if (varbinaryResult != null) {
return varbinaryResult;
}
}
return super.leastRestrictive(types);
}

private @Nullable RelDataType leastRestrictiveVarbinaryVarchar(List<RelDataType> types) {
Comment thread
vinaykpud marked this conversation as resolved.
boolean hasVarbinary = false;
boolean anyNullable = false;
for (RelDataType t : types) {
SqlTypeName name = t.getSqlTypeName();
if (name == SqlTypeName.VARBINARY) {
hasVarbinary = true;
} else if (name != SqlTypeName.VARCHAR && name != SqlTypeName.CHAR) {
return null;
}
anyNullable |= t.isNullable();
}
if (!hasVarbinary) {
return null;
}
return createTypeWithNullability(createSqlType(SqlTypeName.VARBINARY), anyNullable);
}

/**
* Checks if the RelDataType represents a time-based field (timestamp, date, or time). Supports
* both standard SQL time types (including TIMESTAMP, TIMESTAMP_WITH_LOCAL_TIME_ZONE, DATE, TIME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,12 @@ public static boolean hasString(List<RexNode> rexNodeList) {
(left, right) -> ExprCoreType.TIMESTAMP),
CoercionRule.of(
(left, right) -> hasString(left, right) && hasNumber(left, right),
(left, right) -> ExprCoreType.DOUBLE));
(left, right) -> ExprCoreType.DOUBLE),
// (BINARY, STRING) → BINARY: ip/binary columns compared with string literals.
// Triggers ExtendedRexBuilder.makeCast which wraps the literal with BINARY.
CoercionRule.of(
(left, right) -> hasString(left, right) && hasBinary(left, right),
(left, right) -> ExprCoreType.BINARY));

private static boolean hasString(ExprType left, ExprType right) {
return left == ExprCoreType.STRING || right == ExprCoreType.STRING;
Expand All @@ -195,6 +200,10 @@ private static boolean hasBoolean(ExprType left, ExprType right) {
return left == ExprCoreType.BOOLEAN || right == ExprCoreType.BOOLEAN;
}

private static boolean hasBinary(ExprType left, ExprType right) {
return left == ExprCoreType.BINARY || right == ExprCoreType.BINARY;
}

private record CoercionRule(
BiPredicate<ExprType, ExprType> predicate, BinaryOperator<ExprType> resolver) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
import org.opensearch.sql.expression.function.udf.condition.EarliestFunction;
import org.opensearch.sql.expression.function.udf.condition.EnhancedCoalesceFunction;
import org.opensearch.sql.expression.function.udf.condition.LatestFunction;
import org.opensearch.sql.expression.function.udf.conversion.BinaryFunction;
import org.opensearch.sql.expression.function.udf.datetime.AddSubDateFunction;
import org.opensearch.sql.expression.function.udf.datetime.CurrentFunction;
import org.opensearch.sql.expression.function.udf.datetime.DateAddSubFunction;
Expand Down Expand Up @@ -179,6 +180,9 @@ public class PPLBuiltinOperators extends ReflectiveSqlOperatorTable {
public static final SqlOperator EARLIEST = new EarliestFunction().toUDF("EARLIEST");
public static final SqlOperator LATEST = new LatestFunction().toUDF("LATEST");

// VARBINARY conversion (placeholder for ip/binary fields rewritten by analytics backend adapter)
public static final SqlOperator BINARY = new BinaryFunction().toUDF("BINARY");

// Datetime function
public static final SqlOperator TIMESTAMP = new TimestampFunction().toUDF("TIMESTAMP");
public static final SqlOperator DATE =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@
import static org.opensearch.sql.expression.function.BuiltinFunctionName.YEARWEEK;

import com.google.common.collect.ImmutableMap;
import inet.ipaddr.IPAddress;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -282,6 +283,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.apache.calcite.avatica.util.ByteString;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.rex.RexLambda;
Expand Down Expand Up @@ -311,8 +313,10 @@
import org.opensearch.sql.calcite.utils.PlanUtils;
import org.opensearch.sql.calcite.utils.UserDefinedFunctionUtils;
import org.opensearch.sql.exception.ExpressionEvaluationException;
import org.opensearch.sql.exception.SemanticCheckException;
import org.opensearch.sql.executor.QueryType;
import org.opensearch.sql.expression.function.CollectionUDF.MVIndexFunctionImp;
import org.opensearch.sql.utils.IPUtils;

public class PPLFuncImpTable {
private static final Logger logger = LogManager.getLogger(PPLFuncImpTable.class);
Expand Down Expand Up @@ -908,6 +912,29 @@ void populate() {
registerDivideFunction(DIVIDEFUNCTION);
registerOperator(SHA2, PPLBuiltinOperators.SHA2);
registerOperator(CIDRMATCH, PPLBuiltinOperators.CIDRMATCH);
// (VARBINARY, VARCHAR) overload for ip / binary columns. The lambda parses the cidr
// literal at plan time and emits AND(col >= low, col <= high) directly.
// Only literal cidrs are expanded.
register(
CIDRMATCH,
(FunctionImp2)
(builder, col, cidr) -> {
if (cidr instanceof RexLiteral lit
&& col.getType().getSqlTypeName() == SqlTypeName.VARBINARY) {
byte[][] range = parseCidrToIpv6Range(lit.getValueAs(String.class));
RelDataType varbinary =
builder.getTypeFactory().createSqlType(SqlTypeName.VARBINARY);
RexNode low = builder.makeLiteral(new ByteString(range[0]), varbinary, false);
RexNode high = builder.makeLiteral(new ByteString(range[1]), varbinary, false);
// makeCall(AND, ...) auto-flattens at construction, so no Filter.isFlat issue.
return builder.makeCall(
SqlStdOperatorTable.AND,
builder.makeCall(SqlStdOperatorTable.GREATER_THAN_OR_EQUAL, col, low),
builder.makeCall(SqlStdOperatorTable.LESS_THAN_OR_EQUAL, col, high));
Comment thread
vinaykpud marked this conversation as resolved.
}
return builder.makeCall(PPLBuiltinOperators.CIDRMATCH, col, cidr);
},
PPLTypeChecker.family(SqlTypeFamily.BINARY, SqlTypeFamily.STRING));
Comment thread
vinaykpud marked this conversation as resolved.
registerOperator(INTERNAL_GROK, PPLBuiltinOperators.GROK);
registerOperator(INTERNAL_PARSE, PPLBuiltinOperators.PARSE);
registerOperator(MATCH, PPLBuiltinOperators.MATCH);
Expand Down Expand Up @@ -1589,4 +1616,22 @@ private static SqlOperandTypeChecker extractTypeCheckerFromUDF(SqlOperator opera
}
return typeChecker;
}

/**
* Parses a CIDR string and returns its lower and upper bounds in canonical 16-byte IPv6-mapped
* form. Used by the (BINARY, STRING) {@code cidrmatch} overload to expand into a byte-range
* conjunction at plan time.
*
* <p>Delegates to {@link IPUtils#toRange(String)} for parsing; converts both bounds to IPv6 to
* guarantee 16-byte output regardless of whether the input cidr is IPv4 or IPv6.
*/
private static byte[][] parseCidrToIpv6Range(String cidr) {
if (cidr == null) {
throw new SemanticCheckException("cidrmatch range argument is null");
}
IPAddress range = IPUtils.toRange(cidr);
byte[] low = range.getLower().toIPv6().getBytes();
byte[] high = range.getUpper().toIPv6().getBytes();
return new byte[][] {low, high};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.expression.function.udf.conversion;

import static org.opensearch.sql.calcite.utils.OpenSearchTypeFactory.TYPE_FACTORY;

import java.util.List;
import org.apache.calcite.adapter.enumerable.NotNullImplementor;
import org.apache.calcite.adapter.enumerable.NullPolicy;
import org.apache.calcite.adapter.enumerable.RexToLixTranslator;
import org.apache.calcite.linq4j.tree.Expression;
import org.apache.calcite.rex.RexCall;
import org.apache.calcite.sql.type.FamilyOperandTypeChecker;
import org.apache.calcite.sql.type.OperandTypes;
import org.apache.calcite.sql.type.ReturnTypes;
import org.apache.calcite.sql.type.SqlReturnTypeInference;
import org.apache.calcite.sql.type.SqlTypeName;
import org.opensearch.sql.expression.function.ImplementorUDF;
import org.opensearch.sql.expression.function.UDFOperandMetadata;

/** Placeholder UDF that wraps a VARCHAR literal cast to VARBINARY for ip/binary fields. */
public class BinaryFunction extends ImplementorUDF {

private static final SqlReturnTypeInference VARBINARY_FORCE_NULLABLE =
ReturnTypes.explicit(
TYPE_FACTORY.createTypeWithNullability(
TYPE_FACTORY.createSqlType(SqlTypeName.VARBINARY), true));

public BinaryFunction() {
super(new PassThroughImplementor(), NullPolicy.STRICT);
}

@Override
public SqlReturnTypeInference getReturnTypeInference() {
return VARBINARY_FORCE_NULLABLE;
}

@Override
public UDFOperandMetadata getOperandMetadata() {
return UDFOperandMetadata.wrap((FamilyOperandTypeChecker) OperandTypes.CHARACTER);
}

public static class PassThroughImplementor implements NotNullImplementor {
@Override
public Expression implement(
RexToLixTranslator translator, RexCall call, List<Expression> translatedOperands) {
return translatedOperands.get(0);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.calcite;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.opensearch.sql.calcite.utils.OpenSearchTypeFactory.TYPE_FACTORY;

import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.rex.RexCall;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.sql.type.SqlTypeName;
import org.junit.jupiter.api.Test;
import org.opensearch.sql.expression.function.PPLBuiltinOperators;

class ExtendedRexBuilderTest {

private static final RexBuilder REX_BUILDER =
new ExtendedRexBuilder(new RexBuilder(TYPE_FACTORY));

/**
* VARCHAR → VARBINARY casts must be rewritten as a {@code BINARY(varchar)} placeholder {@code
* RexCall}.
*/
@Test
void castVarcharToVarbinaryEmitsBinaryPlaceholder() {
// Use makeInputRef to construct a VARCHAR-typed RexNode reliably. makeLiteral(String) folds
// to CHAR, which would make this test pass-through default cast instead of our placeholder.
RelDataType varchar = TYPE_FACTORY.createSqlType(SqlTypeName.VARCHAR);
RexNode varcharRef = REX_BUILDER.makeInputRef(varchar, 0);
RelDataType varbinary = TYPE_FACTORY.createSqlType(SqlTypeName.VARBINARY);

RexNode result = REX_BUILDER.makeCast(varbinary, varcharRef);

assertInstanceOf(RexCall.class, result);
RexCall call = (RexCall) result;
assertEquals(PPLBuiltinOperators.BINARY, call.getOperator());
assertEquals("BINARY", call.getOperator().getName());
assertEquals(SqlTypeName.VARBINARY, call.getType().getSqlTypeName());
assertEquals(1, call.getOperands().size());
assertEquals(SqlTypeName.VARCHAR, call.getOperands().get(0).getType().getSqlTypeName());
}

/**
* Casts targeting a SqlTypeName other than VARBINARY must NOT trigger the BINARY rewrite — they
* fall through to Calcite's default cast handling.
*/
@Test
void castVarcharToIntegerDoesNotEmitBinaryPlaceholder() {
RexNode varcharLiteral = REX_BUILDER.makeLiteral("42");
RelDataType integer = TYPE_FACTORY.createSqlType(SqlTypeName.INTEGER);

RexNode result = REX_BUILDER.makeCast(integer, varcharLiteral);

assertNotNull(result);
if (result instanceof RexCall call) {
assertEquals(
"BINARY".equals(call.getOperator().getName()),
false,
"VARCHAR → INTEGER must not emit BINARY placeholder");
}
}

/**
* Casts whose source is not VARCHAR must also fall through. The placeholder is only meant for the
* (VARCHAR → VARBINARY) case where a string IP / base64 literal is being compared against a
* VARBINARY column — non-string sources have well-defined Calcite cast semantics that should not
* be hijacked.
*/
@Test
void castIntegerToVarbinaryDoesNotEmitBinaryPlaceholder() {
RexNode intLiteral = REX_BUILDER.makeExactLiteral(java.math.BigDecimal.ONE);
RelDataType varbinary = TYPE_FACTORY.createSqlType(SqlTypeName.VARBINARY);

RexNode result = REX_BUILDER.makeCast(varbinary, intLiteral);

assertNotNull(result);
if (result instanceof RexCall call) {
assertEquals(
"BINARY".equals(call.getOperator().getName()),
false,
"non-VARCHAR → VARBINARY must not emit BINARY placeholder");
}
}
}
Loading
Loading