diff --git a/api/build.gradle b/api/build.gradle index 5ce00169597..26fd419984d 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -16,6 +16,7 @@ dependencies { testImplementation group: 'junit', name: 'junit', version: '4.13.2' testImplementation group: 'org.hamcrest', name: 'hamcrest-library', version: "${hamcrest_version}" testImplementation group: 'org.mockito', name: 'mockito-core', version: "${mockito_version}" + testImplementation group: 'org.mockito', name: 'mockito-junit-jupiter', version: "${mockito_version}" testImplementation group: 'org.apache.calcite', name: 'calcite-testkit', version: '1.41.0' } diff --git a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryPlanner.java b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryPlanner.java index 6a524ec307a..9e981bdda17 100644 --- a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryPlanner.java +++ b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryPlanner.java @@ -32,6 +32,7 @@ import org.opensearch.sql.calcite.SysLimit; import org.opensearch.sql.common.antlr.Parser; import org.opensearch.sql.common.antlr.SyntaxCheckException; +import org.opensearch.sql.common.setting.Settings; import org.opensearch.sql.executor.QueryType; import org.opensearch.sql.ppl.antlr.PPLSyntaxParser; import org.opensearch.sql.ppl.parser.AstBuilder; @@ -52,21 +53,26 @@ public class UnifiedQueryPlanner { /** Calcite framework configuration used during logical plan construction. */ private final FrameworkConfig config; + private final Settings settings; + /** AST-to-RelNode visitor that builds logical plans from the parsed AST. */ private final CalciteRelNodeVisitor relNodeVisitor = new CalciteRelNodeVisitor(new EmptyDataSourceService()); /** - * Constructs a UnifiedQueryPlanner for a given query type and schema root. + * Constructs a UnifiedQueryPlanner for a given query type, schema root, and settings. * * @param queryType the query language type (e.g., PPL) * @param rootSchema the root Calcite schema containing all catalogs and tables * @param defaultPath dot-separated path of schema to set as default schema + * @param settings configuration settings for query processing */ - public UnifiedQueryPlanner(QueryType queryType, SchemaPlus rootSchema, String defaultPath) { + public UnifiedQueryPlanner( + QueryType queryType, SchemaPlus rootSchema, String defaultPath, Settings settings) { this.queryType = queryType; this.parser = buildQueryParser(queryType); this.config = buildCalciteConfig(rootSchema, defaultPath); + this.settings = settings; } /** @@ -124,7 +130,8 @@ private UnresolvedPlan parse(String query) { ParseTree cst = parser.parse(query); AstStatementBuilder astStmtBuilder = new AstStatementBuilder( - new AstBuilder(query), AstStatementBuilder.StatementBuilderContext.builder().build()); + new AstBuilder(query, settings), + AstStatementBuilder.StatementBuilderContext.builder().build()); Statement statement = cst.accept(astStmtBuilder); if (statement instanceof Query) { @@ -164,6 +171,7 @@ public static class Builder { private String defaultNamespace; private QueryType queryType; private boolean cacheMetadata; + private Settings settings; /** * Sets the query language frontend to be used by the planner. @@ -176,6 +184,17 @@ public Builder language(QueryType queryType) { return this; } + /** + * Sets the settings for query processing configuration. + * + * @param settings the Settings instance for configuration + * @return this builder instance + */ + public Builder settings(Settings settings) { + this.settings = settings; + return this; + } + /** * Registers a catalog with the specified name and its associated schema. The schema can be a * flat or nested structure (e.g., catalog → schema → table), depending on how data is @@ -221,7 +240,7 @@ public UnifiedQueryPlanner build() { Objects.requireNonNull(queryType, "Must specify language before build"); SchemaPlus rootSchema = CalciteSchema.createRootSchema(true, cacheMetadata).plus(); catalogs.forEach(rootSchema::add); - return new UnifiedQueryPlanner(queryType, rootSchema, defaultNamespace); + return new UnifiedQueryPlanner(queryType, rootSchema, defaultNamespace, settings); } } } diff --git a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java index 754e36c092e..f40d63db1fe 100644 --- a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java +++ b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java @@ -13,9 +13,14 @@ import org.apache.calcite.schema.Schema; import org.apache.calcite.schema.impl.AbstractSchema; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; import org.opensearch.sql.common.antlr.SyntaxCheckException; +import org.opensearch.sql.common.setting.Settings; import org.opensearch.sql.executor.QueryType; +@RunWith(MockitoJUnitRunner.class) public class UnifiedQueryPlannerTest extends UnifiedQueryTestBase { /** Test catalog consists of test schema above */ @@ -27,6 +32,8 @@ protected Map getSubSchemaMap() { } }; + @Mock private Settings testSettings; + @Test public void testPPLQueryPlanning() { UnifiedQueryPlanner planner = @@ -155,4 +162,17 @@ public void testPlanPropagatingSyntaxCheckException() { planner.plan("source = employees | eval"); // Trigger syntax error from parser } + + @Test + public void testJoinQuery() { + UnifiedQueryPlanner planner = + UnifiedQueryPlanner.builder() + .language(QueryType.PPL) + .catalog("opensearch", testSchema) + .defaultNamespace("opensearch") + .settings(testSettings) + .build(); + + planner.plan("source = employees | join on employees.id = payslips.id payslips"); + } } diff --git a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryTestBase.java b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryTestBase.java index f63bfed09ec..e679c4b2488 100644 --- a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryTestBase.java +++ b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryTestBase.java @@ -31,7 +31,7 @@ public void setUp() { new AbstractSchema() { @Override protected Map getTableMap() { - return Map.of("employees", createEmployeesTable()); + return Map.of("employees", createEmployeesTable(), "payslips", createPayslipsTable()); } }; @@ -57,4 +57,18 @@ public RelDataType getRowType(RelDataTypeFactory typeFactory) { } }; } + + protected Table createPayslipsTable() { + return new AbstractTable() { + @Override + public RelDataType getRowType(RelDataTypeFactory typeFactory) { + return typeFactory.createStructType( + List.of( + typeFactory.createSqlType(SqlTypeName.INTEGER), + typeFactory.createSqlType(SqlTypeName.DATE), + typeFactory.createSqlType(SqlTypeName.FLOAT)), + List.of("id", "date", "salary")); + } + }; + } }