Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ public static class Builder {
* Setting values with defaults from SysLimit.DEFAULT. Only includes planning-required settings
* to avoid coupling with OpenSearchSettings.
*
* <p>{@link Settings.Key#PPL_JOIN_SUBSEARCH_MAXOUT} defaults to {@code 0} to avoid injecting
* {@code LogicalSystemLimit} into the logical plan, which is an OpenSearch-specific operational
* concern irrelevant to external consumers of the unified query API. {@link
* Settings.Key#PPL_SUBSEARCH_MAXOUT} is set to {@code 0} for the same reason.
*
* <p>{@link Settings.Key#CALCITE_ENGINE_ENABLED} defaults to {@code true} here because the
* unified query path is by definition Calcite-based — every query reaching this context flows
* through Calcite's planner, never the v2 engine. The PPL {@link
Expand All @@ -142,8 +147,8 @@ public static class Builder {
new HashMap<Settings.Key, Object>(
Map.of(
QUERY_SIZE_LIMIT, SysLimit.DEFAULT.querySizeLimit(),
PPL_SUBSEARCH_MAXOUT, SysLimit.DEFAULT.subsearchLimit(),
PPL_JOIN_SUBSEARCH_MAXOUT, SysLimit.DEFAULT.joinSubsearchLimit(),
PPL_SUBSEARCH_MAXOUT, SysLimit.UNLIMITED_SUBSEARCH.subsearchLimit(),
PPL_JOIN_SUBSEARCH_MAXOUT, SysLimit.UNLIMITED_SUBSEARCH.joinSubsearchLimit(),
CALCITE_ENGINE_ENABLED, true,
PPL_REX_MAX_MATCH_LIMIT, 10));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,29 @@

package org.opensearch.sql.api.parser;

import static org.opensearch.sql.ast.dsl.AstDSL.join;
import static org.opensearch.sql.ast.dsl.AstDSL.minus;
import static org.opensearch.sql.ast.dsl.AstDSL.union;

import java.util.List;
import java.util.Optional;
import org.antlr.v4.runtime.tree.ParseTree;
import org.opensearch.sql.ast.expression.UnresolvedExpression;
import org.opensearch.sql.ast.expression.subquery.ExistsSubquery;
import org.opensearch.sql.ast.expression.subquery.InSubquery;
import org.opensearch.sql.ast.statement.Query;
import org.opensearch.sql.ast.statement.Statement;
import org.opensearch.sql.ast.tree.Join.JoinType;
import org.opensearch.sql.ast.tree.UnresolvedPlan;
import org.opensearch.sql.sql.antlr.SQLSyntaxParser;
import org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser;
import org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.ExistsSubqueryExpressionAtomContext;
import org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.InSubqueryPredicateContext;
import org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.JoinClauseContext;
import org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.MinusSelectContext;
import org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.UnionSelectContext;
import org.opensearch.sql.sql.parser.AstBuilder;
import org.opensearch.sql.sql.parser.AstExpressionBuilder;
import org.opensearch.sql.sql.parser.AstStatementBuilder;

/** SQL query parser that produces {@link UnresolvedPlan} using the V2 ANTLR grammar. */
Expand All @@ -24,7 +41,8 @@ public UnresolvedPlan parse(String query) {
ParseTree cst = syntaxParser.parse(query);
AstStatementBuilder astStmtBuilder =
new AstStatementBuilder(
new AstBuilder(query), AstStatementBuilder.StatementBuilderContext.builder().build());
new ExtendedAstBuilder(query),
AstStatementBuilder.StatementBuilderContext.builder().build());
Statement statement = cst.accept(astStmtBuilder);

if (statement instanceof Query) {
Expand All @@ -33,4 +51,66 @@ public UnresolvedPlan parse(String query) {
throw new UnsupportedOperationException(
"Only query statements are supported but got " + statement.getClass().getSimpleName());
}

/**
* Extends the V2 AstBuilder with legacy-only SQL features (JOIN, UNION, MINUS) that the base
* AstBuilder rejects with SyntaxCheckException to trigger legacy engine fallback.
*/
private static class ExtendedAstBuilder extends AstBuilder {

ExtendedAstBuilder(String query) {
super(query);
}

@Override
protected AstExpressionBuilder createExpressionBuilder() {
return new ExtendedAstExpressionBuilder();
}

@Override
public UnresolvedPlan visitJoinClause(JoinClauseContext ctx) {
JoinType joinType = toJoinType(ctx);
UnresolvedPlan right = visit(ctx.relation());
Optional<UnresolvedExpression> condition =
Optional.ofNullable(ctx.expression()).map(this::visitAstExpression);
return join(right, joinType, condition);
}

@Override
public UnresolvedPlan visitUnionSelect(UnionSelectContext ctx) {
boolean distinct = ctx.ALL().isEmpty();
return union(ctx.querySpecification().stream().map(this::visit).toList(), distinct);
}

@Override
public UnresolvedPlan visitMinusSelect(MinusSelectContext ctx) {
return minus(ctx.querySpecification().stream().map(this::visit).toList());
}

private JoinType toJoinType(JoinClauseContext ctx) {
return switch (ctx.getStart().getType()) {
case OpenSearchSQLParser.LEFT -> JoinType.LEFT;
case OpenSearchSQLParser.RIGHT -> JoinType.RIGHT;
case OpenSearchSQLParser.CROSS -> JoinType.CROSS;
default -> JoinType.INNER;
};
}

/** Expression builder with IN/EXISTS subquery support. */
private class ExtendedAstExpressionBuilder extends AstExpressionBuilder {

@Override
public UnresolvedExpression visitInSubqueryPredicate(InSubqueryPredicateContext ctx) {
UnresolvedPlan subquery = ExtendedAstBuilder.this.visit(ctx.querySpecification());
return new InSubquery(List.of(visit(ctx.predicate())), subquery);
}

@Override
public UnresolvedExpression visitExistsSubqueryExpressionAtom(
ExistsSubqueryExpressionAtomContext ctx) {
UnresolvedPlan subquery = ExtendedAstBuilder.this.visit(ctx.querySpecification());
return new ExistsSubquery(subquery);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ public void testContextCreationWithDefaults() {
assertNotNull("PlanContext should be created", context.getPlanContext());
assertNotNull("Settings should be created", context.getSettings());
assertEquals(
"Settings should have default system limits",
SysLimit.DEFAULT,
"Settings should have unlimited subsearch limits for clean logical plans",
new SysLimit(SysLimit.DEFAULT.querySizeLimit(), 0, 0),
SysLimit.fromSettings(context.getSettings()));
assertEquals(
"PPL_REX_MAX_MATCH_LIMIT default should be 10",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@

package org.opensearch.sql.api;

import static org.apache.calcite.sql.type.SqlTypeName.INTEGER;
import static org.apache.calcite.sql.type.SqlTypeName.VARCHAR;

import java.util.Map;
import org.apache.calcite.schema.Table;
import org.apache.calcite.schema.impl.AbstractSchema;
import org.junit.Before;
import org.junit.Test;
import org.opensearch.sql.executor.QueryType;

/**
* Tests for basic SQL query planning through the V2 ANTLR parser path. Covers SELECT, WHERE, ORDER
* BY operations that produce valid RelNode plans.
* Tests for SQL query planning through the V2 ANTLR parser path. Covers SELECT, WHERE, ORDER BY,
* JOIN, UNION, and MINUS operations that produce valid RelNode plans.
*/
public class UnifiedQueryPlannerSqlV2Test extends UnifiedQueryTestBase {

Expand All @@ -19,17 +26,35 @@ protected QueryType queryType() {
return QueryType.SQL;
}

@Test
public void selectStar() {
givenQuery("SELECT * FROM catalog.employees")
.assertPlan(
"""
LogicalTableScan(table=[[catalog, employees]])
""");
@Before
@Override
public void setUp() {
testSchema =
new AbstractSchema() {
@Override
protected Map<String, Table> getTableMap() {
return Map.of(
"employees", createEmployeesTable(),
"departments", createDepartmentsTable());
}
};

context = contextBuilder().build();
planner = new UnifiedQueryPlanner(context);
}

private Table createDepartmentsTable() {
return SimpleTable.builder()
.col("dept_id", INTEGER)
.col("dept_name", VARCHAR)
.row(new Object[] {1, "Engineering"})
.row(new Object[] {2, "Sales"})
.row(new Object[] {3, "Marketing"})
.build();
}

@Test
public void selectStarFields() {
public void selectStar() {
givenQuery("SELECT * FROM catalog.employees")
.assertPlan(
"""
Expand Down Expand Up @@ -68,4 +93,162 @@ public void testFilterAndOrderBy() {
LogicalTableScan(table=[[catalog, employees]])
""");
}

@Test
public void testJoinTypes() {
Map.of("JOIN", "inner", "LEFT JOIN", "left", "RIGHT JOIN", "right")
.forEach(
(syntax, type) ->
givenQuery(
"""
SELECT * FROM catalog.employees %s catalog.departments
ON employees.department = departments.dept_name
"""
.formatted(syntax))
.assertPlan(
"""
LogicalJoin(condition=[=($3, $5)], joinType=[%s])
LogicalTableScan(table=[[catalog, employees]])
LogicalTableScan(table=[[catalog, departments]])
"""
.formatted(type)));
}

@Test
public void testCrossJoin() {
givenQuery("SELECT * FROM catalog.employees CROSS JOIN catalog.departments")
.assertPlan(
"""
LogicalJoin(condition=[true], joinType=[inner])
LogicalTableScan(table=[[catalog, employees]])
LogicalTableScan(table=[[catalog, departments]])
""");
}

@Test
public void testJoinWithFilterAndOrderBy() {
givenQuery(
"""
SELECT * FROM catalog.employees JOIN catalog.departments
ON employees.department = departments.dept_name
WHERE employees.age > 30 ORDER BY employees.name
""")
.assertPlan(
"""
LogicalSort(sort0=[$1], dir0=[ASC-nulls-first])
LogicalFilter(condition=[>($2, 30)])
LogicalJoin(condition=[=($3, $5)], joinType=[inner])
LogicalTableScan(table=[[catalog, employees]])
LogicalTableScan(table=[[catalog, departments]])
""");
}

@Test
public void testUnionAll() {
givenQuery(
"""
SELECT name, age FROM catalog.employees
UNION ALL SELECT dept_name, dept_id FROM catalog.departments
""")
.assertPlan(
"""
LogicalUnion(all=[true])
LogicalProject(name=[$1], age=[$2])
LogicalTableScan(table=[[catalog, employees]])
LogicalProject(dept_name=[$1], dept_id=[$0])
LogicalTableScan(table=[[catalog, departments]])
""");
}

@Test
public void testUnion() {
givenQuery(
"""
SELECT name, age FROM catalog.employees
UNION SELECT dept_name, dept_id FROM catalog.departments
""")
.assertPlan(
"""
LogicalUnion(all=[false])
LogicalProject(name=[$1], age=[$2])
LogicalTableScan(table=[[catalog, employees]])
LogicalProject(dept_name=[$1], dept_id=[$0])
LogicalTableScan(table=[[catalog, departments]])
""");
}

@Test
public void testMultiWayUnion() {
givenQuery(
"""
SELECT name, age FROM catalog.employees
UNION ALL SELECT dept_name, dept_id FROM catalog.departments
UNION ALL SELECT name, age FROM catalog.employees
""")
.assertPlan(
"""
LogicalUnion(all=[true])
LogicalProject(name=[$1], age=[$2])
LogicalTableScan(table=[[catalog, employees]])
LogicalProject(dept_name=[$1], dept_id=[$0])
LogicalTableScan(table=[[catalog, departments]])
LogicalProject(name=[$1], age=[$2])
LogicalTableScan(table=[[catalog, employees]])
""");
}

@Test
public void testMinus() {
givenQuery(
"""
SELECT name, age FROM catalog.employees
MINUS SELECT dept_name, dept_id FROM catalog.departments
""")
.assertPlan(
"""
LogicalMinus(all=[false])
LogicalProject(name=[$1], age=[$2])
LogicalTableScan(table=[[catalog, employees]])
LogicalProject(dept_name=[$1], dept_id=[$0])
LogicalTableScan(table=[[catalog, departments]])
""");
}

@Test
public void testInSubquery() {
givenQuery(
"""
SELECT name FROM catalog.employees
WHERE age IN (SELECT age FROM catalog.departments WHERE dept_name = 'Engineering')
""")
.assertPlan(
"""
LogicalProject(name=[$1])
LogicalFilter(condition=[IN($2, {
LogicalProject(age=[$cor0.age])
LogicalFilter(condition=[=($1, 'Engineering')])
LogicalTableScan(table=[[catalog, departments]])
})], variablesSet=[[$cor0]])
LogicalTableScan(table=[[catalog, employees]])
""");
}

@Test
public void testExistsSubquery() {
givenQuery(
"""
SELECT name FROM catalog.employees
WHERE EXISTS (SELECT 1 FROM catalog.departments WHERE dept_id = age)
""")
.assertPlan(
"""
LogicalProject(name=[$1])
LogicalFilter(condition=[EXISTS({
LogicalProject(1=[1])
LogicalFilter(condition=[=($0, $cor0.age)])
LogicalTableScan(table=[[catalog, departments]])
})], variablesSet=[[$cor0]])
LogicalTableScan(table=[[catalog, employees]])
""");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
import org.opensearch.sql.ast.tree.Limit;
import org.opensearch.sql.ast.tree.Lookup;
import org.opensearch.sql.ast.tree.ML;
import org.opensearch.sql.ast.tree.Minus;
import org.opensearch.sql.ast.tree.Multisearch;
import org.opensearch.sql.ast.tree.MvCombine;
import org.opensearch.sql.ast.tree.MvExpand;
Expand Down Expand Up @@ -477,6 +478,10 @@ public T visitUnion(Union node, C context) {
return visitChildren(node, context);
}

public T visitMinus(Minus node, C context) {
return visitChildren(node, context);
}

public T visitAddTotals(AddTotals node, C context) {
return visitChildren(node, context);
}
Expand Down
Loading
Loading