diff --git a/examples/isthmus-api/.gitignore b/examples/isthmus-api/.gitignore new file mode 100644 index 000000000..6ead40eb5 --- /dev/null +++ b/examples/isthmus-api/.gitignore @@ -0,0 +1,4 @@ +_apps +_data +**/*/bin +build diff --git a/examples/isthmus-api/README.md b/examples/isthmus-api/README.md new file mode 100644 index 000000000..897ac2829 --- /dev/null +++ b/examples/isthmus-api/README.md @@ -0,0 +1,139 @@ +# Isthmus API Examples + +The Isthmus library converts Substrait plans to and from SQL Plans. There are two examples showing conversion in each direction. + +## How does this work in theory? + +The [Calcite](https://calcite.apache.org/) library is used to do parsing and generation of the SQL String. Calcite has it's own relational object model, distinct from substrait's. There are classes within Isthmus to convert Substrait to and from Calcite's object model. + +The conversion flows work as follows: + +**SQL to Substrait:** +`SQL ---[Calcite parsing]---> Calcite Object Model ---[Isthmus conversion]---> Substrait` + +**Substrait to SQL:** +`Substrait ---[Isthmus conversion]---> Calcite Object Model ---[Calcite SQL generation]---> SQL` + +## Running the examples + +There are 2 example classes: + +- [FromSql](./src/main/java/io/substrait/examples/FromSql.java) that creates a plan starting from SQL +- [ToSql](./app/src/main/java/io/substrait/examples/ToSQL.java) that reads a plan and creates the SQL + + +### Requirements + +To run these you will need Java 17 or greater, and this repository cloned to you local system. + + +## Creating a Substrait Plan from SQL + +To run [`FromSql.java`](./src/main/java/io/substrait/examples/FromSql.java), execute the command below from the root of this repository. + +```bash +./gradlew examples:isthmus-api:run --args "FromSql substrait.plan" +``` + +The example writes a binary plan to `substrait.plan` and outputs the text format of the protobuf to stdout. The output is quite lengthy, so it has been abbreviated here. + +```bash +> Task :examples:isthmus-api:run +extension_uris { + extension_uri_anchor: 2 + uri: "/functions_aggregate_generic.yaml" +} +extension_uris { + extension_uri_anchor: 1 + uri: "/functions_comparison.yaml" +} +extensions { + extension_function { + extension_uri_reference: 1 + function_anchor: 1 + name: "equal:any_any" + extension_urn_reference: 1 + } +} +extensions { + extension_function { + extension_uri_reference: 2 + function_anchor: 2 + name: "count:" + extension_urn_reference: 2 + } +} +relations {....} +} +version { + minor_number: 77 + producer: "isthmus" +} +extension_urns { + extension_urn_anchor: 1 + urn: "extension:io.substrait:functions_comparison" +} +extension_urns { + extension_urn_anchor: 2 + urn: "extension:io.substrait:functions_aggregate_generic" +} + +File written to substrait.plan +``` + +Please see the code comments for details of how the conversion is done. + +## Creating SQL from a Substrait Plan + +To run [`ToSql.java`](./src/main/java/io/substrait/examples/ToSql.java), execute the command below from the root of this repository. +```bash +./gradlew examples:isthmus-api:run --args "ToSql substrait.plan" +``` + +The example reads from `substrait.plan` (likely the file created by `FromSql`) and outputs SQL. The text format of the protobuf has been abbreviated +```bash +> Task :examples:isthmus-api:run +Reading from substrait.plan +extension_uris { + extension_uri_anchor: 2 + uri: "/functions_aggregate_generic.yaml" +} +extension_uris { + extension_uri_anchor: 1 + uri: "/functions_comparison.yaml" +} +extensions { + extension_function { + extension_uri_reference: 1 + function_anchor: 1 + name: "equal:any_any" + extension_urn_reference: 1 + } +} +extensions {....} +relations {....} +version { + minor_number: 77 + producer: "isthmus" +} +extension_urns { + extension_urn_anchor: 1 + urn: "extension:io.substrait:functions_comparison" +} +extension_urns { + extension_urn_anchor: 2 + urn: "extension:io.substrait:functions_aggregate_generic" +} + + +SELECT `t2`.`colour0` AS `COLOUR`, `t2`.`$f1` AS `COLOURCOUNT` +FROM (SELECT `vehicles`.`colour` AS `colour0`, COUNT(*) AS `$f1` +FROM `vehicles` +INNER JOIN `tests` ON `vehicles`.`vehicle_id` = `tests`.`vehicle_id` +WHERE `tests`.`test_result` = 'P' +GROUP BY `vehicles`.`colour` +ORDER BY COUNT(*) IS NULL, 2) AS `t2` + +``` + +The SQL statement in the selected dialect will be created (MySql is used in the example). diff --git a/examples/isthmus-api/build.gradle.kts b/examples/isthmus-api/build.gradle.kts new file mode 100644 index 000000000..b8764ab30 --- /dev/null +++ b/examples/isthmus-api/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + // Apply the application plugin to add support for building a CLI application in Java. + id("application") + alias(libs.plugins.spotless) + id("substrait.java-conventions") +} + +repositories { mavenCentral() } + +dependencies { + implementation(project(":isthmus")) + implementation(libs.calcite.core) + implementation(libs.calcite.server) +} + +application { mainClass = "io.substrait.examples.IsthmusAppExamples" } + +tasks.named("test") { useJUnitPlatform() } + +java { toolchain { languageVersion.set(JavaLanguageVersion.of(17)) } } + +tasks.pmdMain { dependsOn(":core:shadowJar") } diff --git a/examples/isthmus-api/src/main/java/io/substrait/examples/FromSql.java b/examples/isthmus-api/src/main/java/io/substrait/examples/FromSql.java new file mode 100644 index 000000000..ce045ddaa --- /dev/null +++ b/examples/isthmus-api/src/main/java/io/substrait/examples/FromSql.java @@ -0,0 +1,107 @@ +package io.substrait.examples; + +import io.substrait.examples.IsthmusAppExamples.Action; +import io.substrait.isthmus.SqlToSubstrait; +import io.substrait.isthmus.SubstraitTypeSystem; +import io.substrait.isthmus.sql.SubstraitCreateStatementParser; +import io.substrait.plan.Plan; +import io.substrait.plan.PlanProtoConverter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import org.apache.calcite.config.CalciteConnectionConfig; +import org.apache.calcite.config.CalciteConnectionProperty; +import org.apache.calcite.jdbc.CalciteSchema; +import org.apache.calcite.jdbc.JavaTypeFactoryImpl; +import org.apache.calcite.prepare.CalciteCatalogReader; +import org.apache.calcite.rel.type.RelDataTypeFactory; +import org.apache.calcite.sql.SqlDialect; +import org.apache.calcite.sql.parser.SqlParseException; + +/** + * Substrait from SQL conversions. + * + *

The conversion process involves four steps: + * + *

1. Create a fully typed schema for the inputs. Within a SQL context this represents the CREATE + * TABLE commands, which need to be converted to a Calcite Schema. + * + *

2. Parse the SQL query to convert (in the source SQL dialect). + * + *

3. Convert the SQL query to Calcite Relations. + * + *

4. Convert the Calcite Relations to Substrait relations. + * + *

Note that the schema could be created from other means, such as Calcite's reflection-based + * schema. + */ +public class FromSql implements Action { + + @Override + public void run(final String[] args) { + try { + final String createSql = + """ + CREATE TABLE "vehicles" ("vehicle_id" varchar(15), "make" varchar(40), "model" varchar(40), + "colour" varchar(15), "fuel_type" varchar(15), + "cylinder_capacity" int, "first_use_date" varchar(15)); + + CREATE TABLE "tests" ("test_id" varchar(15), "vehicle_id" varchar(15), + "test_date" varchar(20), "test_class" varchar(20), "test_type" varchar(20), + "test_result" varchar(15),"test_mileage" int, "postcode_area" varchar(15)); + + """; + + // Create the Calcite Schema from the CREATE TABLE statements. + // The Isthmus helper classes assume a standard SQL format for parsing. + final CalciteSchema calciteSchema = CalciteSchema.createRootSchema(false); + SubstraitCreateStatementParser.processCreateStatements(createSql) + .forEach(t -> calciteSchema.add(t.getName(), t)); + + // Type Factory based on Java Types + final RelDataTypeFactory typeFactory = + new JavaTypeFactoryImpl(SubstraitTypeSystem.TYPE_SYSTEM); + + // Default configuration for calcite + final CalciteConnectionConfig calciteDefaultConfig = + CalciteConnectionConfig.DEFAULT.set( + CalciteConnectionProperty.CASE_SENSITIVE, Boolean.FALSE.toString()); + + final CalciteCatalogReader catalogReader = + new CalciteCatalogReader(calciteSchema, List.of(), typeFactory, calciteDefaultConfig); + + // Query that needs to be converted; again this could be in a variety of SQL dialects + final String apacheDerbyQuery = + """ + SELECT vehicles.colour, count(*) as colourcount FROM vehicles INNER JOIN tests + ON vehicles.vehicle_id=tests.vehicle_id WHERE tests.test_result = 'P' + GROUP BY vehicles.colour ORDER BY count(*) + """; + final SqlToSubstrait sqlToSubstrait = new SqlToSubstrait(); + + // choose Apache Derby as an example dialect + final SqlDialect dialect = SqlDialect.DatabaseProduct.DERBY.getDialect(); + final Plan substraitPlan = sqlToSubstrait.convert(apacheDerbyQuery, catalogReader, dialect); + + // Create the proto plan to display to stdout - as it has a better format + final PlanProtoConverter planToProto = new PlanProtoConverter(); + final io.substrait.proto.Plan protoPlan = planToProto.toProto(substraitPlan); + System.out.println(protoPlan); + + // write out to file if given a file name + // convert to a protobuff byte array and write as binary file + if (args.length == 1) { + + final byte[] buffer = protoPlan.toByteArray(); + final Path outputFile = Paths.get(args[0]); + Files.write(outputFile, buffer); + System.out.println("File written to " + outputFile); + } + + } catch (SqlParseException | IOException e) { + e.printStackTrace(); + } + } +} diff --git a/examples/isthmus-api/src/main/java/io/substrait/examples/IsthmusAppExamples.java b/examples/isthmus-api/src/main/java/io/substrait/examples/IsthmusAppExamples.java new file mode 100644 index 000000000..86bce5aea --- /dev/null +++ b/examples/isthmus-api/src/main/java/io/substrait/examples/IsthmusAppExamples.java @@ -0,0 +1,53 @@ +package io.substrait.examples; + +import java.util.Arrays; + +/** Main class */ +public final class IsthmusAppExamples { + + /** Implemented by all examples */ + @FunctionalInterface + public interface Action { + + /** + * Run + * + * @param args String [] + */ + void run(String[] args); + } + + private IsthmusAppExamples() {} + + /** + * Traditional main method + * + * @param args string[] + */ + @SuppressWarnings("unchecked") + public static void main(final String args[]) { + try { + + if (args.length == 0) { + System.err.println( + "Please provide base classname of example to run. eg ToSql to run class io.substrait.examples.ToSql "); + System.exit(-1); + } + final String exampleClass = args[0]; + + final Class clz = + (Class) + Class.forName( + String.format("%s.%s", IsthmusAppExamples.class.getPackageName(), exampleClass)); + final Action action = clz.getDeclaredConstructor().newInstance(); + if (args.length == 1) { + action.run(new String[] {}); + } else { + action.run(Arrays.copyOfRange(args, 1, args.length)); + } + } catch (Exception e) { + e.printStackTrace(); + System.exit(-1); + } + } +} diff --git a/examples/isthmus-api/src/main/java/io/substrait/examples/SchemaHelper.java b/examples/isthmus-api/src/main/java/io/substrait/examples/SchemaHelper.java new file mode 100644 index 000000000..d3fe2cae7 --- /dev/null +++ b/examples/isthmus-api/src/main/java/io/substrait/examples/SchemaHelper.java @@ -0,0 +1,39 @@ +package io.substrait.examples; + +import io.substrait.isthmus.calcite.SubstraitTable; +import io.substrait.isthmus.sql.SubstraitCreateStatementParser; +import java.util.ArrayList; +import java.util.List; +import org.apache.calcite.jdbc.CalciteSchema; +import org.apache.calcite.prepare.CalciteCatalogReader; +import org.apache.calcite.sql.parser.SqlParseException; + +/** Helper functions for schemas. */ +public final class SchemaHelper { + + private SchemaHelper() {} + + /** + * Parses one or more SQL strings containing only CREATE statements into a {@link + * CalciteCatalogReader} + * + * @param createStatements a SQL string containing only CREATE statements + * @return a {@link CalciteCatalogReader} generated from the CREATE statements + * @throws SqlParseException + */ + public static CalciteSchema processCreateStatementsToSchema(final List createStatements) + throws SqlParseException { + + final List tables = new ArrayList<>(); + for (final String statement : createStatements) { + tables.addAll(SubstraitCreateStatementParser.processCreateStatements(statement)); + } + + final CalciteSchema rootSchema = CalciteSchema.createRootSchema(false); + for (final SubstraitTable table : tables) { + rootSchema.add(table.getName(), table); + } + + return rootSchema; + } +} diff --git a/examples/isthmus-api/src/main/java/io/substrait/examples/ToSql.java b/examples/isthmus-api/src/main/java/io/substrait/examples/ToSql.java new file mode 100644 index 000000000..1cb8b70ad --- /dev/null +++ b/examples/isthmus-api/src/main/java/io/substrait/examples/ToSql.java @@ -0,0 +1,81 @@ +package io.substrait.examples; + +import io.substrait.examples.IsthmusAppExamples.Action; +import io.substrait.extension.DefaultExtensionCatalog; +import io.substrait.extension.SimpleExtension; +import io.substrait.isthmus.SubstraitToCalcite; +import io.substrait.isthmus.SubstraitTypeSystem; +import io.substrait.plan.Plan; +import io.substrait.plan.PlanProtoConverter; +import io.substrait.plan.ProtoPlanConverter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import org.apache.calcite.jdbc.JavaTypeFactoryImpl; +import org.apache.calcite.rel.rel2sql.RelToSqlConverter; +import org.apache.calcite.sql.SqlDialect; + +/** + * Substrait to SQL conversions. + * + *

The conversion process involves three steps: + * + *

1. Load the plan into the protobuf object and create an in-memory POJO representation. + * + *

2. Create a Converter to map the Substrait plan to Calcite relations. This requires the type + * system to use and the collection of extensions from the substrait plan. + * + *

3. Convert the Calcite relational nodes to SQL statements using the specified SQL dialect + * configuration. + * + *

It is possible to get multiple SQL statements from a single Substrait plan. + */ +public class ToSql implements Action { + + @Override + public void run(String[] args) { + + try { + + // Load the protobuf binary file into a Substrait Plan POJO + System.out.println("Reading from " + args[0]); + final byte[] buffer = Files.readAllBytes(Paths.get(args[0])); + + final io.substrait.proto.Plan proto = io.substrait.proto.Plan.parseFrom(buffer); + final ProtoPlanConverter protoToPlan = new ProtoPlanConverter(); + final Plan substraitPlan = protoToPlan.from(proto); + + // Create the proto plan to display to stdout - as it has a better format + final PlanProtoConverter planToProto = new PlanProtoConverter(); + final io.substrait.proto.Plan protoPlan = planToProto.toProto(substraitPlan); + System.out.println(protoPlan); + + final SimpleExtension.ExtensionCollection extensions = + DefaultExtensionCatalog.DEFAULT_COLLECTION; + final SubstraitToCalcite converter = + new SubstraitToCalcite( + extensions, new JavaTypeFactoryImpl(SubstraitTypeSystem.TYPE_SYSTEM)); + + // Determine which SQL Dialect we want the converted queries to be in + final SqlDialect sqlDialect = SqlDialect.DatabaseProduct.MYSQL.getDialect(); + + // Create the Sql to Calcite Relation Parser + final RelToSqlConverter relToSql = new RelToSqlConverter(sqlDialect); + + System.out.println("\n"); + + // Convert each of the Substrait plan roots to SQL + substraitPlan.getRoots().stream() + // Substrait -> Calcite Rel + .map(root -> converter.convert(root).project(true)) + // Calcite Rel -> Calcite SQL + .map(calciteRelNode -> relToSql.visitRoot(calciteRelNode).asStatement()) + // Calcite SQL -> SQL String + .map(sqlNode -> sqlNode.toSqlString(sqlDialect).getSql()) + .forEachOrdered(System.out::println); + + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/isthmus/src/main/java/io/substrait/isthmus/SqlToSubstrait.java b/isthmus/src/main/java/io/substrait/isthmus/SqlToSubstrait.java index abe935a75..e60494244 100644 --- a/isthmus/src/main/java/io/substrait/isthmus/SqlToSubstrait.java +++ b/isthmus/src/main/java/io/substrait/isthmus/SqlToSubstrait.java @@ -10,9 +10,11 @@ import io.substrait.plan.PlanProtoConverter; import java.util.List; import org.apache.calcite.prepare.Prepare; +import org.apache.calcite.sql.SqlDialect; import org.apache.calcite.sql.SqlOperator; import org.apache.calcite.sql.SqlOperatorTable; import org.apache.calcite.sql.parser.SqlParseException; +import org.apache.calcite.sql.parser.SqlParser; import org.apache.calcite.sql.util.SqlOperatorTables; /** Take a SQL statement and a set of table definitions and return a substrait plan. */ @@ -62,7 +64,8 @@ public SqlToSubstrait(SimpleExtension.ExtensionCollection extensions, FeatureBoa public io.substrait.proto.Plan execute(String sqlStatements, Prepare.CatalogReader catalogReader) throws SqlParseException { PlanProtoConverter planToProto = new PlanProtoConverter(); - return planToProto.toProto(convert(sqlStatements, catalogReader)); + return planToProto.toProto( + convert(sqlStatements, catalogReader, SqlDialect.DatabaseProduct.CALCITE.getDialect())); } /** @@ -74,7 +77,7 @@ public io.substrait.proto.Plan execute(String sqlStatements, Prepare.CatalogRead * @return the Substrait {@link Plan} * @throws SqlParseException if there is an error while parsing the SQL statements */ - public Plan convert(String sqlStatements, Prepare.CatalogReader catalogReader) + public Plan convert(final String sqlStatements, final Prepare.CatalogReader catalogReader) throws SqlParseException { Builder builder = io.substrait.plan.Plan.builder(); builder.version(Version.builder().from(Version.DEFAULT_VERSION).producer("isthmus").build()); @@ -86,4 +89,32 @@ public Plan convert(String sqlStatements, Prepare.CatalogReader catalogReader) return builder.build(); } + + /** + * Converts one or more SQL statements into a Substrait {@link Plan}. + * + * @param sqlStatements a string containing one more SQL statements + * @param catalogReader the {@link Prepare.CatalogReader} for finding tables/views referenced in + * the SQL statements + * @param sqlDialect The sql dialect to use for parsing. + * @return the Substrait {@link Plan} + * @throws SqlParseException if there is an error while parsing the SQL statements + */ + public Plan convert( + final String sqlStatements, + final Prepare.CatalogReader catalogReader, + final SqlDialect sqlDialect) + throws SqlParseException { + Builder builder = io.substrait.plan.Plan.builder(); + builder.version(Version.builder().from(Version.DEFAULT_VERSION).producer("isthmus").build()); + + final SqlParser.Config sqlParserConfig = sqlDialect.configureParser(SqlParser.config()); + + // TODO: consider case in which one sql passes conversion while others don't + SubstraitSqlToCalcite.convertQueries(sqlStatements, catalogReader, sqlParserConfig).stream() + .map(root -> SubstraitRelVisitor.convert(root, extensionCollection, featureBoard)) + .forEach(root -> builder.addRoots(root)); + + return builder.build(); + } } diff --git a/isthmus/src/main/java/io/substrait/isthmus/sql/SubstraitSqlStatementParser.java b/isthmus/src/main/java/io/substrait/isthmus/sql/SubstraitSqlStatementParser.java index 0f7891b5b..8da220c84 100644 --- a/isthmus/src/main/java/io/substrait/isthmus/sql/SubstraitSqlStatementParser.java +++ b/isthmus/src/main/java/io/substrait/isthmus/sql/SubstraitSqlStatementParser.java @@ -30,7 +30,20 @@ public class SubstraitSqlStatementParser { * @throws SqlParseException if there is an error while parsing the SQL statements */ public static List parseStatements(String sqlStatements) throws SqlParseException { - SqlParser parser = SqlParser.create(sqlStatements, PARSER_CONFIG); + return parseStatements(sqlStatements, PARSER_CONFIG); + } + + /** + * Parse one or more SQL statements to a list of {@link SqlNode}s. + * + * @param sqlStatements a string containing one or more SQL statements + * @param parserConfig Calcite SqlParser.Config to control the parser + * @return a list of {@link SqlNode}s corresponding to the given statements + * @throws SqlParseException if there is an error while parsing the SQL statements + */ + public static List parseStatements(String sqlStatements, SqlParser.Config parserConfig) + throws SqlParseException { + SqlParser parser = SqlParser.create(sqlStatements, parserConfig); return parser.parseStmtList(); } } diff --git a/isthmus/src/main/java/io/substrait/isthmus/sql/SubstraitSqlToCalcite.java b/isthmus/src/main/java/io/substrait/isthmus/sql/SubstraitSqlToCalcite.java index b46d30e7c..d2ec84111 100644 --- a/isthmus/src/main/java/io/substrait/isthmus/sql/SubstraitSqlToCalcite.java +++ b/isthmus/src/main/java/io/substrait/isthmus/sql/SubstraitSqlToCalcite.java @@ -17,6 +17,7 @@ import org.apache.calcite.sql.SqlNode; import org.apache.calcite.sql.SqlOperatorTable; import org.apache.calcite.sql.parser.SqlParseException; +import org.apache.calcite.sql.parser.SqlParser; import org.apache.calcite.sql.validate.SqlValidator; import org.apache.calcite.sql2rel.SqlToRelConverter; import org.apache.calcite.sql2rel.StandardConvertletTable; @@ -124,6 +125,27 @@ public static List convertQueries( return convertQueries(sqlStatements, catalogReader, validator, createDefaultRelOptCluster()); } + /** + * Converts one or more SQL statements to a List of {@link RelRoot}, with one {@link RelRoot} per + * statement. + * + * @param sqlStatements a string containing one or more SQL statements + * @param catalogReader the {@link Prepare.CatalogReader} for finding tables/views referenced in + * the SQL statements + * @param parserConfig Calcite Parser config to use with the given SQL Statements + * @return a list of {@link RelRoot}s corresponding to the given SQL statements + * @throws SqlParseException if there is an error while parsing the SQL statements + */ + public static List convertQueries( + String sqlStatements, + Prepare.CatalogReader catalogReader, + final SqlParser.Config parserConfig) + throws SqlParseException { + SqlValidator validator = new SubstraitSqlValidator(catalogReader); + return convertQueries( + sqlStatements, catalogReader, validator, createDefaultRelOptCluster(), parserConfig); + } + /** * Converts one or more SQL statements to a List of {@link RelRoot}, with one {@link RelRoot} per * statement. @@ -150,6 +172,35 @@ public static List convertQueries( return convert(sqlNodes, catalogReader, validator, cluster); } + /** + * Converts one or more SQL statements to a List of {@link RelRoot}, with one {@link RelRoot} per + * statement. + * + * @param sqlStatements a string containing one or more SQL statements + * @param catalogReader the {@link Prepare.CatalogReader} for finding tables/views referenced in + * the SQL statements + * @param validator the {@link SqlValidator} used to validate SQL statements. Allows for + * additional control of SQL functions and operators via {@link + * SqlValidator#getOperatorTable()} + * @param cluster the {@link RelOptCluster} used when creating {@link RelNode}s during statement + * processing. Calcite expects that the {@link RelOptCluster} used during statement processing + * is the same as that used during query optimization. + * @param parserConfig Calcite Parser config to use with the given SQL Statements + * @return a list of {@link RelRoot}s corresponding to the given SQL statements + * @throws SqlParseException if there is an error while parsing the SQL statements + */ + public static List convertQueries( + String sqlStatements, + Prepare.CatalogReader catalogReader, + SqlValidator validator, + RelOptCluster cluster, + SqlParser.Config parserConfig) + throws SqlParseException { + List sqlNodes = + SubstraitSqlStatementParser.parseStatements(sqlStatements, parserConfig); + return convert(sqlNodes, catalogReader, validator, cluster); + } + static List convert( List sqlNodes, Prepare.CatalogReader catalogReader, diff --git a/settings.gradle.kts b/settings.gradle.kts index 1c2b51ef0..500c43c3c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,4 +2,12 @@ rootProject.name = "substrait" includeBuild("build-logic") -include("bom", "core", "isthmus", "isthmus-cli", "spark", "examples:substrait-spark") +include( + "bom", + "core", + "isthmus", + "isthmus-cli", + "spark", + "examples:substrait-spark", + "examples:isthmus-api", +)