Skip to content

Commit 649ce41

Browse files
authored
[Enhancement] Add error handling for known limitation of sql JOIN (#4344)
1 parent b049ac1 commit 649ce41

File tree

4 files changed

+99
-1
lines changed

4 files changed

+99
-1
lines changed

legacy/src/main/java/org/opensearch/sql/legacy/antlr/visitor/AntlrSqlParseTreeVisitor.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
import org.opensearch.sql.legacy.antlr.parser.OpenSearchLegacySqlParser.SubqueryTableItemContext;
5454
import org.opensearch.sql.legacy.antlr.parser.OpenSearchLegacySqlParser.TableNamePatternContext;
5555
import org.opensearch.sql.legacy.antlr.parser.OpenSearchLegacySqlParserBaseVisitor;
56+
import org.opensearch.sql.legacy.exception.SqlParseException;
57+
import org.opensearch.sql.legacy.utils.Util;
5658

5759
/** ANTLR parse tree visitor to drive the analysis process. */
5860
public class AntlrSqlParseTreeVisitor<T extends Reducible>
@@ -264,6 +266,42 @@ public T visitFunctionNameBase(FunctionNameBaseContext ctx) {
264266
return visitor.visitFunctionName(ctx.getText());
265267
}
266268

269+
@Override
270+
public T visitGroupByItem(OpenSearchLegacySqlParser.GroupByItemContext ctx) {
271+
ParserRuleContext fromClause = ctx.getParent();
272+
273+
boolean hasJoin = detectJoinInFromClause(fromClause);
274+
275+
if (hasJoin) {
276+
String errorMessage =
277+
Util.JOIN_AGGREGATION_ERROR_PREFIX
278+
+ Util.DOC_REDIRECT_MESSAGE
279+
+ Util.getJoinAggregationDocumentationUrl(AntlrSqlParseTreeVisitor.class);
280+
throw new RuntimeException(errorMessage, new SqlParseException(errorMessage));
281+
}
282+
return super.visitGroupByItem(ctx);
283+
}
284+
285+
boolean detectJoinInFromClause(ParserRuleContext fromClause) {
286+
return fromClause.accept(
287+
new OpenSearchLegacySqlParserBaseVisitor<Boolean>() {
288+
@Override
289+
public Boolean visitTableSourceBase(TableSourceBaseContext ctx) {
290+
return !ctx.joinPart().isEmpty();
291+
}
292+
293+
@Override
294+
protected Boolean defaultResult() {
295+
return false;
296+
}
297+
298+
@Override
299+
protected Boolean aggregateResult(Boolean aggregate, Boolean nextResult) {
300+
return aggregate || nextResult;
301+
}
302+
});
303+
}
304+
267305
@Override
268306
public T visitBinaryComparisonPredicate(BinaryComparisonPredicateContext ctx) {
269307
if (isNamedArgument(ctx)) { // Essentially named argument is assign instead of comparison

legacy/src/main/java/org/opensearch/sql/legacy/parser/SqlParser.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.opensearch.sql.legacy.domain.hints.HintFactory;
4949
import org.opensearch.sql.legacy.exception.SqlParseException;
5050
import org.opensearch.sql.legacy.query.multi.MultiQuerySelect;
51+
import org.opensearch.sql.legacy.utils.Util;
5152

5253
/**
5354
* OpenSearch sql support
@@ -365,6 +366,15 @@ public JoinSelect parseJoinSelect(SQLQueryExpr sqlExpr) throws SqlParseException
365366

366367
MySqlSelectQueryBlock query = (MySqlSelectQueryBlock) sqlExpr.getSubQuery().getQuery();
367368

369+
// Check for JOIN + GROUP BY combination and throw error
370+
if (query.getGroupBy() != null && !query.getGroupBy().getItems().isEmpty()) {
371+
String errorMessage =
372+
Util.JOIN_AGGREGATION_ERROR_PREFIX
373+
+ Util.DOC_REDIRECT_MESSAGE
374+
+ Util.getJoinAggregationDocumentationUrl(SqlParser.class);
375+
throw new SqlParseException(errorMessage);
376+
}
377+
368378
List<From> joinedFrom = findJoinedFrom(query.getFrom());
369379
if (joinedFrom.size() != 2) {
370380
throw new RuntimeException("currently supports only 2 tables join");
@@ -399,7 +409,6 @@ public JoinSelect parseJoinSelect(SQLQueryExpr sqlExpr) throws SqlParseException
399409

400410
updateJoinLimit(query.getLimit(), joinSelect);
401411

402-
// todo: throw error feature not supported: no group bys on joins ?
403412
return joinSelect;
404413
}
405414

legacy/src/main/java/org/opensearch/sql/legacy/utils/Util.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ public class Util {
4141

4242
public static final String NESTED_JOIN_TYPE = "NestedJoinType";
4343

44+
public static final String JOIN_AGGREGATION_ERROR_PREFIX =
45+
"JOIN queries do not support aggregations on the joined result.";
46+
47+
public static final String DOC_REDIRECT_MESSAGE = " For more information, see ";
48+
49+
public static final String OPENSEARCH_DOC_BASE_URL = "https://docs.opensearch.org/";
50+
51+
public static final String LIMITATION_DOC_PATH = "/search-plugins/sql/limitation/";
52+
4453
public static String joiner(List<KVValue> lists, String oper) {
4554

4655
if (lists.size() == 0) {
@@ -280,4 +289,33 @@ public static SQLExpr toSqlExpr(String sql) {
280289
}
281290
return expr;
282291
}
292+
293+
/**
294+
* Gets the OpenSearch major.minor version for documentation links. Converts "x.y.z" format to
295+
* "x.y".
296+
*
297+
* @param clazz The class to get package implementation version from
298+
* @return The major.minor version string, or "latest" if version cannot be determined
299+
*/
300+
public static String getDocumentationVersion(Class<?> clazz) {
301+
String version = clazz.getPackage().getImplementationVersion();
302+
if (version == null) {
303+
return "latest";
304+
}
305+
String[] parts = version.split("\\.");
306+
if (parts.length >= 2) {
307+
return parts[0] + "." + parts[1];
308+
}
309+
return "latest";
310+
}
311+
312+
/**
313+
* Builds a complete OpenSearch documentation URL for JOIN aggregation limitation.
314+
*
315+
* @param clazz The class to get package implementation version from
316+
* @return Complete documentation URL with version
317+
*/
318+
public static String getJoinAggregationDocumentationUrl(Class<?> clazz) {
319+
return OPENSEARCH_DOC_BASE_URL + getDocumentationVersion(clazz) + LIMITATION_DOC_PATH;
320+
}
283321
}

legacy/src/test/java/org/opensearch/sql/legacy/antlr/visitor/AntlrSqlParseTreeVisitorTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.opensearch.sql.legacy.antlr.SqlAnalysisConfig;
2121
import org.opensearch.sql.legacy.antlr.semantic.scope.SemanticContext;
2222
import org.opensearch.sql.legacy.antlr.semantic.types.Type;
23+
import org.opensearch.sql.legacy.antlr.semantic.types.base.OpenSearchIndex;
2324
import org.opensearch.sql.legacy.antlr.semantic.types.special.Product;
2425
import org.opensearch.sql.legacy.antlr.semantic.visitor.TypeChecker;
2526
import org.opensearch.sql.legacy.exception.SqlFeatureNotImplementedException;
@@ -31,6 +32,11 @@ public class AntlrSqlParseTreeVisitorTest {
3132
new TypeChecker(new SemanticContext()) {
3233
@Override
3334
public Type visitIndexName(String indexName) {
35+
// Special case: for JOIN tests (both implicit and explicit JOIN + GROUP BY validation)
36+
// Return proper OpenSearchIndex to enable JOIN semantic analysis for "testIndex"
37+
if ("testIndex".equals(indexName)) {
38+
return new OpenSearchIndex(indexName, OpenSearchIndex.IndexType.INDEX);
39+
}
3440
return null; // avoid querying mapping on null LocalClusterState
3541
}
3642

@@ -101,6 +107,13 @@ public void visitFunctionAsAggregatorShouldThrowException() {
101107
visit("SELECT max(abs(age)) FROM test");
102108
}
103109

110+
@Test
111+
public void visitExplicitJoinWithGroupByShouldThrowException() {
112+
exceptionRule.expect(RuntimeException.class);
113+
exceptionRule.expectMessage("JOIN queries do not support aggregations on the joined result.");
114+
visit("SELECT COUNT(*) FROM testIndex t1 JOIN testIndex t2 ON t1.id = t2.id GROUP BY t1.field");
115+
}
116+
104117
@Test
105118
public void visitUnsupportedOperatorShouldThrowException() {
106119
exceptionRule.expect(SqlFeatureNotImplementedException.class);

0 commit comments

Comments
 (0)