This is an automated email from the ASF dual-hosted git repository.
mbudiu 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 07852af44c [CALCITE-7068] ElasticSearch adapter support LIKE operator
07852af44c is described below
commit 07852af44c0eb8a98ccf786f845e1ea2766fc5e4
Author: xuzifu666 <[email protected]>
AuthorDate: Mon Jun 23 16:15:27 2025 +0800
[CALCITE-7068] ElasticSearch adapter support LIKE operator
---
.../adapter/elasticsearch/PredicateAnalyzer.java | 21 +++-
.../adapter/elasticsearch/QueryBuilders.java | 85 ++++++++++++-
.../elasticsearch/ElasticSearchAdapterTest.java | 139 ++++++++++++++++++++-
3 files changed, 239 insertions(+), 6 deletions(-)
diff --git
a/elasticsearch/src/main/java/org/apache/calcite/adapter/elasticsearch/PredicateAnalyzer.java
b/elasticsearch/src/main/java/org/apache/calcite/adapter/elasticsearch/PredicateAnalyzer.java
index 897da25e26..cfc8f20d82 100644
---
a/elasticsearch/src/main/java/org/apache/calcite/adapter/elasticsearch/PredicateAnalyzer.java
+++
b/elasticsearch/src/main/java/org/apache/calcite/adapter/elasticsearch/PredicateAnalyzer.java
@@ -348,7 +348,7 @@ private QueryExpression binary(RexCall call) {
checkForIncompatibleDateTimeOperands(call);
- checkState(call.getOperands().size() == 2);
+ checkState(call.getOperands().size() == 2 || call.isA(SqlKind.LIKE));
final Expression a = call.getOperands().get(0).accept(this);
final Expression b = call.getOperands().get(1).accept(this);
@@ -373,7 +373,12 @@ private QueryExpression binary(RexCall call) {
case CONTAINS:
return QueryExpression.create(pair.getKey()).contains(pair.getValue());
case LIKE:
- throw new UnsupportedOperationException("LIKE not yet supported");
+ if (call.getOperands().size() == 3) {
+ final Expression e = call.getOperands().get(2).accept(this);
+ LiteralExpression escape = expressAsLiteral(e);
+ return QueryExpression.create(pair.getKey()).like(pair.getValue(),
escape);
+ }
+ return QueryExpression.create(pair.getKey()).like(pair.getValue());
case EQUALS:
return QueryExpression.create(pair.getKey()).equals(pair.getValue());
case NOT_EQUALS:
@@ -586,6 +591,8 @@ public boolean isPartial() {
public abstract QueryExpression like(LiteralExpression literal);
+ public abstract QueryExpression like(LiteralExpression literal,
LiteralExpression escape);
+
public abstract QueryExpression notLike(LiteralExpression literal);
public abstract QueryExpression equals(LiteralExpression literal);
@@ -698,6 +705,11 @@ private CompoundQueryExpression(boolean partial,
BoolQueryBuilder builder) {
+ "cannot be applied to a compound expression");
}
+ @Override public QueryExpression like(LiteralExpression literal,
LiteralExpression escape) {
+ throw new PredicateAnalyzerException("SqlOperatorImpl ['like'] "
+ + "cannot be applied to a compound expression");
+ }
+
@Override public QueryExpression notLike(LiteralExpression literal) {
throw new PredicateAnalyzerException("SqlOperatorImpl ['notLike'] "
+ "cannot be applied to a compound expression");
@@ -796,6 +808,11 @@ private SimpleQueryExpression(NamedFieldExpression rel) {
return this;
}
+ @Override public QueryExpression like(LiteralExpression literal,
LiteralExpression escape) {
+ builder = regexpQuery(getFieldReference(), literal.stringValue(),
escape.stringValue());
+ return this;
+ }
+
@Override public QueryExpression contains(LiteralExpression literal) {
builder = QueryBuilders.matchQuery(getFieldReference(), literal.value());
return this;
diff --git
a/elasticsearch/src/main/java/org/apache/calcite/adapter/elasticsearch/QueryBuilders.java
b/elasticsearch/src/main/java/org/apache/calcite/adapter/elasticsearch/QueryBuilders.java
index 7b5459e445..17d289499b 100644
---
a/elasticsearch/src/main/java/org/apache/calcite/adapter/elasticsearch/QueryBuilders.java
+++
b/elasticsearch/src/main/java/org/apache/calcite/adapter/elasticsearch/QueryBuilders.java
@@ -20,7 +20,9 @@
import java.io.IOException;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import static java.util.Objects.requireNonNull;
@@ -169,6 +171,17 @@ static RegexpQueryBuilder regexpQuery(String name, String
regexp) {
return new RegexpQueryBuilder(name, regexp);
}
+ /**
+ * A Query that matches documents containing terms with a specified regular
expression.
+ *
+ * @param name The name of the field
+ * @param regexp The regular expression
+ * @param escape The regular escape
+ */
+ static RegexpQueryBuilder regexpQuery(String name, String regexp, String
escape) {
+ return new RegexpQueryBuilder(name, regexp, escape);
+ }
+
/**
* A Query that matches documents matching boolean combinations of other
queries.
@@ -497,14 +510,80 @@ static class RegexpQueryBuilder extends QueryBuilder {
private final String fieldName;
@SuppressWarnings("unused")
private final String value;
+ @SuppressWarnings("unused")
+ private String escape;
RegexpQueryBuilder(final String fieldName, final String value) {
+ this(fieldName, value, "\\");
+ }
+
+ RegexpQueryBuilder(final String fieldName, final String value, final
String escape) {
+ requireNonNull(fieldName, "fieldName");
+ requireNonNull(value, "value");
+ requireNonNull(escape, "escape");
this.fieldName = fieldName;
- this.value = value;
+ this.escape = escape;
+ // replace % to * and _ to ? for sql with like operator
+ HashMap<String, String> kv = new HashMap<>();
+ kv.put("%", "*");
+ kv.put("_", "?");
+ this.value = replaceWildcard(value, kv, escape);
+ }
+
+ public static String replaceWildcard(String value, Map<String, String> kv,
String escape) {
+ ArrayList<String> ret = new ArrayList<>();
+ int escapeCount = 0;
+ for (int index = 0; index < value.length(); index++) {
+ String current = value.substring(index, index + 1);
+ if (index == 0) {
+ if (!current.equals(escape)) {
+ current = kv.keySet().contains(current) ? kv.get(current) :
current;
+ ret.add(current);
+ } else {
+ escapeCount++;
+ }
+ continue;
+ }
+
+ if (!kv.keySet().contains(current) && !current.equals(escape)) {
+ ret.add(current);
+ escapeCount = 0;
+ continue;
+ }
+
+ if (current.equals(escape)) {
+ escapeCount++;
+ if (escapeCount % 2 == 0) {
+ ret.add(current);
+ }
+ continue;
+ }
+
+ String last = value.substring(index - 1, index);
+ if (kv.keySet().contains(current)) {
+ if (!last.equals(escape)) {
+ ret.add(kv.get(current));
+ } else {
+ if (escapeCount % 2 == 0) {
+ ret.add(kv.get(current));
+ } else {
+ ret.add(current);
+ }
+ }
+ escapeCount = 0;
+ }
+ }
+ return String.join("", ret);
}
- @Override void writeJson(final JsonGenerator generator) {
- throw new UnsupportedOperationException();
+ @Override void writeJson(final JsonGenerator generator) throws IOException
{
+ generator.writeStartObject();
+ generator.writeFieldName("wildcard");
+ generator.writeStartObject();
+ generator.writeFieldName(fieldName);
+ writeObject(generator, value);
+ generator.writeEndObject();
+ generator.writeEndObject();
}
}
diff --git
a/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/ElasticSearchAdapterTest.java
b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/ElasticSearchAdapterTest.java
index b3e769accf..eb112e5138 100644
---
a/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/ElasticSearchAdapterTest.java
+++
b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/ElasticSearchAdapterTest.java
@@ -188,6 +188,8 @@ private CalciteAssert.AssertThat calciteAssert() {
assertNotNull(esSchmea);
}
+ /** Test for <a
href="https://issues.apache.org/jira/browse/CALCITE-7068">[CALCITE-7068]
+ * ElasticSearch adapter support LIKE operator</a>. */
@Test void basic() {
calciteAssert()
// by default elastic returns max 10 records
@@ -214,11 +216,146 @@ private CalciteAssert.AssertThat calciteAssert() {
.query("select * from elastic.zips where _MAP['CITY'] = 'BROOKLYN'")
.returnsCount(0);
-
// limit 0
calciteAssert()
.query("select * from elastic.zips limit 0")
.returnsCount(0);
+
+ // test with ESCAPE
+ calciteAssert()
+ // this case would covnert to like 'BRO%IK*', match no one, count is 0
+ .query(
+ "select * from elastic.zips where _MAP['city'] like 'BRO\\%OK%'
ESCAPE '\\' limit 10")
+ .returnsOrdered("")
+ .returnsCount(0);
+
+ calciteAssert()
+ // this case would covnert to like 'BRO*IK*', match one result, count
is 1
+ .query(
+ "select * from elastic.zips where _MAP['city'] like 'B\\R\\O%OK%'
ESCAPE '\\' limit 10")
+ .returnsOrdered(
+
+ "_MAP={id=11226, city=BROOKLYN, loc=[-73.956985, 40.646694],
pop=111396, state=NY}")
+ .returnsCount(1);
+
+
+ calciteAssert()
+ // this case would covnert to like 'BROIK*', match one result, count
is 1
+ .query(
+ "select * from elastic.zips where _MAP['city'] like 'BRO!OK%'
ESCAPE '!' limit 10")
+ .returnsOrdered(
+
+ "_MAP={id=11226, city=BROOKLYN, loc=[-73.956985, 40.646694],
pop=111396, state=NY}")
+ .returnsCount(1);
+
+ calciteAssert()
+ // this case would covnert to like 'BR!OIK*', match no one, count is 0
+ .query(
+ "select * from elastic.zips where _MAP['city'] like 'BRO!!OK%'
ESCAPE '!' limit 10")
+ .returnsOrdered("")
+ .returnsCount(0);
+
+ // test with %
+ calciteAssert()
+ .query("select * from elastic.zips where _MAP['city'] like 'BROOK%'
limit 10")
+ .returnsOrdered(
+ "_MAP={id=11226, city=BROOKLYN, loc=[-73.956985, 40.646694],
pop=111396, state=NY}")
+ .returnsCount(1);
+
+ calciteAssert()
+ .query("select * from elastic.zips where _MAP['city'] like '%ROOK%'
limit 10")
+ .returnsOrdered(
+ "_MAP={id=11226, city=BROOKLYN, loc=[-73.956985, 40.646694],
pop=111396, state=NY}")
+ .returnsCount(1);
+
+ // test with _
+ calciteAssert()
+ .query("select * from elastic.zips where _MAP['city'] like 'BROOKLY_'
limit 10")
+ .returnsOrdered(
+ "_MAP={id=11226, city=BROOKLYN, loc=[-73.956985, 40.646694],
pop=111396, state=NY}")
+ .returnsCount(1);
+
+ calciteAssert()
+ .query("select * from elastic.zips where _MAP['city'] like 'BROOKL_'
limit 10")
+ .returnsOrdered("")
+ .returnsCount(0);
+
+ calciteAssert()
+ .query("select * from elastic.zips where _MAP['city'] like 'BROOKL__'
limit 10")
+ .returnsOrdered(
+ "_MAP={id=11226, city=BROOKLYN, loc=[-73.956985, 40.646694],
pop=111396, state=NY}")
+ .returnsCount(1);
+
+ calciteAssert()
+ .query("select * from elastic.zips where _MAP['city'] like '_ROOKLY_'
limit 10")
+ .returnsOrdered(
+ "_MAP={id=11226, city=BROOKLYN, loc=[-73.956985, 40.646694],
pop=111396, state=NY}")
+ .returnsCount(1);
+
+ calciteAssert()
+ .query("select * from elastic.zips where _MAP['city'] like '_ROO*Y_'
limit 10")
+ .returnsOrdered(
+ "_MAP={id=11226, city=BROOKLYN, loc=[-73.956985, 40.646694],
pop=111396, state=NY}")
+ .returnsCount(1);
+ }
+
+
+ /**
+ * A test for ReplaceWildcard with escape.
+ */
+ @Test void testReplaceWildcard() {
+ HashMap<String, String> kv = new HashMap<>();
+ kv.put("%", "*");
+ kv.put("_", "?");
+
+ String value = "aa\\%b%";
+ String source = "%";
+ String target = "*";
+ String escape = "\\";
+ assertEquals(QueryBuilders.RegexpQueryBuilder.replaceWildcard(value, kv,
escape),
+ "aa%b*");
+
+ value = "aa\\\\%b%";
+ source = "%";
+ target = "*";
+ escape = "\\";
+ assertEquals(QueryBuilders.RegexpQueryBuilder.replaceWildcard(value, kv,
escape),
+ "aa\\*b*");
+
+ value = "aa\\\\\\%b%";
+ source = "%";
+ target = "*";
+ escape = "\\";
+ assertEquals(QueryBuilders.RegexpQueryBuilder.replaceWildcard(value, kv,
escape),
+ "aa\\%b*");
+
+ value = "aa!%b%";
+ source = "%";
+ target = "*";
+ escape = "!";
+ assertEquals(QueryBuilders.RegexpQueryBuilder.replaceWildcard(value, kv,
escape),
+ "aa%b*");
+
+ value = "aa!!%b%";
+ source = "%";
+ target = "*";
+ escape = "!";
+ assertEquals(QueryBuilders.RegexpQueryBuilder.replaceWildcard(value, kv,
escape),
+ "aa!*b*");
+
+ value = "aa!!!!%b%";
+ source = "%";
+ target = "*";
+ escape = "!";
+ assertEquals(QueryBuilders.RegexpQueryBuilder.replaceWildcard(value, kv,
escape),
+ "aa!!*b*");
+
+ value = "aa!!!%b%";
+ source = "%";
+ target = "*";
+ escape = "!";
+ assertEquals(QueryBuilders.RegexpQueryBuilder.replaceWildcard(value, kv,
escape),
+ "aa!%b*");
}
@Test void testAlias() {