From d582e20cae57109a693fb3109ab2a015a84e85d9 Mon Sep 17 00:00:00 2001 From: Chen Dai Date: Tue, 25 Nov 2025 15:42:48 -0800 Subject: [PATCH 1/8] Add basic transpiler impl Signed-off-by: Chen Dai --- api/README.md | 45 ++++++++++++++++- .../transpiler/UnifiedQueryTranspiler.java | 36 ++++++++++++++ .../sql/api/UnifiedQueryTestBase.java | 49 +++++++++++++++++++ .../UnifiedQueryTranspilerTest.java | 45 +++++++++++++++++ 4 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspiler.java create mode 100644 api/src/test/java/org/opensearch/sql/api/UnifiedQueryTestBase.java create mode 100644 api/src/test/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspilerTest.java diff --git a/api/README.md b/api/README.md index 0288b7ad22c..791a4bb3cc2 100644 --- a/api/README.md +++ b/api/README.md @@ -4,10 +4,17 @@ This module provides a high-level integration layer for the Calcite-based query ## Overview -The `UnifiedQueryPlanner` serves as the primary entry point for external consumers. It accepts PPL (Piped Processing Language) queries and returns Calcite `RelNode` logical plans as intermediate representation. +This module provides two primary components: + +- **`UnifiedQueryPlanner`**: Accepts PPL (Piped Processing Language) queries and returns Calcite `RelNode` logical plans as intermediate representation. +- **`UnifiedQueryTranspiler`**: Converts Calcite logical plans (`RelNode`) into SQL strings for various target databases using different SQL dialects. + +Together, these components enable a complete workflow: parse PPL queries into logical plans, then transpile those plans into target database SQL. ## Usage +### UnifiedQueryPlanner + Use the declarative, fluent builder API to initialize the `UnifiedQueryPlanner`. ```java @@ -21,6 +28,42 @@ UnifiedQueryPlanner planner = UnifiedQueryPlanner.builder() 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. + +```java +UnifiedQueryTranspiler transpiler = new UnifiedQueryTranspiler(); +String sql = transpiler.toSql(plan, SparkSqlDialect.DEFAULT); +``` + +### Complete Workflow Example + +Combining both components to transpile PPL queries into target database SQL: + +```java +// Step 1: Initialize planner +UnifiedQueryPlanner planner = UnifiedQueryPlanner.builder() + .language(QueryType.PPL) + .catalog("catalog", schema) + .defaultNamespace("catalog") + .build(); + +// Step 2: Parse PPL query into logical plan +RelNode plan = planner.plan("source = employees | where age > 30"); + +// Step 3: Transpile to target SQL dialect +UnifiedQueryTranspiler transpiler = new UnifiedQueryTranspiler(); +String sparkSql = transpiler.toSql(plan, SparkSqlDialect.DEFAULT); +// Result: SELECT * FROM `catalog`.`employees` WHERE `age` > 30 +``` + +Supported SQL dialects include: +- `SparkSqlDialect.DEFAULT` - Apache Spark SQL +- `PostgresqlSqlDialect.DEFAULT` - PostgreSQL +- `MysqlSqlDialect.DEFAULT` - MySQL +- And other Calcite-supported dialects + ## Development & Testing A set of unit tests is provided to validate planner behavior. diff --git a/api/src/main/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspiler.java b/api/src/main/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspiler.java new file mode 100644 index 00000000000..01feb4ea668 --- /dev/null +++ b/api/src/main/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspiler.java @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.api.transpiler; + +import org.apache.calcite.rel.RelNode; +import org.apache.calcite.rel.rel2sql.RelToSqlConverter; +import org.apache.calcite.sql.SqlDialect; +import org.apache.calcite.sql.SqlNode; + +/** + * Transpiles Calcite logical plans ({@link RelNode}) into SQL strings for various target databases. + * Uses Calcite's {@link RelToSqlConverter} to perform the conversion, respecting the specified SQL + * dialect and formatting options. + */ +public class UnifiedQueryTranspiler { + + /** + * Converts a Calcite logical plan to a SQL string using the specified transpile options. + * + * @param plan the logical plan to convert (must not be null) + * @param options the transpilation options including target dialect and formatting preferences + * @return the generated SQL string + */ + public String toSql(RelNode plan, SqlDialect target) { + try { + RelToSqlConverter converter = new RelToSqlConverter(target); + SqlNode sqlNode = converter.visitRoot(plan).asStatement(); + return sqlNode.toSqlString(target).getSql(); + } catch (Exception e) { + throw new IllegalStateException("Failed to transpile logical plan to SQL", e); + } + } +} diff --git a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryTestBase.java b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryTestBase.java new file mode 100644 index 00000000000..70ebab9b616 --- /dev/null +++ b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryTestBase.java @@ -0,0 +1,49 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.api; + +import java.util.List; +import java.util.Map; +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rel.type.RelDataTypeFactory; +import org.apache.calcite.schema.Table; +import org.apache.calcite.schema.impl.AbstractSchema; +import org.apache.calcite.schema.impl.AbstractTable; +import org.apache.calcite.sql.type.SqlTypeName; +import org.junit.Before; + +/** Base class for unified query tests providing common test schema and utilities. */ +public abstract class UnifiedQueryTestBase { + + /** Test schema containing sample tables for testing */ + protected AbstractSchema testSchema; + + @Before + public void setUp() { + testSchema = + new AbstractSchema() { + @Override + protected Map getTableMap() { + return Map.of("employees", createEmployeesTable()); + } + }; + } + + protected Table createEmployeesTable() { + return new AbstractTable() { + @Override + public RelDataType getRowType(RelDataTypeFactory typeFactory) { + return typeFactory.createStructType( + List.of( + typeFactory.createSqlType(SqlTypeName.INTEGER), + typeFactory.createSqlType(SqlTypeName.VARCHAR), + typeFactory.createSqlType(SqlTypeName.INTEGER), + typeFactory.createSqlType(SqlTypeName.VARCHAR)), + List.of("id", "name", "age", "department")); + } + }; + } +} 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 new file mode 100644 index 00000000000..f835f5d64c7 --- /dev/null +++ b/api/src/test/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspilerTest.java @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.api.transpiler; + +import static org.junit.Assert.assertEquals; + +import org.apache.calcite.rel.RelNode; +import org.apache.calcite.sql.dialect.SparkSqlDialect; +import org.junit.Before; +import org.junit.Test; +import org.opensearch.sql.api.UnifiedQueryPlanner; +import org.opensearch.sql.api.UnifiedQueryTestBase; +import org.opensearch.sql.executor.QueryType; + +public class UnifiedQueryTranspilerTest extends UnifiedQueryTestBase { + + private UnifiedQueryPlanner planner; + private UnifiedQueryTranspiler transpiler; + + @Before + public void setUp() { + super.setUp(); + planner = + UnifiedQueryPlanner.builder() + .language(QueryType.PPL) + .catalog("catalog", testSchema) + .defaultNamespace("catalog") + .build(); + + transpiler = new UnifiedQueryTranspiler(); + } + + @Test + public void testToSql() { + String pplQuery = "source = employees"; + RelNode plan = planner.plan(pplQuery); + + String sql = transpiler.toSql(plan, SparkSqlDialect.DEFAULT); + String expectedSql = "SELECT *\nFROM `catalog`.`employees`"; + assertEquals("Generated SQL should match expected", expectedSql, sql); + } +} From 30b8d14b7238a596576d78b50241d44252980fde Mon Sep 17 00:00:00 2001 From: Chen Dai Date: Wed, 26 Nov 2025 09:12:00 -0800 Subject: [PATCH 2/8] Add builder Signed-off-by: Chen Dai --- api/README.md | 17 ++++-- .../transpiler/UnifiedQueryTranspiler.java | 56 +++++++++++++++++-- .../UnifiedQueryTranspilerTest.java | 4 +- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/api/README.md b/api/README.md index 791a4bb3cc2..1f42b1071c6 100644 --- a/api/README.md +++ b/api/README.md @@ -33,8 +33,11 @@ RelNode plan = planner.plan("source = opensearch.test"); Use `UnifiedQueryTranspiler` to convert Calcite logical plans into SQL strings for target databases. The transpiler supports various SQL dialects through Calcite's `SqlDialect` interface. ```java -UnifiedQueryTranspiler transpiler = new UnifiedQueryTranspiler(); -String sql = transpiler.toSql(plan, SparkSqlDialect.DEFAULT); +UnifiedQueryTranspiler transpiler = UnifiedQueryTranspiler.builder() + .dialect(SparkSqlDialect.DEFAULT) + .build(); + +String sql = transpiler.toSql(plan); ``` ### Complete Workflow Example @@ -52,9 +55,13 @@ UnifiedQueryPlanner planner = UnifiedQueryPlanner.builder() // Step 2: Parse PPL query into logical plan RelNode plan = planner.plan("source = employees | where age > 30"); -// Step 3: Transpile to target SQL dialect -UnifiedQueryTranspiler transpiler = new UnifiedQueryTranspiler(); -String sparkSql = transpiler.toSql(plan, SparkSqlDialect.DEFAULT); +// Step 3: Initialize transpiler with target dialect +UnifiedQueryTranspiler transpiler = UnifiedQueryTranspiler.builder() + .dialect(SparkSqlDialect.DEFAULT) + .build(); + +// Step 4: 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/transpiler/UnifiedQueryTranspiler.java b/api/src/main/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspiler.java index 01feb4ea668..b51a3d10d44 100644 --- a/api/src/main/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspiler.java +++ b/api/src/main/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspiler.java @@ -17,20 +17,66 @@ */ public class UnifiedQueryTranspiler { + /** Target SQL dialect */ + private final SqlDialect dialect; + + private UnifiedQueryTranspiler(Builder builder) { + this.dialect = builder.dialect; + } + /** - * Converts a Calcite logical plan to a SQL string using the specified transpile options. + * Converts a Calcite logical plan to a SQL string using the configured target dialect. * * @param plan the logical plan to convert (must not be null) - * @param options the transpilation options including target dialect and formatting preferences * @return the generated SQL string */ - public String toSql(RelNode plan, SqlDialect target) { + public String toSql(RelNode plan) { try { - RelToSqlConverter converter = new RelToSqlConverter(target); + RelToSqlConverter converter = new RelToSqlConverter(dialect); SqlNode sqlNode = converter.visitRoot(plan).asStatement(); - return sqlNode.toSqlString(target).getSql(); + return sqlNode.toSqlString(dialect).getSql(); } catch (Exception e) { throw new IllegalStateException("Failed to transpile logical plan to SQL", e); } } + + /** + * Creates a new builder for constructing a UnifiedQueryTranspiler. + * + * @return a new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** Builder for UnifiedQueryTranspiler. */ + public static class Builder { + private SqlDialect dialect; + + private Builder() {} + + /** + * Sets the target SQL dialect for transpilation. + * + * @param dialect the SQL dialect to use (must not be null) + * @return this builder + */ + public Builder dialect(SqlDialect dialect) { + this.dialect = dialect; + return this; + } + + /** + * Builds a new UnifiedQueryTranspiler instance. + * + * @return a new UnifiedQueryTranspiler + * @throws IllegalStateException if dialect has not been set + */ + public UnifiedQueryTranspiler build() { + if (dialect == null) { + throw new IllegalStateException("SQL dialect must be specified"); + } + return new UnifiedQueryTranspiler(this); + } + } } 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 f835f5d64c7..60e0736b1c3 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 @@ -30,7 +30,7 @@ public void setUp() { .defaultNamespace("catalog") .build(); - transpiler = new UnifiedQueryTranspiler(); + transpiler = UnifiedQueryTranspiler.builder().dialect(SparkSqlDialect.DEFAULT).build(); } @Test @@ -38,7 +38,7 @@ public void testToSql() { String pplQuery = "source = employees"; RelNode plan = planner.plan(pplQuery); - String sql = transpiler.toSql(plan, SparkSqlDialect.DEFAULT); + String sql = transpiler.toSql(plan); String expectedSql = "SELECT *\nFROM `catalog`.`employees`"; assertEquals("Generated SQL should match expected", expectedSql, sql); } From 09098d6053d05040b98143fb7057a62f188fa39f Mon Sep 17 00:00:00 2001 From: Chen Dai Date: Wed, 26 Nov 2025 09:31:18 -0800 Subject: [PATCH 3/8] Use lombok builder Signed-off-by: Chen Dai --- api/build.gradle | 1 + .../transpiler/UnifiedQueryTranspiler.java | 47 +------------------ 2 files changed, 3 insertions(+), 45 deletions(-) diff --git a/api/build.gradle b/api/build.gradle index dfd0e25b902..e58084ba500 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -5,6 +5,7 @@ plugins { id 'java-library' + id "io.freefair.lombok" id 'jacoco' id 'com.diffplug.spotless' } diff --git a/api/src/main/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspiler.java b/api/src/main/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspiler.java index b51a3d10d44..ec966dfe2c5 100644 --- a/api/src/main/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspiler.java +++ b/api/src/main/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspiler.java @@ -5,6 +5,7 @@ package org.opensearch.sql.api.transpiler; +import lombok.Builder; import org.apache.calcite.rel.RelNode; import org.apache.calcite.rel.rel2sql.RelToSqlConverter; import org.apache.calcite.sql.SqlDialect; @@ -15,15 +16,11 @@ * Uses Calcite's {@link RelToSqlConverter} to perform the conversion, respecting the specified SQL * dialect and formatting options. */ +@Builder public class UnifiedQueryTranspiler { - /** Target SQL dialect */ private final SqlDialect dialect; - private UnifiedQueryTranspiler(Builder builder) { - this.dialect = builder.dialect; - } - /** * Converts a Calcite logical plan to a SQL string using the configured target dialect. * @@ -39,44 +36,4 @@ public String toSql(RelNode plan) { throw new IllegalStateException("Failed to transpile logical plan to SQL", e); } } - - /** - * Creates a new builder for constructing a UnifiedQueryTranspiler. - * - * @return a new builder instance - */ - public static Builder builder() { - return new Builder(); - } - - /** Builder for UnifiedQueryTranspiler. */ - public static class Builder { - private SqlDialect dialect; - - private Builder() {} - - /** - * Sets the target SQL dialect for transpilation. - * - * @param dialect the SQL dialect to use (must not be null) - * @return this builder - */ - public Builder dialect(SqlDialect dialect) { - this.dialect = dialect; - return this; - } - - /** - * Builds a new UnifiedQueryTranspiler instance. - * - * @return a new UnifiedQueryTranspiler - * @throws IllegalStateException if dialect has not been set - */ - public UnifiedQueryTranspiler build() { - if (dialect == null) { - throw new IllegalStateException("SQL dialect must be specified"); - } - return new UnifiedQueryTranspiler(this); - } - } } From edfbd0aa042d2bcff9e242833ef7e101a1318263 Mon Sep 17 00:00:00 2001 From: Chen Dai Date: Wed, 26 Nov 2025 09:40:39 -0800 Subject: [PATCH 4/8] Modify unified query planner UT to extend new test base class Signed-off-by: Chen Dai --- .../sql/api/UnifiedQueryPlannerTest.java | 52 +++++-------------- .../sql/api/UnifiedQueryTestBase.java | 11 ++++ .../UnifiedQueryTranspilerTest.java | 10 ---- 3 files changed, 25 insertions(+), 48 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 0f7754ba501..754e36c092e 100644 --- a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java +++ b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java @@ -8,41 +8,15 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; -import java.util.List; import java.util.Map; import org.apache.calcite.rel.RelNode; -import org.apache.calcite.rel.type.RelDataType; -import org.apache.calcite.rel.type.RelDataTypeFactory; import org.apache.calcite.schema.Schema; -import org.apache.calcite.schema.Table; import org.apache.calcite.schema.impl.AbstractSchema; -import org.apache.calcite.schema.impl.AbstractTable; -import org.apache.calcite.sql.type.SqlTypeName; import org.junit.Test; import org.opensearch.sql.common.antlr.SyntaxCheckException; import org.opensearch.sql.executor.QueryType; -public class UnifiedQueryPlannerTest { - - /** Test schema consists of a test table with id and name columns */ - private final AbstractSchema testSchema = - new AbstractSchema() { - @Override - protected Map getTableMap() { - return Map.of( - "index", - new AbstractTable() { - @Override - public RelDataType getRowType(RelDataTypeFactory typeFactory) { - return typeFactory.createStructType( - List.of( - typeFactory.createSqlType(SqlTypeName.INTEGER), - typeFactory.createSqlType(SqlTypeName.VARCHAR)), - List.of("id", "name")); - } - }); - } - }; +public class UnifiedQueryPlannerTest extends UnifiedQueryTestBase { /** Test catalog consists of test schema above */ private final AbstractSchema testDeepSchema = @@ -61,7 +35,7 @@ public void testPPLQueryPlanning() { .catalog("opensearch", testSchema) .build(); - RelNode plan = planner.plan("source = opensearch.index | eval f = abs(id)"); + RelNode plan = planner.plan("source = opensearch.employees | eval f = abs(id)"); assertNotNull("Plan should be created", plan); } @@ -74,8 +48,8 @@ public void testPPLQueryPlanningWithDefaultNamespace() { .defaultNamespace("opensearch") .build(); - assertNotNull("Plan should be created", planner.plan("source = opensearch.index")); - assertNotNull("Plan should be created", planner.plan("source = index")); + assertNotNull("Plan should be created", planner.plan("source = opensearch.employees")); + assertNotNull("Plan should be created", planner.plan("source = employees")); } @Test @@ -87,12 +61,12 @@ public void testPPLQueryPlanningWithDefaultNamespaceMultiLevel() { .defaultNamespace("catalog.opensearch") .build(); - assertNotNull("Plan should be created", planner.plan("source = catalog.opensearch.index")); - assertNotNull("Plan should be created", planner.plan("source = index")); + assertNotNull("Plan should be created", planner.plan("source = catalog.opensearch.employees")); + assertNotNull("Plan should be created", planner.plan("source = employees")); // This is valid in SparkSQL, but Calcite requires "catalog" as the default root schema to // resolve it - assertThrows(IllegalStateException.class, () -> planner.plan("source = opensearch.index")); + assertThrows(IllegalStateException.class, () -> planner.plan("source = opensearch.employees")); } @Test @@ -105,7 +79,8 @@ public void testPPLQueryPlanningWithMultipleCatalogs() { .build(); RelNode plan = - planner.plan("source = catalog1.index | lookup catalog2.index id | eval f = abs(id)"); + planner.plan( + "source = catalog1.employees | lookup catalog2.employees id | eval f = abs(id)"); assertNotNull("Plan should be created with multiple catalogs", plan); } @@ -119,7 +94,8 @@ public void testPPLQueryPlanningWithMultipleCatalogsAndDefaultNamespace() { .defaultNamespace("catalog2") .build(); - RelNode plan = planner.plan("source = catalog1.index | lookup index id | eval f = abs(id)"); + RelNode plan = + planner.plan("source = catalog1.employees | lookup employees id | eval f = abs(id)"); assertNotNull("Plan should be created with multiple catalogs", plan); } @@ -132,7 +108,7 @@ public void testPPLQueryPlanningWithMetadataCaching() { .cacheMetadata(true) .build(); - RelNode plan = planner.plan("source = opensearch.index"); + RelNode plan = planner.plan("source = opensearch.employees"); assertNotNull("Plan should be created", plan); } @@ -166,7 +142,7 @@ public void testUnsupportedStatementType() { .catalog("opensearch", testSchema) .build(); - planner.plan("explain source = index"); // explain statement + planner.plan("explain source = employees"); // explain statement } @Test(expected = SyntaxCheckException.class) @@ -177,6 +153,6 @@ public void testPlanPropagatingSyntaxCheckException() { .catalog("opensearch", testSchema) .build(); - planner.plan("source = index | eval"); // Trigger syntax error from parser + 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 70ebab9b616..f63bfed09ec 100644 --- a/api/src/test/java/org/opensearch/sql/api/UnifiedQueryTestBase.java +++ b/api/src/test/java/org/opensearch/sql/api/UnifiedQueryTestBase.java @@ -14,6 +14,7 @@ import org.apache.calcite.schema.impl.AbstractTable; import org.apache.calcite.sql.type.SqlTypeName; import org.junit.Before; +import org.opensearch.sql.executor.QueryType; /** Base class for unified query tests providing common test schema and utilities. */ public abstract class UnifiedQueryTestBase { @@ -21,6 +22,9 @@ public abstract class UnifiedQueryTestBase { /** Test schema containing sample tables for testing */ protected AbstractSchema testSchema; + /** Unified query planner configured with test schema */ + protected UnifiedQueryPlanner planner; + @Before public void setUp() { testSchema = @@ -30,6 +34,13 @@ protected Map getTableMap() { return Map.of("employees", createEmployeesTable()); } }; + + planner = + UnifiedQueryPlanner.builder() + .language(QueryType.PPL) + .catalog("catalog", testSchema) + .defaultNamespace("catalog") + .build(); } protected Table createEmployeesTable() { 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 60e0736b1c3..b525850c679 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 @@ -11,25 +11,15 @@ import org.apache.calcite.sql.dialect.SparkSqlDialect; import org.junit.Before; import org.junit.Test; -import org.opensearch.sql.api.UnifiedQueryPlanner; import org.opensearch.sql.api.UnifiedQueryTestBase; -import org.opensearch.sql.executor.QueryType; public class UnifiedQueryTranspilerTest extends UnifiedQueryTestBase { - private UnifiedQueryPlanner planner; private UnifiedQueryTranspiler transpiler; @Before public void setUp() { super.setUp(); - planner = - UnifiedQueryPlanner.builder() - .language(QueryType.PPL) - .catalog("catalog", testSchema) - .defaultNamespace("catalog") - .build(); - transpiler = UnifiedQueryTranspiler.builder().dialect(SparkSqlDialect.DEFAULT).build(); } From 34587d7fb5feaca39733fbc7e61cc85bac843375 Mon Sep 17 00:00:00 2001 From: Chen Dai Date: Wed, 26 Nov 2025 11:21:23 -0800 Subject: [PATCH 5/8] Update doc with API design caveat Signed-off-by: Chen Dai --- api/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/README.md b/api/README.md index 1f42b1071c6..b554cd53efb 100644 --- a/api/README.md +++ b/api/README.md @@ -11,6 +11,10 @@ This module provides two primary components: Together, these components enable a complete workflow: parse PPL queries into logical plans, then transpile those plans into target database SQL. +### Experimental API Design + +**This API is currently experimental.** The design intentionally exposes Calcite abstractions (`Schema` for catalogs, `RelNode` as IR, `SqlDialect` for dialects) rather than creating custom wrapper interfaces. This is to avoid overdesign by leveraging the flexible Calcite interface in the short term. If a more abstracted API becomes necessary in the future, a compatibility layer can be added without breaking existing integrations. + ## Usage ### UnifiedQueryPlanner From b0b154a7168f648ccec8968d8ccc4296adc1274c Mon Sep 17 00:00:00 2001 From: Chen Dai Date: Mon, 1 Dec 2025 10:58:39 -0800 Subject: [PATCH 6/8] Move opensearch spark sql dialect out of test folder Signed-off-by: Chen Dai --- .../api/transpiler/UnifiedQueryTranspiler.java | 3 ++- .../transpiler/UnifiedQueryTranspilerTest.java | 18 ++++++++++++++++++ .../ppl/calcite/OpenSearchSparkSqlDialect.java | 0 3 files changed, 20 insertions(+), 1 deletion(-) rename ppl/src/{test => main}/java/org/opensearch/sql/ppl/calcite/OpenSearchSparkSqlDialect.java (100%) diff --git a/api/src/main/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspiler.java b/api/src/main/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspiler.java index ec966dfe2c5..dc8c131c5e7 100644 --- a/api/src/main/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspiler.java +++ b/api/src/main/java/org/opensearch/sql/api/transpiler/UnifiedQueryTranspiler.java @@ -14,11 +14,12 @@ /** * Transpiles Calcite logical plans ({@link RelNode}) into SQL strings for various target databases. * Uses Calcite's {@link RelToSqlConverter} to perform the conversion, respecting the specified SQL - * dialect and formatting options. + * dialect. */ @Builder public class UnifiedQueryTranspiler { + /** Target SQL dialect */ private final SqlDialect dialect; /** 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 b525850c679..8d864606fe6 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 @@ -12,6 +12,7 @@ import org.junit.Before; import org.junit.Test; import org.opensearch.sql.api.UnifiedQueryTestBase; +import org.opensearch.sql.ppl.calcite.OpenSearchSparkSqlDialect; public class UnifiedQueryTranspilerTest extends UnifiedQueryTestBase { @@ -32,4 +33,21 @@ public void testToSql() { String expectedSql = "SELECT *\nFROM `catalog`.`employees`"; assertEquals("Generated SQL should match expected", expectedSql, sql); } + + @Test + public void testToSqlWithCustomDialect() { + String pplQuery = "source = employees | where name = 123"; + RelNode plan = planner.plan(pplQuery); + + // OpenSearchSparkSqlDialect translates SAFE_CAST to TRY_CAST for Spark SQL compatibility + UnifiedQueryTranspiler customTranspiler = + UnifiedQueryTranspiler.builder().dialect(OpenSearchSparkSqlDialect.DEFAULT).build(); + String sql = customTranspiler.toSql(plan); + String expectedCustomSql = + "SELECT *\n" + + "FROM `catalog`.`employees`\n" + + "WHERE TRY_CAST(`name` AS DOUBLE) = 1.230E2"; + assertEquals( + "OpenSearchSparkSqlDialect should translate SAFE_CAST to TRY_CAST", expectedCustomSql, sql); + } } diff --git a/ppl/src/test/java/org/opensearch/sql/ppl/calcite/OpenSearchSparkSqlDialect.java b/ppl/src/main/java/org/opensearch/sql/ppl/calcite/OpenSearchSparkSqlDialect.java similarity index 100% rename from ppl/src/test/java/org/opensearch/sql/ppl/calcite/OpenSearchSparkSqlDialect.java rename to ppl/src/main/java/org/opensearch/sql/ppl/calcite/OpenSearchSparkSqlDialect.java From 19e2f5466b6e347e20b0baa80d7a1f83ee4765b7 Mon Sep 17 00:00:00 2001 From: Chen Dai Date: Mon, 1 Dec 2025 11:31:12 -0800 Subject: [PATCH 7/8] Update doc and test assertion message Signed-off-by: Chen Dai --- api/README.md | 2 +- .../transpiler/UnifiedQueryTranspilerTest.java | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/api/README.md b/api/README.md index b554cd53efb..c380a1a7128 100644 --- a/api/README.md +++ b/api/README.md @@ -13,7 +13,7 @@ Together, these components enable a complete workflow: parse PPL queries into lo ### Experimental API Design -**This API is currently experimental.** The design intentionally exposes Calcite abstractions (`Schema` for catalogs, `RelNode` as IR, `SqlDialect` for dialects) rather than creating custom wrapper interfaces. This is to avoid overdesign by leveraging the flexible Calcite interface in the short term. If a more abstracted API becomes necessary in the future, a compatibility layer can be added without breaking existing integrations. +**This API is currently experimental.** The design intentionally exposes Calcite abstractions (`Schema` for catalogs, `RelNode` as IR, `SqlDialect` for dialects) rather than creating custom wrapper interfaces. This is to avoid overdesign by leveraging the flexible Calcite interface in the short term. If a more abstracted API becomes necessary in the future, breaking changes may be introduced with the new abstraction layer. ## Usage 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 8d864606fe6..9bdacc8ff99 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 @@ -29,9 +29,10 @@ public void testToSql() { String pplQuery = "source = employees"; RelNode plan = planner.plan(pplQuery); - String sql = transpiler.toSql(plan); + String actualSql = transpiler.toSql(plan); String expectedSql = "SELECT *\nFROM `catalog`.`employees`"; - assertEquals("Generated SQL should match expected", expectedSql, sql); + assertEquals( + "Transpiled SQL using SparkSqlDialect should match expected SQL", expectedSql, actualSql); } @Test @@ -39,15 +40,14 @@ public void testToSqlWithCustomDialect() { String pplQuery = "source = employees | where name = 123"; RelNode plan = planner.plan(pplQuery); - // OpenSearchSparkSqlDialect translates SAFE_CAST to TRY_CAST for Spark SQL compatibility UnifiedQueryTranspiler customTranspiler = UnifiedQueryTranspiler.builder().dialect(OpenSearchSparkSqlDialect.DEFAULT).build(); - String sql = customTranspiler.toSql(plan); - String expectedCustomSql = - "SELECT *\n" - + "FROM `catalog`.`employees`\n" - + "WHERE TRY_CAST(`name` AS DOUBLE) = 1.230E2"; + String actualSql = customTranspiler.toSql(plan); + String expectedSql = + "SELECT *\nFROM `catalog`.`employees`\nWHERE TRY_CAST(`name` AS DOUBLE) = 1.230E2"; assertEquals( - "OpenSearchSparkSqlDialect should translate SAFE_CAST to TRY_CAST", expectedCustomSql, sql); + "Transpiled query using OpenSearchSparkSqlDialect should translate SAFE_CAST to TRY_CAST", + expectedSql, + actualSql); } } From dc620798072e167546f6e316117cc0ea68cd4ac7 Mon Sep 17 00:00:00 2001 From: Chen Dai Date: Mon, 1 Dec 2025 13:44:08 -0800 Subject: [PATCH 8/8] Fix line separator and license header Signed-off-by: Chen Dai --- api/build.gradle | 4 ++++ .../org/opensearch/sql/api/EmptyDataSourceService.java | 5 +++++ .../sql/api/transpiler/UnifiedQueryTranspilerTest.java | 10 ++++++++-- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/api/build.gradle b/api/build.gradle index e58084ba500..5ce00169597 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -26,6 +26,10 @@ spotless { exclude '**/build/**', '**/build-*/**', 'src/main/gen/**' } importOrder() + licenseHeader("/*\n" + + " * Copyright OpenSearch Contributors\n" + + " * SPDX-License-Identifier: Apache-2.0\n" + + " */\n\n") removeUnusedImports() trimTrailingWhitespace() endWithNewline() diff --git a/api/src/main/java/org/opensearch/sql/api/EmptyDataSourceService.java b/api/src/main/java/org/opensearch/sql/api/EmptyDataSourceService.java index 0fa0c38ad3c..0b42279d9db 100644 --- a/api/src/main/java/org/opensearch/sql/api/EmptyDataSourceService.java +++ b/api/src/main/java/org/opensearch/sql/api/EmptyDataSourceService.java @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + package org.opensearch.sql.api; import java.util.Map; 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 9bdacc8ff99..f0ad4133c92 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 @@ -30,7 +30,7 @@ public void testToSql() { RelNode plan = planner.plan(pplQuery); String actualSql = transpiler.toSql(plan); - String expectedSql = "SELECT *\nFROM `catalog`.`employees`"; + String expectedSql = normalize("SELECT *\nFROM `catalog`.`employees`"); assertEquals( "Transpiled SQL using SparkSqlDialect should match expected SQL", expectedSql, actualSql); } @@ -44,10 +44,16 @@ public void testToSqlWithCustomDialect() { UnifiedQueryTranspiler.builder().dialect(OpenSearchSparkSqlDialect.DEFAULT).build(); String actualSql = customTranspiler.toSql(plan); String expectedSql = - "SELECT *\nFROM `catalog`.`employees`\nWHERE TRY_CAST(`name` AS DOUBLE) = 1.230E2"; + normalize( + "SELECT *\nFROM `catalog`.`employees`\nWHERE TRY_CAST(`name` AS DOUBLE) = 1.230E2"); assertEquals( "Transpiled query using OpenSearchSparkSqlDialect should translate SAFE_CAST to TRY_CAST", expectedSql, actualSql); } + + /** Normalizes line endings to platform-specific format for cross-platform test compatibility. */ + private String normalize(String sql) { + return sql.replace("\n", System.lineSeparator()); + } }