Skip to content

Commit 75d9cf4

Browse files
committed
rebase upstream
Signed-off-by: Kyle Hounslow <kylhouns@amazon.com>
2 parents c9d6355 + 9a6f2b0 commit 75d9cf4

17 files changed

Lines changed: 600 additions & 39 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
name: SQL CLI Integration Test
2+
3+
# This workflow tests sql-cli against the current SQL changes
4+
# to catch breaking changes before they're published
5+
6+
on:
7+
pull_request:
8+
paths:
9+
- '**/*.java'
10+
- '**/*.g4'
11+
- '!sql-jdbc/**'
12+
- '**gradle*'
13+
- '**lombok*'
14+
- 'integ-test/**'
15+
- '**/*.jar'
16+
- '**/*.pom'
17+
- '.github/workflows/sql-cli-integration-test.yml'
18+
push:
19+
branches:
20+
- main
21+
- '[0-9]+.[0-9]+'
22+
- '[0-9]+.x'
23+
paths:
24+
- '**/*.java'
25+
- '**/*.g4'
26+
- '!sql-jdbc/**'
27+
- '**gradle*'
28+
- '**lombok*'
29+
- 'integ-test/**'
30+
- '**/*.jar'
31+
- '**/*.pom'
32+
- '.github/workflows/sql-cli-integration-test.yml'
33+
workflow_dispatch:
34+
35+
jobs:
36+
test-sql-cli-integration:
37+
runs-on: ubuntu-latest
38+
strategy:
39+
fail-fast: false
40+
matrix:
41+
java: [21]
42+
43+
steps:
44+
- name: Checkout SQL CLI repository (latest main)
45+
uses: actions/checkout@v4
46+
with:
47+
repository: opensearch-project/sql-cli
48+
path: sql-cli
49+
ref: main
50+
51+
- name: Make a directory for the SQL repo
52+
working-directory: sql-cli
53+
run: mkdir remote
54+
55+
- name: Checkout SQL repository (current changes)
56+
uses: actions/checkout@v4
57+
with:
58+
path: sql-cli/remote/sql
59+
60+
- name: Set up JDK ${{ matrix.java }}
61+
uses: actions/setup-java@v4
62+
with:
63+
distribution: 'temurin'
64+
java-version: ${{ matrix.java }}
65+
66+
- name: Build and publish SQL modules to Maven Local
67+
working-directory: sql-cli/remote/sql
68+
run: |
69+
echo "Building SQL modules from current branch..."
70+
./gradlew publishToMavenLocal -x test -x integTest
71+
echo "SQL modules published to Maven Local"
72+
73+
- name: Run SQL CLI tests with local SQL modules
74+
working-directory: sql-cli
75+
run: |
76+
echo "Running SQL CLI tests against local SQL modules..."
77+
./gradlew test -PuseLocalSql=true -PskipSqlRepoPull=true
78+
79+
- name: Upload SQL CLI test reports
80+
if: always()
81+
uses: actions/upload-artifact@v4
82+
continue-on-error: true
83+
with:
84+
name: sql-cli-test-reports-java-${{ matrix.java }}
85+
path: |
86+
sql-cli/build/reports/**
87+
sql-cli/build/test-results/**
88+
89+
- name: Test Summary
90+
if: always()
91+
run: |
92+
echo "## SQL CLI Integration Test Results" >> $GITHUB_STEP_SUMMARY
93+
echo "" >> $GITHUB_STEP_SUMMARY
94+
echo "Tested SQL CLI against SQL changes from: \`${{ github.ref }}\`" >> $GITHUB_STEP_SUMMARY
95+
echo "SQL CLI version: main branch (latest)" >> $GITHUB_STEP_SUMMARY
96+
echo "Java version: ${{ matrix.java }}" >> $GITHUB_STEP_SUMMARY

api/README.md

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,21 @@ This module provides a high-level integration layer for the Calcite-based query
44

55
## Overview
66

7-
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.
7+
This module provides two primary components:
8+
9+
- **`UnifiedQueryPlanner`**: Accepts PPL (Piped Processing Language) queries and returns Calcite `RelNode` logical plans as intermediate representation.
10+
- **`UnifiedQueryTranspiler`**: Converts Calcite logical plans (`RelNode`) into SQL strings for various target databases using different SQL dialects.
11+
12+
Together, these components enable a complete workflow: parse PPL queries into logical plans, then transpile those plans into target database SQL.
13+
14+
### Experimental API Design
15+
16+
**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.
817

918
## Usage
1019

20+
### UnifiedQueryPlanner
21+
1122
Use the declarative, fluent builder API to initialize the `UnifiedQueryPlanner`.
1223

1324
```java
@@ -21,6 +32,49 @@ UnifiedQueryPlanner planner = UnifiedQueryPlanner.builder()
2132
RelNode plan = planner.plan("source = opensearch.test");
2233
```
2334

35+
### UnifiedQueryTranspiler
36+
37+
Use `UnifiedQueryTranspiler` to convert Calcite logical plans into SQL strings for target databases. The transpiler supports various SQL dialects through Calcite's `SqlDialect` interface.
38+
39+
```java
40+
UnifiedQueryTranspiler transpiler = UnifiedQueryTranspiler.builder()
41+
.dialect(SparkSqlDialect.DEFAULT)
42+
.build();
43+
44+
String sql = transpiler.toSql(plan);
45+
```
46+
47+
### Complete Workflow Example
48+
49+
Combining both components to transpile PPL queries into target database SQL:
50+
51+
```java
52+
// Step 1: Initialize planner
53+
UnifiedQueryPlanner planner = UnifiedQueryPlanner.builder()
54+
.language(QueryType.PPL)
55+
.catalog("catalog", schema)
56+
.defaultNamespace("catalog")
57+
.build();
58+
59+
// Step 2: Parse PPL query into logical plan
60+
RelNode plan = planner.plan("source = employees | where age > 30");
61+
62+
// Step 3: Initialize transpiler with target dialect
63+
UnifiedQueryTranspiler transpiler = UnifiedQueryTranspiler.builder()
64+
.dialect(SparkSqlDialect.DEFAULT)
65+
.build();
66+
67+
// Step 4: Transpile to target SQL
68+
String sparkSql = transpiler.toSql(plan);
69+
// Result: SELECT * FROM `catalog`.`employees` WHERE `age` > 30
70+
```
71+
72+
Supported SQL dialects include:
73+
- `SparkSqlDialect.DEFAULT` - Apache Spark SQL
74+
- `PostgresqlSqlDialect.DEFAULT` - PostgreSQL
75+
- `MysqlSqlDialect.DEFAULT` - MySQL
76+
- And other Calcite-supported dialects
77+
2478
## Development & Testing
2579

2680
A set of unit tests is provided to validate planner behavior.

api/build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
plugins {
77
id 'java-library'
8+
id "io.freefair.lombok"
89
id 'jacoco'
910
id 'com.diffplug.spotless'
1011
}
@@ -25,6 +26,10 @@ spotless {
2526
exclude '**/build/**', '**/build-*/**', 'src/main/gen/**'
2627
}
2728
importOrder()
29+
licenseHeader("/*\n" +
30+
" * Copyright OpenSearch Contributors\n" +
31+
" * SPDX-License-Identifier: Apache-2.0\n" +
32+
" */\n\n")
2833
removeUnusedImports()
2934
trimTrailingWhitespace()
3035
endWithNewline()

api/src/main/java/org/opensearch/sql/api/EmptyDataSourceService.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
16
package org.opensearch.sql.api;
27

38
import java.util.Map;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.api.transpiler;
7+
8+
import lombok.Builder;
9+
import org.apache.calcite.rel.RelNode;
10+
import org.apache.calcite.rel.rel2sql.RelToSqlConverter;
11+
import org.apache.calcite.sql.SqlDialect;
12+
import org.apache.calcite.sql.SqlNode;
13+
14+
/**
15+
* Transpiles Calcite logical plans ({@link RelNode}) into SQL strings for various target databases.
16+
* Uses Calcite's {@link RelToSqlConverter} to perform the conversion, respecting the specified SQL
17+
* dialect.
18+
*/
19+
@Builder
20+
public class UnifiedQueryTranspiler {
21+
22+
/** Target SQL dialect */
23+
private final SqlDialect dialect;
24+
25+
/**
26+
* Converts a Calcite logical plan to a SQL string using the configured target dialect.
27+
*
28+
* @param plan the logical plan to convert (must not be null)
29+
* @return the generated SQL string
30+
*/
31+
public String toSql(RelNode plan) {
32+
try {
33+
RelToSqlConverter converter = new RelToSqlConverter(dialect);
34+
SqlNode sqlNode = converter.visitRoot(plan).asStatement();
35+
return sqlNode.toSqlString(dialect).getSql();
36+
} catch (Exception e) {
37+
throw new IllegalStateException("Failed to transpile logical plan to SQL", e);
38+
}
39+
}
40+
}

api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java

Lines changed: 14 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,15 @@
88
import static org.junit.Assert.assertNotNull;
99
import static org.junit.Assert.assertThrows;
1010

11-
import java.util.List;
1211
import java.util.Map;
1312
import org.apache.calcite.rel.RelNode;
14-
import org.apache.calcite.rel.type.RelDataType;
15-
import org.apache.calcite.rel.type.RelDataTypeFactory;
1613
import org.apache.calcite.schema.Schema;
17-
import org.apache.calcite.schema.Table;
1814
import org.apache.calcite.schema.impl.AbstractSchema;
19-
import org.apache.calcite.schema.impl.AbstractTable;
20-
import org.apache.calcite.sql.type.SqlTypeName;
2115
import org.junit.Test;
2216
import org.opensearch.sql.common.antlr.SyntaxCheckException;
2317
import org.opensearch.sql.executor.QueryType;
2418

25-
public class UnifiedQueryPlannerTest {
26-
27-
/** Test schema consists of a test table with id and name columns */
28-
private final AbstractSchema testSchema =
29-
new AbstractSchema() {
30-
@Override
31-
protected Map<String, Table> getTableMap() {
32-
return Map.of(
33-
"index",
34-
new AbstractTable() {
35-
@Override
36-
public RelDataType getRowType(RelDataTypeFactory typeFactory) {
37-
return typeFactory.createStructType(
38-
List.of(
39-
typeFactory.createSqlType(SqlTypeName.INTEGER),
40-
typeFactory.createSqlType(SqlTypeName.VARCHAR)),
41-
List.of("id", "name"));
42-
}
43-
});
44-
}
45-
};
19+
public class UnifiedQueryPlannerTest extends UnifiedQueryTestBase {
4620

4721
/** Test catalog consists of test schema above */
4822
private final AbstractSchema testDeepSchema =
@@ -61,7 +35,7 @@ public void testPPLQueryPlanning() {
6135
.catalog("opensearch", testSchema)
6236
.build();
6337

64-
RelNode plan = planner.plan("source = opensearch.index | eval f = abs(id)");
38+
RelNode plan = planner.plan("source = opensearch.employees | eval f = abs(id)");
6539
assertNotNull("Plan should be created", plan);
6640
}
6741

@@ -74,8 +48,8 @@ public void testPPLQueryPlanningWithDefaultNamespace() {
7448
.defaultNamespace("opensearch")
7549
.build();
7650

77-
assertNotNull("Plan should be created", planner.plan("source = opensearch.index"));
78-
assertNotNull("Plan should be created", planner.plan("source = index"));
51+
assertNotNull("Plan should be created", planner.plan("source = opensearch.employees"));
52+
assertNotNull("Plan should be created", planner.plan("source = employees"));
7953
}
8054

8155
@Test
@@ -87,12 +61,12 @@ public void testPPLQueryPlanningWithDefaultNamespaceMultiLevel() {
8761
.defaultNamespace("catalog.opensearch")
8862
.build();
8963

90-
assertNotNull("Plan should be created", planner.plan("source = catalog.opensearch.index"));
91-
assertNotNull("Plan should be created", planner.plan("source = index"));
64+
assertNotNull("Plan should be created", planner.plan("source = catalog.opensearch.employees"));
65+
assertNotNull("Plan should be created", planner.plan("source = employees"));
9266

9367
// This is valid in SparkSQL, but Calcite requires "catalog" as the default root schema to
9468
// resolve it
95-
assertThrows(IllegalStateException.class, () -> planner.plan("source = opensearch.index"));
69+
assertThrows(IllegalStateException.class, () -> planner.plan("source = opensearch.employees"));
9670
}
9771

9872
@Test
@@ -105,7 +79,8 @@ public void testPPLQueryPlanningWithMultipleCatalogs() {
10579
.build();
10680

10781
RelNode plan =
108-
planner.plan("source = catalog1.index | lookup catalog2.index id | eval f = abs(id)");
82+
planner.plan(
83+
"source = catalog1.employees | lookup catalog2.employees id | eval f = abs(id)");
10984
assertNotNull("Plan should be created with multiple catalogs", plan);
11085
}
11186

@@ -119,7 +94,8 @@ public void testPPLQueryPlanningWithMultipleCatalogsAndDefaultNamespace() {
11994
.defaultNamespace("catalog2")
12095
.build();
12196

122-
RelNode plan = planner.plan("source = catalog1.index | lookup index id | eval f = abs(id)");
97+
RelNode plan =
98+
planner.plan("source = catalog1.employees | lookup employees id | eval f = abs(id)");
12399
assertNotNull("Plan should be created with multiple catalogs", plan);
124100
}
125101

@@ -132,7 +108,7 @@ public void testPPLQueryPlanningWithMetadataCaching() {
132108
.cacheMetadata(true)
133109
.build();
134110

135-
RelNode plan = planner.plan("source = opensearch.index");
111+
RelNode plan = planner.plan("source = opensearch.employees");
136112
assertNotNull("Plan should be created", plan);
137113
}
138114

@@ -166,7 +142,7 @@ public void testUnsupportedStatementType() {
166142
.catalog("opensearch", testSchema)
167143
.build();
168144

169-
planner.plan("explain source = index"); // explain statement
145+
planner.plan("explain source = employees"); // explain statement
170146
}
171147

172148
@Test(expected = SyntaxCheckException.class)
@@ -177,6 +153,6 @@ public void testPlanPropagatingSyntaxCheckException() {
177153
.catalog("opensearch", testSchema)
178154
.build();
179155

180-
planner.plan("source = index | eval"); // Trigger syntax error from parser
156+
planner.plan("source = employees | eval"); // Trigger syntax error from parser
181157
}
182158
}

0 commit comments

Comments
 (0)