diff --git a/.editorconfig b/.editorconfig index 998c28f6c..43a99e28f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,7 +10,7 @@ trim_trailing_whitespace = true [*.{yaml,yml}] indent_size = 2 -[{**/*.sql,**/OuterReferenceResolver.md,**gradlew.bat,**/*.parquet,**/*.orc}] +[{**/*.sql,**/OuterReferenceResolver.md,**gradlew.bat,**/*.parquet,**/*.orc,**/*.plan}] charset = unset end_of_line = unset insert_final_newline = unset diff --git a/gradle.properties b/gradle.properties index ec7984743..88c94f516 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,7 +15,7 @@ com.github.vlsi.vlsi-release-plugins.version=1.74 # library version antlr.version=4.13.1 -calcite.version=1.38.0 +calcite.version=1.39.0 guava.version=32.1.3-jre immutables.version=2.10.1 jackson.version=2.16.1 diff --git a/isthmus/build.gradle.kts b/isthmus/build.gradle.kts index 7e44b6eb3..d4b9dd400 100644 --- a/isthmus/build.gradle.kts +++ b/isthmus/build.gradle.kts @@ -7,9 +7,15 @@ plugins { id("com.diffplug.spotless") version "6.19.0" id("com.github.johnrengelman.shadow") version "8.1.1" id("com.google.protobuf") version "0.9.4" + id("com.adarshr.test-logger") version "4.0.0" signing } +testlogger { + showStandardStreams = false + showFailedStandardStreams = true +} + publishing { publications { create("maven-publish") { diff --git a/isthmus/src/main/java/io/substrait/isthmus/SqlConverterBase.java b/isthmus/src/main/java/io/substrait/isthmus/SqlConverterBase.java index 052546656..0ad1f4f6f 100644 --- a/isthmus/src/main/java/io/substrait/isthmus/SqlConverterBase.java +++ b/isthmus/src/main/java/io/substrait/isthmus/SqlConverterBase.java @@ -10,7 +10,6 @@ import org.apache.calcite.config.CalciteConnectionProperty; import org.apache.calcite.jdbc.CalciteSchema; import org.apache.calcite.jdbc.JavaTypeFactoryImpl; -import org.apache.calcite.jdbc.LookupCalciteSchema; import org.apache.calcite.plan.Contexts; import org.apache.calcite.plan.RelOptCluster; import org.apache.calcite.plan.RelOptCostImpl; @@ -23,7 +22,6 @@ import org.apache.calcite.rel.type.RelDataTypeFactory; import org.apache.calcite.rex.RexBuilder; import org.apache.calcite.schema.Schema; -import org.apache.calcite.schema.Table; import org.apache.calcite.schema.impl.AbstractTable; import org.apache.calcite.sql.SqlNode; import org.apache.calcite.sql.SqlNodeList; @@ -95,19 +93,13 @@ Pair registerCreateTables(List table Pair registerCreateTables( Function, NamedStruct> tableLookup) throws SqlParseException { - Function, Table> lookup = - id -> { - NamedStruct table = tableLookup.apply(id); - if (table == null) { - return null; - } - return new DefinedTable( - id.get(id.size() - 1), - factory, - TypeConverter.DEFAULT.toCalcite(factory, table.struct(), table.names())); - }; - - CalciteSchema rootSchema = LookupCalciteSchema.createRootSchema(lookup); + CalciteSchema rootSchema = + SubstraitCalciteSchema.builder() + .withTableLookup(tableLookup) + .withTypeFactory(factory) + .withTypeConverter(TypeConverter.DEFAULT) + .build() + .getRootSchema(); CalciteCatalogReader catalogReader = new CalciteCatalogReader(rootSchema, List.of(), factory, config); SqlValidator validator = Validator.create(factory, catalogReader, SqlValidator.Config.DEFAULT); @@ -153,7 +145,8 @@ protected List parseCreateTable( for (SqlNode node : create.columnList) { if (!(node instanceof SqlColumnDeclaration)) { if (node instanceof SqlKeyConstraint) { - // key constraints declarations, like primary key declaration, are valid and should not + // key constraints declarations, like primary key declaration, are valid and + // should not // result in parse exceptions. Ignore the constraint declaration. continue; } @@ -217,9 +210,6 @@ public DefinedTable(String name, RelDataTypeFactory factory, RelDataType type) { @Override public RelDataType getRowType(RelDataTypeFactory typeFactory) { - // if (factory != typeFactory) { - // throw new IllegalStateException("Different type factory than previously used."); - // } return type; } diff --git a/isthmus/src/main/java/io/substrait/isthmus/SubstraitCalciteSchema.java b/isthmus/src/main/java/io/substrait/isthmus/SubstraitCalciteSchema.java new file mode 100644 index 000000000..8c9f33c0e --- /dev/null +++ b/isthmus/src/main/java/io/substrait/isthmus/SubstraitCalciteSchema.java @@ -0,0 +1,284 @@ +package io.substrait.isthmus; + +import com.google.common.collect.ImmutableSet; +import io.substrait.isthmus.SqlConverterBase.DefinedTable; +import io.substrait.relation.NamedScan; +import io.substrait.relation.Rel; +import io.substrait.relation.RelCopyOnWriteVisitor; +import io.substrait.type.NamedStruct; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.apache.calcite.jdbc.CalciteSchema; +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.lookup.LikePattern; +import org.apache.calcite.schema.lookup.Lookup; +import org.apache.calcite.schema.lookup.Named; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A subclass of the Calcite Schema for creation from a Substrait relation + * + *

Implementation note: + * + *

The external Isthmus API can take a function that will return the table schema when needed, + * rather than it being available up front. + * + *

This was implemented by a special subclass of the Calcite simple schema. Since this was + * changed in Calcite 1.39.0; it failed to work; the protected methods it extended from changed. + * + *

The change, ironically, was to support a lazy approach to looking up Calcite schemas. Good - + * *but* the external function in Isthmus is assuming it's going to get called with a fully + * namespaced table name. Which Calcite though sees as being subschemas. + * + *

This results in some 'complex' code below to try and map the lazy way Calcite now works and + * maintain the existing Isthmus API + * + *

If that Ishtmus API hadn't existed, this code would be a lot simpler! Maybe a case for future + * deprecation. + */ +public class SubstraitCalciteSchema extends AbstractSchema { + + private Map tables; + private Function, NamedStruct> tableLookup; + + private RelDataTypeFactory typeFactory; + private TypeConverter typeConverter; + + /** + * Maintain a track of the 'prefix' of this schema... i.e. allows recreation of the fully + * qualified name of this subschema + */ + private List prefix = new ArrayList<>(); + + protected SubstraitCalciteSchema(Map tables) { + this.tables = tables; + } + + protected SubstraitCalciteSchema( + Function, NamedStruct> tableLookup, + RelDataTypeFactory typeFactory, + TypeConverter typeConverter) { + this.tableLookup = tableLookup; + this.typeFactory = typeFactory; + this.typeConverter = typeConverter; + } + + @Override + protected Map getTableMap() { + return tables; + } + + @Override + public Lookup subSchemas() { + var defaultLookup = super.subSchemas(); + + // Note ono the lookups, calcite prefers calling getIgnoreCase() initially + + return new Lookup<>() { + + @Override + public @Nullable Schema get(String name) { + + // before we create the next subschema, we need to check if this + // is actually the final value. i.e. we need to call the lookup + // if it is the final table, we then return null here. + // Calcite sees that, knows there are no more schemas and instead + // calls the tables() look up to get a table name. + var lookupNameList = new ArrayList(prefix); + lookupNameList.add(name); + + NamedStruct table = tableLookup.apply(lookupNameList); + if (table != null) { + return null; + } + + var scs = new SubstraitCalciteSchema(tableLookup, typeFactory, typeConverter); + scs.prefix = lookupNameList; + return scs; + } + + @Override + public @Nullable Named getIgnoreCase(String name) { + + // before we create the next subschema, we need to check if this + // is actually the final value. i.e. we need to call the lookup + // if it is the final table, we then return null here/ + // Calcite sees that there's no more schemas and instead + // calls the tables() lazy look up to get a table name. + var lookupNameList = new ArrayList(prefix); + lookupNameList.add(name); + + NamedStruct table = tableLookup.apply(lookupNameList); + if (table != null) { + return null; + } + + var scs = new SubstraitCalciteSchema(tableLookup, typeFactory, typeConverter); + scs.prefix = lookupNameList; + return new Named<>(name, scs); + } + + @Override + public Set getNames(LikePattern pattern) { + return defaultLookup.getNames(pattern); + } + }; + } + + @Override + public Lookup tables() { + if (this.tables != null) { + // If we do have the list of tables already specified, delegate to the super class to return + // those + return super.tables(); + } + + return new Lookup
() { + + @Override + public @Nullable Table get(String name) { + List p = new ArrayList<>(prefix); + p.add(name); + + NamedStruct table = tableLookup.apply(p); + if (table == null) { + return null; + } + + return new DefinedTable( + name, typeFactory, typeConverter.toCalcite(typeFactory, table.struct(), table.names())); + } + + @Override + public @Nullable Named
getIgnoreCase(String name) { + /** Delegate to the noremal lookup */ + return new Named
(name, get(name)); + } + + @Override + public Set getNames(LikePattern pattern) { + return ImmutableSet.of(); + } + }; + } + + /** + * Turn this into a root Calciteschema Choice of settings is based on current isthmus behaviour + */ + public CalciteSchema getRootSchema() { + return CalciteSchema.createRootSchema(false, false, "", this); + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class to assist with creating the CalciteSchema + * + *

Can be created from a Rel or a Lookup function + */ + public static class Builder { + + private Rel rel; + private RelDataTypeFactory typeFactory; + private TypeConverter typeConverter; + private Function, NamedStruct> tableLookup; + + public Builder withTableLookup(Function, NamedStruct> tableLookup) { + this.tableLookup = tableLookup; + return this; + } + + public Builder withTypeFactory(RelDataTypeFactory typeFactory) { + this.typeFactory = typeFactory; + return this; + } + + public Builder withTypeConverter(TypeConverter typeConverter) { + this.typeConverter = typeConverter; + return this; + } + + public Builder withSubstraitRel(Rel rel) { + this.rel = rel; + return this; + } + + public SubstraitCalciteSchema build() { + if (typeConverter == null) { + throw new IllegalArgumentException("TypeConverter must be specified"); + } + + if (typeFactory == null) { + throw new IllegalArgumentException("TypeFactory must be specified"); + } + + if (rel != null && tableLookup != null) { + throw new IllegalArgumentException("Specify either 'rel' or 'tableLookup' "); + } + + if (rel != null) { + // If there are any named structs within the relation, gather these and convert + // them to a map of tables + // index by name; note that the name of the table is 'un-namespaced' here. + // This was the existing logic so it has not been altered. + Map, NamedStruct> tableMap = NamedStructGatherer.gatherTables(rel); + + Map tables = + tableMap.entrySet().stream() + .map( + entry -> { + var id = entry.getKey(); + var name = id.get(id.size() - 1); + var table = entry.getValue(); + var value = + new SqlConverterBase.DefinedTable( + name, + typeFactory, + typeConverter.toCalcite(typeFactory, table.struct(), table.names())); + return Map.entry(name, value); + }) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + return new SubstraitCalciteSchema(tables); + } else { + return new SubstraitCalciteSchema(tableLookup, typeFactory, typeConverter); + } + } + } + + private static final class NamedStructGatherer extends RelCopyOnWriteVisitor { + Map, NamedStruct> tableMap; + + private NamedStructGatherer() { + super(); + this.tableMap = new HashMap<>(); + } + + public static Map, NamedStruct> gatherTables(Rel rel) { + var visitor = new NamedStructGatherer(); + rel.accept(visitor); + return visitor.tableMap; + } + + @Override + public Optional visit(NamedScan namedScan) { + Optional result = super.visit(namedScan); + + List tableName = namedScan.getNames(); + tableMap.put(tableName, namedScan.getInitialSchema()); + + return result; + } + } +} diff --git a/isthmus/src/main/java/io/substrait/isthmus/SubstraitToCalcite.java b/isthmus/src/main/java/io/substrait/isthmus/SubstraitToCalcite.java index a96185a22..fd8066168 100644 --- a/isthmus/src/main/java/io/substrait/isthmus/SubstraitToCalcite.java +++ b/isthmus/src/main/java/io/substrait/isthmus/SubstraitToCalcite.java @@ -2,24 +2,15 @@ import io.substrait.extension.SimpleExtension; import io.substrait.plan.Plan; -import io.substrait.relation.NamedScan; import io.substrait.relation.Rel; -import io.substrait.relation.RelCopyOnWriteVisitor; -import io.substrait.type.NamedStruct; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.Function; import org.apache.calcite.jdbc.CalciteSchema; -import org.apache.calcite.jdbc.LookupCalciteSchema; import org.apache.calcite.rel.RelNode; import org.apache.calcite.rel.RelRoot; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.rel.type.RelDataTypeFactory; import org.apache.calcite.rel.type.RelDataTypeField; -import org.apache.calcite.schema.Table; import org.apache.calcite.sql.SqlKind; import org.apache.calcite.tools.Frameworks; import org.apache.calcite.tools.RelBuilder; @@ -57,19 +48,12 @@ public SubstraitToCalcite( *

Override this method to customize schema extraction. */ protected CalciteSchema toSchema(Rel rel) { - Map, NamedStruct> tableMap = NamedStructGatherer.gatherTables(rel); - Function, Table> lookup = - id -> { - NamedStruct table = tableMap.get(id); - if (table == null) { - return null; - } - return new SqlConverterBase.DefinedTable( - id.get(id.size() - 1), - typeFactory, - typeConverter.toCalcite(typeFactory, table.struct(), table.names())); - }; - return LookupCalciteSchema.createRootSchema(lookup); + return SubstraitCalciteSchema.builder() + .withSubstraitRel(rel) + .withTypeFactory(typeFactory) + .withTypeConverter(typeConverter) + .build() + .getRootSchema(); } /** @@ -179,29 +163,4 @@ private Pair renameFields( return Pair.of(currentIndex, type); } } - - private static class NamedStructGatherer extends RelCopyOnWriteVisitor { - Map, NamedStruct> tableMap; - - private NamedStructGatherer() { - super(); - this.tableMap = new HashMap<>(); - } - - public static Map, NamedStruct> gatherTables(Rel rel) { - var visitor = new NamedStructGatherer(); - rel.accept(visitor); - return visitor.tableMap; - } - - @Override - public Optional visit(NamedScan namedScan) { - Optional result = super.visit(namedScan); - - List tableName = namedScan.getNames(); - tableMap.put(tableName, namedScan.getInitialSchema()); - - return result; - } - } } diff --git a/isthmus/src/main/java/org/apache/calcite/jdbc/LookupCalciteSchema.java b/isthmus/src/main/java/org/apache/calcite/jdbc/LookupCalciteSchema.java deleted file mode 100644 index 7222dbc6f..000000000 --- a/isthmus/src/main/java/org/apache/calcite/jdbc/LookupCalciteSchema.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.apache.calcite.jdbc; - -import com.google.common.collect.Maps; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import org.apache.calcite.schema.Schema; -import org.apache.calcite.schema.Table; -import org.apache.calcite.schema.impl.AbstractSchema; -import org.checkerframework.checker.nullness.qual.Nullable; - -public class LookupCalciteSchema extends SimpleCalciteSchema { - private final Function, Table> lookup; - private final Map, Table> cache = Maps.newHashMap(); - - LookupCalciteSchema( - @Nullable CalciteSchema parent, - Schema schema, - String name, - Function, Table> lookup) { - super(parent, schema, name); - this.lookup = lookup; - } - - @Override - public CalciteSchema add(String name, Schema schema) { - final CalciteSchema calciteSchema = new LookupCalciteSchema(this, schema, name, lookup); - subSchemaMap.put(name, calciteSchema); - return calciteSchema; - } - - @Override - protected @Nullable CalciteSchema getImplicitSubSchema(String schemaName, boolean caseSensitive) { - if (cache.computeIfAbsent(path(schemaName), lookup) != null) { - return null; - } - plus().add(schemaName, AbstractSchema.Factory.INSTANCE.create(null, null, null)); - return super.getSubSchema(schemaName, caseSensitive); - } - - @Override - protected @Nullable TableEntry getImplicitTable(String tableName, boolean caseSensitive) { - Table table = cache.computeIfAbsent(path(tableName), lookup); - if (table == null) { - return null; - } - add(tableName, table); - return getTable(tableName, caseSensitive); - } - - public static CalciteSchema createRootSchema(Function, Table> lookup) { - Schema rootSchema = new CalciteConnectionImpl.RootSchema(); - return new LookupCalciteSchema(null, rootSchema, "", lookup); - } -} diff --git a/isthmus/src/test/java/io/substrait/isthmus/SubstraitExpressionConverterTest.java b/isthmus/src/test/java/io/substrait/isthmus/SubstraitExpressionConverterTest.java index ad6106d6e..d0b28a3e4 100644 --- a/isthmus/src/test/java/io/substrait/isthmus/SubstraitExpressionConverterTest.java +++ b/isthmus/src/test/java/io/substrait/isthmus/SubstraitExpressionConverterTest.java @@ -148,6 +148,7 @@ public void unspecifiedSetPredicate() { * @return the Substrait {@link Rel} equivalent of the above SQL query */ Rel createSubQueryRel() { + return b.project( input -> List.of(b.fieldReference(input, 0)), Remap.of(List.of(3)), diff --git a/isthmus/src/test/java/io/substrait/isthmus/api/ExampleCalciteReflectiveSchema.java b/isthmus/src/test/java/io/substrait/isthmus/api/ExampleCalciteReflectiveSchema.java new file mode 100644 index 000000000..c46761a64 --- /dev/null +++ b/isthmus/src/test/java/io/substrait/isthmus/api/ExampleCalciteReflectiveSchema.java @@ -0,0 +1,29 @@ +package io.substrait.isthmus.api; + +public class ExampleCalciteReflectiveSchema { + + class VehicleTest { + public int test_id; + public int vehicle_id; + public String test_date; + public String test_type; + public String test_class; + public String test_result; + public int test_mileage; + public String postcode_area; + } + + class Vehicle { + public int vehicle_id; + + public String make; + public String model; + public String colour; + public String fuel_type; + public int cylinder_capacity; + public String first_use_date; + } + + public VehicleTest[] tests = {}; + public Vehicle[] vehicles = {}; +} diff --git a/isthmus/src/test/java/io/substrait/isthmus/api/TestIsthmusEndToEnd.java b/isthmus/src/test/java/io/substrait/isthmus/api/TestIsthmusEndToEnd.java new file mode 100644 index 000000000..589f2056f --- /dev/null +++ b/isthmus/src/test/java/io/substrait/isthmus/api/TestIsthmusEndToEnd.java @@ -0,0 +1,97 @@ +package io.substrait.isthmus.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.common.io.Resources; +import io.substrait.extension.SimpleExtension; +import io.substrait.isthmus.SqlToSubstrait; +import io.substrait.isthmus.SubstraitToCalcite; +import io.substrait.isthmus.SubstraitToSql; +import io.substrait.isthmus.SubstraitTypeSystem; +import io.substrait.plan.Plan.Root; +import io.substrait.plan.ProtoPlanConverter; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.List; +import org.apache.calcite.adapter.java.ReflectiveSchema; +import org.apache.calcite.jdbc.JavaTypeFactoryImpl; +import org.apache.calcite.schema.Schema; +import org.apache.calcite.sql.parser.SqlParseException; +import org.junit.jupiter.api.Test; + +/** API level testing */ +public class TestIsthmusEndToEnd { + + /** Conversion of a Substrait Protobuf to */ + @Test + public void substraitToSqlViaCalcite() throws IOException { + + // create the protobuf Substrait plan + byte[] planProtobuf = Resources.toByteArray(Resources.getResource("substrait_sql_003.plan")); + io.substrait.proto.Plan protoPlan = io.substrait.proto.Plan.parseFrom(planProtobuf); + // convert this to the Substrait Plan POJO + ProtoPlanConverter converter = new io.substrait.plan.ProtoPlanConverter(); + io.substrait.plan.Plan pojoPlan = converter.from(protoPlan); + + System.out.println("POJO Substrait Plan::\n" + pojoPlan); + + // To convert from Substrait first need to convert to Calcite, and then Calcite + // to SQL + SimpleExtension.ExtensionCollection extensions = SimpleExtension.loadDefaults(); + SubstraitToCalcite substrait2Calcite = + new SubstraitToCalcite( + extensions, new JavaTypeFactoryImpl(SubstraitTypeSystem.TYPE_SYSTEM)); + + List planRoots = pojoPlan.getRoots(); + assertEquals(planRoots.size(), 1); + + var calciteRel = substrait2Calcite.convert(planRoots.get(0)).project(); + String sql = SubstraitToSql.toSql(calciteRel).replace("\n", " "); + + assertEquals( + sql, + Resources.toString( + Resources.getResource("substrait_sql_003.sql"), Charset.defaultCharset())); + System.out.println(sql); + } + + /** SQL to Substrait using a set of `CREATE TABLE...` statements to define the schema */ + @Test + public void sqlToSubstraitCreateTables() throws SqlParseException { + + String sqlQuery = + "SELECT * FROM vehicles INNER JOIN tests ON vehicles.vehicle_id=tests.vehicle_id WHERE tests.test_result = 'F' and tests.test_mileage < 70000"; + + String createTests = + "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)) "; + + String createVehicles = + "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))"; + + SqlToSubstrait sqlToSubstrait = new SqlToSubstrait(); + io.substrait.proto.Plan protoPlan = + sqlToSubstrait.execute(sqlQuery, List.of(createTests, createVehicles)); + + ProtoPlanConverter converter = new io.substrait.plan.ProtoPlanConverter(); + io.substrait.plan.Plan pojoPlan = converter.from(protoPlan); + + System.out.println("POJO Substrait Plan::\n" + pojoPlan); + } + + @Test + public void sqltoSubstraitCalciteSchema() throws SqlParseException { + + String sqlQuery = + "SELECT * FROM vehicles INNER JOIN tests ON vehicles.vehicle_id=tests.vehicle_id WHERE tests.test_result = 'F' and tests.test_mileage < 70000"; + + SqlToSubstrait sqlToSubstrait = new SqlToSubstrait(); + Schema schema = new ReflectiveSchema(new ExampleCalciteReflectiveSchema()); + + io.substrait.proto.Plan protoPlan = sqlToSubstrait.execute(sqlQuery, "ExampleSchema", schema); + + ProtoPlanConverter converter = new io.substrait.plan.ProtoPlanConverter(); + io.substrait.plan.Plan pojoPlan = converter.from(protoPlan); + + System.out.println("POJO Substrait Plan::\n" + pojoPlan); + } +} diff --git a/isthmus/src/test/resources/substrait_sql_003.plan b/isthmus/src/test/resources/substrait_sql_003.plan new file mode 100644 index 000000000..dd9c7ab01 Binary files /dev/null and b/isthmus/src/test/resources/substrait_sql_003.plan differ diff --git a/isthmus/src/test/resources/substrait_sql_003.sql b/isthmus/src/test/resources/substrait_sql_003.sql new file mode 100644 index 000000000..505c08658 --- /dev/null +++ b/isthmus/src/test/resources/substrait_sql_003.sql @@ -0,0 +1 @@ +SELECT vehicles.vehicle_id AS vehicle_id, vehicles.make AS make, vehicles.model AS model, vehicles.colour AS colour, vehicles.fuel_type AS fuel_type, vehicles.cylinder_capacity AS cylinder_capacity, vehicles.first_use_date AS first_use_date, tests.test_id AS test_id, tests.vehicle_id AS vehicle_id0, tests.test_date AS test_date, tests.test_type AS test_type, tests.test_class AS test_class, tests.test_result AS test_result, tests.test_mileage AS test_mileage, tests.postcode_area AS postcode_area FROM vehicles INNER JOIN tests ON vehicles.vehicle_id = tests.vehicle_id WHERE tests.test_result = 'F' AND tests.test_mileage < 70000 \ No newline at end of file