From 407357a1addf99367d3f996f47d255ef1c75dc2f Mon Sep 17 00:00:00 2001 From: Peng Huo Date: Fri, 4 Oct 2019 11:23:36 -0700 Subject: [PATCH] Support NOT EXISTS for nested query (#200) --- .../sql/domain/Condition.java | 2 + .../sql/query/maker/Maker.java | 4 +- .../sql/rewriter/nestedfield/SQLClause.java | 3 + .../sql/rewriter/nestedfield/Where.java | 14 ++-- .../rewriter/parent/SQLExprParentSetter.java | 10 +++ .../rewriter/subquery/RewriterContext.java | 8 +- .../rewriter/subquery/SubQueryRewriter.java | 8 +- .../rewriter/NestedExistsRewriter.java | 29 ++++--- .../sql/esintgtest/SubqueryIT.java | 51 +++++++++++- .../sql/unittest/NestedFieldRewriterTest.java | 78 +++++++++++++++++-- .../subquery/ExistsSubQueryRewriterTest.java | 56 ++++++++++--- 11 files changed, 221 insertions(+), 42 deletions(-) diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/sql/domain/Condition.java b/src/main/java/com/amazon/opendistroforelasticsearch/sql/domain/Condition.java index ce9d9b0f4e..3aee248189 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/sql/domain/Condition.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/sql/domain/Condition.java @@ -58,6 +58,7 @@ public enum OPEAR { TERM, IDS_QUERY, NESTED_COMPLEX, + NOT_EXISTS_NESTED_COMPLEX, CHILDREN_COMPLEX, SCRIPT, NIN_TERMS, @@ -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 { diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/sql/query/maker/Maker.java b/src/main/java/com/amazon/opendistroforelasticsearch/sql/query/maker/Maker.java index ff58120b0f..3a478a0194 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/sql/query/maker/Maker.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/sql/query/maker/Maker.java @@ -88,7 +88,8 @@ public abstract class Maker { private static final Set 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) { @@ -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"); } diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/nestedfield/SQLClause.java b/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/nestedfield/SQLClause.java index b011810cc2..f1116b3bff 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/nestedfield/SQLClause.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/nestedfield/SQLClause.java @@ -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; @@ -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); } diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/nestedfield/Where.java b/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/nestedfield/Where.java index c7b18d600a..dab973daa9 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/nestedfield/Where.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/nestedfield/Where.java @@ -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; /** @@ -46,7 +47,7 @@ void rewrite(Scope scope) { right().mergeNestedField(scope); } } - mergeIfHaveTagAndIsRootOfWhere(scope); + mergeIfHaveTagAndIsRootOfWhereOrNot(scope); } private boolean isLeftChildCondition() { @@ -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); } } diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/parent/SQLExprParentSetter.java b/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/parent/SQLExprParentSetter.java index f9fa675926..06dee94ca2 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/parent/SQLExprParentSetter.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/parent/SQLExprParentSetter.java @@ -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; @@ -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; + } } diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/subquery/RewriterContext.java b/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/subquery/RewriterContext.java index aa0ec2333a..d06a4a728b 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/subquery/RewriterContext.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/subquery/RewriterContext.java @@ -34,7 +34,7 @@ */ public class RewriterContext { private final Deque tableStack = new ArrayDeque<>(); - private final Deque binaryOpStack = new ArrayDeque<>(); + private final Deque conditionStack = new ArrayDeque<>(); private final List sqlInSubQueryExprs = new ArrayList<>(); private final List sqlExistsExprs = new ArrayList<>(); private final NestedQueryContext nestedQueryDetector = new NestedQueryContext(); @@ -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); } /** diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/subquery/SubQueryRewriter.java b/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/subquery/SubQueryRewriter.java index 7f2ae20d59..ae911b87ba 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/subquery/SubQueryRewriter.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/subquery/SubQueryRewriter.java @@ -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; } diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/subquery/rewriter/NestedExistsRewriter.java b/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/subquery/rewriter/NestedExistsRewriter.java index 7571740293..28e1a65e7d 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/subquery/rewriter/NestedExistsRewriter.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/sql/rewriter/subquery/rewriter/NestedExistsRewriter.java @@ -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; @@ -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; diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/sql/esintgtest/SubqueryIT.java b/src/test/java/com/amazon/opendistroforelasticsearch/sql/esintgtest/SubqueryIT.java index 4ac6ada378..da6c5b07d0 100644 --- a/src/test/java/com/amazon/opendistroforelasticsearch/sql/esintgtest/SubqueryIT.java +++ b/src/test/java/com/amazon/opendistroforelasticsearch/sql/esintgtest/SubqueryIT.java @@ -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")) + ) + ); } } diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/sql/unittest/NestedFieldRewriterTest.java b/src/test/java/com/amazon/opendistroforelasticsearch/sql/unittest/NestedFieldRewriterTest.java index 6ef675aa3f..e13a991296 100644 --- a/src/test/java/com/amazon/opendistroforelasticsearch/sql/unittest/NestedFieldRewriterTest.java +++ b/src/test/java/com/amazon/opendistroforelasticsearch/sql/unittest/NestedFieldRewriterTest.java @@ -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; @@ -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')") ); } @@ -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( @@ -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))); @@ -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; } diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/sql/unittest/rewriter/subquery/ExistsSubQueryRewriterTest.java b/src/test/java/com/amazon/opendistroforelasticsearch/sql/unittest/rewriter/subquery/ExistsSubQueryRewriterTest.java index 9eed1e943d..cc0234b757 100644 --- a/src/test/java/com/amazon/opendistroforelasticsearch/sql/unittest/rewriter/subquery/ExistsSubQueryRewriterTest.java +++ b/src/test/java/com/amazon/opendistroforelasticsearch/sql/unittest/rewriter/subquery/ExistsSubQueryRewriterTest.java @@ -32,7 +32,7 @@ public void nonCorrelatedExists() { sqlString(expr( "SELECT e.name " + "FROM employee e, e.projects p " + - "WHERE p IS NOT NULL")), + "WHERE p IS NOT MISSING")), sqlString(rewrite(expr( "SELECT e.name " + "FROM employee as e " + @@ -46,7 +46,7 @@ public void nonCorrelatedExistsWhere() { sqlString(expr( "SELECT e.name " + "FROM employee e, e.projects p " + - "WHERE p IS NOT NULL AND p.name LIKE 'security'")), + "WHERE p IS NOT MISSING AND p.name LIKE 'security'")), sqlString(rewrite(expr( "SELECT e.name " + "FROM employee as e " + @@ -60,7 +60,7 @@ public void nonCorrelatedExistsParentWhere() { sqlString(expr( "SELECT e.name " + "FROM employee e, e.projects p " + - "WHERE p IS NOT NULL AND e.name LIKE 'security'")), + "WHERE p IS NOT MISSING AND e.name LIKE 'security'")), sqlString(rewrite(expr( "SELECT e.name " + "FROM employee as e " + @@ -69,23 +69,55 @@ public void nonCorrelatedExistsParentWhere() { } @Test - public void nonCorrlatedExistsAnd() { - exceptionRule.expect(IllegalStateException.class); - exceptionRule.expectMessage("Unsupported subquery"); - rewrite(expr( - "SELECT e.name " + - "FROM employee as e " + - "WHERE EXISTS (SELECT * FROM e.projects as p) AND EXISTS (SELECT * FROM e.comments as c)")); + public void nonCorrelatedNotExists() { + assertEquals( + sqlString(expr( + "SELECT e.name " + + "FROM employee e, e.projects p " + + "WHERE NOT (p IS NOT MISSING)")), + sqlString(rewrite(expr( + "SELECT e.name " + + "FROM employee as e " + + "WHERE NOT EXISTS (SELECT * FROM e.projects as p)"))) + ); } @Test - public void nonCorrlatedNotExistsUnsupported() throws Exception { + public void nonCorrelatedNotExistsWhere() { + assertEquals( + sqlString(expr( + "SELECT e.name " + + "FROM employee e, e.projects p " + + "WHERE NOT (p IS NOT MISSING AND p.name LIKE 'security')")), + sqlString(rewrite(expr( + "SELECT e.name " + + "FROM employee as e " + + "WHERE NOT EXISTS (SELECT * FROM e.projects as p WHERE p.name LIKE 'security')"))) + ); + } + + @Test + public void nonCorrelatedNotExistsParentWhere() { + assertEquals( + sqlString(expr( + "SELECT e.name " + + "FROM employee e, e.projects p " + + "WHERE NOT (p IS NOT MISSING) AND e.name LIKE 'security'")), + sqlString(rewrite(expr( + "SELECT e.name " + + "FROM employee as e " + + "WHERE NOT EXISTS (SELECT * FROM e.projects as p) AND e.name LIKE 'security'"))) + ); + } + + @Test + public void nonCorrelatedExistsAnd() { exceptionRule.expect(IllegalStateException.class); exceptionRule.expectMessage("Unsupported subquery"); rewrite(expr( "SELECT e.name " + "FROM employee as e " + - "WHERE NOT EXISTS (SELECT * FROM e.projects as p)")); + "WHERE EXISTS (SELECT * FROM e.projects as p) AND EXISTS (SELECT * FROM e.comments as c)")); } } \ No newline at end of file