diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java index 8f726822fdf..4afa6bb74fd 100644 --- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java +++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java @@ -3104,23 +3104,89 @@ public static int position(ByteString seek, ByteString s) { /** SQL {@code POSITION(seek IN string FROM integer)} function. */ public static int position(String seek, String s, int from) { - final int from0 = from - 1; // 0-based - if (from0 > s.length() || from0 < 0) { + if (from > 0) { + final int from0 = from - 1; // 0-based + if (from0 > s.length() || from0 < 0) { + return 0; + } + + return s.indexOf(seek, from0) + 1; + } + final int rightIndex = from + s.length(); // negative position to positive index + if (rightIndex <= 0) { return 0; } - - return s.indexOf(seek, from0) + 1; + return s.substring(0, rightIndex).lastIndexOf(seek) + 1; } /** SQL {@code POSITION(seek IN string FROM integer)} function for byte * strings. */ public static int position(ByteString seek, ByteString s, int from) { - final int from0 = from - 1; - if (from0 > s.length() || from0 < 0) { + if (from > 0) { + final int from0 = from - 1; // 0-based + if (from0 > s.length() || from0 < 0) { + return 0; + } + + return s.indexOf(seek, from0) + 1; + } + final int rightIndex = from + s.length(); + if (rightIndex <= 0) { return 0; } + return -1; + //s.lastIndexOf(seek, rightIndex) + 1; + } + + /** SQL {@code POSITION(seek, string, from, occurrence)} function. */ + public static int position(String seek, String s, int from, int occurrence) { + if (from > 0){ + int rollingFrom = from; + for (int i = 0; i< occurrence; i++) { + rollingFrom = position(seek, s, rollingFrom); + if (rollingFrom == 0) { + return 0; + } + } + return rollingFrom; + } + int rollingFromNeg = from; + int rollingFromPos = 0; + for (int i = 0; i< occurrence; i++) { + rollingFromPos = position(seek, s, rollingFromNeg); + if (rollingFromPos == 0) { + return 0; + } + rollingFromNeg = rollingFromPos - s.length(); + } + return rollingFromPos; + + } + + /** SQL {@code POSITION(seek, string, from, occurrence)} function for byte + * strings. */ + public static int position(ByteString seek, ByteString s, int from, int occurrence) { + if (from > 0){ + int rollingFrom = from; + for (int i = 0; i< occurrence; i++) { + rollingFrom = position(seek, s, rollingFrom); + if (rollingFrom == 0) { + return 0; + } + } + return rollingFrom; + } + int rollingFromNeg = from; + int rollingFromPos = 0; + for (int i = 0; i< occurrence; i++) { + rollingFromPos = position(seek, s, rollingFromNeg); + if (rollingFromPos == 0) { + return 0; + } + rollingFromNeg = rollingFromPos - s.length(); + } + return rollingFromPos; - return s.indexOf(seek, from0) + 1; } /** Helper for rounding. Truncate(12345, 1000) returns 12000. */ diff --git a/core/src/main/java/org/apache/calcite/sql/dialect/BigQuerySqlDialect.java b/core/src/main/java/org/apache/calcite/sql/dialect/BigQuerySqlDialect.java index f5d963a4c13..297234f77d1 100644 --- a/core/src/main/java/org/apache/calcite/sql/dialect/BigQuerySqlDialect.java +++ b/core/src/main/java/org/apache/calcite/sql/dialect/BigQuerySqlDialect.java @@ -150,6 +150,7 @@ public BigQuerySqlDialect(SqlDialect.Context context) { final int rightPrec) { switch (call.getKind()) { case POSITION: + //TODO: add case on number of operands to unparse 3,4 to INSTR instead of STRPOS final SqlWriter.Frame frame = writer.startFunCall("STRPOS"); writer.sep(","); call.operand(1).unparse(writer, leftPrec, rightPrec); diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java index ed3ebd5ff89..b9846c96aa9 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java @@ -332,12 +332,17 @@ static RelDataType deriveTypeSplit(SqlOperatorBinding operatorBinding, @LibraryOperator(libraries = {BIG_QUERY, POSTGRESQL}) public static final SqlFunction STRPOS = new SqlPositionFunction("STRPOS"); + /** The "INSTR(string, substring [, position [, occurrence]])" function. */ + @LibraryOperator(libraries = {BIG_QUERY, ORACLE}) + public static final SqlFunction INSTR = new SqlPositionFunction("INSTR"); + /** Generic "SUBSTR(string, position [, substringLength ])" function. */ private static final SqlBasicFunction SUBSTR = SqlBasicFunction.create("SUBSTR", ReturnTypes.ARG0_NULLABLE_VARYING, OperandTypes.STRING_INTEGER_OPTIONAL_INTEGER, SqlFunctionCategory.STRING); + /** The "ENDS_WITH(value1, value2)" function (BigQuery). */ @LibraryOperator(libraries = {BIG_QUERY}) public static final SqlFunction ENDS_WITH = diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlPositionFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlPositionFunction.java index 753ba93f8c0..623ac7d4d5e 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlPositionFunction.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlPositionFunction.java @@ -25,6 +25,8 @@ import org.apache.calcite.sql.type.OperandTypes; import org.apache.calcite.sql.type.ReturnTypes; import org.apache.calcite.sql.type.SqlOperandTypeChecker; +import org.apache.calcite.sql.type.SqlTypeFamily; + /** * The POSITION function. @@ -37,8 +39,10 @@ public class SqlPositionFunction extends SqlFunction { // as part of rtiDyadicStringSumPrecision private static final SqlOperandTypeChecker OTC_CUSTOM = - OperandTypes.STRING_SAME_SAME - .or(OperandTypes.STRING_SAME_SAME_INTEGER); +// OperandTypes.STRING_SAME_SAME +// .or(OperandTypes.STRING_SAME_SAME_INTEGER) + (OperandTypes.STRING_SAME_SAME_INTEGER_INTEGER); +// .or(OperandTypes.STRING_SAME_SAME_INTEGER_INTEGER); public SqlPositionFunction(String name) { super(name, SqlKind.POSITION, ReturnTypes.INTEGER_NULLABLE, null, @@ -60,6 +64,10 @@ public SqlPositionFunction(String name) { writer.sep("FROM"); call.operand(2).unparse(writer, leftPrec, rightPrec); } + if (4 == call.operandCount()) { + writer.sep(", "); + call.operand(3).unparse(writer, leftPrec, rightPrec); + } writer.endFunCall(frame); } @@ -69,6 +77,8 @@ public SqlPositionFunction(String name) { return "{0}({1} IN {2})"; case 3: return "{0}({1} IN {2} FROM {3})"; + case 4: + return "{0}({1}, {2}, {3}, {4})"; default: throw new AssertionError(); } @@ -88,6 +98,9 @@ public SqlPositionFunction(String name) { return OperandTypes.SAME_SAME_INTEGER.checkOperandTypes( callBinding, throwOnFailure) && super.checkOperandTypes(callBinding, throwOnFailure); + case 4: + return true; + //OperandTypes.and(OperandTypes.SAME_SAME_INTEGER, OperandTypes.INTEGER).checkOperandTypes(callBinding, throwOnFailure) && super.checkOperandTypes(callBinding, throwOnFailure); default: throw new AssertionError(); } diff --git a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java index e8029903b7e..a5fbecf5cc4 100644 --- a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java +++ b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java @@ -597,6 +597,8 @@ private boolean hasFractionalPart(BigDecimal bd) { public static final SqlSingleOperandTypeChecker SAME_SAME_INTEGER = new SameOperandTypeExceptLastOperandChecker(3, "INTEGER"); + public static final SqlSingleOperandTypeChecker SAME_SAME_INTEGER_INTEGER = + SAME_SAME_INTEGER.and(family(ImmutableList.of(SqlTypeFamily.INTEGER,SqlTypeFamily.INTEGER,SqlTypeFamily.INTEGER))); /** * Operand type-checking strategy where three operands must all be in the * same type family. @@ -716,6 +718,9 @@ public static SqlSingleOperandTypeChecker same(int operandCount, public static final SqlSingleOperandTypeChecker STRING_SAME_SAME_INTEGER = STRING_STRING_INTEGER.and(SAME_SAME_INTEGER); + public static final SqlSingleOperandTypeChecker STRING_SAME_SAME_INTEGER_INTEGER = + STRING_STRING_INTEGER_INTEGER.and(SAME_SAME_INTEGER_INTEGER); + public static final SqlSingleOperandTypeChecker STRING_SAME_SAME_OR_ARRAY_SAME_SAME = or(STRING_SAME_SAME, and(OperandTypes.SAME_SAME, family(SqlTypeFamily.ARRAY, SqlTypeFamily.ARRAY))); diff --git a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java index fae2a1ea8b2..80ace3f5144 100644 --- a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java +++ b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java @@ -279,6 +279,13 @@ private StandardConvertletTable() { SqlStdOperatorTable.POSITION.createCall(SqlParserPos.ZERO, call.operand(1), call.operand(0)))); + // "INSTR(string, substring, position, occurrence) is equivalent to + // "POSITION(substring, string, position, occurrence)" + registerOp(SqlLibraryOperators.INSTR, + (cx, call) -> cx.convertExpression( + SqlStdOperatorTable.POSITION.createCall(SqlParserPos.ZERO, + call.operand(1), call.operand(0), call.operand(2), call.operand(3)))); + // REVIEW jvs 24-Apr-2006: This only seems to be working from within a // windowed agg. I have added an optimizer rule // org.apache.calcite.rel.rules.AggregateReduceFunctionsRule which handles diff --git a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java index 8cae0d8debd..7094db51d5d 100644 --- a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java +++ b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java @@ -2228,6 +2228,15 @@ private SqlDialect nonOrdinalDialect() { sql(query).withBigQuery().ok(expected); } + @Test void testBigQueryInstrFunction() { + final String query = "SELECT INSTR('A', 'ABC', 1, 1) from \"product\""; + final String expected = "SELECT INSTR('A', 'ABC', 1, 1)\n" + + "FROM foodmart.product"; + final Sql sql = fixture().withBigQuery().withLibrary(SqlLibrary.BIG_QUERY); + sql.withSql(query).withBigQuery().ok(expected); + } + + /** Tests that we escape single-quotes in character literals using back-slash * in BigQuery. The norm is to escape single-quotes with single-quotes. */ @Test void testCharLiteralForBigQuery() {