Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ plugins {

dependencies {
api project(':ppl')
api group: 'org.apache.calcite', name: 'calcite-babel', version: '1.41.0'

testImplementation testFixtures(project(':api'))
testImplementation group: 'junit', name: 'junit', version: '4.13.2'
Expand Down
46 changes: 25 additions & 21 deletions api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,20 @@
import java.util.concurrent.Callable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.apache.calcite.avatica.util.Casing;
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.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.sql.util.SqlOperatorTables;
import org.apache.calcite.tools.FrameworkConfig;
import org.apache.calcite.tools.Frameworks;
import org.apache.calcite.tools.Programs;
import org.opensearch.sql.api.parser.CalciteSqlQueryParser;
import org.opensearch.sql.api.parser.PPLQueryParser;
import org.opensearch.sql.api.parser.UnifiedQueryParser;
import org.opensearch.sql.api.spec.UnifiedFunctionSpec;
import org.opensearch.sql.api.spec.LanguageSpec;
import org.opensearch.sql.api.spec.UnifiedPplSpec;
import org.opensearch.sql.api.spec.UnifiedSqlSpec;
import org.opensearch.sql.calcite.CalcitePlanContext;
import org.opensearch.sql.calcite.SysLimit;
import org.opensearch.sql.common.setting.Settings;
Expand Down Expand Up @@ -60,6 +58,9 @@ public class UnifiedQueryContext implements AutoCloseable {
/** Query parser created eagerly from this context's configuration. */
private final UnifiedQueryParser<?> parser;

/** Language spec for the query's frontend (SQL or PPL). */
private final LanguageSpec langSpec;

/**
* Returns the profiling result. Call after query execution to retrieve collected metrics. Returns
* empty if profiling was not enabled.
Expand Down Expand Up @@ -208,12 +209,18 @@ public Builder setting(String name, Object value) {
public UnifiedQueryContext build() {
Objects.requireNonNull(queryType, "Must specify language before build");

LanguageSpec langSpec =
switch (queryType) {
case SQL -> UnifiedSqlSpec.extended();
case PPL -> UnifiedPplSpec.create();
};
Settings settings = buildSettings();
CalcitePlanContext planContext =
CalcitePlanContext.create(
buildFrameworkConfig(), SysLimit.fromSettings(settings), queryType);
buildFrameworkConfig(langSpec), SysLimit.fromSettings(settings), queryType);
QueryProfiling.activate(profiling);
return new UnifiedQueryContext(planContext, settings, createParser(planContext, settings));
return new UnifiedQueryContext(
planContext, settings, createParser(planContext, settings), langSpec);
}

private UnifiedQueryParser<?> createParser(CalcitePlanContext planContext, Settings settings) {
Expand All @@ -239,25 +246,22 @@ public List<?> getSettings() {
}

@SuppressWarnings({"rawtypes"})
private FrameworkConfig buildFrameworkConfig() {
private FrameworkConfig buildFrameworkConfig(LanguageSpec langSpec) {
SchemaPlus rootSchema = CalciteSchema.createRootSchema(true, cacheMetadata).plus();
catalogs.forEach(rootSchema::add);

SchemaPlus defaultSchema = findSchemaByPath(rootSchema, defaultNamespace);
return Frameworks.newConfigBuilder()
.parserConfig(buildParserConfig())
.operatorTable(
SqlOperatorTables.chain(
SqlStdOperatorTable.instance(), UnifiedFunctionSpec.RELEVANCE.operatorTable()))
.defaultSchema(defaultSchema)
.traitDefs((List<RelTraitDef>) null)
.programs(Programs.calc(DefaultRelMetadataProvider.INSTANCE))
.build();
}
Frameworks.ConfigBuilder builder =
Frameworks.newConfigBuilder()
.defaultSchema(defaultSchema)
.traitDefs((List<RelTraitDef>) null)
.programs(Programs.calc(DefaultRelMetadataProvider.INSTANCE));

private SqlParser.Config buildParserConfig() {
// Preserve identifier case for lowercase OpenSearch index names
return SqlParser.Config.DEFAULT.withUnquotedCasing(Casing.UNCHANGED);
return builder
.parserConfig(langSpec.parserConfig())
.sqlValidatorConfig(langSpec.validatorConfig())
.operatorTable(langSpec.operatorTable())
.build();
}

private SchemaPlus findSchemaByPath(SchemaPlus rootSchema, String defaultPath) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
import org.apache.calcite.rel.logical.LogicalSort;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.util.SqlVisitor;
import org.apache.calcite.tools.Frameworks;
import org.apache.calcite.tools.Planner;
import org.opensearch.sql.api.parser.NamedArgRewriter;
import org.opensearch.sql.api.parser.UnifiedQueryParser;
import org.opensearch.sql.ast.tree.UnresolvedPlan;
import org.opensearch.sql.calcite.CalciteRelNodeVisitor;
Expand Down Expand Up @@ -87,7 +87,12 @@ public RelNode plan(String query) throws Exception {
"Only query statements are supported. Got: " + parsed.getKind());
}

SqlNode rewritten = parsed.accept(NamedArgRewriter.INSTANCE);
// TODO: move post-parse rewriting into CalciteSqlQueryParser
SqlNode rewritten = parsed;
for (SqlVisitor<SqlNode> visitor : context.getLangSpec().postParseRules()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small thing to flag: the SqlKind.QUERY gate runs before this loop, so extensions would not be able to register rewrite rules for non-QUERY statements (e.g. DDL). Should the gate move after postParseRules()?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, after I finish this TODO item, the rewrite logic will be moved to UnifiedQueryParser.

rewritten = rewritten.accept(visitor);
}

SqlNode validated = planner.validate(rewritten);
RelRoot relRoot = planner.rel(validated);
return relRoot.project();
Expand Down
89 changes: 89 additions & 0 deletions api/src/main/java/org/opensearch/sql/api/spec/LanguageSpec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.api.spec;

import java.util.ArrayList;
import java.util.List;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlOperatorTable;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.sql.util.SqlOperatorTables;
import org.apache.calcite.sql.util.SqlVisitor;
import org.apache.calcite.sql.validate.SqlValidator;

/**
* Language specification defining the dialect the engine accepts. Provides parser configuration,
* validator configuration, and composable {@link LanguageExtension}s that contribute operators and
* post-parse rewrite rules.
*
* <p>Implementations define a complete language surface — for example, {@link UnifiedSqlSpec}
* provides ANSI and extended SQL modes. A future PPL spec would implement this same interface once
* PPL converges on the Calcite pipeline.
*/
public interface LanguageSpec {

/**
* A composable language extension that contributes operators and post-parse rewrite rules. All
* methods have defaults so extensions only override what they need.
*/
interface LanguageExtension {

/**
* Operators (functions, aggregates) this extension adds. Chained with the standard operator
* table during validation.
*/
default SqlOperatorTable operators() {
return SqlOperatorTables.of();
}

/**
* AST rewrite rules applied after parsing and before validation. Each visitor transforms the
* parse tree (e.g., rewriting named arguments into MAP literals).
*/
default List<SqlVisitor<SqlNode>> postParseRules() {
return List.of();
}
}

/**
* Parser configuration controlling how SQL text is tokenized and parsed into a parse tree,
* including parser factory, lexical rules, and conformance.
*/
SqlParser.Config parserConfig();

/**
* Validator configuration controlling what SQL semantics the validator accepts, such as GROUP BY
* behavior, LIMIT syntax, and type coercion.
*/
SqlValidator.Config validatorConfig();

/**
* Language extensions registered with this spec. Each extension contributes operators and
* post-parse rewrite rules that are composed by {@link #operatorTable()} and {@link
* #postParseRules()}.
*/
List<LanguageExtension> extensions();

/**
* Chained operator table combining the standard Calcite operators with all operators contributed
* by registered extensions.
*/
default SqlOperatorTable operatorTable() {
List<SqlOperatorTable> tables = new ArrayList<>();
tables.add(SqlStdOperatorTable.instance());
extensions().forEach(ext -> tables.add(ext.operators()));
return SqlOperatorTables.chain(tables);
}

/**
* All post-parse rewrite rules from registered extensions, flattened in registration order.
* Applied to the parse tree after parsing and before validation.
*/
default List<SqlVisitor<SqlNode>> postParseRules() {
return extensions().stream().flatMap(ext -> ext.postParseRules().stream()).toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.api.spec;

import java.util.List;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.sql.validate.SqlValidator;

/**
* PPL language specification.
*
* <p>Note: PPL currently has its own parsing and analyzing pipeline, so only configuration and
* extensions applied after RelNode construction are in use. The parser and validator configs
* returned here are inert for the PPL path.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class UnifiedPplSpec implements LanguageSpec {

public static UnifiedPplSpec create() {
return new UnifiedPplSpec();
}

@Override
public SqlParser.Config parserConfig() {
return SqlParser.config();
}

@Override
public SqlValidator.Config validatorConfig() {
return SqlValidator.Config.DEFAULT;
}

@Override
public List<LanguageExtension> extensions() {
return List.of();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.api.spec;

import java.util.List;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import org.apache.calcite.config.Lex;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.sql.parser.SqlParserImplFactory;
import org.apache.calcite.sql.parser.babel.SqlBabelParserImpl;
import org.apache.calcite.sql.validate.SqlConformanceEnum;
import org.apache.calcite.sql.validate.SqlValidator;
import org.opensearch.sql.api.spec.search.SearchExtension;

/**
* SQL language specification. Configures Calcite's parser, validator, and composable extensions for
* OpenSearch SQL compatibility.
*
* <p>Use {@link #extended()} for the default configuration with lenient syntax, hyphenated
* identifiers, and search functions.
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@Accessors(fluent = true)
public class UnifiedSqlSpec implements LanguageSpec {

/** Lexical rules: identifier quoting, character escaping, and special identifier support. */
private final Lex lex;

/** Parser implementation: controls keyword reservation and grammar extensions. */
private final SqlParserImplFactory parserFactory;

/** Validation rules: what SQL semantics the validator accepts (GROUP BY, LIMIT, coercion). */
private final SqlConformanceEnum conformance;

/** Composable extensions contributing operators and post-parse rewrite rules. */
@Getter private final List<LanguageExtension> extensions;

/**
* Extended SQL spec: Babel parser, BIG_QUERY lex (hyphenated identifiers, backtick quoting),
* BABEL conformance (lenient GROUP BY, LIMIT, optional FROM), and search functions.
*/
public static UnifiedSqlSpec extended() {
return new UnifiedSqlSpec(
Lex.BIG_QUERY,
SqlBabelParserImpl.FACTORY,
SqlConformanceEnum.BABEL,
List.of(new SearchExtension()));
}

@Override
public SqlParser.Config parserConfig() {
return SqlParser.config()
.withParserFactory(parserFactory)
.withLex(lex)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential behavior change: .withLex(lex) would inherit Lex.BIG_QUERY's case-insensitive identifier matching, which may differ from the prior behavior. Worth a quick check to confirm this is intentional.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I've added a TODO item above. Will double confirm. Thanks!

.withConformance(conformance);
}

@Override
public SqlValidator.Config validatorConfig() {
return SqlValidator.Config.DEFAULT.withConformance(conformance);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.api.parser;
package org.opensearch.sql.api.spec.search;

import java.util.List;
import lombok.AccessLevel;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.api.spec.search;

import java.util.List;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlOperatorTable;
import org.apache.calcite.sql.util.SqlVisitor;
import org.opensearch.sql.api.spec.LanguageSpec;
import org.opensearch.sql.api.spec.UnifiedFunctionSpec;

/** Search Extension: relevance functions and named argument rewriting. */
public class SearchExtension implements LanguageSpec.LanguageExtension {

@Override
public SqlOperatorTable operators() {
return UnifiedFunctionSpec.RELEVANCE.operatorTable();
}

@Override
public List<SqlVisitor<SqlNode>> postParseRules() {
return List.of(NamedArgRewriter.INSTANCE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -245,17 +245,21 @@ public void testNonQueryStatementsBlockedByWhitelist() {

@Test
public void testNonQueryStatementsBlockedByParser() {
List.of(
// Babel parser rejects CREATE MATERIALIZED VIEW
givenInvalidQuery(
"""
CREATE MATERIALIZED VIEW mv AS
SELECT department, count(*)
FROM catalog.employees
GROUP BY department\
""",
""")
.assertErrorMessage("Encountered");

// Babel parser accepts SHOW TABLES but it's blocked by query-type whitelist
givenInvalidQuery(
"""
SHOW TABLES\
""")
.forEach(
sql -> givenInvalidQuery(sql).assertErrorMessage("Incorrect syntax near the keyword"));
.assertErrorMessage("Only query statements are supported");
}
}
Loading
Loading