Skip to content
This repository has been archived by the owner on Aug 2, 2022. It is now read-only.

Commit

Permalink
Support NOT EXISTS for nested query (#200)
Browse files Browse the repository at this point in the history
  • Loading branch information
penghuo authored Oct 4, 2019
1 parent 6ff810f commit 407357a
Show file tree
Hide file tree
Showing 11 changed files with 221 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public enum OPEAR {
TERM,
IDS_QUERY,
NESTED_COMPLEX,
NOT_EXISTS_NESTED_COMPLEX,
CHILDREN_COMPLEX,
SCRIPT,
NIN_TERMS,
Expand Down Expand Up @@ -133,6 +134,7 @@ public enum OPEAR {
negatives.put(IS, ISN);
negatives.put(IN, NIN);
negatives.put(BETWEEN, NBETWEEN);
negatives.put(NESTED_COMPLEX, NOT_EXISTS_NESTED_COMPLEX);
}

static {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ public abstract class Maker {

private static final Set<Condition.OPEAR> NOT_OPEAR_SET = ImmutableSet.of(
Condition.OPEAR.N, Condition.OPEAR.NIN, Condition.OPEAR.ISN, Condition.OPEAR.NBETWEEN,
Condition.OPEAR.NLIKE, Condition.OPEAR.NIN_TERMS, Condition.OPEAR.NTERM
Condition.OPEAR.NLIKE, Condition.OPEAR.NIN_TERMS, Condition.OPEAR.NTERM,
Condition.OPEAR.NOT_EXISTS_NESTED_COMPLEX
);

protected Maker(Boolean isQuery) {
Expand Down Expand Up @@ -319,6 +320,7 @@ private ToXContent make(Condition cond, String name, Object value) throws SqlPar
toXContent = QueryBuilders.idsQuery(type).addIds(ids);
break;
case NESTED_COMPLEX:
case NOT_EXISTS_NESTED_COMPLEX:
if (value == null || !(value instanceof Where)) {
throw new SqlParseException("unsupported nested condition");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.alibaba.druid.sql.ast.expr.SQLCharExpr;
import com.alibaba.druid.sql.ast.expr.SQLInSubQueryExpr;
import com.alibaba.druid.sql.ast.expr.SQLMethodInvokeExpr;
import com.alibaba.druid.sql.ast.expr.SQLNotExpr;
import com.alibaba.druid.sql.ast.statement.SQLSelectItem;
import com.alibaba.druid.sql.ast.statement.SQLSelectOrderByItem;
import com.alibaba.druid.sql.dialect.mysql.ast.expr.MySqlSelectGroupByExpr;
Expand Down Expand Up @@ -83,6 +84,8 @@ SQLMethodInvokeExpr replaceByNestedFunction(SQLExpr expr) {
}
} else if (parent instanceof MySqlSelectQueryBlock) {
((MySqlSelectQueryBlock) parent).setWhere(nestedFunc);
} else if (parent instanceof SQLNotExpr) {
((SQLNotExpr) parent).setExpr(nestedFunc);
} else {
throw new IllegalStateException("Unsupported place to use nested field under parent: " + parent);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import com.alibaba.druid.sql.ast.expr.SQLBinaryOpExpr;
import com.alibaba.druid.sql.ast.expr.SQLCharExpr;
import com.alibaba.druid.sql.ast.expr.SQLNotExpr;
import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlSelectQueryBlock;

/**
Expand Down Expand Up @@ -46,7 +47,7 @@ void rewrite(Scope scope) {
right().mergeNestedField(scope);
}
}
mergeIfHaveTagAndIsRootOfWhere(scope);
mergeIfHaveTagAndIsRootOfWhereOrNot(scope);
}

private boolean isLeftChildCondition() {
Expand All @@ -64,11 +65,14 @@ private void useAnyChildTag(Scope scope) {
}

/**
* Merge anyway if the root of WHERE clause be reached
* Merge anyway if the root of WHERE clause or {@link SQLNotExpr} be reached.
*/
private void mergeIfHaveTagAndIsRootOfWhere(Scope scope) {
if (!scope.getConditionTag(expr).isEmpty()
&& expr.getParent() instanceof MySqlSelectQueryBlock) {
private void mergeIfHaveTagAndIsRootOfWhereOrNot(Scope scope) {
if (scope.getConditionTag(expr).isEmpty()) {
return;
}
if (expr.getParent() instanceof MySqlSelectQueryBlock
|| expr.getParent() instanceof SQLNotExpr) {
mergeNestedField(scope);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import com.alibaba.druid.sql.ast.expr.SQLInListExpr;
import com.alibaba.druid.sql.ast.expr.SQLInSubQueryExpr;
import com.alibaba.druid.sql.ast.expr.SQLNotExpr;
import com.alibaba.druid.sql.ast.expr.SQLQueryExpr;
import com.alibaba.druid.sql.dialect.mysql.visitor.MySqlASTVisitorAdapter;

Expand All @@ -42,4 +43,13 @@ public boolean visit(SQLInListExpr expr) {
expr.getExpr().setParent(expr);
return true;
}

/**
* Fix the expr in {@link SQLNotExpr} without parent.
*/
@Override
public boolean visit(SQLNotExpr notExpr) {
notExpr.getExpr().setParent(notExpr);
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
*/
public class RewriterContext {
private final Deque<SQLTableSource> tableStack = new ArrayDeque<>();
private final Deque<SQLBinaryOpExpr> binaryOpStack = new ArrayDeque<>();
private final Deque<SQLExpr> conditionStack = new ArrayDeque<>();
private final List<SQLInSubQueryExpr> sqlInSubQueryExprs = new ArrayList<>();
private final List<SQLExistsExpr> sqlExistsExprs = new ArrayList<>();
private final NestedQueryContext nestedQueryDetector = new NestedQueryContext();
Expand All @@ -44,11 +44,11 @@ public SQLTableSource popJoin() {
}

public SQLExpr popWhere() {
return binaryOpStack.pop();
return conditionStack.pop();
}

public void addWhere(SQLBinaryOpExpr expr) {
binaryOpStack.push(expr);
public void addWhere(SQLExpr expr) {
conditionStack.push(expr);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,12 @@ private SQLExpr convertWhere(SQLExpr expr) {
return ctx.popWhere();
} else if (expr instanceof SQLBinaryOpExpr) {
SQLBinaryOpExpr binaryOpExpr = (SQLBinaryOpExpr) expr;
binaryOpExpr.setLeft(convertWhere(binaryOpExpr.getLeft()));
binaryOpExpr.setRight(convertWhere(binaryOpExpr.getRight()));
SQLExpr left = convertWhere(binaryOpExpr.getLeft());
left.setParent(binaryOpExpr);
binaryOpExpr.setLeft(left);
SQLExpr right = convertWhere(binaryOpExpr.getRight());
right.setParent(binaryOpExpr);
binaryOpExpr.setRight(right);
}
return expr;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import com.alibaba.druid.sql.ast.expr.SQLBinaryOperator;
import com.alibaba.druid.sql.ast.expr.SQLExistsExpr;
import com.alibaba.druid.sql.ast.expr.SQLIdentifierExpr;
import com.alibaba.druid.sql.ast.expr.SQLNullExpr;
import com.alibaba.druid.sql.ast.expr.SQLNotExpr;
import com.alibaba.druid.sql.ast.statement.SQLExprTableSource;
import com.alibaba.druid.sql.ast.statement.SQLJoinTableSource.JoinType;
import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlSelectQueryBlock;
Expand Down Expand Up @@ -60,32 +60,43 @@ public NestedExistsRewriter(SQLExistsExpr existsExpr, RewriterContext board) {
}

/**
* The from table must be nested field and
* The NOT EXISTS is not supported yet.
* The from table must be nested field.
*/
@Override
public boolean canRewrite() {
return ctx.isNestedQuery(from) && !existsExpr.isNot();
return ctx.isNestedQuery(from);
}

@Override
public void rewrite() {
ctx.addJoin(from, JoinType.COMMA);
ctx.addWhere(rewriteExistsWhere());
}

SQLBinaryOpExpr nullOp = generateNullOp();
private SQLExpr rewriteExistsWhere() {
SQLBinaryOpExpr translatedWhere;
SQLBinaryOpExpr notMissingOp = buildNotMissingOp();
if (null == where) {
ctx.addWhere(nullOp);
translatedWhere = notMissingOp;
} else if (where instanceof SQLBinaryOpExpr) {
ctx.addWhere(and(nullOp, (SQLBinaryOpExpr) where));
translatedWhere = and(notMissingOp, (SQLBinaryOpExpr) where);
} else {
throw new IllegalStateException("unsupported expression in where " + where.getClass());
}

if (existsExpr.isNot()) {
SQLNotExpr sqlNotExpr = new SQLNotExpr(translatedWhere);
translatedWhere.setParent(sqlNotExpr);
return sqlNotExpr;
} else {
return translatedWhere;
}
}

private SQLBinaryOpExpr generateNullOp() {
private SQLBinaryOpExpr buildNotMissingOp() {
SQLBinaryOpExpr binaryOpExpr = new SQLBinaryOpExpr();
binaryOpExpr.setLeft(new SQLIdentifierExpr(from.getAlias()));
binaryOpExpr.setRight(new SQLNullExpr());
binaryOpExpr.setRight(new SQLIdentifierExpr("MISSING"));
binaryOpExpr.setOperator(SQLBinaryOperator.IsNot);

return binaryOpExpr;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,14 +214,57 @@ public void nonCorrelatedExistsParentWhere() throws IOException {
}

@Test
public void nonCorrelatedNotExistsUnsupported() throws IOException {
exceptionRule.expect(ResponseException.class);
exceptionRule.expectMessage("Unsupported subquery");
public void nonCorrelatedNotExists() throws IOException {
String query = String.format(Locale.ROOT,
"SELECT e.name " +
"FROM %s as e " +
"WHERE NOT EXISTS (SELECT * FROM e.projects as p)",
TEST_INDEX_EMPLOYEE_NESTED);
executeQuery(query);

JSONObject response = executeQuery(query);
assertThat(
response,
hitAll(
kvString("/_source/name", is("Susan Smith")),
kvString("/_source/name", is("John Doe"))
)
);
}

@Test
public void nonCorrelatedNotExistsWhere() throws IOException {
String query = String.format(Locale.ROOT,
"SELECT e.name " +
"FROM %s as e " +
"WHERE NOT EXISTS (SELECT * FROM e.projects as p WHERE p.name LIKE 'aurora')",
TEST_INDEX_EMPLOYEE_NESTED);

JSONObject response = executeQuery(query);
assertThat(
response,
hitAll(
kvString("/_source/name", is("Susan Smith")),
kvString("/_source/name", is("Jane Smith")),
kvString("/_source/name", is("John Doe"))
)
);
}

@Test
public void nonCorrelatedNotExistsParentWhere() throws IOException {
String query = String.format(Locale.ROOT,
"SELECT e.name " +
"FROM %s as e " +
"WHERE NOT EXISTS (SELECT * FROM e.projects as p WHERE p.name LIKE 'security') " +
"AND e.name LIKE 'smith'",
TEST_INDEX_EMPLOYEE_NESTED);

JSONObject response = executeQuery(query);
assertThat(
response,
hitAll(
kvString("/_source/name", is("Susan Smith"))
)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import com.alibaba.druid.sql.ast.statement.SQLUnionQuery;
import com.alibaba.druid.sql.dialect.mysql.ast.expr.MySqlSelectGroupByExpr;
import com.amazon.opendistroforelasticsearch.sql.rewriter.nestedfield.NestedFieldRewriter;
import com.amazon.opendistroforelasticsearch.sql.util.SqlParserUtils;
import org.junit.Test;

import java.util.List;
Expand Down Expand Up @@ -250,16 +251,24 @@ public void subQueryWitSameAlias() {
@Test
public void isNotNull() {
same(
query("SELECT e.name FROM employee as e, e.projects as p WHERE p IS NOT NULL"),
query("SELECT name FROM employee WHERE nested(projects, 'projects') IS NOT NULL")
query("SELECT e.name " +
"FROM employee as e, e.projects as p " +
"WHERE p IS NOT MISSING"),
query("SELECT name " +
"FROM employee " +
"WHERE nested(projects, 'projects') IS NOT MISSING")
);
}

@Test
public void isNotNullAndCondition() {
same(
query("SELECT e.name FROM employee as e, e.projects as p WHERE p IS NOT NULL AND p.name LIKE 'security'"),
query("SELECT name FROM employee WHERE nested('projects', projects IS NOT NULL AND projects.name LIKE 'security')")
query("SELECT e.name " +
"FROM employee as e, e.projects as p " +
"WHERE p IS NOT MISSING AND p.name LIKE 'security'"),
query("SELECT name " +
"FROM employee " +
"WHERE nested('projects', projects IS NOT MISSING AND projects.name LIKE 'security')")
);
}

Expand All @@ -271,6 +280,18 @@ public void multiCondition() {
);
}

@Test
public void nestedAndParentCondition() {
same(
query("SELECT name " +
"FROM employee " +
"WHERE nested(projects, 'projects') IS NOT MISSING AND name LIKE 'security'"),
query("SELECT e.name " +
"FROM employee e, e.projects p " +
"WHERE p IS NOT MISSING AND e.name LIKE 'security'")
);
}

@Test
public void aggWithWhereOnParent() {
same(
Expand Down Expand Up @@ -465,6 +486,53 @@ public void aggInHavingWithWhereOnNestedOrNested() {
);
}

@Test
public void notIsNotNull() {
same(
query("SELECT name " +
"FROM employee " +
"WHERE not (nested(projects, 'projects') IS NOT MISSING)"),
query("SELECT e.name " +
"FROM employee as e, e.projects as p " +
"WHERE not (p IS NOT MISSING)")
);
}

@Test
public void notIsNotNullAndCondition() {
same(
query("SELECT e.name " +
"FROM employee as e, e.projects as p " +
"WHERE not (p IS NOT MISSING AND p.name LIKE 'security')"),
query("SELECT name " +
"FROM employee " +
"WHERE not nested('projects', projects IS NOT MISSING AND projects.name LIKE 'security')")
);
}

@Test
public void notMultiCondition() {
same(
query("SELECT name " +
"FROM employee " +
"WHERE not nested('projects', projects.year = 2016 AND projects.name LIKE 'security')"),
query("SELECT e.name " +
"FROM employee as e, e.projects as p " +
"WHERE not (p.year = 2016 and p.name LIKE 'security')")
);
}

@Test
public void notNestedAndParentCondition() {
same(
query("SELECT name " +
"FROM employee " +
"WHERE (not nested(projects, 'projects') IS NOT MISSING) AND name LIKE 'security'"),
query("SELECT e.name " +
"FROM employee e, e.projects p " +
"WHERE not (p IS NOT MISSING) AND e.name LIKE 'security'")
);
}

private void noImpact(String sql) {
same(parse(sql), rewrite(parse(sql)));
Expand Down Expand Up @@ -585,7 +653,7 @@ private void assertTable(SQLTableSource expect, SQLTableSource actual) {
* @return Node parsed out of sql
*/
private SQLQueryExpr query(String sql) {
SQLQueryExpr expr = parse(sql);
SQLQueryExpr expr = SqlParserUtils.parse(sql);
if (sql.contains("nested")) {
return expr;
}
Expand Down
Loading

0 comments on commit 407357a

Please sign in to comment.