This is an automated email from the ASF dual-hosted git repository.
jhyde pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new 9ea862478d [CALCITE-3959] Add INSTR function (enabled in BigQuery,
MySQL, Oracle libraries)
9ea862478d is described below
commit 9ea862478d88b6163e47c57521e222ea5e582e47
Author: jhugomoore <[email protected]>
AuthorDate: Thu Apr 20 13:15:35 2023 -0700
[CALCITE-3959] Add INSTR function (enabled in BigQuery, MySQL, Oracle
libraries)
Close apache/calcite#3184
---
.../apache/calcite/runtime/CalciteResource.java | 6 +
.../org/apache/calcite/runtime/SqlFunctions.java | 148 +++++++++++++++++++--
.../calcite/sql/dialect/BigQuerySqlDialect.java | 36 ++++-
.../calcite/sql/dialect/MysqlSqlDialect.java | 8 ++
.../calcite/sql/dialect/OracleSqlDialect.java | 18 +++
.../calcite/sql/fun/SqlLibraryOperators.java | 4 +
.../calcite/sql/fun/SqlPositionFunction.java | 35 ++++-
.../calcite/sql2rel/StandardConvertletTable.java | 39 ++++++
.../calcite/runtime/CalciteResource.properties | 2 +
.../calcite/rel/rel2sql/RelToSqlConverterTest.java | 33 ++++-
.../org/apache/calcite/test/SqlFunctionsTest.java | 55 ++++++++
site/_docs/reference.md | 12 +-
.../org/apache/calcite/test/SqlOperatorTest.java | 35 ++++-
13 files changed, 402 insertions(+), 29 deletions(-)
diff --git a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
index 4bf68b3771..7695ad8364 100644
--- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
+++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
@@ -1020,6 +1020,12 @@ public interface CalciteResource {
@BaseMessage("No operator for ''{0}'' with kind: ''{1}'', syntax: ''{2}''
during JSON deserialization")
ExInst<CalciteException> noOperator(String name, String kind, String syntax);
+ @BaseMessage("Invalid input for POSITION function: from operand value must
not be zero")
+ ExInst<CalciteException> fromNotZero();
+
+ @BaseMessage("Invalid input for POSITION function: occurrence operand value
must be positive")
+ ExInst<CalciteException> occurrenceNotZero();
+
@BaseMessage("Only tables with set semantics may be partitioned. Invalid
PARTITION BY clause in the {0,number,#}-th operand of table function ''{1}''")
ExInst<SqlValidatorException> invalidPartitionKeys(int idx, String funcName);
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 f44195b491..0e5a12dc56 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -3186,23 +3186,155 @@ public class SqlFunctions {
/** 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) {
- return 0;
+ if (from == 0) {
+ throw RESOURCE.fromNotZero().ex();
+ }
+ if (from > 0) {
+ return positionForwards(seek, s, from);
+ } else {
+ from += s.length(); // convert negative position to positive index
+ return positionBackwards(seek, s, from);
}
-
- return s.indexOf(seek, from0) + 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) {
+ throw RESOURCE.fromNotZero().ex();
+ }
+ if (from > 0) {
+ return positionForwards(seek, s, from);
+ } else {
+ from += s.length(); // convert negative position to 0-based index
+ return positionBackwards(seek, s, from);
+ }
+ }
+
+ /** Returns the position (1-based) of {@code seek} in string {@code s}
+ * seeking forwards from {@code from} (1-based). */
+ private static int positionForwards(String seek, String s, int from) {
+ final int from0 = from - 1; // 0-based
+ if (from0 >= s.length()) {
return 0;
+ } else {
+ return s.indexOf(seek, from0) + 1;
}
+ }
+
+ /** Returns the position (1-based) of {@code seek} in byte string {@code s}
+ * seeking forwards from {@code from} (1-based). */
+ private static int positionForwards(ByteString seek, ByteString s,
+ int from) {
+ final int from0 = from - 1; // 0-based
+ if (from0 >= s.length()) {
+ return 0;
+ } else {
+ return s.indexOf(seek, from0) + 1;
+ }
+ }
+
+ /** Returns the position (1-based) of {@code seek} in string {@code s}
+ * seeking backwards from {@code rightIndex} (0-based). */
+ private static int positionBackwards(String seek, String s, int rightIndex) {
+ if (rightIndex <= 0) {
+ return 0;
+ }
+ int lastIndex = s.lastIndexOf(seek) + 1;
+ while (lastIndex > rightIndex + 1) {
+ lastIndex = s.substring(0, lastIndex - 1).lastIndexOf(seek) + 1;
+ if (lastIndex == 0) {
+ return 0;
+ }
+ }
+ return lastIndex;
+ }
- return s.indexOf(seek, from0) + 1;
+ /** Returns the position (1-based) of {@code seek} in byte string {@code s}
+ * seeking backwards from {@code rightIndex} (0-based). */
+ private static int positionBackwards(ByteString seek, ByteString s,
+ int rightIndex) {
+ if (rightIndex <= 0) {
+ return 0;
+ }
+ int lastIndex = 0;
+ while (lastIndex < rightIndex) {
+ // NOTE: When [CALCITE-5682] is fixed, use ByteString.lastIndexOf
+ int indexOf = s.substring(lastIndex).indexOf(seek) + 1;
+ if (indexOf == 0 || lastIndex + indexOf > rightIndex + 1) {
+ break;
+ }
+ lastIndex += indexOf;
+ }
+ return lastIndex;
+ }
+
+ /** SQL {@code POSITION(seek, string, from, occurrence)} function. */
+ public static int position(String seek, String s, int from, int occurrence) {
+ if (from == 0) {
+ throw RESOURCE.fromNotZero().ex();
+ }
+ if (occurrence == 0) {
+ throw RESOURCE.occurrenceNotZero().ex();
+ }
+ if (from > 0) {
+ // Forwards
+ --from; // compensate for the '++from' 2 lines down
+ for (int i = 0; i < occurrence; i++) {
+ ++from; // move on to next occurrence
+ from = positionForwards(seek, s, from);
+ if (from == 0) {
+ return 0;
+ }
+ }
+ } else {
+ // Backwards
+ from += s.length() + 1; // convert negative position to positive index
+ ++from; // compensate for the '--from' 2 lines down
+ for (int i = 0; i < occurrence; i++) {
+ --from; // move on to next occurrence
+ from = positionBackwards(seek, s, from - 1);
+ if (from == 0) {
+ return 0;
+ }
+ }
+ }
+ return from;
+ }
+
+ /** 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) {
+ throw RESOURCE.fromNotZero().ex();
+ }
+ if (occurrence == 0) {
+ throw RESOURCE.occurrenceNotZero().ex();
+ }
+ if (from > 0) {
+ // Forwards
+ --from; // compensate for the '++from' 2 lines down
+ for (int i = 0; i < occurrence; i++) {
+ ++from; // move on to next occurrence
+ from = positionForwards(seek, s, from);
+ if (from == 0) {
+ return 0;
+ }
+ }
+ } else {
+ // Backwards
+ from += s.length() + 1; // convert negative position to positive index
+ ++from; // compensate for the '--from' 2 lines down
+ for (int i = 0; i < occurrence; i++) {
+ --from; // move on to next occurrence
+ from = positionBackwards(seek, s, from - 1);
+ if (from == 0) {
+ return 0;
+ }
+ }
+ }
+ return from;
}
/** 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 f5d963a4c1..3ccb062219 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,13 +150,35 @@ public class BigQuerySqlDialect extends SqlDialect {
final int rightPrec) {
switch (call.getKind()) {
case POSITION:
- final SqlWriter.Frame frame = writer.startFunCall("STRPOS");
- writer.sep(",");
- call.operand(1).unparse(writer, leftPrec, rightPrec);
- writer.sep(",");
- call.operand(0).unparse(writer, leftPrec, rightPrec);
- if (3 == call.operandCount()) {
- throw new RuntimeException("3rd operand Not Supported for Function
STRPOS in Big Query");
+ final SqlWriter.Frame frame = writer.startFunCall("INSTR");
+ switch (call.operandCount()) {
+ case 2:
+ writer.sep(",");
+ call.operand(1).unparse(writer, leftPrec, rightPrec);
+ writer.sep(",");
+ call.operand(0).unparse(writer, leftPrec, rightPrec);
+ break;
+ case 3:
+ writer.sep(",");
+ call.operand(1).unparse(writer, leftPrec, rightPrec);
+ writer.sep(",");
+ call.operand(0).unparse(writer, leftPrec, rightPrec);
+ writer.sep(",");
+ call.operand(2).unparse(writer, leftPrec, rightPrec);
+ break;
+ case 4:
+ writer.sep(",");
+ call.operand(1).unparse(writer, leftPrec, rightPrec);
+ writer.sep(",");
+ call.operand(0).unparse(writer, leftPrec, rightPrec);
+ writer.sep(",");
+ call.operand(2).unparse(writer, leftPrec, rightPrec);
+ writer.sep(",");
+ call.operand(3).unparse(writer, leftPrec, rightPrec);
+ break;
+ default:
+ throw new RuntimeException("BigQuery does not support " +
call.operandCount()
+ + " operands in the position function");
}
writer.endFunCall(frame);
break;
diff --git
a/core/src/main/java/org/apache/calcite/sql/dialect/MysqlSqlDialect.java
b/core/src/main/java/org/apache/calcite/sql/dialect/MysqlSqlDialect.java
index fb3cf64c25..041bfd865a 100644
--- a/core/src/main/java/org/apache/calcite/sql/dialect/MysqlSqlDialect.java
+++ b/core/src/main/java/org/apache/calcite/sql/dialect/MysqlSqlDialect.java
@@ -218,6 +218,14 @@ public class MysqlSqlDialect extends SqlDialect {
@Override public void unparseCall(SqlWriter writer, SqlCall call,
int leftPrec, int rightPrec) {
switch (call.getKind()) {
+ case POSITION:
+ final SqlWriter.Frame frame = writer.startFunCall("INSTR");
+ writer.sep(",");
+ call.operand(1).unparse(writer, leftPrec, rightPrec);
+ writer.sep(",");
+ call.operand(0).unparse(writer, leftPrec, rightPrec);
+ writer.endFunCall(frame);
+ break;
case FLOOR:
if (call.operandCount() != 2) {
super.unparseCall(writer, call, leftPrec, rightPrec);
diff --git
a/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java
b/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java
index 790028ea91..fbd9aef93b 100644
--- a/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java
+++ b/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java
@@ -150,6 +150,24 @@ public class OracleSqlDialect extends SqlDialect {
call, false);
} else {
switch (call.getKind()) {
+ case POSITION:
+ final SqlWriter.Frame frame = writer.startFunCall("INSTR");
+ writer.sep(",");
+ call.operand(1).unparse(writer, leftPrec, rightPrec);
+ writer.sep(",");
+ call.operand(0).unparse(writer, leftPrec, rightPrec);
+ if (3 == call.operandCount()) {
+ writer.sep(",");
+ call.operand(2).unparse(writer, leftPrec, rightPrec);
+ }
+ if (4 == call.operandCount()) {
+ writer.sep(",");
+ call.operand(2).unparse(writer, leftPrec, rightPrec);
+ writer.sep(",");
+ call.operand(3).unparse(writer, leftPrec, rightPrec);
+ }
+ writer.endFunCall(frame);
+ break;
case FLOOR:
if (call.operandCount() != 2) {
super.unparseCall(writer, call, 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 75a27f2c53..6c6a7b022b 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,6 +332,10 @@ public abstract class SqlLibraryOperators {
@LibraryOperator(libraries = {BIG_QUERY, POSTGRESQL})
public static final SqlFunction STRPOS = new SqlPositionFunction("STRPOS");
+ /** The "INSTR(string, substring [, position [, occurrence]])" function. */
+ @LibraryOperator(libraries = {BIG_QUERY, MYSQL, 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,
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 753ba93f8c..4a625fc52d 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
@@ -38,7 +38,11 @@ public class SqlPositionFunction extends SqlFunction {
private static final SqlOperandTypeChecker OTC_CUSTOM =
OperandTypes.STRING_SAME_SAME
- .or(OperandTypes.STRING_SAME_SAME_INTEGER);
+ .or(OperandTypes.STRING_SAME_SAME_INTEGER)
+ .or(
+ OperandTypes.sequence("INSTR(<STRING>, <STRING>, <INTEGER>,
<INTEGER>)",
+ OperandTypes.STRING, OperandTypes.STRING, OperandTypes.INTEGER,
+ OperandTypes.INTEGER));
public SqlPositionFunction(String name) {
super(name, SqlKind.POSITION, ReturnTypes.INTEGER_NULLABLE, null,
@@ -53,12 +57,23 @@ public class SqlPositionFunction extends SqlFunction {
int leftPrec,
int rightPrec) {
final SqlWriter.Frame frame = writer.startFunCall(getName());
- call.operand(0).unparse(writer, leftPrec, rightPrec);
- writer.sep("IN");
- call.operand(1).unparse(writer, leftPrec, rightPrec);
- if (3 == call.operandCount()) {
- writer.sep("FROM");
+ if (call.operandCount() == 2 || call.operandCount() == 3) {
+ call.operand(0).unparse(writer, leftPrec, rightPrec);
+ writer.sep("IN");
+ call.operand(1).unparse(writer, leftPrec, rightPrec);
+ if (call.operandCount() == 3) {
+ writer.sep("FROM");
+ call.operand(2).unparse(writer, leftPrec, rightPrec);
+ }
+ }
+ if (call.operandCount() == 4) {
+ call.operand(0).unparse(writer, leftPrec, rightPrec);
+ writer.sep(",");
+ call.operand(1).unparse(writer, leftPrec, rightPrec);
+ writer.sep(",");
call.operand(2).unparse(writer, leftPrec, rightPrec);
+ writer.sep(",");
+ call.operand(3).unparse(writer, leftPrec, rightPrec);
}
writer.endFunCall(frame);
}
@@ -69,6 +84,8 @@ public class SqlPositionFunction extends SqlFunction {
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 +105,12 @@ public class SqlPositionFunction extends SqlFunction {
return OperandTypes.SAME_SAME_INTEGER.checkOperandTypes(
callBinding, throwOnFailure)
&& super.checkOperandTypes(callBinding, throwOnFailure);
+ case 4:
+ return OperandTypes.sequence("INSTR(<STRING>, <STRING>, <INTEGER>,
<INTEGER>)",
+ OperandTypes.STRING, OperandTypes.STRING, OperandTypes.INTEGER,
+ OperandTypes.INTEGER).checkOperandTypes(
+ callBinding, throwOnFailure)
+ && super.checkOperandTypes(callBinding, throwOnFailure);
default:
throw new AssertionError();
}
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 a8288700cc..60718dff6e 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
@@ -280,6 +280,11 @@ public class StandardConvertletTable extends
ReflectiveConvertletTable {
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,
StandardConvertletTable::convertInstr);
+
+
// 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
@@ -403,6 +408,40 @@ public class StandardConvertletTable extends
ReflectiveConvertletTable {
operand1)));
}
+ /** Converts a call to the INSTR function.
+ * INSTR(string, substring, position, occurrence) is equivalent to
+ * POSITION(substring, string, position, occurrence) */
+ private static RexNode convertInstr(SqlRexContext cx, SqlCall call) {
+ final RexBuilder rexBuilder = cx.getRexBuilder();
+ final List<RexNode> operands =
+ convertOperands(cx, call, SqlOperandTypeChecker.Consistency.NONE);
+ final RelDataType type =
+ cx.getValidator().getValidatedNodeType(call);
+ final List<RexNode> exprs = new ArrayList<>();
+ switch (call.operandCount()) {
+ // Must reverse order of first 2 operands.
+ case 2:
+ exprs.add(operands.get(1)); // Substring
+ exprs.add(operands.get(0)); // String
+ break;
+ case 3:
+ exprs.add(operands.get(1)); // Substring
+ exprs.add(operands.get(0)); // String
+ exprs.add(operands.get(2)); // Position
+ break;
+ case 4:
+ exprs.add(operands.get(1)); // Substring
+ exprs.add(operands.get(0)); // String
+ exprs.add(operands.get(2)); // Position
+ exprs.add(operands.get(3)); // Occurrence
+ break;
+ default:
+ throw new UnsupportedOperationException("Position does not accept "
+ + call.operandCount() + " operands");
+ }
+ return rexBuilder.makeCall(type, SqlStdOperatorTable.POSITION, exprs);
+ }
+
/** Converts a call to the DECODE function. */
private static RexNode convertDecode(SqlRexContext cx, SqlCall call) {
final RexBuilder rexBuilder = cx.getRexBuilder();
diff --git
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
index 5c604ca60a..1f7f847eba 100644
---
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
+++
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
@@ -332,6 +332,8 @@ InvalidInputForExtractValue=Invalid input for EXTRACTVALUE:
xml: ''{0}'', xpath
InvalidInputForExtractXml=Invalid input for EXTRACT xpath: ''{0}'', namespace:
''{1}''
InvalidInputForExistsNode=Invalid input for EXISTSNODE xpath: ''{0}'',
namespace: ''{1}''
DifferentLengthForBitwiseOperands=Different length for bitwise operands: the
first: {0,number,#}, the second: {1,number,#}
+FromNotZero=Invalid input for POSITION function: from operand value must not
be zero
+OccurrenceNotZero=Invalid input for POSITION function: occurrence operand
value must be positive
InvalidPartitionKeys=Only tables with set semantics may be partitioned.
Invalid PARTITION BY clause in the {0,number,#}-th operand of table function
''{1}''
InvalidOrderBy=Only tables with set semantics may be ordered. Invalid ORDER BY
clause in the {0,number,#}-th operand of table function ''{1}''
MultipleRowSemanticsTables=A table function at most has one input table with
row semantics. Table function ''{0}'' has multiple input tables with row
semantics
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 1d7431b922..8193d0ab0c 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
@@ -2325,13 +2325,44 @@ class RelToSqlConverterTest {
sql(query).withHive().ok(expected);
}
+ @Test void testPositionFunctionForMySql() {
+ final String query = "select position('A' IN 'ABC') from \"product\"";
+ final String expected = "SELECT INSTR('ABC', 'A')\n"
+ + "FROM `foodmart`.`product`";
+ sql(query).withMysql().ok(expected);
+ }
+
@Test void testPositionFunctionForBigQuery() {
final String query = "select position('A' IN 'ABC') from \"product\"";
- final String expected = "SELECT STRPOS('ABC', 'A')\n"
+ final String expected = "SELECT INSTR('ABC', 'A')\n"
+ "FROM foodmart.product";
sql(query).withBigQuery().ok(expected);
}
+ @Test void testInstrFunction4Operands() {
+ final String query = "SELECT INSTR('ABC', 'A', 1, 1) from \"product\"";
+ final String expectedBQ = "SELECT INSTR('ABC', 'A', 1, 1)\n"
+ + "FROM foodmart.product";
+ final String expected_oracle = "SELECT INSTR('ABC', 'A', 1, 1)\n"
+ + "FROM \"foodmart\".\"product\"";
+ final Sql sqlOracle =
fixture().withOracle().withLibrary(SqlLibrary.ORACLE);
+ sqlOracle.withSql(query).withOracle().ok(expected_oracle);
+ final Sql sqlBQ =
fixture().withBigQuery().withLibrary(SqlLibrary.BIG_QUERY);
+ sqlBQ.withSql(query).withBigQuery().ok(expectedBQ);
+ }
+
+ @Test void testInstrFunction3Operands() {
+ final String query = "SELECT INSTR('ABC', 'A', 1) from \"product\"";
+ final String expectedBQ = "SELECT INSTR('ABC', 'A', 1)\n"
+ + "FROM foodmart.product";
+ final String expectedOracle = "SELECT INSTR('ABC', 'A', 1)\n"
+ + "FROM \"foodmart\".\"product\"";
+ final Sql sqlOracle =
fixture().withOracle().withLibrary(SqlLibrary.ORACLE);
+ sqlOracle.withSql(query).withOracle().ok(expectedOracle);
+ final Sql sqlBQ =
fixture().withBigQuery().withLibrary(SqlLibrary.BIG_QUERY);
+ sqlBQ.withSql(query).withBigQuery().ok(expectedBQ);
+ }
+
/** 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() {
diff --git a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
index 2d44371722..60c9895b84 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
@@ -53,6 +53,7 @@ import static org.apache.calcite.runtime.SqlFunctions.lesser;
import static org.apache.calcite.runtime.SqlFunctions.lower;
import static org.apache.calcite.runtime.SqlFunctions.ltrim;
import static org.apache.calcite.runtime.SqlFunctions.md5;
+import static org.apache.calcite.runtime.SqlFunctions.position;
import static org.apache.calcite.runtime.SqlFunctions.posixRegex;
import static org.apache.calcite.runtime.SqlFunctions.regexpReplace;
import static org.apache.calcite.runtime.SqlFunctions.rtrim;
@@ -1055,6 +1056,60 @@ class SqlFunctionsTest {
}
}
+ @Test void testPosition() {
+ assertThat(position("c", "abcdec"), is(3));
+ assertThat(position("c", "abcdec", 2), is(3));
+ assertThat(position("c", "abcdec", -2), is(3));
+ assertThat(position("c", "abcdec", 4), is(6));
+ assertThat(position("c", "abcdec", 1, 2), is(6));
+ assertThat(position("cde", "abcdecde", -2, 1), is(6));
+ assertThat(position("c", "abcdec", -1, 2), is(3));
+ assertThat(position("f", "abcdec", 1, 1), is(0));
+ assertThat(position("c", "abcdec", 1, 3), is(0));
+ try {
+ int i = position("c", "abcdec", 0, 1);
+ fail("expected error, got: " + i);
+ } catch (CalciteException e) {
+ assertThat(e.getMessage(),
+ is("Invalid input for POSITION function: from operand value must not
be zero"));
+ }
+ try {
+ int i = position("c", "abcdec", 1, 0);
+ fail("expected error, got: " + i);
+ } catch (CalciteException e) {
+ assertThat(e.getMessage(),
+ is("Invalid input for POSITION function: occurrence operand value
must be positive"));
+ }
+ final ByteString abcdec = ByteString.of("aabbccddeecc", 16);
+ final ByteString c = ByteString.of("cc", 16);
+ final ByteString dec = ByteString.of("ddeecc", 16);
+ final ByteString f = ByteString.of("ff", 16);
+ assertThat(position(c, abcdec), is(3));
+ assertThat(position(c, abcdec, 2), is(3));
+ assertThat(position(c, abcdec, -2), is(3));
+ assertThat(position(c, abcdec, 4), is(6));
+ assertThat(position(dec, abcdec, -2), is(4));
+ assertThat(position(c, abcdec, 1, 2), is(6));
+ assertThat(position(c, abcdec, -1, 2), is(3));
+ assertThat(position(f, abcdec, 1, 1), is(0));
+ assertThat(position(c, abcdec, 1, 3), is(0));
+ try {
+ int i = position(c, abcdec, 0, 1);
+ fail("expected error, got: " + i);
+ } catch (CalciteException e) {
+ assertThat(e.getMessage(),
+ is("Invalid input for POSITION function: from operand value must not
be zero"));
+ }
+ try {
+ int i = position(c, abcdec, 1, 0);
+ fail("expected error, got: " + i);
+ } catch (CalciteException e) {
+ assertThat(e.getMessage(),
+ is("Invalid input for POSITION function: occurrence operand value
must be positive"));
+ }
+ }
+
+
/**
* Tests that a date in the local time zone converts to a Unix timestamp in
* UTC.
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 6c3cfcf807..9d8d11beb3 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -2685,7 +2685,7 @@ BigQuery's type system uses confusingly different names
for types and functions:
| o | DECODE(value, value1, result1 [, valueN, resultN ]* [, default ]) |
Compares *value* to each *valueN* value one by one; if *value* is equal to a
*valueN*, returns the corresponding *resultN*, else returns *default*, or NULL
if *default* is not specified
| p | DIFFERENCE(string, string) | Returns a measure of
the similarity of two strings, namely the number of character positions that
their `SOUNDEX` values have in common: 4 if the `SOUNDEX` values are same and 0
if the `SOUNDEX` values are totally different
| b | ENDS_WITH(string1, string2) | Returns whether
*string2* is a suffix of *string1*
-| o | EXTRACT(xml, xpath, [, namespaces ]) | Returns the xml
fragment of the element or elements matched by the XPath expression. The
optional namespace value that specifies a default mapping or namespace mapping
for prefixes, which is used when evaluating the XPath expression
+| o | EXTRACT(xml, xpath, [, namespaces ]) | Returns the XML
fragment of the element or elements matched by the XPath expression. The
optional namespace value that specifies a default mapping or namespace mapping
for prefixes, which is used when evaluating the XPath expression
| o | EXISTSNODE(xml, xpath, [, namespaces ]) | Determines whether
traversal of a XML document using a specified xpath results in any nodes.
Returns 0 if no nodes remain after applying the XPath traversal on the document
fragment of the element or elements matched by the XPath expression. Returns 1
if any nodes remain. The optional namespace value that specifies a default
mapping or namespace mapping for prefixes, which is used when evaluating the
XPath expression.
| m | EXTRACTVALUE(xml, xpathExpr)) | Returns the text of the
first text node which is a child of the element or elements matched by the
XPath expression.
| b | FORMAT_DATE(string, date) | Formats *date*
according to the specified format *string*
@@ -2695,15 +2695,17 @@ BigQuery's type system uses confusingly different names
for types and functions:
| b o | GREATEST(expr [, expr ]*) | Returns the greatest of
the expressions
| b h s | IF(condition, value1, value2) | Returns *value1* if
*condition* is TRUE, *value2* otherwise
| b | IFNULL(value1, value2) | Equivalent to
`NVL(value1, value2)`
+| b o | INSTR(string, substring [, from [, occurrence ] ]) | Returns the
position of *substring* in *string*, searching starting at *from* (default 1),
and until locating the nth *occurrence* (default 1) of *substring*
+| m | INSTR(string, substring) | Equivalent to
`POSITION(substring IN string)`
| p | string1 ILIKE string2 [ ESCAPE string3 ] | Whether *string1*
matches pattern *string2*, ignoring case (similar to `LIKE`)
| p | string1 NOT ILIKE string2 [ ESCAPE string3 ] | Whether *string1* does
not match pattern *string2*, ignoring case (similar to `NOT LIKE`)
| m | JSON_TYPE(jsonValue) | Returns a string value
indicating the type of *jsonValue*
| m | JSON_DEPTH(jsonValue) | Returns an integer
value indicating the depth of *jsonValue*
| m | JSON_PRETTY(jsonValue) | Returns a
pretty-printing of *jsonValue*
| m | JSON_LENGTH(jsonValue [, path ]) | Returns a integer
indicating the length of *jsonValue*
-| m | JSON_INSERT(jsonValue, path, val[, path, val]*) | Returns a JSON
document insert a data of *jsonValue*, *path*, *val*
+| m | JSON_INSERT(jsonValue, path, val [, path, val ]*) | Returns a JSON
document insert a data of *jsonValue*, *path*, *val*
| m | JSON_KEYS(jsonValue [, path ]) | Returns a string
indicating the keys of a JSON *jsonValue*
-| m | JSON_REMOVE(jsonValue, path[, path]) | Removes data from
*jsonValue* using a series of *path* expressions and returns the result
+| m | JSON_REMOVE(jsonValue, path [, path ]) | Removes data from
*jsonValue* using a series of *path* expressions and returns the result
| m | JSON_REPLACE(jsonValue, path, val[, path, val]*) | Returns a JSON
document replace a data of *jsonValue*, *path*, *val*
| m | JSON_SET(jsonValue, path, val[, path, val]*) | Returns a JSON document
set a data of *jsonValue*, *path*, *val*
| m | JSON_STORAGE_SIZE(jsonValue) | Returns the number of
bytes used to store the binary representation of *jsonValue*
@@ -2711,7 +2713,7 @@ BigQuery's type system uses confusingly different names
for types and functions:
| b m p | LEFT(string, length) | Returns the leftmost
*length* characters from the *string*
| b | LENGTH(string) | Equivalent to
`CHAR_LENGTH(string)`
| b | LOG(numeric1 [, numeric2 ]) | Returns the logarithm
of *numeric1* to base *numeric2*, or base e if *numeric2* is not present
-| b o | LPAD(string, length[, pattern ]) | Returns a string or
bytes value that consists of *string* prepended to *length* with *pattern*
+| b o | LPAD(string, length [, pattern ]) | Returns a string or
bytes value that consists of *string* prepended to *length* with *pattern*
| m | TO_BASE64(string) | Converts the *string*
to base-64 encoded form and returns a encoded string
| b m | FROM_BASE64(string) | Returns the decoded
result of a base-64 *string* as a string
| b o | LTRIM(string) | Returns *string* with
all blanks removed from the start
@@ -2723,7 +2725,7 @@ BigQuery's type system uses confusingly different names
for types and functions:
| b | PARSE_TIME(format, string) | Uses format specified
by *format* to convert *string* representation of time to a TIME value
| b | PARSE_TIMESTAMP(format, string[, timeZone]) | Uses format specified
by *format* to convert *string* representation of timestamp to a TIMESTAMP WITH
LOCAL TIME ZONE value in *timeZone*
| b | POW(numeric1, numeric2) | Returns *numeric1*
raised to the power *numeric2*
-| m o | REGEXP_REPLACE(string, regexp, rep, [, pos [, occurrence [,
matchType]]]) | Replaces all substrings of *string* that match *regexp* with
*rep* at the starting *pos* in expr (if omitted, the default is 1),
*occurrence* means which occurrence of a match to search for (if omitted, the
default is 1), *matchType* specifies how to perform matching
+| m o | REGEXP_REPLACE(string, regexp, rep [, pos [, occurrence [,
matchType]]]) | Replaces all substrings of *string* that match *regexp* with
*rep* at the starting *pos* in expr (if omitted, the default is 1),
*occurrence* means which occurrence of a match to search for (if omitted, the
default is 1), *matchType* specifies how to perform matching
| b m p | REPEAT(string, integer) | Returns a string
consisting of *string* repeated of *integer* times; returns an empty string if
*integer* is less than 1
| b m | REVERSE(string) | Returns *string* with
the order of the characters reversed
| b m p | RIGHT(string, length) | Returns the rightmost
*length* characters from the *string*
diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
index 45e9d98ab7..b2f6f468a5 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -3761,7 +3761,7 @@ public class SqlOperatorTest {
f.checkScalarExact("position('b' in 'abcabc' FROM 3)", 5);
f.checkScalarExact("position('b' in 'abcabc' FROM 5)", 5);
f.checkScalarExact("position('b' in 'abcabc' FROM 6)", 0);
- f.checkScalarExact("position('b' in 'abcabc' FROM -5)", 0);
+ f.checkScalarExact("position('b' in 'abcabc' FROM -5)", 2);
f.checkScalarExact("position('' in 'abc' FROM 3)", 3);
f.checkScalarExact("position('' in 'abc' FROM 10)", 0);
@@ -3770,7 +3770,7 @@ public class SqlOperatorTest {
f.checkScalarExact("position(x'bb' in x'aabbccaabbcc' FROM 3)", 5);
f.checkScalarExact("position(x'bb' in x'aabbccaabbcc' FROM 5)", 5);
f.checkScalarExact("position(x'bb' in x'aabbccaabbcc' FROM 6)", 0);
- f.checkScalarExact("position(x'bb' in x'aabbccaabbcc' FROM -5)", 0);
+ f.checkScalarExact("position(x'bb' in x'aabbccaabbcc' FROM -5)", 2);
f.checkScalarExact("position(x'cc' in x'aabbccdd' FROM 2)", 3);
f.checkScalarExact("position(x'' in x'aabbcc' FROM 3)", 3);
f.checkScalarExact("position(x'' in x'aabbcc' FROM 10)", 0);
@@ -6308,6 +6308,37 @@ public class SqlOperatorTest {
f.checkNull("STRPOS(x'', null)");
}
+ @Test void testInstrFunction() {
+ final SqlOperatorFixture f0 = fixture()
+ .setFor(SqlLibraryOperators.CHR, VM_FENNEL, VM_JAVA);
+ f0.checkFails("^INSTR('abc', 'a', 1, 1)^",
+ "No match found for function signature INSTR\\(<CHARACTER>,
<CHARACTER>,"
+ + " <NUMERIC>, <NUMERIC>\\)", false);
+
+ final Consumer<SqlOperatorFixture> consumer = f -> {
+ // test for CHAR
+ f.checkScalar("INSTR('abc', 'a', 1, 1)", "1", "INTEGER NOT NULL");
+ f.checkScalar("INSTR('abcabc', 'bc', 1, 2)", "5", "INTEGER NOT NULL");
+ f.checkScalar("INSTR('abcabc', 'd', 1, 1)", "0", "INTEGER NOT NULL");
+ f.checkScalar("INSTR('dabcabcd', 'd', 4, 1)", "8", "INTEGER NOT NULL");
+ f.checkScalar("INSTR('abc', '', 1, 1)", "1", "INTEGER NOT NULL");
+ f.checkScalar("INSTR('', 'a', 1, 1)", "0", "INTEGER NOT NULL");
+ f.checkNull("INSTR(null, 'a', 1, 1)");
+ f.checkNull("INSTR('a', null, 1, 1)");
+
+ // test for BINARY
+ f.checkScalar("INSTR(x'2212', x'12', -1, 1)", "2", "INTEGER NOT NULL");
+ f.checkScalar("INSTR(x'2122', x'12', 1, 1)", "0", "INTEGER NOT NULL");
+ f.checkScalar("INSTR(x'122212', x'12', -1, 2)", "1", "INTEGER NOT NULL");
+ f.checkScalar("INSTR(x'1111', x'22', 1, 1)", "0", "INTEGER NOT NULL");
+ f.checkScalar("INSTR(x'2122', x'', 1, 1)", "1", "INTEGER NOT NULL");
+ f.checkScalar("INSTR(x'', x'12', 1, 1)", "0", "INTEGER NOT NULL");
+ f.checkNull("INSTR(null, x'', 1, 1)");
+ f.checkNull("INSTR(x'', null, 1, 1)");
+ };
+ f0.forEachLibrary(list(SqlLibrary.BIG_QUERY, SqlLibrary.ORACLE), consumer);
+ }
+
@Test void testStartsWithFunction() {
final SqlOperatorFixture f = fixture().withLibrary(SqlLibrary.BIG_QUERY);
f.setFor(SqlLibraryOperators.STARTS_WITH);