From b0d5ad91fd28da8bd46d3a50d13343668d6a3296 Mon Sep 17 00:00:00 2001 From: Chen Dai Date: Tue, 9 Dec 2025 14:49:38 -0800 Subject: [PATCH 1/9] Extract UnifiedQueryPlanner.Builder to UnifiedQueryContext Signed-off-by: Chen Dai --- api/README.md | 41 ++++- .../sql/api/UnifiedQueryContext.java | 158 ++++++++++++++++++ .../sql/api/UnifiedQueryPlanner.java | 153 ++++++++++------- .../sql/api/UnifiedQueryPlannerTest.java | 2 +- 4 files changed, 286 insertions(+), 68 deletions(-) create mode 100644 api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java diff --git a/api/README.md b/api/README.md index c380a1a7128..24c37a3e6d8 100644 --- a/api/README.md +++ b/api/README.md @@ -17,9 +17,31 @@ Together, these components enable a complete workflow: parse PPL queries into lo ## Usage -### UnifiedQueryPlanner +### UnifiedQueryContext (Recommended) -Use the declarative, fluent builder API to initialize the `UnifiedQueryPlanner`. +The recommended approach is to create a reusable `UnifiedQueryContext` that encapsulates a `CalcitePlanContext` with all catalog configuration and query type. This context can be shared across multiple queries. + +```java +// Create a reusable context with query type +UnifiedQueryContext context = UnifiedQueryContext.builder() + .queryType(QueryType.PPL) + .catalog("opensearch", opensearchSchema) + .catalog("spark_catalog", sparkSchema) + .defaultNamespace("opensearch") + .cacheMetadata(true) + .build(); + +// Create planner with context +UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context); + +// Plan multiple queries (context is reused) +RelNode plan1 = planner.plan("source = logs | where status = 200"); +RelNode plan2 = planner.plan("source = metrics | stats avg(cpu)"); +``` + +### UnifiedQueryPlanner (Legacy Builder API) + +The legacy builder API is still supported for backward compatibility: ```java UnifiedQueryPlanner planner = UnifiedQueryPlanner.builder() @@ -49,22 +71,25 @@ String sql = transpiler.toSql(plan); Combining both components to transpile PPL queries into target database SQL: ```java -// Step 1: Initialize planner -UnifiedQueryPlanner planner = UnifiedQueryPlanner.builder() - .language(QueryType.PPL) +// Step 1: Create reusable context with query type +UnifiedQueryContext context = UnifiedQueryContext.builder() + .queryType(QueryType.PPL) .catalog("catalog", schema) .defaultNamespace("catalog") .build(); -// Step 2: Parse PPL query into logical plan +// Step 2: Initialize planner with context +UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context); + +// Step 3: Parse PPL query into logical plan RelNode plan = planner.plan("source = employees | where age > 30"); -// Step 3: Initialize transpiler with target dialect +// Step 4: Initialize transpiler with target dialect UnifiedQueryTranspiler transpiler = UnifiedQueryTranspiler.builder() .dialect(SparkSqlDialect.DEFAULT) .build(); -// Step 4: Transpile to target SQL +// Step 5: Transpile to target SQL String sparkSql = transpiler.toSql(plan); // Result: SELECT * FROM `catalog`.`employees` WHERE `age` > 30 ``` diff --git a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java new file mode 100644 index 00000000000..c3ba8c20f54 --- /dev/null +++ b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java @@ -0,0 +1,158 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.api; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.Getter; +import lombok.Value; +import org.apache.calcite.jdbc.CalciteSchema; +import org.apache.calcite.plan.RelTraitDef; +import org.apache.calcite.rel.metadata.DefaultRelMetadataProvider; +import org.apache.calcite.schema.Schema; +import org.apache.calcite.schema.SchemaPlus; +import org.apache.calcite.sql.parser.SqlParser; +import org.apache.calcite.tools.FrameworkConfig; +import org.apache.calcite.tools.Frameworks; +import org.apache.calcite.tools.Programs; +import org.opensearch.sql.calcite.CalcitePlanContext; +import org.opensearch.sql.calcite.SysLimit; +import org.opensearch.sql.executor.QueryType; + +/** + * Represents a unified query context that encapsulates a CalcitePlanContext. + * Contexts are immutable and thread-safe, designed to be reused across multiple queries. + * + *

Example usage: + *

{@code
+ * UnifiedQueryContext context = UnifiedQueryContext.builder()
+ *     .queryType(QueryType.PPL)
+ *     .catalog("opensearch", mySchema)
+ *     .defaultNamespace("opensearch")
+ *     .build();
+ *
+ * UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context);
+ * RelNode plan = planner.plan("source=logs | where status=200");
+ * }
+ */ +@Value +public class UnifiedQueryContext { + + /** + * The CalcitePlanContext that holds all Calcite configuration and query type. + * This is the only field stored - everything else is just for building this. + */ + CalcitePlanContext planContext; + + /** + * Creates a new builder for UnifiedQueryContext. + */ + public static UnifiedQueryContextBuilder builder() { + return new UnifiedQueryContextBuilder(); + } + + /** + * Builder for UnifiedQueryContext with validation. + * Builds the CalcitePlanContext with all necessary configuration. + */ + public static class UnifiedQueryContextBuilder { + private QueryType queryType; + private final Map catalogs = new HashMap<>(); + private String defaultNamespace; + private boolean cacheMetadata = false; + private SysLimit sysLimit = new SysLimit(10000, 10000, 10000); + + /** + * Sets the query type. + */ + public UnifiedQueryContextBuilder queryType(QueryType queryType) { + this.queryType = queryType; + return this; + } + + /** + * Registers a catalog with the specified name and schema. + */ + public UnifiedQueryContextBuilder catalog(String name, Schema schema) { + catalogs.put(name, schema); + return this; + } + + /** + * Sets the default namespace path. + */ + public UnifiedQueryContextBuilder defaultNamespace(String namespace) { + this.defaultNamespace = namespace; + return this; + } + + /** + * Enables or disables metadata caching. + */ + public UnifiedQueryContextBuilder cacheMetadata(boolean cache) { + this.cacheMetadata = cache; + return this; + } + + /** + * Sets the query execution limits. + */ + public UnifiedQueryContextBuilder sysLimit(SysLimit limit) { + this.sysLimit = limit; + return this; + } + + /** + * Builds the UnifiedQueryContext with validation. + * Validates the default namespace path to fail fast on invalid configuration. + */ + public UnifiedQueryContext build() { + if (queryType == null) { + throw new IllegalArgumentException("QueryType must be specified"); + } + + // Build and validate framework config + FrameworkConfig frameworkConfig = buildFrameworkConfig(); + + // Create CalcitePlanContext with all configuration + CalcitePlanContext planContext = + CalcitePlanContext.create(frameworkConfig, sysLimit, queryType); + + return new UnifiedQueryContext(planContext); + } + + @SuppressWarnings({"rawtypes"}) + private FrameworkConfig buildFrameworkConfig() { + SchemaPlus rootSchema = CalciteSchema.createRootSchema(true, cacheMetadata).plus(); + catalogs.forEach(rootSchema::add); + + SchemaPlus defaultSchema = findSchemaByPath(rootSchema, defaultNamespace); + return Frameworks.newConfigBuilder() + .parserConfig(SqlParser.Config.DEFAULT) + .defaultSchema(defaultSchema) + .traitDefs((List) null) + .programs(Programs.calc(DefaultRelMetadataProvider.INSTANCE)) + .build(); + } + + @SuppressWarnings("deprecation") + private static SchemaPlus findSchemaByPath(SchemaPlus rootSchema, String defaultPath) { + if (defaultPath == null) { + return rootSchema; + } + + SchemaPlus current = rootSchema; + for (String part : defaultPath.split("\\.")) { + current = current.getSubSchema(part); + if (current == null) { + throw new IllegalArgumentException("Invalid default catalog path: " + defaultPath); + } + } + return current; + } + } +} 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..87295ee5011 100644 --- a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryPlanner.java +++ b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryPlanner.java @@ -6,30 +6,20 @@ package org.opensearch.sql.api; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Objects; import org.antlr.v4.runtime.tree.ParseTree; -import org.apache.calcite.jdbc.CalciteSchema; -import org.apache.calcite.plan.RelTraitDef; import org.apache.calcite.rel.RelCollation; import org.apache.calcite.rel.RelCollations; import org.apache.calcite.rel.RelNode; import org.apache.calcite.rel.core.Sort; import org.apache.calcite.rel.logical.LogicalSort; -import org.apache.calcite.rel.metadata.DefaultRelMetadataProvider; import org.apache.calcite.schema.Schema; import org.apache.calcite.schema.SchemaPlus; -import org.apache.calcite.sql.parser.SqlParser; -import org.apache.calcite.tools.FrameworkConfig; -import org.apache.calcite.tools.Frameworks; -import org.apache.calcite.tools.Programs; import org.opensearch.sql.ast.statement.Query; import org.opensearch.sql.ast.statement.Statement; import org.opensearch.sql.ast.tree.UnresolvedPlan; -import org.opensearch.sql.calcite.CalcitePlanContext; import org.opensearch.sql.calcite.CalciteRelNodeVisitor; -import org.opensearch.sql.calcite.SysLimit; import org.opensearch.sql.common.antlr.Parser; import org.opensearch.sql.common.antlr.SyntaxCheckException; import org.opensearch.sql.executor.QueryType; @@ -43,30 +33,40 @@ * such as Spark or command-line tools, abstracting away Calcite internals. */ public class UnifiedQueryPlanner { - /** The type of query language being used (e.g., PPL). */ - private final QueryType queryType; - /** The parser instance responsible for converting query text into a parse tree. */ private final Parser parser; - /** Calcite framework configuration used during logical plan construction. */ - private final FrameworkConfig config; + /** Unified query context containing CalcitePlanContext with all configuration. */ + private final UnifiedQueryContext context; /** AST-to-RelNode visitor that builds logical plans from the parsed AST. */ private final CalciteRelNodeVisitor relNodeVisitor = new CalciteRelNodeVisitor(new EmptyDataSourceService()); + /** + * Constructs a UnifiedQueryPlanner with a unified query context. + * This is the recommended constructor for new code. + * + * @param context the unified query context containing CalcitePlanContext + */ + public UnifiedQueryPlanner(UnifiedQueryContext context) { + this.parser = buildQueryParser(context.getPlanContext().queryType); + this.context = context; + } + /** * Constructs a UnifiedQueryPlanner for a given query type and schema root. + * This constructor is maintained for backward compatibility. * * @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 + * @deprecated Use {@link #UnifiedQueryPlanner(UnifiedQueryContext)} instead */ + @Deprecated public UnifiedQueryPlanner(QueryType queryType, SchemaPlus rootSchema, String defaultPath) { - this.queryType = queryType; this.parser = buildQueryParser(queryType); - this.config = buildCalciteConfig(rootSchema, defaultPath); + this.context = createContextFromLegacyParams(queryType, rootSchema, defaultPath); } /** @@ -94,30 +94,32 @@ private Parser buildQueryParser(QueryType queryType) { throw new IllegalArgumentException("Unsupported query type: " + queryType); } - private FrameworkConfig buildCalciteConfig(SchemaPlus rootSchema, String defaultPath) { - SchemaPlus defaultSchema = findSchemaByPath(rootSchema, defaultPath); - return Frameworks.newConfigBuilder() - .parserConfig(SqlParser.Config.DEFAULT) - .defaultSchema(defaultSchema) - .traitDefs((List) null) - .programs(Programs.calc(DefaultRelMetadataProvider.INSTANCE)) - .build(); - } - - private static SchemaPlus findSchemaByPath(SchemaPlus rootSchema, String defaultPath) { - if (defaultPath == null) { - return rootSchema; + /** + * Creates a UnifiedQueryContext from legacy constructor parameters. + * Used internally to support backward compatibility. + */ + @SuppressWarnings("deprecation") + private static UnifiedQueryContext createContextFromLegacyParams( + QueryType queryType, SchemaPlus rootSchema, String defaultPath) { + // Extract catalogs from root schema + Map catalogs = new HashMap<>(); + for (String catalogName : rootSchema.getSubSchemaNames()) { + Schema catalog = rootSchema.getSubSchema(catalogName); + if (catalog != null) { + catalogs.put(catalogName, catalog); + } } - // Find schema by the path recursively - SchemaPlus current = rootSchema; - for (String part : defaultPath.split("\\.")) { - current = current.getSubSchema(part); - if (current == null) { - throw new IllegalArgumentException("Invalid default catalog path: " + defaultPath); - } + // Build context with extracted catalogs + UnifiedQueryContext.UnifiedQueryContextBuilder builder = + UnifiedQueryContext.builder().queryType(queryType); + catalogs.forEach(builder::catalog); + + if (defaultPath != null) { + builder.defaultNamespace(defaultPath); } - return current; + + return builder.build(); } private UnresolvedPlan parse(String query) { @@ -135,10 +137,7 @@ private UnresolvedPlan parse(String query) { } private RelNode analyze(UnresolvedPlan ast) { - // TODO: Hardcoded query size limit (10000) for now as only logical plan is generated. - CalcitePlanContext calcitePlanContext = - CalcitePlanContext.create(config, new SysLimit(10000, 10000, 10000), queryType); - return relNodeVisitor.analyze(ast, calcitePlanContext); + return relNodeVisitor.analyze(ast, context.getPlanContext()); } private RelNode preserveCollation(RelNode logical) { @@ -156,59 +155,79 @@ public static Builder builder() { } /** - * Builder for {@link UnifiedQueryPlanner}, supporting both declarative and dynamic schema - * registration for use in query planning. + * Builder for {@link UnifiedQueryPlanner}, supporting both new context-based API + * and legacy catalog-based API for backward compatibility. + * Delegates all configuration to UnifiedQueryContext.Builder. */ public static class Builder { - private final Map catalogs = new HashMap<>(); - private String defaultNamespace; - private QueryType queryType; - private boolean cacheMetadata; + private UnifiedQueryContext context; + private UnifiedQueryContext.UnifiedQueryContextBuilder contextBuilder; /** * Sets the query language frontend to be used by the planner. + * Delegates to context builder. * * @param queryType the {@link QueryType}, such as PPL * @return this builder instance */ public Builder language(QueryType queryType) { - this.queryType = queryType; + ensureContextBuilder(); + contextBuilder.queryType(queryType); 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 - * organized. + * Sets the unified query context directly (new API). + * + * @param context the unified query context + * @return this builder instance + */ + public Builder context(UnifiedQueryContext context) { + if (contextBuilder != null) { + throw new IllegalStateException( + "Cannot set context after using language/catalog/defaultNamespace/cacheMetadata methods"); + } + this.context = context; + return this; + } + + /** + * Registers a catalog with the specified name and its associated schema. + * Delegates to context builder. * * @param name the name of the catalog to register * @param schema the schema representing the structure of the catalog * @return this builder instance */ public Builder catalog(String name, Schema schema) { - catalogs.put(name, schema); + ensureContextBuilder(); + contextBuilder.catalog(name, schema); return this; } /** * Sets the default namespace path for resolving unqualified table names. + * Delegates to context builder. * * @param namespace dot-separated path (e.g., "spark_catalog.default" or "opensearch") * @return this builder instance */ public Builder defaultNamespace(String namespace) { - this.defaultNamespace = namespace; + ensureContextBuilder(); + contextBuilder.defaultNamespace(namespace); return this; } /** * Enables or disables catalog metadata caching in the root schema. + * Delegates to context builder. * * @param cache whether to enable metadata caching * @return this builder instance */ public Builder cacheMetadata(boolean cache) { - this.cacheMetadata = cache; + ensureContextBuilder(); + contextBuilder.cacheMetadata(cache); return this; } @@ -218,10 +237,26 @@ public Builder cacheMetadata(boolean cache) { * @return a new instance of {@link UnifiedQueryPlanner} */ 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); + // Build context if not provided directly + if (context == null) { + if (contextBuilder == null) { + throw new IllegalStateException( + "Must provide either context or use language/catalog configuration"); + } + context = contextBuilder.build(); + } + + return new UnifiedQueryPlanner(context); + } + + private void ensureContextBuilder() { + if (context != null) { + throw new IllegalStateException( + "Cannot use language/catalog/defaultNamespace/cacheMetadata after setting context"); + } + if (contextBuilder == null) { + contextBuilder = UnifiedQueryContext.builder(); + } } } } 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..31fa102f611 100644 --- a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java +++ b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java @@ -112,7 +112,7 @@ public void testPPLQueryPlanningWithMetadataCaching() { assertNotNull("Plan should be created", plan); } - @Test(expected = NullPointerException.class) + @Test(expected = IllegalArgumentException.class) public void testMissingQueryLanguage() { UnifiedQueryPlanner.builder().catalog("opensearch", testSchema).build(); } From 89f3542a8084d9e3079f78e7142377c7f6054986 Mon Sep 17 00:00:00 2001 From: Chen Dai Date: Tue, 9 Dec 2025 15:08:06 -0800 Subject: [PATCH 2/9] Remove backward compatibility code Signed-off-by: Chen Dai --- api/README.md | 21 +-- .../sql/api/UnifiedQueryContext.java | 44 ++--- .../sql/api/UnifiedQueryPlanner.java | 160 ------------------ .../sql/api/UnifiedQueryPlannerTest.java | 72 ++++---- .../sql/api/UnifiedQueryTestBase.java | 7 +- 5 files changed, 64 insertions(+), 240 deletions(-) diff --git a/api/README.md b/api/README.md index 24c37a3e6d8..f4e635272ff 100644 --- a/api/README.md +++ b/api/README.md @@ -17,12 +17,12 @@ Together, these components enable a complete workflow: parse PPL queries into lo ## Usage -### UnifiedQueryContext (Recommended) +### UnifiedQueryContext and UnifiedQueryPlanner -The recommended approach is to create a reusable `UnifiedQueryContext` that encapsulates a `CalcitePlanContext` with all catalog configuration and query type. This context can be shared across multiple queries. +Create a reusable `UnifiedQueryContext` that encapsulates a `CalcitePlanContext` with all catalog configuration and query type. This context can be shared across multiple queries. ```java -// Create a reusable context with query type +// Create a reusable context with query type and catalogs UnifiedQueryContext context = UnifiedQueryContext.builder() .queryType(QueryType.PPL) .catalog("opensearch", opensearchSchema) @@ -39,21 +39,6 @@ RelNode plan1 = planner.plan("source = logs | where status = 200"); RelNode plan2 = planner.plan("source = metrics | stats avg(cpu)"); ``` -### UnifiedQueryPlanner (Legacy Builder API) - -The legacy builder API is still supported for backward compatibility: - -```java -UnifiedQueryPlanner planner = UnifiedQueryPlanner.builder() - .language(QueryType.PPL) - .catalog("opensearch", schema) - .defaultNamespace("opensearch") - .cacheMetadata(true) - .build(); - -RelNode plan = planner.plan("source = opensearch.test"); -``` - ### UnifiedQueryTranspiler Use `UnifiedQueryTranspiler` to convert Calcite logical plans into SQL strings for target databases. The transpiler supports various SQL dialects through Calcite's `SqlDialect` interface. diff --git a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java index c3ba8c20f54..4b6ac137dcb 100644 --- a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java +++ b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java @@ -8,7 +8,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import lombok.Getter; import lombok.Value; import org.apache.calcite.jdbc.CalciteSchema; import org.apache.calcite.plan.RelTraitDef; @@ -24,10 +23,11 @@ import org.opensearch.sql.executor.QueryType; /** - * Represents a unified query context that encapsulates a CalcitePlanContext. - * Contexts are immutable and thread-safe, designed to be reused across multiple queries. + * Represents a unified query context that encapsulates a CalcitePlanContext. Contexts are immutable + * and thread-safe, designed to be reused across multiple queries. * *

Example usage: + * *

{@code
  * UnifiedQueryContext context = UnifiedQueryContext.builder()
  *     .queryType(QueryType.PPL)
@@ -43,21 +43,19 @@
 public class UnifiedQueryContext {
 
   /**
-   * The CalcitePlanContext that holds all Calcite configuration and query type.
-   * This is the only field stored - everything else is just for building this.
+   * The CalcitePlanContext that holds all Calcite configuration and query type. This is the only
+   * field stored - everything else is just for building this.
    */
   CalcitePlanContext planContext;
 
-  /**
-   * Creates a new builder for UnifiedQueryContext.
-   */
+  /** Creates a new builder for UnifiedQueryContext. */
   public static UnifiedQueryContextBuilder builder() {
     return new UnifiedQueryContextBuilder();
   }
 
   /**
-   * Builder for UnifiedQueryContext with validation.
-   * Builds the CalcitePlanContext with all necessary configuration.
+   * Builder for UnifiedQueryContext with validation. Builds the CalcitePlanContext with all
+   * necessary configuration.
    */
   public static class UnifiedQueryContextBuilder {
     private QueryType queryType;
@@ -66,49 +64,39 @@ public static class UnifiedQueryContextBuilder {
     private boolean cacheMetadata = false;
     private SysLimit sysLimit = new SysLimit(10000, 10000, 10000);
 
-    /**
-     * Sets the query type.
-     */
+    /** Sets the query type. */
     public UnifiedQueryContextBuilder queryType(QueryType queryType) {
       this.queryType = queryType;
       return this;
     }
 
-    /**
-     * Registers a catalog with the specified name and schema.
-     */
+    /** Registers a catalog with the specified name and schema. */
     public UnifiedQueryContextBuilder catalog(String name, Schema schema) {
       catalogs.put(name, schema);
       return this;
     }
 
-    /**
-     * Sets the default namespace path.
-     */
+    /** Sets the default namespace path. */
     public UnifiedQueryContextBuilder defaultNamespace(String namespace) {
       this.defaultNamespace = namespace;
       return this;
     }
 
-    /**
-     * Enables or disables metadata caching.
-     */
+    /** Enables or disables metadata caching. */
     public UnifiedQueryContextBuilder cacheMetadata(boolean cache) {
       this.cacheMetadata = cache;
       return this;
     }
 
-    /**
-     * Sets the query execution limits.
-     */
+    /** Sets the query execution limits. */
     public UnifiedQueryContextBuilder sysLimit(SysLimit limit) {
       this.sysLimit = limit;
       return this;
     }
 
     /**
-     * Builds the UnifiedQueryContext with validation.
-     * Validates the default namespace path to fail fast on invalid configuration.
+     * Builds the UnifiedQueryContext with validation. Validates the default namespace path to fail
+     * fast on invalid configuration.
      */
     public UnifiedQueryContext build() {
       if (queryType == null) {
@@ -117,7 +105,7 @@ public UnifiedQueryContext build() {
 
       // Build and validate framework config
       FrameworkConfig frameworkConfig = buildFrameworkConfig();
-      
+
       // Create CalcitePlanContext with all configuration
       CalcitePlanContext planContext =
           CalcitePlanContext.create(frameworkConfig, sysLimit, queryType);
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 87295ee5011..b1d3687de93 100644
--- a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryPlanner.java
+++ b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryPlanner.java
@@ -5,17 +5,12 @@
 
 package org.opensearch.sql.api;
 
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
 import org.antlr.v4.runtime.tree.ParseTree;
 import org.apache.calcite.rel.RelCollation;
 import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Sort;
 import org.apache.calcite.rel.logical.LogicalSort;
-import org.apache.calcite.schema.Schema;
-import org.apache.calcite.schema.SchemaPlus;
 import org.opensearch.sql.ast.statement.Query;
 import org.opensearch.sql.ast.statement.Statement;
 import org.opensearch.sql.ast.tree.UnresolvedPlan;
@@ -45,7 +40,6 @@ public class UnifiedQueryPlanner {
 
   /**
    * Constructs a UnifiedQueryPlanner with a unified query context.
-   * This is the recommended constructor for new code.
    *
    * @param context the unified query context containing CalcitePlanContext
    */
@@ -54,21 +48,6 @@ public UnifiedQueryPlanner(UnifiedQueryContext context) {
     this.context = context;
   }
 
-  /**
-   * Constructs a UnifiedQueryPlanner for a given query type and schema root.
-   * This constructor is maintained for backward compatibility.
-   *
-   * @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
-   * @deprecated Use {@link #UnifiedQueryPlanner(UnifiedQueryContext)} instead
-   */
-  @Deprecated
-  public UnifiedQueryPlanner(QueryType queryType, SchemaPlus rootSchema, String defaultPath) {
-    this.parser = buildQueryParser(queryType);
-    this.context = createContextFromLegacyParams(queryType, rootSchema, defaultPath);
-  }
-
   /**
    * Parses and analyzes a query string into a Calcite logical plan (RelNode). TODO: Generate
    * optimal physical plan to fully unify query execution and leverage Calcite's optimizer.
@@ -94,34 +73,6 @@ private Parser buildQueryParser(QueryType queryType) {
     throw new IllegalArgumentException("Unsupported query type: " + queryType);
   }
 
-  /**
-   * Creates a UnifiedQueryContext from legacy constructor parameters.
-   * Used internally to support backward compatibility.
-   */
-  @SuppressWarnings("deprecation")
-  private static UnifiedQueryContext createContextFromLegacyParams(
-      QueryType queryType, SchemaPlus rootSchema, String defaultPath) {
-    // Extract catalogs from root schema
-    Map catalogs = new HashMap<>();
-    for (String catalogName : rootSchema.getSubSchemaNames()) {
-      Schema catalog = rootSchema.getSubSchema(catalogName);
-      if (catalog != null) {
-        catalogs.put(catalogName, catalog);
-      }
-    }
-
-    // Build context with extracted catalogs
-    UnifiedQueryContext.UnifiedQueryContextBuilder builder =
-        UnifiedQueryContext.builder().queryType(queryType);
-    catalogs.forEach(builder::catalog);
-
-    if (defaultPath != null) {
-      builder.defaultNamespace(defaultPath);
-    }
-
-    return builder.build();
-  }
-
   private UnresolvedPlan parse(String query) {
     ParseTree cst = parser.parse(query);
     AstStatementBuilder astStmtBuilder =
@@ -148,115 +99,4 @@ private RelNode preserveCollation(RelNode logical) {
     }
     return calcitePlan;
   }
-
-  /** Builder for {@link UnifiedQueryPlanner}, supporting declarative fluent API. */
-  public static Builder builder() {
-    return new Builder();
-  }
-
-  /**
-   * Builder for {@link UnifiedQueryPlanner}, supporting both new context-based API
-   * and legacy catalog-based API for backward compatibility.
-   * Delegates all configuration to UnifiedQueryContext.Builder.
-   */
-  public static class Builder {
-    private UnifiedQueryContext context;
-    private UnifiedQueryContext.UnifiedQueryContextBuilder contextBuilder;
-
-    /**
-     * Sets the query language frontend to be used by the planner.
-     * Delegates to context builder.
-     *
-     * @param queryType the {@link QueryType}, such as PPL
-     * @return this builder instance
-     */
-    public Builder language(QueryType queryType) {
-      ensureContextBuilder();
-      contextBuilder.queryType(queryType);
-      return this;
-    }
-
-    /**
-     * Sets the unified query context directly (new API).
-     *
-     * @param context the unified query context
-     * @return this builder instance
-     */
-    public Builder context(UnifiedQueryContext context) {
-      if (contextBuilder != null) {
-        throw new IllegalStateException(
-            "Cannot set context after using language/catalog/defaultNamespace/cacheMetadata methods");
-      }
-      this.context = context;
-      return this;
-    }
-
-    /**
-     * Registers a catalog with the specified name and its associated schema.
-     * Delegates to context builder.
-     *
-     * @param name the name of the catalog to register
-     * @param schema the schema representing the structure of the catalog
-     * @return this builder instance
-     */
-    public Builder catalog(String name, Schema schema) {
-      ensureContextBuilder();
-      contextBuilder.catalog(name, schema);
-      return this;
-    }
-
-    /**
-     * Sets the default namespace path for resolving unqualified table names.
-     * Delegates to context builder.
-     *
-     * @param namespace dot-separated path (e.g., "spark_catalog.default" or "opensearch")
-     * @return this builder instance
-     */
-    public Builder defaultNamespace(String namespace) {
-      ensureContextBuilder();
-      contextBuilder.defaultNamespace(namespace);
-      return this;
-    }
-
-    /**
-     * Enables or disables catalog metadata caching in the root schema.
-     * Delegates to context builder.
-     *
-     * @param cache whether to enable metadata caching
-     * @return this builder instance
-     */
-    public Builder cacheMetadata(boolean cache) {
-      ensureContextBuilder();
-      contextBuilder.cacheMetadata(cache);
-      return this;
-    }
-
-    /**
-     * Builds a {@link UnifiedQueryPlanner} with the configuration.
-     *
-     * @return a new instance of {@link UnifiedQueryPlanner}
-     */
-    public UnifiedQueryPlanner build() {
-      // Build context if not provided directly
-      if (context == null) {
-        if (contextBuilder == null) {
-          throw new IllegalStateException(
-              "Must provide either context or use language/catalog configuration");
-        }
-        context = contextBuilder.build();
-      }
-
-      return new UnifiedQueryPlanner(context);
-    }
-
-    private void ensureContextBuilder() {
-      if (context != null) {
-        throw new IllegalStateException(
-            "Cannot use language/catalog/defaultNamespace/cacheMetadata after setting context");
-      }
-      if (contextBuilder == null) {
-        contextBuilder = UnifiedQueryContext.builder();
-      }
-    }
-  }
 }
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 31fa102f611..eaf915b0939 100644
--- a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java
+++ b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java
@@ -29,11 +29,12 @@ protected Map getSubSchemaMap() {
 
   @Test
   public void testPPLQueryPlanning() {
-    UnifiedQueryPlanner planner =
-        UnifiedQueryPlanner.builder()
-            .language(QueryType.PPL)
+    UnifiedQueryContext context =
+        UnifiedQueryContext.builder()
+            .queryType(QueryType.PPL)
             .catalog("opensearch", testSchema)
             .build();
+    UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context);
 
     RelNode plan = planner.plan("source = opensearch.employees | eval f = abs(id)");
     assertNotNull("Plan should be created", plan);
@@ -41,12 +42,13 @@ public void testPPLQueryPlanning() {
 
   @Test
   public void testPPLQueryPlanningWithDefaultNamespace() {
-    UnifiedQueryPlanner planner =
-        UnifiedQueryPlanner.builder()
-            .language(QueryType.PPL)
+    UnifiedQueryContext context =
+        UnifiedQueryContext.builder()
+            .queryType(QueryType.PPL)
             .catalog("opensearch", testSchema)
             .defaultNamespace("opensearch")
             .build();
+    UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context);
 
     assertNotNull("Plan should be created", planner.plan("source = opensearch.employees"));
     assertNotNull("Plan should be created", planner.plan("source = employees"));
@@ -54,12 +56,13 @@ public void testPPLQueryPlanningWithDefaultNamespace() {
 
   @Test
   public void testPPLQueryPlanningWithDefaultNamespaceMultiLevel() {
-    UnifiedQueryPlanner planner =
-        UnifiedQueryPlanner.builder()
-            .language(QueryType.PPL)
+    UnifiedQueryContext context =
+        UnifiedQueryContext.builder()
+            .queryType(QueryType.PPL)
             .catalog("catalog", testDeepSchema)
             .defaultNamespace("catalog.opensearch")
             .build();
+    UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context);
 
     assertNotNull("Plan should be created", planner.plan("source = catalog.opensearch.employees"));
     assertNotNull("Plan should be created", planner.plan("source = employees"));
@@ -71,12 +74,13 @@ public void testPPLQueryPlanningWithDefaultNamespaceMultiLevel() {
 
   @Test
   public void testPPLQueryPlanningWithMultipleCatalogs() {
-    UnifiedQueryPlanner planner =
-        UnifiedQueryPlanner.builder()
-            .language(QueryType.PPL)
+    UnifiedQueryContext context =
+        UnifiedQueryContext.builder()
+            .queryType(QueryType.PPL)
             .catalog("catalog1", testSchema)
             .catalog("catalog2", testSchema)
             .build();
+    UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context);
 
     RelNode plan =
         planner.plan(
@@ -86,13 +90,14 @@ public void testPPLQueryPlanningWithMultipleCatalogs() {
 
   @Test
   public void testPPLQueryPlanningWithMultipleCatalogsAndDefaultNamespace() {
-    UnifiedQueryPlanner planner =
-        UnifiedQueryPlanner.builder()
-            .language(QueryType.PPL)
+    UnifiedQueryContext context =
+        UnifiedQueryContext.builder()
+            .queryType(QueryType.PPL)
             .catalog("catalog1", testSchema)
             .catalog("catalog2", testSchema)
             .defaultNamespace("catalog2")
             .build();
+    UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context);
 
     RelNode plan =
         planner.plan("source = catalog1.employees | lookup employees id | eval f = abs(id)");
@@ -101,12 +106,13 @@ public void testPPLQueryPlanningWithMultipleCatalogsAndDefaultNamespace() {
 
   @Test
   public void testPPLQueryPlanningWithMetadataCaching() {
-    UnifiedQueryPlanner planner =
-        UnifiedQueryPlanner.builder()
-            .language(QueryType.PPL)
+    UnifiedQueryContext context =
+        UnifiedQueryContext.builder()
+            .queryType(QueryType.PPL)
             .catalog("opensearch", testSchema)
             .cacheMetadata(true)
             .build();
+    UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context);
 
     RelNode plan = planner.plan("source = opensearch.employees");
     assertNotNull("Plan should be created", plan);
@@ -114,21 +120,23 @@ public void testPPLQueryPlanningWithMetadataCaching() {
 
   @Test(expected = IllegalArgumentException.class)
   public void testMissingQueryLanguage() {
-    UnifiedQueryPlanner.builder().catalog("opensearch", testSchema).build();
+    UnifiedQueryContext.builder().catalog("opensearch", testSchema).build();
   }
 
   @Test(expected = IllegalArgumentException.class)
   public void testUnsupportedQueryLanguage() {
-    UnifiedQueryPlanner.builder()
-        .language(QueryType.SQL) // only PPL is supported for now
-        .catalog("opensearch", testSchema)
-        .build();
+    UnifiedQueryContext context =
+        UnifiedQueryContext.builder()
+            .queryType(QueryType.SQL) // only PPL is supported for now
+            .catalog("opensearch", testSchema)
+            .build();
+    new UnifiedQueryPlanner(context);
   }
 
   @Test(expected = IllegalArgumentException.class)
   public void testInvalidDefaultNamespacePath() {
-    UnifiedQueryPlanner.builder()
-        .language(QueryType.PPL)
+    UnifiedQueryContext.builder()
+        .queryType(QueryType.PPL)
         .catalog("opensearch", testSchema)
         .defaultNamespace("nonexistent") // nonexistent namespace path
         .build();
@@ -136,22 +144,24 @@ public void testInvalidDefaultNamespacePath() {
 
   @Test(expected = IllegalStateException.class)
   public void testUnsupportedStatementType() {
-    UnifiedQueryPlanner planner =
-        UnifiedQueryPlanner.builder()
-            .language(QueryType.PPL)
+    UnifiedQueryContext context =
+        UnifiedQueryContext.builder()
+            .queryType(QueryType.PPL)
             .catalog("opensearch", testSchema)
             .build();
+    UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context);
 
     planner.plan("explain source = employees"); // explain statement
   }
 
   @Test(expected = SyntaxCheckException.class)
   public void testPlanPropagatingSyntaxCheckException() {
-    UnifiedQueryPlanner planner =
-        UnifiedQueryPlanner.builder()
-            .language(QueryType.PPL)
+    UnifiedQueryContext context =
+        UnifiedQueryContext.builder()
+            .queryType(QueryType.PPL)
             .catalog("opensearch", testSchema)
             .build();
+    UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context);
 
     planner.plan("source = employees | eval"); // Trigger syntax error from parser
   }
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..c5e5b03a677 100644
--- a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryTestBase.java
+++ b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryTestBase.java
@@ -35,12 +35,13 @@ protected Map getTableMap() {
           }
         };
 
-    planner =
-        UnifiedQueryPlanner.builder()
-            .language(QueryType.PPL)
+    UnifiedQueryContext context =
+        UnifiedQueryContext.builder()
+            .queryType(QueryType.PPL)
             .catalog("catalog", testSchema)
             .defaultNamespace("catalog")
             .build();
+    planner = new UnifiedQueryPlanner(context);
   }
 
   protected Table createEmployeesTable() {

From d9d852a120fb1b782dc9aeecdc419d102f61584b Mon Sep 17 00:00:00 2001
From: Chen Dai 
Date: Wed, 10 Dec 2025 09:22:24 -0800
Subject: [PATCH 3/9] Refactor unified query context with setting setter

Signed-off-by: Chen Dai 
---
 api/README.md                                 | 16 ++++-
 .../sql/api/UnifiedQueryContext.java          | 71 +++++++++++++++----
 .../sql/api/UnifiedQueryPlanner.java          |  3 +-
 .../sql/api/UnifiedQueryPlannerTest.java      | 18 +++++
 4 files changed, 90 insertions(+), 18 deletions(-)

diff --git a/api/README.md b/api/README.md
index f4e635272ff..12b8162afee 100644
--- a/api/README.md
+++ b/api/README.md
@@ -19,7 +19,7 @@ Together, these components enable a complete workflow: parse PPL queries into lo
 
 ### UnifiedQueryContext and UnifiedQueryPlanner
 
-Create a reusable `UnifiedQueryContext` that encapsulates a `CalcitePlanContext` with all catalog configuration and query type. This context can be shared across multiple queries.
+Create a reusable `UnifiedQueryContext` that encapsulates a `CalcitePlanContext` with all catalog configuration, query type, and settings. This context can be shared across multiple queries.
 
 ```java
 // Create a reusable context with query type and catalogs
@@ -29,7 +29,7 @@ UnifiedQueryContext context = UnifiedQueryContext.builder()
     .catalog("spark_catalog", sparkSchema)
     .defaultNamespace("opensearch")
     .cacheMetadata(true)
-    .build();
+    .build();  // Uses default settings
 
 // Create planner with context
 UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context);
@@ -39,6 +39,18 @@ RelNode plan1 = planner.plan("source = logs | where status = 200");
 RelNode plan2 = planner.plan("source = metrics | stats avg(cpu)");
 ```
 
+For queries requiring specific settings (e.g., joins):
+
+```java
+UnifiedQueryContext context = UnifiedQueryContext.builder()
+    .queryType(QueryType.PPL)
+    .catalog("opensearch", schema)
+    .setting("plugins.query.size_limit", 10000)
+    .setting("plugins.ppl.subsearch.maxout", 10000)
+    .setting("plugins.ppl.join.subsearch_maxout", 50000)
+    .build();
+```
+
 ### UnifiedQueryTranspiler
 
 Use `UnifiedQueryTranspiler` to convert Calcite logical plans into SQL strings for target databases. The transpiler supports various SQL dialects through Calcite's `SqlDialect` interface.
diff --git a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java
index 4b6ac137dcb..0c57e7cb127 100644
--- a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java
+++ b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java
@@ -42,55 +42,63 @@
 @Value
 public class UnifiedQueryContext {
 
+  /** The CalcitePlanContext that holds all Calcite configuration and query type. */
+  CalcitePlanContext planContext;
+
   /**
-   * The CalcitePlanContext that holds all Calcite configuration and query type. This is the only
-   * field stored - everything else is just for building this.
+   * Settings for query execution configuration. Used by parsers to validate query features (e.g.,
+   * join types).
    */
-  CalcitePlanContext planContext;
+  org.opensearch.sql.common.setting.Settings settings;
 
   /** Creates a new builder for UnifiedQueryContext. */
-  public static UnifiedQueryContextBuilder builder() {
-    return new UnifiedQueryContextBuilder();
+  public static Builder builder() {
+    return new Builder();
   }
 
   /**
    * Builder for UnifiedQueryContext with validation. Builds the CalcitePlanContext with all
    * necessary configuration.
    */
-  public static class UnifiedQueryContextBuilder {
+  public static class Builder {
     private QueryType queryType;
     private final Map catalogs = new HashMap<>();
     private String defaultNamespace;
     private boolean cacheMetadata = false;
-    private SysLimit sysLimit = new SysLimit(10000, 10000, 10000);
+    private final Map settingValues = new HashMap<>();
 
     /** Sets the query type. */
-    public UnifiedQueryContextBuilder queryType(QueryType queryType) {
+    public Builder queryType(QueryType queryType) {
       this.queryType = queryType;
       return this;
     }
 
     /** Registers a catalog with the specified name and schema. */
-    public UnifiedQueryContextBuilder catalog(String name, Schema schema) {
+    public Builder catalog(String name, Schema schema) {
       catalogs.put(name, schema);
       return this;
     }
 
     /** Sets the default namespace path. */
-    public UnifiedQueryContextBuilder defaultNamespace(String namespace) {
+    public Builder defaultNamespace(String namespace) {
       this.defaultNamespace = namespace;
       return this;
     }
 
     /** Enables or disables metadata caching. */
-    public UnifiedQueryContextBuilder cacheMetadata(boolean cache) {
+    public Builder cacheMetadata(boolean cache) {
       this.cacheMetadata = cache;
       return this;
     }
 
-    /** Sets the query execution limits. */
-    public UnifiedQueryContextBuilder sysLimit(SysLimit limit) {
-      this.sysLimit = limit;
+    /**
+     * Sets a specific setting value by name.
+     *
+     * @param name the setting key name (e.g., "plugins.query.size_limit")
+     * @param value the setting value
+     */
+    public Builder setting(String name, Object value) {
+      settingValues.put(name, value);
       return this;
     }
 
@@ -103,14 +111,47 @@ public UnifiedQueryContext build() {
         throw new IllegalArgumentException("QueryType must be specified");
       }
 
+      // Build settings from provided values
+      org.opensearch.sql.common.setting.Settings settings = buildSettings();
+
       // Build and validate framework config
       FrameworkConfig frameworkConfig = buildFrameworkConfig();
 
+      // Create SysLimit from settings
+      SysLimit sysLimit = SysLimit.fromSettings(settings);
+
       // Create CalcitePlanContext with all configuration
       CalcitePlanContext planContext =
           CalcitePlanContext.create(frameworkConfig, sysLimit, queryType);
 
-      return new UnifiedQueryContext(planContext);
+      return new UnifiedQueryContext(planContext, settings);
+    }
+
+    /**
+     * Builds Settings from the provided setting values. Returns a Settings instance that looks up
+     * values from the settingValues map. Users must configure all required settings explicitly.
+     */
+    private org.opensearch.sql.common.setting.Settings buildSettings() {
+      final Map settingsMap =
+          new HashMap<>();
+      settingValues.forEach(
+          (name, value) -> {
+            org.opensearch.sql.common.setting.Settings.Key.of(name)
+                .ifPresent(key -> settingsMap.put(key, value));
+          });
+
+      return new org.opensearch.sql.common.setting.Settings() {
+        @Override
+        @SuppressWarnings("unchecked")
+        public  T getSettingValue(Key key) {
+          return (T) settingsMap.get(key);
+        }
+
+        @Override
+        public List getSettings() {
+          return List.copyOf(settingsMap.entrySet());
+        }
+      };
     }
 
     @SuppressWarnings({"rawtypes"})
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 b1d3687de93..91e35335e20 100644
--- a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryPlanner.java
+++ b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryPlanner.java
@@ -77,7 +77,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, context.getSettings()),
+            AstStatementBuilder.StatementBuilderContext.builder().build());
     Statement statement = cst.accept(astStmtBuilder);
 
     if (statement instanceof Query) {
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 eaf915b0939..8ff2c530205 100644
--- a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java
+++ b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java
@@ -40,6 +40,24 @@ public void testPPLQueryPlanning() {
     assertNotNull("Plan should be created", plan);
   }
 
+  @Test
+  public void testPPLJoinQueryPlanning() {
+    UnifiedQueryContext context =
+        UnifiedQueryContext.builder()
+            .queryType(QueryType.PPL)
+            .catalog("opensearch", testSchema)
+            .defaultNamespace("opensearch")
+            .setting("plugins.query.size_limit", 10000)
+            .setting("plugins.ppl.subsearch.maxout", 10000)
+            .setting("plugins.ppl.join.subsearch_maxout", 50000)
+            .build();
+    UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context);
+
+    RelNode plan =
+        planner.plan("source = employees | join on employees.id = employees.age employees");
+    assertNotNull("Join query should be created", plan);
+  }
+
   @Test
   public void testPPLQueryPlanningWithDefaultNamespace() {
     UnifiedQueryContext context =

From 2e5da1a342642f2c3c4117e3b9b15fe317e3ad04 Mon Sep 17 00:00:00 2001
From: Chen Dai 
Date: Wed, 10 Dec 2025 11:43:44 -0800
Subject: [PATCH 4/9] Initialize unified query context with default system
 limits

Signed-off-by: Chen Dai 
---
 .../sql/api/UnifiedQueryContext.java          | 54 +++++++-------
 .../sql/api/UnifiedQueryContextTest.java      | 73 +++++++++++++++++++
 .../sql/api/UnifiedQueryPlannerTest.java      | 41 -----------
 3 files changed, 99 insertions(+), 69 deletions(-)
 create mode 100644 api/src/test/java/org/opensearch/sql/api/UnifiedQueryContextTest.java

diff --git a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java
index 0c57e7cb127..c91507bd0b7 100644
--- a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java
+++ b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java
@@ -5,9 +5,12 @@
 
 package org.opensearch.sql.api;
 
+import static org.opensearch.sql.common.setting.Settings.Key.*;
+
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import lombok.Value;
 import org.apache.calcite.jdbc.CalciteSchema;
 import org.apache.calcite.plan.RelTraitDef;
@@ -20,6 +23,8 @@
 import org.apache.calcite.tools.Programs;
 import org.opensearch.sql.calcite.CalcitePlanContext;
 import org.opensearch.sql.calcite.SysLimit;
+import org.opensearch.sql.common.setting.Settings;
+import org.opensearch.sql.common.setting.Settings.Key;
 import org.opensearch.sql.executor.QueryType;
 
 /**
@@ -49,7 +54,7 @@ public class UnifiedQueryContext {
    * Settings for query execution configuration. Used by parsers to validate query features (e.g.,
    * join types).
    */
-  org.opensearch.sql.common.setting.Settings settings;
+  Settings settings;
 
   /** Creates a new builder for UnifiedQueryContext. */
   public static Builder builder() {
@@ -65,7 +70,17 @@ public static class Builder {
     private final Map catalogs = new HashMap<>();
     private String defaultNamespace;
     private boolean cacheMetadata = false;
-    private final Map settingValues = new HashMap<>();
+
+    /**
+     * Setting values with defaults from SysLimit.DEFAULT. Only includes planning-required settings
+     * to avoid coupling with OpenSearchSettings.
+     */
+    private final Map settingValues =
+        new HashMap<>(
+            Map.of(
+                QUERY_SIZE_LIMIT.getKeyValue(), SysLimit.DEFAULT.querySizeLimit(),
+                PPL_SUBSEARCH_MAXOUT.getKeyValue(), SysLimit.DEFAULT.subsearchLimit(),
+                PPL_JOIN_SUBSEARCH_MAXOUT.getKeyValue(), SysLimit.DEFAULT.joinSubsearchLimit()));
 
     /** Sets the query type. */
     public Builder queryType(QueryType queryType) {
@@ -107,40 +122,23 @@ public Builder setting(String name, Object value) {
      * fast on invalid configuration.
      */
     public UnifiedQueryContext build() {
-      if (queryType == null) {
-        throw new IllegalArgumentException("QueryType must be specified");
-      }
-
-      // Build settings from provided values
-      org.opensearch.sql.common.setting.Settings settings = buildSettings();
+      Objects.requireNonNull(queryType, "QueryType must be specified");
 
-      // Build and validate framework config
-      FrameworkConfig frameworkConfig = buildFrameworkConfig();
-
-      // Create SysLimit from settings
-      SysLimit sysLimit = SysLimit.fromSettings(settings);
-
-      // Create CalcitePlanContext with all configuration
       CalcitePlanContext planContext =
-          CalcitePlanContext.create(frameworkConfig, sysLimit, queryType);
-
-      return new UnifiedQueryContext(planContext, settings);
+          CalcitePlanContext.create(
+              buildFrameworkConfig(), SysLimit.fromSettings(buildSettings()), queryType);
+      return new UnifiedQueryContext(planContext, buildSettings());
     }
 
-    /**
-     * Builds Settings from the provided setting values. Returns a Settings instance that looks up
-     * values from the settingValues map. Users must configure all required settings explicitly.
-     */
-    private org.opensearch.sql.common.setting.Settings buildSettings() {
-      final Map settingsMap =
-          new HashMap<>();
+    /** Builds Settings from the settingValues map (which includes defaults). */
+    private Settings buildSettings() {
+      final Map settingsMap = new HashMap<>();
       settingValues.forEach(
           (name, value) -> {
-            org.opensearch.sql.common.setting.Settings.Key.of(name)
-                .ifPresent(key -> settingsMap.put(key, value));
+            Key.of(name).ifPresent(key -> settingsMap.put(key, value));
           });
 
-      return new org.opensearch.sql.common.setting.Settings() {
+      return new Settings() {
         @Override
         @SuppressWarnings("unchecked")
         public  T getSettingValue(Key key) {
diff --git a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryContextTest.java b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryContextTest.java
new file mode 100644
index 00000000000..37d64d0e84b
--- /dev/null
+++ b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryContextTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.opensearch.sql.api;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.opensearch.sql.common.setting.Settings.Key.*;
+
+import org.junit.Test;
+import org.opensearch.sql.calcite.SysLimit;
+import org.opensearch.sql.executor.QueryType;
+
+public class UnifiedQueryContextTest extends UnifiedQueryTestBase {
+
+  @Test
+  public void testContextCreationWithDefaults() {
+    UnifiedQueryContext context =
+        UnifiedQueryContext.builder()
+            .queryType(QueryType.PPL)
+            .catalog("opensearch", testSchema)
+            .defaultNamespace("opensearch")
+            .build();
+
+    assertNotNull("Context should be created", context);
+    assertNotNull("PlanContext should be created", context.getPlanContext());
+    assertNotNull("Settings should be created", context.getSettings());
+    assertEquals(
+        "Settings should have default system limits",
+        SysLimit.DEFAULT,
+        SysLimit.fromSettings(context.getSettings()));
+  }
+
+  @Test
+  public void testContextCreationWithCustomSettings() {
+    UnifiedQueryContext context =
+        UnifiedQueryContext.builder()
+            .queryType(QueryType.PPL)
+            .catalog("opensearch", testSchema)
+            .cacheMetadata(false)
+            .setting("plugins.query.size_limit", 200)
+            .build();
+
+    Integer querySizeLimit = context.getSettings().getSettingValue(QUERY_SIZE_LIMIT);
+    assertEquals(Integer.valueOf(200), querySizeLimit);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testMissingQueryType() {
+    UnifiedQueryContext.builder().catalog("opensearch", testSchema).build();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUnsupportedQueryType() {
+    UnifiedQueryContext context =
+        UnifiedQueryContext.builder()
+            .queryType(QueryType.SQL) // only PPL is supported for now
+            .catalog("opensearch", testSchema)
+            .build();
+    new UnifiedQueryPlanner(context);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testInvalidDefaultNamespace() {
+    UnifiedQueryContext.builder()
+        .queryType(QueryType.PPL)
+        .catalog("opensearch", testSchema)
+        .defaultNamespace("nonexistent")
+        .build();
+  }
+}
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 8ff2c530205..e5ec5d0fa4d 100644
--- a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java
+++ b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java
@@ -47,9 +47,6 @@ public void testPPLJoinQueryPlanning() {
             .queryType(QueryType.PPL)
             .catalog("opensearch", testSchema)
             .defaultNamespace("opensearch")
-            .setting("plugins.query.size_limit", 10000)
-            .setting("plugins.ppl.subsearch.maxout", 10000)
-            .setting("plugins.ppl.join.subsearch_maxout", 50000)
             .build();
     UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context);
 
@@ -122,44 +119,6 @@ public void testPPLQueryPlanningWithMultipleCatalogsAndDefaultNamespace() {
     assertNotNull("Plan should be created with multiple catalogs", plan);
   }
 
-  @Test
-  public void testPPLQueryPlanningWithMetadataCaching() {
-    UnifiedQueryContext context =
-        UnifiedQueryContext.builder()
-            .queryType(QueryType.PPL)
-            .catalog("opensearch", testSchema)
-            .cacheMetadata(true)
-            .build();
-    UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context);
-
-    RelNode plan = planner.plan("source = opensearch.employees");
-    assertNotNull("Plan should be created", plan);
-  }
-
-  @Test(expected = IllegalArgumentException.class)
-  public void testMissingQueryLanguage() {
-    UnifiedQueryContext.builder().catalog("opensearch", testSchema).build();
-  }
-
-  @Test(expected = IllegalArgumentException.class)
-  public void testUnsupportedQueryLanguage() {
-    UnifiedQueryContext context =
-        UnifiedQueryContext.builder()
-            .queryType(QueryType.SQL) // only PPL is supported for now
-            .catalog("opensearch", testSchema)
-            .build();
-    new UnifiedQueryPlanner(context);
-  }
-
-  @Test(expected = IllegalArgumentException.class)
-  public void testInvalidDefaultNamespacePath() {
-    UnifiedQueryContext.builder()
-        .queryType(QueryType.PPL)
-        .catalog("opensearch", testSchema)
-        .defaultNamespace("nonexistent") // nonexistent namespace path
-        .build();
-  }
-
   @Test(expected = IllegalStateException.class)
   public void testUnsupportedStatementType() {
     UnifiedQueryContext context =

From 5d4595cedc37c704eae6fdd36925ba64d139e9df Mon Sep 17 00:00:00 2001
From: Chen Dai 
Date: Wed, 10 Dec 2025 13:20:48 -0800
Subject: [PATCH 5/9] Refactor setting map read

Signed-off-by: Chen Dai 
---
 api/README.md                                 | 36 +++++-----
 .../sql/api/UnifiedQueryContext.java          | 70 ++++++-------------
 .../sql/api/UnifiedQueryContextTest.java      | 15 +++-
 3 files changed, 51 insertions(+), 70 deletions(-)

diff --git a/api/README.md b/api/README.md
index 12b8162afee..abae10cc97d 100644
--- a/api/README.md
+++ b/api/README.md
@@ -17,20 +17,28 @@ Together, these components enable a complete workflow: parse PPL queries into lo
 
 ## Usage
 
-### UnifiedQueryContext and UnifiedQueryPlanner
+### UnifiedQueryContext
 
-Create a reusable `UnifiedQueryContext` that encapsulates a `CalcitePlanContext` with all catalog configuration, query type, and settings. This context can be shared across multiple queries.
+`UnifiedQueryContext` is a reusable session-level abstraction shared across unified query components (planner, compiler, etc.). It bundles `CalcitePlanContext` and `Settings` into a single object, centralizing configuration for all unified query operations.
+
+Create a context with catalog configuration, query type, and optional settings:
 
 ```java
-// Create a reusable context with query type and catalogs
 UnifiedQueryContext context = UnifiedQueryContext.builder()
     .queryType(QueryType.PPL)
     .catalog("opensearch", opensearchSchema)
     .catalog("spark_catalog", sparkSchema)
     .defaultNamespace("opensearch")
     .cacheMetadata(true)
-    .build();  // Uses default settings
+    .setting("plugins.query.size_limit", 200)  // Optional: override defaults
+    .build();
+```
+
+### UnifiedQueryPlanner
 
+Use `UnifiedQueryPlanner` to parse and analyze PPL queries into Calcite logical plans. The planner accepts a `UnifiedQueryContext` and can be reused for multiple queries.
+
+```java
 // Create planner with context
 UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context);
 
@@ -39,18 +47,6 @@ RelNode plan1 = planner.plan("source = logs | where status = 200");
 RelNode plan2 = planner.plan("source = metrics | stats avg(cpu)");
 ```
 
-For queries requiring specific settings (e.g., joins):
-
-```java
-UnifiedQueryContext context = UnifiedQueryContext.builder()
-    .queryType(QueryType.PPL)
-    .catalog("opensearch", schema)
-    .setting("plugins.query.size_limit", 10000)
-    .setting("plugins.ppl.subsearch.maxout", 10000)
-    .setting("plugins.ppl.join.subsearch_maxout", 50000)
-    .build();
-```
-
 ### UnifiedQueryTranspiler
 
 Use `UnifiedQueryTranspiler` to convert Calcite logical plans into SQL strings for target databases. The transpiler supports various SQL dialects through Calcite's `SqlDialect` interface.
@@ -65,23 +61,23 @@ String sql = transpiler.toSql(plan);
 
 ### Complete Workflow Example
 
-Combining both components to transpile PPL queries into target database SQL:
+Combining all components to transpile PPL queries into target database SQL:
 
 ```java
-// Step 1: Create reusable context with query type
+// Step 1: Create reusable context (shared across components)
 UnifiedQueryContext context = UnifiedQueryContext.builder()
     .queryType(QueryType.PPL)
     .catalog("catalog", schema)
     .defaultNamespace("catalog")
     .build();
 
-// Step 2: Initialize planner with context
+// Step 2: Create planner with context
 UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context);
 
 // Step 3: Parse PPL query into logical plan
 RelNode plan = planner.plan("source = employees | where age > 30");
 
-// Step 4: Initialize transpiler with target dialect
+// Step 4: Create transpiler with target dialect
 UnifiedQueryTranspiler transpiler = UnifiedQueryTranspiler.builder()
     .dialect(SparkSqlDialect.DEFAULT)
     .build();
diff --git a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java
index c91507bd0b7..a6c55109f30 100644
--- a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java
+++ b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java
@@ -5,7 +5,9 @@
 
 package org.opensearch.sql.api;
 
-import static org.opensearch.sql.common.setting.Settings.Key.*;
+import static org.opensearch.sql.common.setting.Settings.Key.PPL_JOIN_SUBSEARCH_MAXOUT;
+import static org.opensearch.sql.common.setting.Settings.Key.PPL_SUBSEARCH_MAXOUT;
+import static org.opensearch.sql.common.setting.Settings.Key.QUERY_SIZE_LIMIT;
 
 import java.util.HashMap;
 import java.util.List;
@@ -24,36 +26,20 @@
 import org.opensearch.sql.calcite.CalcitePlanContext;
 import org.opensearch.sql.calcite.SysLimit;
 import org.opensearch.sql.common.setting.Settings;
-import org.opensearch.sql.common.setting.Settings.Key;
 import org.opensearch.sql.executor.QueryType;
 
 /**
- * Represents a unified query context that encapsulates a CalcitePlanContext. Contexts are immutable
- * and thread-safe, designed to be reused across multiple queries.
- *
- * 

Example usage: - * - *

{@code
- * UnifiedQueryContext context = UnifiedQueryContext.builder()
- *     .queryType(QueryType.PPL)
- *     .catalog("opensearch", mySchema)
- *     .defaultNamespace("opensearch")
- *     .build();
- *
- * UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context);
- * RelNode plan = planner.plan("source=logs | where status=200");
- * }
+ * A reusable session-level abstraction shared across unified query components (planner, compiler, + * etc.). This centralizes configuration for catalog schemas, query type, execution limits, and + * other settings, enabling consistent behavior across all unified query operations. */ @Value public class UnifiedQueryContext { - /** The CalcitePlanContext that holds all Calcite configuration and query type. */ + /** CalcitePlanContext containing Calcite framework configuration and query type. */ CalcitePlanContext planContext; - /** - * Settings for query execution configuration. Used by parsers to validate query features (e.g., - * join types). - */ + /** Settings containing execution limits and feature flags used by parsers and planners. */ Settings settings; /** Creates a new builder for UnifiedQueryContext. */ @@ -61,10 +47,7 @@ public static Builder builder() { return new Builder(); } - /** - * Builder for UnifiedQueryContext with validation. Builds the CalcitePlanContext with all - * necessary configuration. - */ + /** Builder that constructs UnifiedQueryContext. */ public static class Builder { private QueryType queryType; private final Map catalogs = new HashMap<>(); @@ -75,12 +58,12 @@ public static class Builder { * Setting values with defaults from SysLimit.DEFAULT. Only includes planning-required settings * to avoid coupling with OpenSearchSettings. */ - private final Map settingValues = - new HashMap<>( + private final Map settings = + new HashMap( Map.of( - QUERY_SIZE_LIMIT.getKeyValue(), SysLimit.DEFAULT.querySizeLimit(), - PPL_SUBSEARCH_MAXOUT.getKeyValue(), SysLimit.DEFAULT.subsearchLimit(), - PPL_JOIN_SUBSEARCH_MAXOUT.getKeyValue(), SysLimit.DEFAULT.joinSubsearchLimit())); + QUERY_SIZE_LIMIT, SysLimit.DEFAULT.querySizeLimit(), + PPL_SUBSEARCH_MAXOUT, SysLimit.DEFAULT.subsearchLimit(), + PPL_JOIN_SUBSEARCH_MAXOUT, SysLimit.DEFAULT.joinSubsearchLimit())); /** Sets the query type. */ public Builder queryType(QueryType queryType) { @@ -111,16 +94,17 @@ public Builder cacheMetadata(boolean cache) { * * @param name the setting key name (e.g., "plugins.query.size_limit") * @param value the setting value + * @throws IllegalArgumentException if the setting name is not recognized */ public Builder setting(String name, Object value) { - settingValues.put(name, value); + Settings.Key key = + Settings.Key.of(name) + .orElseThrow(() -> new IllegalArgumentException("Unknown setting name: " + name)); + settings.put(key, value); return this; } - /** - * Builds the UnifiedQueryContext with validation. Validates the default namespace path to fail - * fast on invalid configuration. - */ + /** Builds the UnifiedQueryContext from the provided configuration. */ public UnifiedQueryContext build() { Objects.requireNonNull(queryType, "QueryType must be specified"); @@ -130,24 +114,17 @@ public UnifiedQueryContext build() { return new UnifiedQueryContext(planContext, buildSettings()); } - /** Builds Settings from the settingValues map (which includes defaults). */ private Settings buildSettings() { - final Map settingsMap = new HashMap<>(); - settingValues.forEach( - (name, value) -> { - Key.of(name).ifPresent(key -> settingsMap.put(key, value)); - }); - return new Settings() { @Override @SuppressWarnings("unchecked") public T getSettingValue(Key key) { - return (T) settingsMap.get(key); + return (T) settings.get(key); } @Override public List getSettings() { - return List.copyOf(settingsMap.entrySet()); + return List.copyOf(settings.entrySet()); } }; } @@ -166,8 +143,7 @@ private FrameworkConfig buildFrameworkConfig() { .build(); } - @SuppressWarnings("deprecation") - private static SchemaPlus findSchemaByPath(SchemaPlus rootSchema, String defaultPath) { + private SchemaPlus findSchemaByPath(SchemaPlus rootSchema, String defaultPath) { if (defaultPath == null) { return rootSchema; } diff --git a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryContextTest.java b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryContextTest.java index 37d64d0e84b..999afd17b05 100644 --- a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryContextTest.java +++ b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryContextTest.java @@ -34,17 +34,26 @@ public void testContextCreationWithDefaults() { } @Test - public void testContextCreationWithCustomSettings() { + public void testContextCreationWithCustomConfig() { UnifiedQueryContext context = UnifiedQueryContext.builder() .queryType(QueryType.PPL) .catalog("opensearch", testSchema) - .cacheMetadata(false) + .cacheMetadata(true) .setting("plugins.query.size_limit", 200) .build(); Integer querySizeLimit = context.getSettings().getSettingValue(QUERY_SIZE_LIMIT); - assertEquals(Integer.valueOf(200), querySizeLimit); + assertEquals("Custom setting should be applied", Integer.valueOf(200), querySizeLimit); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidSettingName() { + UnifiedQueryContext.builder() + .queryType(QueryType.PPL) + .catalog("opensearch", testSchema) + .setting("invalid.setting.name", 123) + .build(); } @Test(expected = NullPointerException.class) From d82c836044225fa685495e9366341052cd09c49f Mon Sep 17 00:00:00 2001 From: Chen Dai Date: Wed, 10 Dec 2025 13:50:05 -0800 Subject: [PATCH 6/9] Update javadoc and rename queryType to language Signed-off-by: Chen Dai --- api/README.md | 6 +-- .../sql/api/UnifiedQueryContext.java | 41 +++++++++++++++---- .../sql/api/UnifiedQueryContextTest.java | 10 ++--- .../sql/api/UnifiedQueryPlannerTest.java | 16 ++++---- .../sql/api/UnifiedQueryTestBase.java | 2 +- 5 files changed, 51 insertions(+), 24 deletions(-) diff --git a/api/README.md b/api/README.md index abae10cc97d..a57644f4659 100644 --- a/api/README.md +++ b/api/README.md @@ -25,12 +25,12 @@ Create a context with catalog configuration, query type, and optional settings: ```java UnifiedQueryContext context = UnifiedQueryContext.builder() - .queryType(QueryType.PPL) + .language(QueryType.PPL) .catalog("opensearch", opensearchSchema) .catalog("spark_catalog", sparkSchema) .defaultNamespace("opensearch") .cacheMetadata(true) - .setting("plugins.query.size_limit", 200) // Optional: override defaults + .setting("plugins.query.size_limit", 200) .build(); ``` @@ -66,7 +66,7 @@ Combining all components to transpile PPL queries into target database SQL: ```java // Step 1: Create reusable context (shared across components) UnifiedQueryContext context = UnifiedQueryContext.builder() - .queryType(QueryType.PPL) + .language(QueryType.PPL) .catalog("catalog", schema) .defaultNamespace("catalog") .build(); diff --git a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java index a6c55109f30..f8ae812b168 100644 --- a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java +++ b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java @@ -65,25 +65,48 @@ public static class Builder { PPL_SUBSEARCH_MAXOUT, SysLimit.DEFAULT.subsearchLimit(), PPL_JOIN_SUBSEARCH_MAXOUT, SysLimit.DEFAULT.joinSubsearchLimit())); - /** Sets the query type. */ - public Builder queryType(QueryType queryType) { + /** + * Sets the query language frontend to be used. + * + * @param queryType the {@link QueryType}, such as PPL + * @return this builder instance + */ + public Builder language(QueryType queryType) { this.queryType = queryType; return this; } - /** Registers a catalog with the specified name and schema. */ + /** + * 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 + * organized. + * + * @param name the name of the catalog to register + * @param schema the schema representing the structure of the catalog + * @return this builder instance + */ public Builder catalog(String name, Schema schema) { catalogs.put(name, schema); return this; } - /** Sets the default namespace path. */ + /** + * Sets the default namespace path for resolving unqualified table names. + * + * @param namespace dot-separated path (e.g., "spark_catalog.default" or "opensearch") + * @return this builder instance + */ public Builder defaultNamespace(String namespace) { this.defaultNamespace = namespace; return this; } - /** Enables or disables metadata caching. */ + /** + * Enables or disables catalog metadata caching in the root schema. + * + * @param cache whether to enable metadata caching + * @return this builder instance + */ public Builder cacheMetadata(boolean cache) { this.cacheMetadata = cache; return this; @@ -104,9 +127,13 @@ public Builder setting(String name, Object value) { return this; } - /** Builds the UnifiedQueryContext from the provided configuration. */ + /** + * Builds a {@link UnifiedQueryContext} with the configuration. + * + * @return a new instance of {@link UnifiedQueryContext} + */ public UnifiedQueryContext build() { - Objects.requireNonNull(queryType, "QueryType must be specified"); + Objects.requireNonNull(queryType, "Must specify language before build"); CalcitePlanContext planContext = CalcitePlanContext.create( diff --git a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryContextTest.java b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryContextTest.java index 999afd17b05..3be36ee435e 100644 --- a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryContextTest.java +++ b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryContextTest.java @@ -19,7 +19,7 @@ public class UnifiedQueryContextTest extends UnifiedQueryTestBase { public void testContextCreationWithDefaults() { UnifiedQueryContext context = UnifiedQueryContext.builder() - .queryType(QueryType.PPL) + .language(QueryType.PPL) .catalog("opensearch", testSchema) .defaultNamespace("opensearch") .build(); @@ -37,7 +37,7 @@ public void testContextCreationWithDefaults() { public void testContextCreationWithCustomConfig() { UnifiedQueryContext context = UnifiedQueryContext.builder() - .queryType(QueryType.PPL) + .language(QueryType.PPL) .catalog("opensearch", testSchema) .cacheMetadata(true) .setting("plugins.query.size_limit", 200) @@ -50,7 +50,7 @@ public void testContextCreationWithCustomConfig() { @Test(expected = IllegalArgumentException.class) public void testInvalidSettingName() { UnifiedQueryContext.builder() - .queryType(QueryType.PPL) + .language(QueryType.PPL) .catalog("opensearch", testSchema) .setting("invalid.setting.name", 123) .build(); @@ -65,7 +65,7 @@ public void testMissingQueryType() { public void testUnsupportedQueryType() { UnifiedQueryContext context = UnifiedQueryContext.builder() - .queryType(QueryType.SQL) // only PPL is supported for now + .language(QueryType.SQL) // only PPL is supported for now .catalog("opensearch", testSchema) .build(); new UnifiedQueryPlanner(context); @@ -74,7 +74,7 @@ public void testUnsupportedQueryType() { @Test(expected = IllegalArgumentException.class) public void testInvalidDefaultNamespace() { UnifiedQueryContext.builder() - .queryType(QueryType.PPL) + .language(QueryType.PPL) .catalog("opensearch", testSchema) .defaultNamespace("nonexistent") .build(); 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 e5ec5d0fa4d..536db8d3b37 100644 --- a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java +++ b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java @@ -31,7 +31,7 @@ protected Map getSubSchemaMap() { public void testPPLQueryPlanning() { UnifiedQueryContext context = UnifiedQueryContext.builder() - .queryType(QueryType.PPL) + .language(QueryType.PPL) .catalog("opensearch", testSchema) .build(); UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context); @@ -44,7 +44,7 @@ public void testPPLQueryPlanning() { public void testPPLJoinQueryPlanning() { UnifiedQueryContext context = UnifiedQueryContext.builder() - .queryType(QueryType.PPL) + .language(QueryType.PPL) .catalog("opensearch", testSchema) .defaultNamespace("opensearch") .build(); @@ -59,7 +59,7 @@ public void testPPLJoinQueryPlanning() { public void testPPLQueryPlanningWithDefaultNamespace() { UnifiedQueryContext context = UnifiedQueryContext.builder() - .queryType(QueryType.PPL) + .language(QueryType.PPL) .catalog("opensearch", testSchema) .defaultNamespace("opensearch") .build(); @@ -73,7 +73,7 @@ public void testPPLQueryPlanningWithDefaultNamespace() { public void testPPLQueryPlanningWithDefaultNamespaceMultiLevel() { UnifiedQueryContext context = UnifiedQueryContext.builder() - .queryType(QueryType.PPL) + .language(QueryType.PPL) .catalog("catalog", testDeepSchema) .defaultNamespace("catalog.opensearch") .build(); @@ -91,7 +91,7 @@ public void testPPLQueryPlanningWithDefaultNamespaceMultiLevel() { public void testPPLQueryPlanningWithMultipleCatalogs() { UnifiedQueryContext context = UnifiedQueryContext.builder() - .queryType(QueryType.PPL) + .language(QueryType.PPL) .catalog("catalog1", testSchema) .catalog("catalog2", testSchema) .build(); @@ -107,7 +107,7 @@ public void testPPLQueryPlanningWithMultipleCatalogs() { public void testPPLQueryPlanningWithMultipleCatalogsAndDefaultNamespace() { UnifiedQueryContext context = UnifiedQueryContext.builder() - .queryType(QueryType.PPL) + .language(QueryType.PPL) .catalog("catalog1", testSchema) .catalog("catalog2", testSchema) .defaultNamespace("catalog2") @@ -123,7 +123,7 @@ public void testPPLQueryPlanningWithMultipleCatalogsAndDefaultNamespace() { public void testUnsupportedStatementType() { UnifiedQueryContext context = UnifiedQueryContext.builder() - .queryType(QueryType.PPL) + .language(QueryType.PPL) .catalog("opensearch", testSchema) .build(); UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context); @@ -135,7 +135,7 @@ public void testUnsupportedStatementType() { public void testPlanPropagatingSyntaxCheckException() { UnifiedQueryContext context = UnifiedQueryContext.builder() - .queryType(QueryType.PPL) + .language(QueryType.PPL) .catalog("opensearch", testSchema) .build(); UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context); 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 c5e5b03a677..c6288f82ed9 100644 --- a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryTestBase.java +++ b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryTestBase.java @@ -37,7 +37,7 @@ protected Map getTableMap() { UnifiedQueryContext context = UnifiedQueryContext.builder() - .queryType(QueryType.PPL) + .language(QueryType.PPL) .catalog("catalog", testSchema) .defaultNamespace("catalog") .build(); From 2137ab63bdba0c766452e77d9f0089452a8dd288 Mon Sep 17 00:00:00 2001 From: Chen Dai Date: Thu, 11 Dec 2025 13:16:39 -0800 Subject: [PATCH 7/9] Address AI comments Signed-off-by: Chen Dai --- api/README.md | 2 +- .../java/org/opensearch/sql/api/UnifiedQueryContext.java | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/api/README.md b/api/README.md index a57644f4659..8e65ddc4f50 100644 --- a/api/README.md +++ b/api/README.md @@ -74,7 +74,7 @@ UnifiedQueryContext context = UnifiedQueryContext.builder() // Step 2: Create planner with context UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context); -// Step 3: Parse PPL query into logical plan +// Step 3: Plan PPL query into logical plan RelNode plan = planner.plan("source = employees | where age > 30"); // Step 4: Create transpiler with target dialect diff --git a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java index f8ae812b168..f16fedb98b8 100644 --- a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java +++ b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java @@ -135,10 +135,11 @@ public Builder setting(String name, Object value) { public UnifiedQueryContext build() { Objects.requireNonNull(queryType, "Must specify language before build"); + Settings settings = buildSettings(); CalcitePlanContext planContext = CalcitePlanContext.create( - buildFrameworkConfig(), SysLimit.fromSettings(buildSettings()), queryType); - return new UnifiedQueryContext(planContext, buildSettings()); + buildFrameworkConfig(), SysLimit.fromSettings(settings), queryType); + return new UnifiedQueryContext(planContext, settings); } private Settings buildSettings() { From 9c965945b76bcf35ef938ae3268fdbd1fdfdd6f1 Mon Sep 17 00:00:00 2001 From: Chen Dai Date: Thu, 11 Dec 2025 15:21:08 -0800 Subject: [PATCH 8/9] Reuse context in test base class Signed-off-by: Chen Dai --- .../sql/api/UnifiedQueryPlannerTest.java | 39 +++---------------- .../sql/api/UnifiedQueryTestBase.java | 8 ++-- .../UnifiedQueryTranspilerTest.java | 4 +- 3 files changed, 13 insertions(+), 38 deletions(-) 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 536db8d3b37..9ad7aa42155 100644 --- a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java +++ b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java @@ -29,29 +29,16 @@ protected Map getSubSchemaMap() { @Test public void testPPLQueryPlanning() { - UnifiedQueryContext context = - UnifiedQueryContext.builder() - .language(QueryType.PPL) - .catalog("opensearch", testSchema) - .build(); - UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context); - - RelNode plan = planner.plan("source = opensearch.employees | eval f = abs(id)"); + RelNode plan = planner.plan("source = catalog.employees | eval f = abs(id)"); assertNotNull("Plan should be created", plan); } @Test public void testPPLJoinQueryPlanning() { - UnifiedQueryContext context = - UnifiedQueryContext.builder() - .language(QueryType.PPL) - .catalog("opensearch", testSchema) - .defaultNamespace("opensearch") - .build(); - UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context); - RelNode plan = - planner.plan("source = employees | join on employees.id = employees.age employees"); + planner.plan( + "source = catalog.employees | join left = l right = r on l.id = r.age" + + " catalog.employees"); assertNotNull("Join query should be created", plan); } @@ -121,25 +108,11 @@ public void testPPLQueryPlanningWithMultipleCatalogsAndDefaultNamespace() { @Test(expected = IllegalStateException.class) public void testUnsupportedStatementType() { - UnifiedQueryContext context = - UnifiedQueryContext.builder() - .language(QueryType.PPL) - .catalog("opensearch", testSchema) - .build(); - UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context); - - planner.plan("explain source = employees"); // explain statement + planner.plan("explain source = catalog.employees"); // explain statement } @Test(expected = SyntaxCheckException.class) public void testPlanPropagatingSyntaxCheckException() { - UnifiedQueryContext context = - UnifiedQueryContext.builder() - .language(QueryType.PPL) - .catalog("opensearch", testSchema) - .build(); - UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context); - - planner.plan("source = employees | eval"); // Trigger syntax error from parser + planner.plan("source = catalog.employees | eval"); // Trigger syntax error from parser } } 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 c6288f82ed9..6064e3d768f 100644 --- a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryTestBase.java +++ b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryTestBase.java @@ -22,7 +22,10 @@ public abstract class UnifiedQueryTestBase { /** Test schema containing sample tables for testing */ protected AbstractSchema testSchema; - /** Unified query planner configured with test schema */ + /** Unified query context configured with test schema */ + protected UnifiedQueryContext context; + + /** Unified query planner configured with test context */ protected UnifiedQueryPlanner planner; @Before @@ -35,11 +38,10 @@ protected Map getTableMap() { } }; - UnifiedQueryContext context = + context = UnifiedQueryContext.builder() .language(QueryType.PPL) .catalog("catalog", testSchema) - .defaultNamespace("catalog") .build(); planner = new UnifiedQueryPlanner(context); } diff --git a/api/src/test/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspilerTest.java b/api/src/test/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspilerTest.java index f0ad4133c92..ebec4da7ed5 100644 --- a/api/src/test/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspilerTest.java +++ b/api/src/test/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspilerTest.java @@ -26,7 +26,7 @@ public void setUp() { @Test public void testToSql() { - String pplQuery = "source = employees"; + String pplQuery = "source = catalog.employees"; RelNode plan = planner.plan(pplQuery); String actualSql = transpiler.toSql(plan); @@ -37,7 +37,7 @@ public void testToSql() { @Test public void testToSqlWithCustomDialect() { - String pplQuery = "source = employees | where name = 123"; + String pplQuery = "source = catalog.employees | where name = 123"; RelNode plan = planner.plan(pplQuery); UnifiedQueryTranspiler customTranspiler = From f650fb5fc90150d973cd4ac0c6adbbf984792ef6 Mon Sep 17 00:00:00 2001 From: Chen Dai Date: Mon, 15 Dec 2025 09:28:22 -0800 Subject: [PATCH 9/9] Remove session in doc Signed-off-by: Chen Dai --- api/README.md | 2 +- .../java/org/opensearch/sql/api/UnifiedQueryContext.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/README.md b/api/README.md index 8e65ddc4f50..486a2a9f58a 100644 --- a/api/README.md +++ b/api/README.md @@ -19,7 +19,7 @@ Together, these components enable a complete workflow: parse PPL queries into lo ### UnifiedQueryContext -`UnifiedQueryContext` is a reusable session-level abstraction shared across unified query components (planner, compiler, etc.). It bundles `CalcitePlanContext` and `Settings` into a single object, centralizing configuration for all unified query operations. +`UnifiedQueryContext` is a reusable abstraction shared across unified query components (planner, compiler, etc.). It bundles `CalcitePlanContext` and `Settings` into a single object, centralizing configuration for all unified query operations. Create a context with catalog configuration, query type, and optional settings: diff --git a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java index f16fedb98b8..029eb2218ae 100644 --- a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java +++ b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java @@ -29,9 +29,9 @@ import org.opensearch.sql.executor.QueryType; /** - * A reusable session-level abstraction shared across unified query components (planner, compiler, - * etc.). This centralizes configuration for catalog schemas, query type, execution limits, and - * other settings, enabling consistent behavior across all unified query operations. + * A reusable abstraction shared across unified query components (planner, compiler, etc.). This + * centralizes configuration for catalog schemas, query type, execution limits, and other settings, + * enabling consistent behavior across all unified query operations. */ @Value public class UnifiedQueryContext {