This is an automated email from the ASF dual-hosted git repository. jhyde pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/calcite.git
commit 9b678f1d7ffe030711d86f475c3a9e9e95bacb8f Author: Julian Hyde <[email protected]> AuthorDate: Sun Jul 19 17:23:08 2020 -0700 [CALCITE-4128] Remove dependency of File adapter on Example CSV adapter Move some classes from 'example/csv' to 'file', and make CSV adapter depend on File adapter. Copy some other classes (so that we can keep the 'example/csv' versions simple, and add functionality to the 'file' adapter). Rename SqlTest to FileAdapterTest, factor utility methods into FileAdapterTests, and copy in a bunch of tests from example/csv/.../CsvTest. --- example/csv/build.gradle.kts | 1 + .../calcite/adapter/csv/CsvFilterableTable.java | 7 +- .../calcite/adapter/csv/CsvScannableTable.java | 7 +- .../org/apache/calcite/adapter/csv/CsvSchema.java | 1 + .../calcite/adapter/csv/CsvSchemaFactory.java | 4 - .../adapter/csv/CsvStreamScannableTable.java | 7 +- .../org/apache/calcite/adapter/csv/CsvTable.java | 2 + .../apache/calcite/adapter/csv/CsvTableScan.java | 3 +- .../calcite/adapter/csv/CsvTranslatableTable.java | 9 +- file/build.gradle.kts | 4 +- .../calcite/adapter/file}/CsvEnumerator.java | 85 +- .../apache/calcite/adapter/file}/CsvFieldType.java | 4 +- .../adapter/file/CsvProjectTableScanRule.java | 83 ++ .../calcite/adapter/file}/CsvStreamReader.java | 2 +- .../org/apache/calcite/adapter/file}/CsvTable.java | 5 +- ...FileSchemaFactory.java => CsvTableFactory.java} | 41 +- .../apache/calcite/adapter/file}/CsvTableScan.java | 8 +- .../adapter/file}/CsvTranslatableTable.java | 18 +- .../apache/calcite/adapter/file/FileRules.java} | 17 +- .../apache/calcite/adapter/file/FileSchema.java | 55 +- .../calcite/adapter/file/FileSchemaFactory.java | 20 +- .../calcite/adapter/file}/JsonEnumerator.java | 2 +- .../calcite/adapter/file}/JsonScannableTable.java | 2 +- .../apache/calcite/adapter/file}/JsonTable.java | 4 +- .../calcite/adapter/file/FileAdapterTest.java | 890 +++++++++++++++++++++ .../calcite/adapter/file/FileAdapterTests.java | 222 +++++ .../org/apache/calcite/adapter/file/SqlTest.java | 419 ---------- .../test/resources/bug.json} | 24 +- file/src/test/resources/bug.yaml | 24 + .../test/resources/bug/ARCHERS.json} | 37 +- file/src/test/resources/bug/DATE.csv | 9 + file/src/test/resources/bug/LONG_EMPS.csv | 6 + file/src/test/resources/bug/WACKY_COLUMN_NAMES.csv | 6 + .../test/resources/filterable-model.json} | 29 +- .../test/resources/model-with-custom-table.json} | 30 +- .../test/resources/model.json} | 26 +- file/src/test/resources/sales/DEPTS.csv | 4 + file/src/test/resources/sales/EMPS.csv.gz | Bin 0 -> 262 bytes .../test/resources/smart.json} | 34 +- 39 files changed, 1523 insertions(+), 628 deletions(-) diff --git a/example/csv/build.gradle.kts b/example/csv/build.gradle.kts index c2ac70f..ca5ea27 100644 --- a/example/csv/build.gradle.kts +++ b/example/csv/build.gradle.kts @@ -21,6 +21,7 @@ val sqllineClasspath by configurations.creating { dependencies { api(project(":core")) + api(project(":file")) api(project(":linq4j")) implementation("com.fasterxml.jackson.core:jackson-core") diff --git a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvFilterableTable.java b/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvFilterableTable.java index f45e4c4..c9cae65 100644 --- a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvFilterableTable.java +++ b/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvFilterableTable.java @@ -17,6 +17,8 @@ package org.apache.calcite.adapter.csv; import org.apache.calcite.DataContext; +import org.apache.calcite.adapter.file.CsvEnumerator; +import org.apache.calcite.adapter.file.CsvFieldType; import org.apache.calcite.linq4j.AbstractEnumerable; import org.apache.calcite.linq4j.Enumerable; import org.apache.calcite.linq4j.Enumerator; @@ -27,6 +29,7 @@ import org.apache.calcite.rex.RexLiteral; import org.apache.calcite.rex.RexNode; import org.apache.calcite.schema.FilterableTable; import org.apache.calcite.sql.SqlKind; +import org.apache.calcite.util.ImmutableIntList; import org.apache.calcite.util.Source; import java.util.List; @@ -53,12 +56,12 @@ public class CsvFilterableTable extends CsvTable final List<CsvFieldType> fieldTypes = getFieldTypes(root.getTypeFactory()); final String[] filterValues = new String[fieldTypes.size()]; filters.removeIf(filter -> addFilter(filter, filterValues)); - final int[] fields = CsvEnumerator.identityList(fieldTypes.size()); + final List<Integer> fields = ImmutableIntList.identity(fieldTypes.size()); final AtomicBoolean cancelFlag = DataContext.Variable.CANCEL_FLAG.get(root); return new AbstractEnumerable<Object[]>() { public Enumerator<Object[]> enumerator() { return new CsvEnumerator<>(source, cancelFlag, false, filterValues, - new CsvEnumerator.ArrayRowConverter(fieldTypes, fields)); + CsvEnumerator.arrayConverter(fieldTypes, fields, false)); } }; } diff --git a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvScannableTable.java b/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvScannableTable.java index 237373d..bdc2af0 100644 --- a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvScannableTable.java +++ b/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvScannableTable.java @@ -17,11 +17,14 @@ package org.apache.calcite.adapter.csv; import org.apache.calcite.DataContext; +import org.apache.calcite.adapter.file.CsvEnumerator; +import org.apache.calcite.adapter.file.CsvFieldType; import org.apache.calcite.linq4j.AbstractEnumerable; import org.apache.calcite.linq4j.Enumerable; import org.apache.calcite.linq4j.Enumerator; import org.apache.calcite.rel.type.RelProtoDataType; import org.apache.calcite.schema.ScannableTable; +import org.apache.calcite.util.ImmutableIntList; import org.apache.calcite.util.Source; import java.util.List; @@ -46,12 +49,12 @@ public class CsvScannableTable extends CsvTable public Enumerable<Object[]> scan(DataContext root) { final List<CsvFieldType> fieldTypes = getFieldTypes(root.getTypeFactory()); - final int[] fields = CsvEnumerator.identityList(fieldTypes.size()); + final List<Integer> fields = ImmutableIntList.identity(fieldTypes.size()); final AtomicBoolean cancelFlag = DataContext.Variable.CANCEL_FLAG.get(root); return new AbstractEnumerable<Object[]>() { public Enumerator<Object[]> enumerator() { return new CsvEnumerator<>(source, cancelFlag, false, null, - new CsvEnumerator.ArrayRowConverter(fieldTypes, fields)); + CsvEnumerator.arrayConverter(fieldTypes, fields, false)); } }; } diff --git a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvSchema.java b/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvSchema.java index c5780a1..addefd3 100644 --- a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvSchema.java +++ b/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvSchema.java @@ -16,6 +16,7 @@ */ package org.apache.calcite.adapter.csv; +import org.apache.calcite.adapter.file.JsonScannableTable; import org.apache.calcite.schema.Table; import org.apache.calcite.schema.impl.AbstractSchema; import org.apache.calcite.util.Source; diff --git a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvSchemaFactory.java b/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvSchemaFactory.java index 624c6e8..cadeb59 100644 --- a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvSchemaFactory.java +++ b/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvSchemaFactory.java @@ -33,10 +33,6 @@ import java.util.Map; */ @SuppressWarnings("UnusedDeclaration") public class CsvSchemaFactory implements SchemaFactory { - /** Name of the column that is implicitly created in a CSV stream table - * to hold the data arrival time. */ - static final String ROWTIME_COLUMN_NAME = "ROWTIME"; - /** Public singleton, per factory contract. */ public static final CsvSchemaFactory INSTANCE = new CsvSchemaFactory(); diff --git a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvStreamScannableTable.java b/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvStreamScannableTable.java index c5b9497..8285473 100644 --- a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvStreamScannableTable.java +++ b/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvStreamScannableTable.java @@ -17,6 +17,8 @@ package org.apache.calcite.adapter.csv; import org.apache.calcite.DataContext; +import org.apache.calcite.adapter.file.CsvEnumerator; +import org.apache.calcite.adapter.file.CsvFieldType; import org.apache.calcite.linq4j.AbstractEnumerable; import org.apache.calcite.linq4j.Enumerable; import org.apache.calcite.linq4j.Enumerator; @@ -24,6 +26,7 @@ import org.apache.calcite.rel.type.RelProtoDataType; import org.apache.calcite.schema.ScannableTable; import org.apache.calcite.schema.StreamableTable; import org.apache.calcite.schema.Table; +import org.apache.calcite.util.ImmutableIntList; import org.apache.calcite.util.Source; import java.util.List; @@ -52,12 +55,12 @@ public class CsvStreamScannableTable extends CsvScannableTable public Enumerable<Object[]> scan(DataContext root) { final List<CsvFieldType> fieldTypes = getFieldTypes(root.getTypeFactory()); - final int[] fields = CsvEnumerator.identityList(fieldTypes.size()); + final List<Integer> fields = ImmutableIntList.identity(fieldTypes.size()); final AtomicBoolean cancelFlag = DataContext.Variable.CANCEL_FLAG.get(root); return new AbstractEnumerable<Object[]>() { public Enumerator<Object[]> enumerator() { return new CsvEnumerator<>(source, cancelFlag, true, null, - new CsvEnumerator.ArrayRowConverter(fieldTypes, fields, true)); + CsvEnumerator.arrayConverter(fieldTypes, fields, true)); } }; } diff --git a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTable.java b/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTable.java index d0f6396..8f7cc78 100644 --- a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTable.java +++ b/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTable.java @@ -16,6 +16,8 @@ */ package org.apache.calcite.adapter.csv; +import org.apache.calcite.adapter.file.CsvEnumerator; +import org.apache.calcite.adapter.file.CsvFieldType; import org.apache.calcite.adapter.java.JavaTypeFactory; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.rel.type.RelDataTypeFactory; diff --git a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTableScan.java b/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTableScan.java index ed0a885..f591e29 100644 --- a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTableScan.java +++ b/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTableScan.java @@ -21,6 +21,7 @@ import org.apache.calcite.adapter.enumerable.EnumerableRel; import org.apache.calcite.adapter.enumerable.EnumerableRelImplementor; import org.apache.calcite.adapter.enumerable.PhysType; import org.apache.calcite.adapter.enumerable.PhysTypeImpl; +import org.apache.calcite.adapter.file.JsonTable; import org.apache.calcite.linq4j.tree.Blocks; import org.apache.calcite.linq4j.tree.Expressions; import org.apache.calcite.linq4j.tree.Primitive; @@ -44,7 +45,7 @@ import java.util.List; /** * Relational expression representing a scan of a CSV file. * - * <p>Like any table scan, it serves as a leaf node of a query tree.</p> + * <p>Like any table scan, it serves as a leaf node of a query tree. */ public class CsvTableScan extends TableScan implements EnumerableRel { final CsvTranslatableTable csvTable; diff --git a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTranslatableTable.java b/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTranslatableTable.java index 7a4e13c..e183200 100644 --- a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTranslatableTable.java +++ b/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTranslatableTable.java @@ -17,6 +17,7 @@ package org.apache.calcite.adapter.csv; import org.apache.calcite.DataContext; +import org.apache.calcite.adapter.file.CsvEnumerator; import org.apache.calcite.linq4j.AbstractEnumerable; import org.apache.calcite.linq4j.Enumerable; import org.apache.calcite.linq4j.Enumerator; @@ -30,6 +31,7 @@ import org.apache.calcite.schema.QueryableTable; import org.apache.calcite.schema.SchemaPlus; import org.apache.calcite.schema.Schemas; import org.apache.calcite.schema.TranslatableTable; +import org.apache.calcite.util.ImmutableIntList; import org.apache.calcite.util.Source; import java.lang.reflect.Type; @@ -49,9 +51,8 @@ public class CsvTranslatableTable extends CsvTable return "CsvTranslatableTable"; } - /** Returns an enumerable over a given projection of the fields. - * - * <p>Called from generated code. */ + /** Returns an enumerable over a given projection of the fields. */ + @SuppressWarnings("unused") // called from generated code public Enumerable<Object> project(final DataContext root, final int[] fields) { final AtomicBoolean cancelFlag = DataContext.Variable.CANCEL_FLAG.get(root); @@ -61,7 +62,7 @@ public class CsvTranslatableTable extends CsvTable source, cancelFlag, getFieldTypes(root.getTypeFactory()), - fields); + ImmutableIntList.of(fields)); } }; } diff --git a/file/build.gradle.kts b/file/build.gradle.kts index 1610a03..53555a8 100644 --- a/file/build.gradle.kts +++ b/file/build.gradle.kts @@ -16,12 +16,14 @@ */ dependencies { api(project(":core")) - api(project(":example:csv")) api(project(":linq4j")) implementation("com.google.guava:guava") implementation("com.joestelmach:natty") + implementation("net.sf.opencsv:opencsv") implementation("org.apache.calcite.avatica:avatica-core") + implementation("commons-io:commons-io") + implementation("org.apache.commons:commons-lang3") implementation("org.jsoup:jsoup") testImplementation(project(":core", "testClasses")) diff --git a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvEnumerator.java b/file/src/main/java/org/apache/calcite/adapter/file/CsvEnumerator.java similarity index 81% rename from example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvEnumerator.java rename to file/src/main/java/org/apache/calcite/adapter/file/CsvEnumerator.java index cdcc967..091a4a1 100644 --- a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvEnumerator.java +++ b/file/src/main/java/org/apache/calcite/adapter/file/CsvEnumerator.java @@ -14,13 +14,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.calcite.adapter.csv; +package org.apache.calcite.adapter.file; import org.apache.calcite.adapter.java.JavaTypeFactory; import org.apache.calcite.avatica.util.DateTimeUtils; import org.apache.calcite.linq4j.Enumerator; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.sql.type.SqlTypeName; +import org.apache.calcite.util.ImmutableIntList; +import org.apache.calcite.util.ImmutableNullableList; import org.apache.calcite.util.Pair; import org.apache.calcite.util.Source; @@ -41,9 +43,9 @@ import java.util.concurrent.atomic.AtomicBoolean; * * @param <E> Row type */ -class CsvEnumerator<E> implements Enumerator<E> { +public class CsvEnumerator<E> implements Enumerator<E> { private final CSVReader reader; - private final String[] filterValues; + private final List<String> filterValues; private final AtomicBoolean cancelFlag; private final RowConverter<E> rowConverter; private E current; @@ -60,23 +62,19 @@ class CsvEnumerator<E> implements Enumerator<E> { FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss", gmt); } - CsvEnumerator(Source source, AtomicBoolean cancelFlag, - List<CsvFieldType> fieldTypes) { - this(source, cancelFlag, fieldTypes, identityList(fieldTypes.size())); - } - - CsvEnumerator(Source source, AtomicBoolean cancelFlag, - List<CsvFieldType> fieldTypes, int[] fields) { + public CsvEnumerator(Source source, AtomicBoolean cancelFlag, + List<CsvFieldType> fieldTypes, List<Integer> fields) { //noinspection unchecked this(source, cancelFlag, false, null, (RowConverter<E>) converter(fieldTypes, fields)); } - CsvEnumerator(Source source, AtomicBoolean cancelFlag, boolean stream, + public CsvEnumerator(Source source, AtomicBoolean cancelFlag, boolean stream, String[] filterValues, RowConverter<E> rowConverter) { this.cancelFlag = cancelFlag; this.rowConverter = rowConverter; - this.filterValues = filterValues; + this.filterValues = filterValues == null ? null + : ImmutableNullableList.copyOf(filterValues); try { if (stream) { this.reader = new CsvStreamReader(source); @@ -90,15 +88,20 @@ class CsvEnumerator<E> implements Enumerator<E> { } private static RowConverter<?> converter(List<CsvFieldType> fieldTypes, - int[] fields) { - if (fields.length == 1) { - final int field = fields[0]; + List<Integer> fields) { + if (fields.size() == 1) { + final int field = fields.get(0); return new SingleColumnRowConverter(fieldTypes.get(field), field); } else { - return new ArrayRowConverter(fieldTypes, fields); + return arrayConverter(fieldTypes, fields, false); } } + public static RowConverter<Object[]> arrayConverter( + List<CsvFieldType> fieldTypes, List<Integer> fields, boolean stream) { + return new ArrayRowConverter(fieldTypes, fields, stream); + } + /** Deduces the names and types of a table's columns by reading the first line * of a CSV file. */ static RelDataType deduceRowType(JavaTypeFactory typeFactory, Source source, @@ -108,12 +111,12 @@ class CsvEnumerator<E> implements Enumerator<E> { /** Deduces the names and types of a table's columns by reading the first line * of a CSV file. */ - static RelDataType deduceRowType(JavaTypeFactory typeFactory, Source source, - List<CsvFieldType> fieldTypes, Boolean stream) { + public static RelDataType deduceRowType(JavaTypeFactory typeFactory, + Source source, List<CsvFieldType> fieldTypes, Boolean stream) { final List<RelDataType> types = new ArrayList<>(); final List<String> names = new ArrayList<>(); if (stream) { - names.add(CsvSchemaFactory.ROWTIME_COLUMN_NAME); + names.add(FileSchemaFactory.ROWTIME_COLUMN_NAME); types.add(typeFactory.createSqlType(SqlTypeName.TIMESTAMP)); } try (CSVReader reader = openCsv(source)) { @@ -161,7 +164,7 @@ class CsvEnumerator<E> implements Enumerator<E> { return typeFactory.createStructType(Pair.zip(names, types)); } - public static CSVReader openCsv(Source source) throws IOException { + static CSVReader openCsv(Source source) throws IOException { Objects.requireNonNull(source, "source"); return new CSVReader(source.reader()); } @@ -193,7 +196,7 @@ class CsvEnumerator<E> implements Enumerator<E> { } if (filterValues != null) { for (int i = 0; i < strings.length; i++) { - String filterValue = filterValues[i]; + String filterValue = filterValues.get(i); if (filterValue != null) { if (!filterValue.equals(strings[i])) { continue outer; @@ -222,7 +225,7 @@ class CsvEnumerator<E> implements Enumerator<E> { } /** Returns an array of integers {0, ..., n - 1}. */ - static int[] identityList(int n) { + public static int[] identityList(int n) { int[] integers = new int[n]; for (int i = 0; i < n; i++) { integers[i] = i; @@ -315,20 +318,16 @@ class CsvEnumerator<E> implements Enumerator<E> { /** Array row converter. */ static class ArrayRowConverter extends RowConverter<Object[]> { - private final CsvFieldType[] fieldTypes; - private final int[] fields; - // whether the row to convert is from a stream + /** Field types. List must not be null, but any element may be null. */ + private final List<CsvFieldType> fieldTypes; + private final ImmutableIntList fields; + /** Whether the row to convert is from a stream. */ private final boolean stream; - ArrayRowConverter(List<CsvFieldType> fieldTypes, int[] fields) { - this.fieldTypes = fieldTypes.toArray(new CsvFieldType[0]); - this.fields = fields; - this.stream = false; - } - - ArrayRowConverter(List<CsvFieldType> fieldTypes, int[] fields, boolean stream) { - this.fieldTypes = fieldTypes.toArray(new CsvFieldType[0]); - this.fields = fields; + ArrayRowConverter(List<CsvFieldType> fieldTypes, List<Integer> fields, + boolean stream) { + this.fieldTypes = ImmutableNullableList.copyOf(fieldTypes); + this.fields = ImmutableIntList.copyOf(fields); this.stream = stream; } @@ -341,27 +340,27 @@ class CsvEnumerator<E> implements Enumerator<E> { } public Object[] convertNormalRow(String[] strings) { - final Object[] objects = new Object[fields.length]; - for (int i = 0; i < fields.length; i++) { - int field = fields[i]; - objects[i] = convert(fieldTypes[field], strings[field]); + final Object[] objects = new Object[fields.size()]; + for (int i = 0; i < fields.size(); i++) { + int field = fields.get(i); + objects[i] = convert(fieldTypes.get(field), strings[field]); } return objects; } public Object[] convertStreamRow(String[] strings) { - final Object[] objects = new Object[fields.length + 1]; + final Object[] objects = new Object[fields.size() + 1]; objects[0] = System.currentTimeMillis(); - for (int i = 0; i < fields.length; i++) { - int field = fields[i]; - objects[i + 1] = convert(fieldTypes[field], strings[field]); + for (int i = 0; i < fields.size(); i++) { + int field = fields.get(i); + objects[i + 1] = convert(fieldTypes.get(field), strings[field]); } return objects; } } /** Single column row converter. */ - private static class SingleColumnRowConverter extends RowConverter { + private static class SingleColumnRowConverter extends RowConverter<Object> { private final CsvFieldType fieldType; private final int fieldIndex; diff --git a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvFieldType.java b/file/src/main/java/org/apache/calcite/adapter/file/CsvFieldType.java similarity index 97% rename from example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvFieldType.java rename to file/src/main/java/org/apache/calcite/adapter/file/CsvFieldType.java index 3e50e59..9f1d28b 100644 --- a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvFieldType.java +++ b/file/src/main/java/org/apache/calcite/adapter/file/CsvFieldType.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.calcite.adapter.csv; +package org.apache.calcite.adapter.file; import org.apache.calcite.adapter.java.JavaTypeFactory; import org.apache.calcite.linq4j.tree.Primitive; @@ -30,7 +30,7 @@ import java.util.Map; * of type {@link #STRING}. But specifying the field type in the header row * makes it easier to write SQL.</p> */ -enum CsvFieldType { +public enum CsvFieldType { STRING(String.class, "string"), BOOLEAN(Primitive.BOOLEAN), BYTE(Primitive.BYTE), diff --git a/file/src/main/java/org/apache/calcite/adapter/file/CsvProjectTableScanRule.java b/file/src/main/java/org/apache/calcite/adapter/file/CsvProjectTableScanRule.java new file mode 100644 index 0000000..4d5416c --- /dev/null +++ b/file/src/main/java/org/apache/calcite/adapter/file/CsvProjectTableScanRule.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.calcite.adapter.file; + +import org.apache.calcite.plan.RelOptRuleCall; +import org.apache.calcite.plan.RelRule; +import org.apache.calcite.rel.logical.LogicalProject; +import org.apache.calcite.rex.RexInputRef; +import org.apache.calcite.rex.RexNode; + +import java.util.List; + +/** + * Planner rule that projects from a {@link CsvTableScan} scan just the columns + * needed to satisfy a projection. If the projection's expressions are trivial, + * the projection is removed. + * + * @see FileRules#PROJECT_SCAN + */ +public class CsvProjectTableScanRule + extends RelRule<CsvProjectTableScanRule.Config> { + + /** Creates a CsvProjectTableScanRule. */ + protected CsvProjectTableScanRule(Config config) { + super(config); + } + + @Override public void onMatch(RelOptRuleCall call) { + final LogicalProject project = call.rel(0); + final CsvTableScan scan = call.rel(1); + int[] fields = getProjectFields(project.getProjects()); + if (fields == null) { + // Project contains expressions more complex than just field references. + return; + } + call.transformTo( + new CsvTableScan( + scan.getCluster(), + scan.getTable(), + scan.csvTable, + fields)); + } + + private int[] getProjectFields(List<RexNode> exps) { + final int[] fields = new int[exps.size()]; + for (int i = 0; i < exps.size(); i++) { + final RexNode exp = exps.get(i); + if (exp instanceof RexInputRef) { + fields[i] = ((RexInputRef) exp).getIndex(); + } else { + return null; // not a simple projection + } + } + return fields; + } + + /** Rule configuration. */ + public interface Config extends RelRule.Config { + Config DEFAULT = EMPTY + .withOperandSupplier(b0 -> + b0.operand(LogicalProject.class).oneInput(b1 -> + b1.operand(CsvTableScan.class).noInputs())) + .as(Config.class); + + @Override default CsvProjectTableScanRule toRule() { + return new CsvProjectTableScanRule(this); + } + } +} diff --git a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvStreamReader.java b/file/src/main/java/org/apache/calcite/adapter/file/CsvStreamReader.java similarity index 99% rename from example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvStreamReader.java rename to file/src/main/java/org/apache/calcite/adapter/file/CsvStreamReader.java index e47b3f4..633079a 100644 --- a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvStreamReader.java +++ b/file/src/main/java/org/apache/calcite/adapter/file/CsvStreamReader.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.calcite.adapter.csv; +package org.apache.calcite.adapter.file; import org.apache.calcite.util.Source; diff --git a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTable.java b/file/src/main/java/org/apache/calcite/adapter/file/CsvTable.java similarity index 94% copy from example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTable.java copy to file/src/main/java/org/apache/calcite/adapter/file/CsvTable.java index d0f6396..20f0023 100644 --- a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTable.java +++ b/file/src/main/java/org/apache/calcite/adapter/file/CsvTable.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.calcite.adapter.csv; +package org.apache.calcite.adapter.file; import org.apache.calcite.adapter.java.JavaTypeFactory; import org.apache.calcite.rel.type.RelDataType; @@ -28,6 +28,9 @@ import java.util.List; /** * Base class for table that reads CSV files. + * + * <p>Copied from {@code CsvFilterableTable} in demo CSV adapter, + * with more advanced features. */ public abstract class CsvTable extends AbstractTable { protected final Source source; diff --git a/file/src/main/java/org/apache/calcite/adapter/file/FileSchemaFactory.java b/file/src/main/java/org/apache/calcite/adapter/file/CsvTableFactory.java similarity index 53% copy from file/src/main/java/org/apache/calcite/adapter/file/FileSchemaFactory.java copy to file/src/main/java/org/apache/calcite/adapter/file/CsvTableFactory.java index a20d1e6..75e992b 100644 --- a/file/src/main/java/org/apache/calcite/adapter/file/FileSchemaFactory.java +++ b/file/src/main/java/org/apache/calcite/adapter/file/CsvTableFactory.java @@ -17,40 +17,37 @@ package org.apache.calcite.adapter.file; import org.apache.calcite.model.ModelHandler; -import org.apache.calcite.schema.Schema; -import org.apache.calcite.schema.SchemaFactory; +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rel.type.RelDataTypeImpl; +import org.apache.calcite.rel.type.RelProtoDataType; import org.apache.calcite.schema.SchemaPlus; +import org.apache.calcite.schema.TableFactory; +import org.apache.calcite.util.Source; +import org.apache.calcite.util.Sources; import java.io.File; -import java.util.List; import java.util.Map; /** - * Factory that creates a {@link FileSchema}. + * Factory that creates a {@link CsvTranslatableTable}. * - * <p>Allows a custom schema to be included in a model.json file. - * See <a href="http://calcite.apache.org/docs/file_adapter.html">File adapter</a>. + * <p>Allows a file-based table to be included in a model.json file, even in a + * schema that is not based upon {@link FileSchema}. */ @SuppressWarnings("UnusedDeclaration") -public class FileSchemaFactory implements SchemaFactory { +public class CsvTableFactory implements TableFactory<CsvTable> { // public constructor, per factory contract - public FileSchemaFactory() { + public CsvTableFactory() { } - public Schema create(SchemaPlus parentSchema, String name, - Map<String, Object> operand) { - @SuppressWarnings("unchecked") List<Map<String, Object>> tables = - (List) operand.get("tables"); - final File baseDirectory = + public CsvTable create(SchemaPlus schema, String name, + Map<String, Object> operand, RelDataType rowType) { + String fileName = (String) operand.get("file"); + final File base = (File) operand.get(ModelHandler.ExtraOperand.BASE_DIRECTORY.camelName); - File directoryFile = baseDirectory; - final String directory = (String) operand.get("directory"); - if (baseDirectory != null && directory != null) { - directoryFile = new File(directory); - if (!directoryFile.isAbsolute()) { - directoryFile = new File(baseDirectory, directory); - } - } - return new FileSchema(parentSchema, name, directoryFile, tables); + final Source source = Sources.file(base, fileName); + final RelProtoDataType protoRowType = + rowType != null ? RelDataTypeImpl.proto(rowType) : null; + return new CsvTranslatableTable(source, protoRowType); } } diff --git a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTableScan.java b/file/src/main/java/org/apache/calcite/adapter/file/CsvTableScan.java similarity index 97% copy from example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTableScan.java copy to file/src/main/java/org/apache/calcite/adapter/file/CsvTableScan.java index ed0a885..8d6eb87 100644 --- a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTableScan.java +++ b/file/src/main/java/org/apache/calcite/adapter/file/CsvTableScan.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.calcite.adapter.csv; +package org.apache.calcite.adapter.file; import org.apache.calcite.adapter.enumerable.EnumerableConvention; import org.apache.calcite.adapter.enumerable.EnumerableRel; @@ -44,11 +44,11 @@ import java.util.List; /** * Relational expression representing a scan of a CSV file. * - * <p>Like any table scan, it serves as a leaf node of a query tree.</p> + * <p>Like any table scan, it serves as a leaf node of a query tree. */ public class CsvTableScan extends TableScan implements EnumerableRel { final CsvTranslatableTable csvTable; - final int[] fields; + private final int[] fields; protected CsvTableScan(RelOptCluster cluster, RelOptTable table, CsvTranslatableTable csvTable, int[] fields) { @@ -80,7 +80,7 @@ public class CsvTableScan extends TableScan implements EnumerableRel { } @Override public void register(RelOptPlanner planner) { - planner.addRule(CsvRules.PROJECT_SCAN); + planner.addRule(FileRules.PROJECT_SCAN); } @Override public RelOptCost computeSelfCost(RelOptPlanner planner, diff --git a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTranslatableTable.java b/file/src/main/java/org/apache/calcite/adapter/file/CsvTranslatableTable.java similarity index 88% copy from example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTranslatableTable.java copy to file/src/main/java/org/apache/calcite/adapter/file/CsvTranslatableTable.java index 7a4e13c..9f86c53 100644 --- a/example/csv/src/main/java/org/apache/calcite/adapter/csv/CsvTranslatableTable.java +++ b/file/src/main/java/org/apache/calcite/adapter/file/CsvTranslatableTable.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.calcite.adapter.csv; +package org.apache.calcite.adapter.file; import org.apache.calcite.DataContext; import org.apache.calcite.linq4j.AbstractEnumerable; @@ -30,6 +30,7 @@ import org.apache.calcite.schema.QueryableTable; import org.apache.calcite.schema.SchemaPlus; import org.apache.calcite.schema.Schemas; import org.apache.calcite.schema.TranslatableTable; +import org.apache.calcite.util.ImmutableIntList; import org.apache.calcite.util.Source; import java.lang.reflect.Type; @@ -37,6 +38,9 @@ import java.util.concurrent.atomic.AtomicBoolean; /** * Table based on a CSV file. + * + * <p>Copied from {@code CsvTranslatableTable} in demo CSV adapter, + * with more advanced features. */ public class CsvTranslatableTable extends CsvTable implements QueryableTable, TranslatableTable { @@ -49,19 +53,15 @@ public class CsvTranslatableTable extends CsvTable return "CsvTranslatableTable"; } - /** Returns an enumerable over a given projection of the fields. - * - * <p>Called from generated code. */ + /** Returns an enumerable over a given projection of the fields. */ + @SuppressWarnings("unused") // called from generated code public Enumerable<Object> project(final DataContext root, final int[] fields) { final AtomicBoolean cancelFlag = DataContext.Variable.CANCEL_FLAG.get(root); return new AbstractEnumerable<Object>() { public Enumerator<Object> enumerator() { - return new CsvEnumerator<>( - source, - cancelFlag, - getFieldTypes(root.getTypeFactory()), - fields); + return new CsvEnumerator<>(source, cancelFlag, + getFieldTypes(root.getTypeFactory()), ImmutableIntList.of(fields)); } }; } diff --git a/file/build.gradle.kts b/file/src/main/java/org/apache/calcite/adapter/file/FileRules.java similarity index 66% copy from file/build.gradle.kts copy to file/src/main/java/org/apache/calcite/adapter/file/FileRules.java index 1610a03..9c7e228 100644 --- a/file/build.gradle.kts +++ b/file/src/main/java/org/apache/calcite/adapter/file/FileRules.java @@ -14,15 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -dependencies { - api(project(":core")) - api(project(":example:csv")) - api(project(":linq4j")) +package org.apache.calcite.adapter.file; - implementation("com.google.guava:guava") - implementation("com.joestelmach:natty") - implementation("org.apache.calcite.avatica:avatica-core") - implementation("org.jsoup:jsoup") +/** Planner rules relating to the File adapter. */ +public abstract class FileRules { + private FileRules() {} - testImplementation(project(":core", "testClasses")) + /** Rule that matches a {@link org.apache.calcite.rel.core.Project} on + * a {@link CsvTableScan} and pushes down projects if possible. */ + public static final CsvProjectTableScanRule PROJECT_SCAN = + CsvProjectTableScanRule.Config.DEFAULT.toRule(); } diff --git a/file/src/main/java/org/apache/calcite/adapter/file/FileSchema.java b/file/src/main/java/org/apache/calcite/adapter/file/FileSchema.java index 74a4003..4fb5622 100644 --- a/file/src/main/java/org/apache/calcite/adapter/file/FileSchema.java +++ b/file/src/main/java/org/apache/calcite/adapter/file/FileSchema.java @@ -16,8 +16,6 @@ */ package org.apache.calcite.adapter.file; -import org.apache.calcite.adapter.csv.CsvFilterableTable; -import org.apache.calcite.adapter.csv.JsonScannableTable; import org.apache.calcite.schema.SchemaPlus; import org.apache.calcite.schema.Table; import org.apache.calcite.schema.impl.AbstractSchema; @@ -47,11 +45,12 @@ class FileSchema extends AbstractSchema { * @param parentSchema Parent schema * @param name Schema name * @param baseDirectory Base directory to look for relative files, or null - * @param tables List containing HTML table identifiers + * @param tables List containing HTML table identifiers, or null */ FileSchema(SchemaPlus parentSchema, String name, File baseDirectory, List<Map<String, Object>> tables) { - this.tables = ImmutableList.copyOf(tables); + this.tables = tables == null ? ImmutableList.of() + : ImmutableList.copyOf(tables); this.baseDirectory = baseDirectory; } @@ -91,29 +90,31 @@ class FileSchema extends AbstractSchema { // Look for files in the directory ending in ".csv", ".csv.gz", ".json", // ".json.gz". - final Source baseSource = Sources.of(baseDirectory); - File[] files = baseDirectory.listFiles((dir, name) -> { - final String nameSansGz = trim(name, ".gz"); - return nameSansGz.endsWith(".csv") - || nameSansGz.endsWith(".json"); - }); - if (files == null) { - System.out.println("directory " + baseDirectory + " not found"); - files = new File[0]; - } - // Build a map from table name to table; each file becomes a table. - for (File file : files) { - Source source = Sources.of(file); - Source sourceSansGz = source.trim(".gz"); - final Source sourceSansJson = sourceSansGz.trimOrNull(".json"); - if (sourceSansJson != null) { - addTable(builder, source, sourceSansJson.relative(baseSource).path(), - null); + if (baseDirectory != null) { + final Source baseSource = Sources.of(baseDirectory); + File[] files = baseDirectory.listFiles((dir, name) -> { + final String nameSansGz = trim(name, ".gz"); + return nameSansGz.endsWith(".csv") + || nameSansGz.endsWith(".json"); + }); + if (files == null) { + System.out.println("directory " + baseDirectory + " not found"); + files = new File[0]; } - final Source sourceSansCsv = sourceSansGz.trimOrNull(".csv"); - if (sourceSansCsv != null) { - addTable(builder, source, sourceSansCsv.relative(baseSource).path(), - null); + // Build a map from table name to table; each file becomes a table. + for (File file : files) { + Source source = Sources.of(file); + Source sourceSansGz = source.trim(".gz"); + final Source sourceSansJson = sourceSansGz.trimOrNull(".json"); + if (sourceSansJson != null) { + addTable(builder, source, sourceSansJson.relative(baseSource).path(), + null); + } + final Source sourceSansCsv = sourceSansGz.trimOrNull(".csv"); + if (sourceSansCsv != null) { + addTable(builder, source, sourceSansCsv.relative(baseSource).path(), + null); + } } } @@ -145,7 +146,7 @@ class FileSchema extends AbstractSchema { } final Source sourceSansCsv = sourceSansGz.trimOrNull(".csv"); if (sourceSansCsv != null) { - final Table table = new CsvFilterableTable(source, null); + final Table table = new CsvTranslatableTable(source, null); builder.put(Util.first(tableName, sourceSansCsv.path()), table); return true; } diff --git a/file/src/main/java/org/apache/calcite/adapter/file/FileSchemaFactory.java b/file/src/main/java/org/apache/calcite/adapter/file/FileSchemaFactory.java index a20d1e6..f0cc654 100644 --- a/file/src/main/java/org/apache/calcite/adapter/file/FileSchemaFactory.java +++ b/file/src/main/java/org/apache/calcite/adapter/file/FileSchemaFactory.java @@ -33,8 +33,14 @@ import java.util.Map; */ @SuppressWarnings("UnusedDeclaration") public class FileSchemaFactory implements SchemaFactory { - // public constructor, per factory contract - public FileSchemaFactory() { + /** Public singleton, per factory contract. */ + public static final FileSchemaFactory INSTANCE = new FileSchemaFactory(); + + /** Name of the column that is implicitly created in a CSV stream table + * to hold the data arrival time. */ + static final String ROWTIME_COLUMN_NAME = "ROWTIME"; + + private FileSchemaFactory() { } public Schema create(SchemaPlus parentSchema, String name, @@ -43,11 +49,15 @@ public class FileSchemaFactory implements SchemaFactory { (List) operand.get("tables"); final File baseDirectory = (File) operand.get(ModelHandler.ExtraOperand.BASE_DIRECTORY.camelName); - File directoryFile = baseDirectory; final String directory = (String) operand.get("directory"); - if (baseDirectory != null && directory != null) { + File directoryFile = null; + if (directory != null) { directoryFile = new File(directory); - if (!directoryFile.isAbsolute()) { + } + if (baseDirectory != null) { + if (directoryFile == null) { + directoryFile = baseDirectory; + } else if (!directoryFile.isAbsolute()) { directoryFile = new File(baseDirectory, directory); } } diff --git a/example/csv/src/main/java/org/apache/calcite/adapter/csv/JsonEnumerator.java b/file/src/main/java/org/apache/calcite/adapter/file/JsonEnumerator.java similarity index 99% rename from example/csv/src/main/java/org/apache/calcite/adapter/csv/JsonEnumerator.java rename to file/src/main/java/org/apache/calcite/adapter/file/JsonEnumerator.java index 0e8f172..7fd0640 100644 --- a/example/csv/src/main/java/org/apache/calcite/adapter/csv/JsonEnumerator.java +++ b/file/src/main/java/org/apache/calcite/adapter/file/JsonEnumerator.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.calcite.adapter.csv; +package org.apache.calcite.adapter.file; import org.apache.calcite.linq4j.Enumerator; import org.apache.calcite.linq4j.Linq4j; diff --git a/example/csv/src/main/java/org/apache/calcite/adapter/csv/JsonScannableTable.java b/file/src/main/java/org/apache/calcite/adapter/file/JsonScannableTable.java similarity index 97% rename from example/csv/src/main/java/org/apache/calcite/adapter/csv/JsonScannableTable.java rename to file/src/main/java/org/apache/calcite/adapter/file/JsonScannableTable.java index 0632fdf..2210b7e 100644 --- a/example/csv/src/main/java/org/apache/calcite/adapter/csv/JsonScannableTable.java +++ b/file/src/main/java/org/apache/calcite/adapter/file/JsonScannableTable.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.calcite.adapter.csv; +package org.apache.calcite.adapter.file; import org.apache.calcite.DataContext; import org.apache.calcite.linq4j.AbstractEnumerable; diff --git a/example/csv/src/main/java/org/apache/calcite/adapter/csv/JsonTable.java b/file/src/main/java/org/apache/calcite/adapter/file/JsonTable.java similarity index 94% rename from example/csv/src/main/java/org/apache/calcite/adapter/csv/JsonTable.java rename to file/src/main/java/org/apache/calcite/adapter/file/JsonTable.java index 049881e..5fade31 100644 --- a/example/csv/src/main/java/org/apache/calcite/adapter/csv/JsonTable.java +++ b/file/src/main/java/org/apache/calcite/adapter/file/JsonTable.java @@ -14,9 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.calcite.adapter.csv; +package org.apache.calcite.adapter.file; -import org.apache.calcite.adapter.csv.JsonEnumerator.JsonDataConverter; +import org.apache.calcite.adapter.file.JsonEnumerator.JsonDataConverter; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.rel.type.RelDataTypeFactory; import org.apache.calcite.schema.Statistic; diff --git a/file/src/test/java/org/apache/calcite/adapter/file/FileAdapterTest.java b/file/src/test/java/org/apache/calcite/adapter/file/FileAdapterTest.java new file mode 100644 index 0000000..5682b77 --- /dev/null +++ b/file/src/test/java/org/apache/calcite/adapter/file/FileAdapterTest.java @@ -0,0 +1,890 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.calcite.adapter.file; + +import org.apache.calcite.jdbc.CalciteConnection; +import org.apache.calcite.schema.Schema; +import org.apache.calcite.sql2rel.SqlToRelConverter; +import org.apache.calcite.util.TestUtil; + +import com.google.common.collect.ImmutableMap; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.sql.Connection; +import java.sql.Date; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.Properties; +import java.util.function.Consumer; + +import static org.apache.calcite.adapter.file.FileAdapterTests.sql; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.isA; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * System test of the Calcite file adapter, which can read and parse + * HTML tables over HTTP, and also read CSV and JSON files from the filesystem. + */ +@ExtendWith(RequiresNetworkExtension.class) +class FileAdapterTest { + /** Reads from a local file and checks the result. */ + @Test void testFileSelect() { + final String sql = "select H1 from T1 where H0 = 'R1C0'"; + sql("testModel", sql).returns("H1=R1C1").ok(); + } + + /** Reads from a local file without table headers <TH> and checks the + * result. */ + @Test @RequiresNetwork void testNoThSelect() { + final String sql = "select \"col1\" from T1_NO_TH where \"col0\" like 'R0%'"; + sql("testModel", sql).returns("col1=R0C1").ok(); + } + + /** Reads from a local file - finds larger table even without <TH> + * elements. */ + @Test void testFindBiggerNoTh() { + final String sql = "select \"col4\" from TABLEX2 where \"col0\" like 'R1%'"; + sql("testModel", sql).returns("col4=R1C4").ok(); + } + + /** Reads from a URL and checks the result. */ + @Disabled("[CALCITE-1789] Wikipedia format change breaks file adapter test") + @Test @RequiresNetwork void testUrlSelect() { + final String sql = "select \"State\", \"Statehood\" from \"States_as_of\"\n" + + "where \"State\" = 'California'"; + sql("wiki", sql).returns("State=California; Statehood=1850-09-09").ok(); + } + + /** Reads the EMPS table. */ + @Test void testSalesEmps() { + final String sql = "select * from sales.emps"; + sql("sales", sql) + .returns("EMPNO=100; NAME=Fred; DEPTNO=30", + "EMPNO=110; NAME=Eric; DEPTNO=20", + "EMPNO=110; NAME=John; DEPTNO=40", + "EMPNO=120; NAME=Wilma; DEPTNO=20", + "EMPNO=130; NAME=Alice; DEPTNO=40") + .ok(); + } + + /** Reads the DEPTS table. */ + @Test void testSalesDepts() { + final String sql = "select * from sales.depts"; + sql("sales", sql) + .returns("DEPTNO=10; NAME=Sales", + "DEPTNO=20; NAME=Marketing", + "DEPTNO=30; NAME=Accounts") + .ok(); + } + + /** Reads the DEPTS table from the CSV schema. */ + @Test void testCsvSalesDepts() { + final String sql = "select * from sales.depts"; + sql("sales-csv", sql) + .returns("DEPTNO=10; NAME=Sales", + "DEPTNO=20; NAME=Marketing", + "DEPTNO=30; NAME=Accounts") + .ok(); + } + + /** Reads the EMPS table from the CSV schema. */ + @Test void testCsvSalesEmps() { + final String sql = "select * from sales.emps"; + final String[] lines = { + "EMPNO=100; NAME=Fred; DEPTNO=10; GENDER=; CITY=; EMPID=30; AGE=25; SLACKER=true; MANAGER=false; JOINEDAT=1996-08-03", + "EMPNO=110; NAME=Eric; DEPTNO=20; GENDER=M; CITY=San Francisco; EMPID=3; AGE=80; SLACKER=null; MANAGER=false; JOINEDAT=2001-01-01", + "EMPNO=110; NAME=John; DEPTNO=40; GENDER=M; CITY=Vancouver; EMPID=2; AGE=null; SLACKER=false; MANAGER=true; JOINEDAT=2002-05-03", + "EMPNO=120; NAME=Wilma; DEPTNO=20; GENDER=F; CITY=; EMPID=1; AGE=5; SLACKER=null; MANAGER=true; JOINEDAT=2005-09-07", + "EMPNO=130; NAME=Alice; DEPTNO=40; GENDER=F; CITY=Vancouver; EMPID=2; AGE=null; SLACKER=false; MANAGER=true; JOINEDAT=2007-01-01", + }; + sql("sales-csv", sql).returns(lines).ok(); + } + + /** Reads the HEADER_ONLY table from the CSV schema. The CSV file has one + * line - the column headers - but no rows of data. */ + @Test void testCsvSalesHeaderOnly() { + final String sql = "select * from sales.header_only"; + sql("sales-csv", sql).returns().ok(); + } + + /** Reads the EMPTY table from the CSV schema. The CSV file has no lines, + * therefore the table has a system-generated column called + * "EmptyFileHasNoColumns". */ + @Test void testCsvSalesEmpty() { + final String sql = "select * from sales.\"EMPTY\""; + sql("sales-csv", sql) + .checking(FileAdapterTest::checkEmpty) + .ok(); + } + + private static void checkEmpty(ResultSet resultSet) { + try { + final ResultSetMetaData metaData = resultSet.getMetaData(); + assertThat(metaData.getColumnCount(), is(1)); + assertThat(metaData.getColumnName(1), is("EmptyFileHasNoColumns")); + assertThat(metaData.getColumnType(1), is(Types.BOOLEAN)); + String actual = FileAdapterTests.toString(resultSet); + assertThat(actual, is("")); + } catch (SQLException e) { + throw TestUtil.rethrow(e); + } + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-1754">[CALCITE-1754] + * In Csv adapter, convert DATE and TIME values to int, and TIMESTAMP values + * to long</a>. */ + @Test void testCsvGroupByTimestampAdd() { + final String sql = "select count(*) as c,\n" + + " {fn timestampadd(SQL_TSI_DAY, 1, JOINEDAT) } as t\n" + + "from EMPS group by {fn timestampadd(SQL_TSI_DAY, 1, JOINEDAT ) } "; + sql("sales-csv", sql) + .returnsUnordered("C=1; T=1996-08-04", + "C=1; T=2002-05-04", + "C=1; T=2005-09-08", + "C=1; T=2007-01-02", + "C=1; T=2001-01-02") + .ok(); + final String sql2 = "select count(*) as c,\n" + + " {fn timestampadd(SQL_TSI_MONTH, 1, JOINEDAT) } as t\n" + + "from EMPS group by {fn timestampadd(SQL_TSI_MONTH, 1, JOINEDAT ) } "; + sql("sales-csv", sql2) + .returnsUnordered("C=1; T=2002-06-03", + "C=1; T=2005-10-07", + "C=1; T=2007-02-01", + "C=1; T=2001-02-01", + "C=1; T=1996-09-03").ok(); + final String sql3 = "select\n" + + " distinct {fn timestampadd(SQL_TSI_MONTH, 1, JOINEDAT) } as t\n" + + "from EMPS"; + sql("sales-csv", sql3) + .returnsUnordered("T=2002-06-03", + "T=2005-10-07", + "T=2007-02-01", + "T=2001-02-01", + "T=1996-09-03").ok(); + } + + /** Reads the DEPTS table from the JSON schema. */ + @Test void testJsonSalesDepts() { + final String sql = "select * from sales.depts"; + sql("sales-json", sql) + .returns("DEPTNO=10; NAME=Sales", + "DEPTNO=20; NAME=Marketing", + "DEPTNO=30; NAME=Accounts") + .ok(); + } + + /** Reads the EMPS table from the JSON schema. */ + @Test void testJsonSalesEmps() { + final String sql = "select * from sales.emps"; + final String[] lines = { + "EMPNO=100; NAME=Fred; DEPTNO=10; GENDER=; CITY=; EMPID=30; AGE=25; SLACKER=true; MANAGER=false; JOINEDAT=1996-08-03", + "EMPNO=110; NAME=Eric; DEPTNO=20; GENDER=M; CITY=San Francisco; EMPID=3; AGE=80; SLACKER=null; MANAGER=false; JOINEDAT=2001-01-01", + "EMPNO=110; NAME=John; DEPTNO=40; GENDER=M; CITY=Vancouver; EMPID=2; AGE=null; SLACKER=false; MANAGER=true; JOINEDAT=2002-05-03", + "EMPNO=120; NAME=Wilma; DEPTNO=20; GENDER=F; CITY=; EMPID=1; AGE=5; SLACKER=null; MANAGER=true; JOINEDAT=2005-09-07", + "EMPNO=130; NAME=Alice; DEPTNO=40; GENDER=F; CITY=Vancouver; EMPID=2; AGE=null; SLACKER=false; MANAGER=true; JOINEDAT=2007-01-01", + }; + sql("sales-json", sql).returns(lines).ok(); + } + + /** Reads the EMPTY table from the JSON schema. The JSON file has no lines, + * therefore the table has a system-generated column called + * "EmptyFileHasNoColumns". */ + @Test void testJsonSalesEmpty() { + final String sql = "select * from sales.\"EMPTY\""; + sql("sales-json", sql) + .checking(FileAdapterTest::checkEmpty) + .ok(); + } + + /** Test returns the result of two json file joins. */ + @Test void testJsonJoinOnString() { + final String sql = "select emps.EMPNO, emps.NAME, depts.deptno from emps\n" + + "join depts on emps.deptno = depts.deptno"; + final String[] lines = { + "EMPNO=100; NAME=Fred; DEPTNO=10", + "EMPNO=110; NAME=Eric; DEPTNO=20", + "EMPNO=120; NAME=Wilma; DEPTNO=20", + }; + sql("sales-json", sql).returns(lines).ok(); + } + + /** The folder contains both JSON files and CSV files joins. */ + @Test void testJsonWithCsvJoin() { + final String sql = "select emps.empno,\n" + + " NAME,\n" + + " \"DATE\".JOINEDAT\n" + + " from \"DATE\"\n" + + "join emps on emps.empno = \"DATE\".EMPNO\n" + + "order by empno, name, joinedat limit 3"; + final String[] lines = { + "EMPNO=100; NAME=Fred; JOINEDAT=1996-08-03", + "EMPNO=110; NAME=Eric; JOINEDAT=2001-01-01", + "EMPNO=110; NAME=Eric; JOINEDAT=2002-05-03", + }; + sql("sales-json", sql) + .returns(lines) + .ok(); + } + + /** Tests an inline schema with a non-existent directory. */ + @Test void testBadDirectory() throws SQLException { + Properties info = new Properties(); + info.put("model", + "inline:" + + "{\n" + + " version: '1.0',\n" + + " schemas: [\n" + + " {\n" + + " type: 'custom',\n" + + " name: 'bad',\n" + + " factory: 'org.apache.calcite.adapter.file.FileSchemaFactory',\n" + + " operand: {\n" + + " directory: '/does/not/exist'\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"); + + Connection connection = + DriverManager.getConnection("jdbc:calcite:", info); + // must print "directory ... not found" to stdout, but not fail + ResultSet tables = + connection.getMetaData().getTables(null, null, null, null); + tables.next(); + tables.close(); + connection.close(); + } + + /** + * Reads from a table. + */ + @Test void testSelect() { + sql("model", "select * from EMPS").ok(); + } + + @Test void testSelectSingleProjectGz() { + sql("smart", "select name from EMPS").ok(); + } + + @Test void testSelectSingleProject() { + sql("smart", "select name from DEPTS").ok(); + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-898">[CALCITE-898] + * Type inference multiplying Java long by SQL INTEGER</a>. */ + @Test void testSelectLongMultiplyInteger() { + final String sql = "select empno * 3 as e3\n" + + "from long_emps where empno = 100"; + + sql("bug", sql).checking(resultSet -> { + try { + assertThat(resultSet.next(), is(true)); + Long o = (Long) resultSet.getObject(1); + assertThat(o, is(300L)); + assertThat(resultSet.next(), is(false)); + } catch (SQLException e) { + throw TestUtil.rethrow(e); + } + }).ok(); + } + + @Test void testCustomTable() { + sql("model-with-custom-table", "select * from CUSTOM_TABLE.EMPS").ok(); + } + + @Test void testPushDownProject() { + final String sql = "explain plan for select * from EMPS"; + final String expected = "PLAN=CsvTableScan(table=[[SALES, EMPS]], " + + "fields=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])\n"; + sql("smart", sql).returns(expected).ok(); + } + + @Test void testPushDownProject2() { + sql("smart", "explain plan for select name, empno from EMPS") + .returns("PLAN=CsvTableScan(table=[[SALES, EMPS]], fields=[[1, 0]])\n") + .ok(); + // make sure that it works... + sql("smart", "select name, empno from EMPS") + .returns("NAME=Fred; EMPNO=100", + "NAME=Eric; EMPNO=110", + "NAME=John; EMPNO=110", + "NAME=Wilma; EMPNO=120", + "NAME=Alice; EMPNO=130") + .ok(); + } + + @Test void testPushDownProjectAggregate() { + final String sql = "explain plan for\n" + + "select gender, count(*) from EMPS group by gender"; + final String expected = "PLAN=" + + "EnumerableAggregate(group=[{0}], EXPR$1=[COUNT()])\n" + + " CsvTableScan(table=[[SALES, EMPS]], fields=[[3]])\n"; + sql("smart", sql).returns(expected).ok(); + } + + @Test void testPushDownProjectAggregateWithFilter() { + final String sql = "explain plan for\n" + + "select max(empno) from EMPS where gender='F'"; + final String expected = "PLAN=" + + "EnumerableAggregate(group=[{}], EXPR$0=[MAX($0)])\n" + + " EnumerableCalc(expr#0..1=[{inputs}], expr#2=['F':VARCHAR], " + + "expr#3=[=($t1, $t2)], proj#0..1=[{exprs}], $condition=[$t3])\n" + + " CsvTableScan(table=[[SALES, EMPS]], fields=[[0, 3]])\n"; + sql("smart", sql).returns(expected).ok(); + } + + @Test void testPushDownProjectAggregateNested() { + final String sql = "explain plan for\n" + + "select gender, max(qty)\n" + + "from (\n" + + " select name, gender, count(*) qty\n" + + " from EMPS\n" + + " group by name, gender) t\n" + + "group by gender"; + final String expected = "PLAN=" + + "EnumerableAggregate(group=[{1}], EXPR$1=[MAX($2)])\n" + + " EnumerableAggregate(group=[{0, 1}], QTY=[COUNT()])\n" + + " CsvTableScan(table=[[SALES, EMPS]], fields=[[1, 3]])\n"; + sql("smart", sql).returns(expected).ok(); + } + + @Test void testFilterableSelect() { + sql("filterable-model", "select name from EMPS").ok(); + } + + @Test void testFilterableSelectStar() { + sql("filterable-model", "select * from EMPS").ok(); + } + + /** Filter that can be fully handled by CsvFilterableTable. */ + @Test void testFilterableWhere() { + final String sql = + "select empno, gender, name from EMPS where name = 'John'"; + sql("filterable-model", sql) + .returns("EMPNO=110; GENDER=M; NAME=John").ok(); + } + + /** Filter that can be partly handled by CsvFilterableTable. */ + @Test void testFilterableWhere2() { + final String sql = "select empno, gender, name from EMPS\n" + + " where gender = 'F' and empno > 125"; + sql("filterable-model", sql) + .returns("EMPNO=130; GENDER=F; NAME=Alice").ok(); + } + + /** Filter that can be slightly handled by CsvFilterableTable. */ + @Test void testFilterableWhere3() { + final String sql = "select empno, gender, name from EMPS\n" + + " where gender <> 'M' and empno > 125"; + sql("filterable-model", sql) + .returns("EMPNO=130; GENDER=F; NAME=Alice") + .ok(); + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-2272">[CALCITE-2272] + * Incorrect result for {@code name like '%E%' and city not like '%W%'}</a>. + */ + @Test void testFilterableWhereWithNot1() { + sql("filterable-model", + "select name, empno from EMPS " + + "where name like '%E%' and city not like '%W%' ") + .returns("NAME=Eric; EMPNO=110") + .ok(); + } + + /** Similar to {@link #testFilterableWhereWithNot1()}; + * But use the same column. */ + @Test void testFilterableWhereWithNot2() { + sql("filterable-model", + "select name, empno from EMPS " + + "where name like '%i%' and name not like '%W%' ") + .returns("NAME=Eric; EMPNO=110", + "NAME=Alice; EMPNO=130") + .ok(); + } + + @Test void testJson() { + final String sql = "select * from archers\n"; + final String[] lines = { + "id=19990101; dow=Friday; longDate=New Years Day; title=Tractor trouble.; " + + "characters=[Alice, Bob, Xavier]; script=Julian Hyde; summary=; " + + "lines=[Bob's tractor got stuck in a field., " + + "Alice and Xavier hatch a plan to surprise Charlie.]", + "id=19990103; dow=Sunday; longDate=Sunday 3rd January; " + + "title=Charlie's surprise.; characters=[Alice, Zebedee, Charlie, Xavier]; " + + "script=William Shakespeare; summary=; " + + "lines=[Charlie is very surprised by Alice and Xavier's surprise plan.]", + }; + sql("bug", sql) + .returns(lines) + .ok(); + } + + @Test void testJoinOnString() { + final String sql = "select * from emps\n" + + "join depts on emps.name = depts.name"; + sql("smart", sql).ok(); + } + + @Test void testWackyColumns() { + final String sql = "select * from wacky_column_names where false"; + sql("bug", sql).returns().ok(); + + final String sql2 = "select \"joined at\", \"naME\"\n" + + "from wacky_column_names\n" + + "where \"2gender\" = 'F'"; + sql("bug", sql2) + .returns("joined at=2005-09-07; naME=Wilma", + "joined at=2007-01-01; naME=Alice") + .ok(); + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-1754">[CALCITE-1754] + * In Csv adapter, convert DATE and TIME values to int, and TIMESTAMP values + * to long</a>. */ + @Test void testGroupByTimestampAdd() { + final String sql = "select count(*) as c,\n" + + " {fn timestampadd(SQL_TSI_DAY, 1, JOINEDAT) } as t\n" + + "from EMPS group by {fn timestampadd(SQL_TSI_DAY, 1, JOINEDAT ) } "; + sql("model", sql) + .returnsUnordered("C=1; T=1996-08-04", + "C=1; T=2002-05-04", + "C=1; T=2005-09-08", + "C=1; T=2007-01-02", + "C=1; T=2001-01-02") + .ok(); + + final String sql2 = "select count(*) as c,\n" + + " {fn timestampadd(SQL_TSI_MONTH, 1, JOINEDAT) } as t\n" + + "from EMPS group by {fn timestampadd(SQL_TSI_MONTH, 1, JOINEDAT ) } "; + sql("model", sql2) + .returnsUnordered("C=1; T=2002-06-03", + "C=1; T=2005-10-07", + "C=1; T=2007-02-01", + "C=1; T=2001-02-01", + "C=1; T=1996-09-03") + .ok(); + } + + @Test void testUnionGroupByWithoutGroupKey() { + final String sql = "select count(*) as c1 from EMPS group by NAME\n" + + "union\n" + + "select count(*) as c1 from EMPS group by NAME"; + sql("model", sql).ok(); + } + + @Test void testBoolean() { + sql("smart", "select empno, slacker from emps where slacker") + .returns("EMPNO=100; SLACKER=true").ok(); + } + + @Test void testReadme() { + final String sql = "SELECT d.name, COUNT(*) cnt" + + " FROM emps AS e" + + " JOIN depts AS d ON e.deptno = d.deptno" + + " GROUP BY d.name"; + sql("smart", sql) + .returns("NAME=Sales; CNT=1", "NAME=Marketing; CNT=2").ok(); + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-824">[CALCITE-824] + * Type inference when converting IN clause to semijoin</a>. */ + @Test void testInToSemiJoinWithCast() { + // Note that the IN list needs at least 20 values to trigger the rewrite + // to a semijoin. Try it both ways. + final String sql = "SELECT e.name\n" + + "FROM emps AS e\n" + + "WHERE cast(e.empno as bigint) in "; + final int threshold = SqlToRelConverter.DEFAULT_IN_SUB_QUERY_THRESHOLD; + sql("smart", sql + range(130, threshold - 5)) + .returns("NAME=Alice").ok(); + sql("smart", sql + range(130, threshold)) + .returns("NAME=Alice").ok(); + sql("smart", sql + range(130, threshold + 1000)) + .returns("NAME=Alice").ok(); + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-1051">[CALCITE-1051] + * Underflow exception due to scaling IN clause literals</a>. */ + @Test void testInToSemiJoinWithoutCast() { + final String sql = "SELECT e.name\n" + + "FROM emps AS e\n" + + "WHERE e.empno in " + + range(130, SqlToRelConverter.DEFAULT_IN_SUB_QUERY_THRESHOLD); + sql("smart", sql).returns("NAME=Alice").ok(); + } + + private String range(int first, int count) { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + sb.append(i == 0 ? "(" : ", ").append(first + i); + } + return sb.append(')').toString(); + } + + @Test void testDateType() throws SQLException { + Properties info = new Properties(); + info.put("model", FileAdapterTests.jsonPath("bug")); + + try (Connection connection = + DriverManager.getConnection("jdbc:calcite:", info)) { + ResultSet res = connection.getMetaData().getColumns(null, null, + "DATE", "JOINEDAT"); + res.next(); + assertEquals(res.getInt("DATA_TYPE"), Types.DATE); + + res = connection.getMetaData().getColumns(null, null, + "DATE", "JOINTIME"); + res.next(); + assertEquals(res.getInt("DATA_TYPE"), Types.TIME); + + res = connection.getMetaData().getColumns(null, null, + "DATE", "JOINTIMES"); + res.next(); + assertEquals(res.getInt("DATA_TYPE"), Types.TIMESTAMP); + + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery( + "select \"JOINEDAT\", \"JOINTIME\", \"JOINTIMES\" from \"DATE\" where EMPNO = 100"); + resultSet.next(); + + // date + assertEquals(Date.class, resultSet.getDate(1).getClass()); + assertEquals(Date.valueOf("1996-08-03"), resultSet.getDate(1)); + + // time + assertEquals(Time.class, resultSet.getTime(2).getClass()); + assertEquals(Time.valueOf("00:01:02"), resultSet.getTime(2)); + + // timestamp + assertEquals(Timestamp.class, resultSet.getTimestamp(3).getClass()); + assertEquals(Timestamp.valueOf("1996-08-03 00:01:02"), + resultSet.getTimestamp(3)); + } + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-1072">[CALCITE-1072] + * CSV adapter incorrectly parses TIMESTAMP values after noon</a>. */ + @Test void testDateType2() throws SQLException { + Properties info = new Properties(); + info.put("model", FileAdapterTests.jsonPath("bug")); + + try (Connection connection = + DriverManager.getConnection("jdbc:calcite:", info)) { + Statement statement = connection.createStatement(); + final String sql = "select * from \"DATE\"\n" + + "where EMPNO >= 140 and EMPNO < 200"; + ResultSet resultSet = statement.executeQuery(sql); + int n = 0; + while (resultSet.next()) { + ++n; + final int empId = resultSet.getInt(1); + final String date = resultSet.getString(2); + final String time = resultSet.getString(3); + final String timestamp = resultSet.getString(4); + assertThat(date, is("2015-12-31")); + switch (empId) { + case 140: + assertThat(time, is("07:15:56")); + assertThat(timestamp, is("2015-12-31 07:15:56")); + break; + case 150: + assertThat(time, is("13:31:21")); + assertThat(timestamp, is("2015-12-31 13:31:21")); + break; + default: + throw new AssertionError(); + } + } + assertThat(n, is(2)); + resultSet.close(); + statement.close(); + } + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-1673">[CALCITE-1673] + * Query with ORDER BY or GROUP BY on TIMESTAMP column throws + * CompileException</a>. */ + @Test void testTimestampGroupBy() throws SQLException { + Properties info = new Properties(); + info.put("model", FileAdapterTests.jsonPath("bug")); + // Use LIMIT to ensure that results are deterministic without ORDER BY + final String sql = "select \"EMPNO\", \"JOINTIMES\"\n" + + "from (select * from \"DATE\" limit 1)\n" + + "group by \"EMPNO\",\"JOINTIMES\""; + try (Connection connection = + DriverManager.getConnection("jdbc:calcite:", info); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(sql)) { + assertThat(resultSet.next(), is(true)); + final Timestamp timestamp = resultSet.getTimestamp(2); + assertThat(timestamp, isA(Timestamp.class)); + // Note: This logic is time zone specific, but the same time zone is + // used in the CSV adapter and this test, so they should cancel out. + assertThat(timestamp, is(Timestamp.valueOf("1996-08-03 00:01:02.0"))); + } + } + + /** As {@link #testTimestampGroupBy()} but with ORDER BY. */ + @Test void testTimestampOrderBy() throws SQLException { + Properties info = new Properties(); + info.put("model", FileAdapterTests.jsonPath("bug")); + final String sql = "select \"EMPNO\",\"JOINTIMES\" from \"DATE\"\n" + + "order by \"JOINTIMES\""; + try (Connection connection = + DriverManager.getConnection("jdbc:calcite:", info); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(sql)) { + assertThat(resultSet.next(), is(true)); + final Timestamp timestamp = resultSet.getTimestamp(2); + assertThat(timestamp, is(Timestamp.valueOf("1996-08-03 00:01:02"))); + } + } + + /** As {@link #testTimestampGroupBy()} but with ORDER BY as well as GROUP + * BY. */ + @Test void testTimestampGroupByAndOrderBy() throws SQLException { + Properties info = new Properties(); + info.put("model", FileAdapterTests.jsonPath("bug")); + final String sql = "select \"EMPNO\", \"JOINTIMES\" from \"DATE\"\n" + + "group by \"EMPNO\",\"JOINTIMES\" order by \"JOINTIMES\""; + try (Connection connection = + DriverManager.getConnection("jdbc:calcite:", info); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(sql)) { + assertThat(resultSet.next(), is(true)); + final Timestamp timestamp = resultSet.getTimestamp(2); + assertThat(timestamp, is(Timestamp.valueOf("1996-08-03 00:01:02"))); + } + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-1031">[CALCITE-1031] + * In prepared statement, CsvScannableTable.scan is called twice</a>. To see + * the bug, place a breakpoint in CsvScannableTable.scan, and note that it is + * called twice. It should only be called once. */ + @Test void testPrepared() throws SQLException { + final Properties properties = new Properties(); + properties.setProperty("caseSensitive", "true"); + try (Connection connection = + DriverManager.getConnection("jdbc:calcite:", properties)) { + final CalciteConnection calciteConnection = connection.unwrap( + CalciteConnection.class); + + final Schema schema = + FileSchemaFactory.INSTANCE + .create(calciteConnection.getRootSchema(), null, + ImmutableMap.of("directory", + FileAdapterTests.resourcePath("sales-csv"), "flavor", "scannable")); + calciteConnection.getRootSchema().add("TEST", schema); + final String sql = "select * from \"TEST\".\"DEPTS\" where \"NAME\" = ?"; + final PreparedStatement statement2 = + calciteConnection.prepareStatement(sql); + + statement2.setString(1, "Sales"); + final ResultSet resultSet1 = statement2.executeQuery(); + Consumer<ResultSet> expect = FileAdapterTests.expect("DEPTNO=10; NAME=Sales"); + expect.accept(resultSet1); + } + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-1054">[CALCITE-1054] + * NPE caused by wrong code generation for Timestamp fields</a>. */ + @Test void testFilterOnNullableTimestamp() throws Exception { + Properties info = new Properties(); + info.put("model", FileAdapterTests.jsonPath("bug")); + + try (Connection connection = + DriverManager.getConnection("jdbc:calcite:", info)) { + final Statement statement = connection.createStatement(); + + // date + final String sql1 = "select JOINEDAT from \"DATE\"\n" + + "where JOINEDAT < {d '2000-01-01'}\n" + + "or JOINEDAT >= {d '2017-01-01'}"; + final ResultSet joinedAt = statement.executeQuery(sql1); + assertThat(joinedAt.next(), is(true)); + assertThat(joinedAt.getDate(1), is(Date.valueOf("1996-08-03"))); + + // time + final String sql2 = "select JOINTIME from \"DATE\"\n" + + "where JOINTIME >= {t '07:00:00'}\n" + + "and JOINTIME < {t '08:00:00'}"; + final ResultSet joinTime = statement.executeQuery(sql2); + assertThat(joinTime.next(), is(true)); + assertThat(joinTime.getTime(1), is(Time.valueOf("07:15:56"))); + + // timestamp + final String sql3 = "select JOINTIMES,\n" + + " {fn timestampadd(SQL_TSI_DAY, 1, JOINTIMES)}\n" + + "from \"DATE\"\n" + + "where (JOINTIMES >= {ts '2003-01-01 00:00:00'}\n" + + "and JOINTIMES < {ts '2006-01-01 00:00:00'})\n" + + "or (JOINTIMES >= {ts '2003-01-01 00:00:00'}\n" + + "and JOINTIMES < {ts '2007-01-01 00:00:00'})"; + final ResultSet joinTimes = statement.executeQuery(sql3); + assertThat(joinTimes.next(), is(true)); + assertThat(joinTimes.getTimestamp(1), + is(Timestamp.valueOf("2005-09-07 00:00:00"))); + assertThat(joinTimes.getTimestamp(2), + is(Timestamp.valueOf("2005-09-08 00:00:00"))); + + final String sql4 = "select JOINTIMES, extract(year from JOINTIMES)\n" + + "from \"DATE\""; + final ResultSet joinTimes2 = statement.executeQuery(sql4); + assertThat(joinTimes2.next(), is(true)); + assertThat(joinTimes2.getTimestamp(1), + is(Timestamp.valueOf("1996-08-03 00:01:02"))); + } + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-1118">[CALCITE-1118] + * NullPointerException in EXTRACT with WHERE ... IN clause if field has null + * value</a>. */ + @Test void testFilterOnNullableTimestamp2() throws Exception { + Properties info = new Properties(); + info.put("model", FileAdapterTests.jsonPath("bug")); + + try (Connection connection = + DriverManager.getConnection("jdbc:calcite:", info)) { + final Statement statement = connection.createStatement(); + final String sql1 = "select extract(year from JOINTIMES)\n" + + "from \"DATE\"\n" + + "where extract(year from JOINTIMES) in (2006, 2007)"; + final ResultSet joinTimes = statement.executeQuery(sql1); + assertThat(joinTimes.next(), is(true)); + assertThat(joinTimes.getInt(1), is(2007)); + + final String sql2 = "select extract(year from JOINTIMES),\n" + + " count(0) from \"DATE\"\n" + + "where extract(year from JOINTIMES) between 2007 and 2016\n" + + "group by extract(year from JOINTIMES)"; + final ResultSet joinTimes2 = statement.executeQuery(sql2); + assertThat(joinTimes2.next(), is(true)); + assertThat(joinTimes2.getInt(1), is(2007)); + assertThat(joinTimes2.getLong(2), is(1L)); + assertThat(joinTimes2.next(), is(true)); + assertThat(joinTimes2.getInt(1), is(2015)); + assertThat(joinTimes2.getLong(2), is(2L)); + } + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-1427">[CALCITE-1427] + * Code generation incorrect (does not compile) for DATE, TIME and TIMESTAMP + * fields</a>. */ + @Test void testNonNullFilterOnDateType() throws SQLException { + Properties info = new Properties(); + info.put("model", FileAdapterTests.jsonPath("bug")); + + try (Connection connection = + DriverManager.getConnection("jdbc:calcite:", info)) { + final Statement statement = connection.createStatement(); + + // date + final String sql1 = "select JOINEDAT from \"DATE\"\n" + + "where JOINEDAT is not null"; + final ResultSet joinedAt = statement.executeQuery(sql1); + assertThat(joinedAt.next(), is(true)); + assertThat(joinedAt.getDate(1).getClass(), equalTo(Date.class)); + assertThat(joinedAt.getDate(1), is(Date.valueOf("1996-08-03"))); + + // time + final String sql2 = "select JOINTIME from \"DATE\"\n" + + "where JOINTIME is not null"; + final ResultSet joinTime = statement.executeQuery(sql2); + assertThat(joinTime.next(), is(true)); + assertThat(joinTime.getTime(1).getClass(), equalTo(Time.class)); + assertThat(joinTime.getTime(1), is(Time.valueOf("00:01:02"))); + + // timestamp + final String sql3 = "select JOINTIMES from \"DATE\"\n" + + "where JOINTIMES is not null"; + final ResultSet joinTimes = statement.executeQuery(sql3); + assertThat(joinTimes.next(), is(true)); + assertThat(joinTimes.getTimestamp(1).getClass(), + equalTo(Timestamp.class)); + assertThat(joinTimes.getTimestamp(1), + is(Timestamp.valueOf("1996-08-03 00:01:02"))); + } + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-1427">[CALCITE-1427] + * Code generation incorrect (does not compile) for DATE, TIME and TIMESTAMP + * fields</a>. */ + @Test void testGreaterThanFilterOnDateType() throws SQLException { + Properties info = new Properties(); + info.put("model", FileAdapterTests.jsonPath("bug")); + + try (Connection connection = + DriverManager.getConnection("jdbc:calcite:", info)) { + final Statement statement = connection.createStatement(); + + // date + final String sql1 = "select JOINEDAT from \"DATE\"\n" + + "where JOINEDAT > {d '1990-01-01'}"; + final ResultSet joinedAt = statement.executeQuery(sql1); + assertThat(joinedAt.next(), is(true)); + assertThat(joinedAt.getDate(1).getClass(), equalTo(Date.class)); + assertThat(joinedAt.getDate(1), is(Date.valueOf("1996-08-03"))); + + // time + final String sql2 = "select JOINTIME from \"DATE\"\n" + + "where JOINTIME > {t '00:00:00'}"; + final ResultSet joinTime = statement.executeQuery(sql2); + assertThat(joinTime.next(), is(true)); + assertThat(joinTime.getTime(1).getClass(), equalTo(Time.class)); + assertThat(joinTime.getTime(1), is(Time.valueOf("00:01:02"))); + + // timestamp + final String sql3 = "select JOINTIMES from \"DATE\"\n" + + "where JOINTIMES > {ts '1990-01-01 00:00:00'}"; + final ResultSet joinTimes = statement.executeQuery(sql3); + assertThat(joinTimes.next(), is(true)); + assertThat(joinTimes.getTimestamp(1).getClass(), + equalTo(Timestamp.class)); + assertThat(joinTimes.getTimestamp(1), + is(Timestamp.valueOf("1996-08-03 00:01:02"))); + } + } +} diff --git a/file/src/test/java/org/apache/calcite/adapter/file/FileAdapterTests.java b/file/src/test/java/org/apache/calcite/adapter/file/FileAdapterTests.java new file mode 100644 index 0000000..1162516 --- /dev/null +++ b/file/src/test/java/org/apache/calcite/adapter/file/FileAdapterTests.java @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.calcite.adapter.file; + +import org.apache.calcite.util.Sources; +import org.apache.calcite.util.TestUtil; +import org.apache.calcite.util.Util; + +import com.google.common.collect.Ordering; + +import java.io.PrintStream; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** Helpers for test suite of the File adapter. */ +abstract class FileAdapterTests { + private FileAdapterTests() { + } + + static Fluent sql(String model, String sql) { + return new Fluent(model, sql, FileAdapterTests::output); + } + + /** Returns a function that checks the contents of a result set against an + * expected string. */ + static Consumer<ResultSet> expect(final String... expected) { + return resultSet -> { + try { + final List<String> lines = new ArrayList<>(); + collect(lines, resultSet); + assertEquals(Arrays.asList(expected), lines); + } catch (SQLException e) { + throw TestUtil.rethrow(e); + } + }; + } + + /** Returns a function that checks the contents of a result set against an + * expected string. */ + private static Consumer<ResultSet> expectUnordered(String... expected) { + final List<String> expectedLines = + Ordering.natural().immutableSortedCopy(Arrays.asList(expected)); + return resultSet -> { + try { + final List<String> lines = new ArrayList<>(); + collect(lines, resultSet); + Collections.sort(lines); + assertEquals(expectedLines, lines); + } catch (SQLException e) { + throw TestUtil.rethrow(e); + } + }; + } + + private static void collect(List<String> result, ResultSet resultSet) + throws SQLException { + final StringBuilder buf = new StringBuilder(); + while (resultSet.next()) { + buf.setLength(0); + int n = resultSet.getMetaData().getColumnCount(); + String sep = ""; + for (int i = 1; i <= n; i++) { + buf.append(sep) + .append(resultSet.getMetaData().getColumnLabel(i)) + .append("=") + .append(resultSet.getString(i)); + sep = "; "; + } + result.add(Util.toLinux(buf.toString())); + } + } + + static String toString(ResultSet resultSet) throws SQLException { + StringBuilder buf = new StringBuilder(); + while (resultSet.next()) { + int n = resultSet.getMetaData().getColumnCount(); + String sep = ""; + for (int i = 1; i <= n; i++) { + buf.append(sep) + .append(resultSet.getMetaData().getColumnLabel(i)) + .append("=") + .append(resultSet.getObject(i)); + sep = "; "; + } + buf.append("\n"); + } + return buf.toString(); + } + + static void checkSql(String sql, String model, Consumer<ResultSet> fn) + throws SQLException { + Connection connection = null; + Statement statement = null; + try { + Properties info = new Properties(); + info.put("model", jsonPath(model)); + connection = DriverManager.getConnection("jdbc:calcite:", info); + statement = connection.createStatement(); + final ResultSet resultSet = + statement.executeQuery( + sql); + fn.accept(resultSet); + } finally { + close(connection, statement); + } + } + + static String jsonPath(String model) { + return resourcePath(model + ".json"); + } + + static String resourcePath(String path) { + return Sources.of(FileAdapterTest.class.getResource("/" + path)).file().getAbsolutePath(); + } + + private static void output(ResultSet resultSet, PrintStream out) + throws SQLException { + final ResultSetMetaData metaData = resultSet.getMetaData(); + final int columnCount = metaData.getColumnCount(); + while (resultSet.next()) { + for (int i = 1;; i++) { + out.print(resultSet.getString(i)); + if (i < columnCount) { + out.print(", "); + } else { + out.println(); + break; + } + } + } + } + + private static void output(ResultSet resultSet) { + try { + output(resultSet, System.out); + } catch (SQLException e) { + throw TestUtil.rethrow(e); + } + } + + static void close(Connection connection, Statement statement) { + if (statement != null) { + try { + statement.close(); + } catch (SQLException e) { + // ignore + } + } + if (connection != null) { + try { + connection.close(); + } catch (SQLException e) { + // ignore + } + } + } + + /** Fluent API to perform test actions. */ + static class Fluent { + private final String model; + private final String sql; + private final Consumer<ResultSet> expect; + + Fluent(String model, String sql, Consumer<ResultSet> expect) { + this.model = model; + this.sql = sql; + this.expect = expect; + } + + /** Runs the test. */ + Fluent ok() { + try { + checkSql(sql, model, expect); + return this; + } catch (SQLException e) { + throw TestUtil.rethrow(e); + } + } + + /** Assigns a function to call to test whether output is correct. */ + Fluent checking(Consumer<ResultSet> expect) { + return new Fluent(model, sql, expect); + } + + /** Sets the rows that are expected to be returned from the SQL query. */ + Fluent returns(String... expectedLines) { + return checking(expect(expectedLines)); + } + + /** Sets the rows that are expected to be returned from the SQL query, + * in no particular order. */ + Fluent returnsUnordered(String... expectedLines) { + return checking(expectUnordered(expectedLines)); + } + } +} diff --git a/file/src/test/java/org/apache/calcite/adapter/file/SqlTest.java b/file/src/test/java/org/apache/calcite/adapter/file/SqlTest.java deleted file mode 100644 index 444f225..0000000 --- a/file/src/test/java/org/apache/calcite/adapter/file/SqlTest.java +++ /dev/null @@ -1,419 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to you under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.calcite.adapter.file; - -import org.apache.calcite.util.Sources; -import org.apache.calcite.util.TestUtil; -import org.apache.calcite.util.Util; - -import com.google.common.collect.Ordering; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.sql.Types; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Properties; -import java.util.function.Function; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** - * System test of the Calcite file adapter, which can also read and parse - * HTML tables over HTTP. - */ -@ExtendWith(RequiresNetworkExtension.class) -class SqlTest { - // helper functions - - private Fluent sql(String model, String sql) { - return new Fluent(model, sql, input -> { - throw new AssertionError(); - }); - } - - private static Function<ResultSet, Void> expect(String... expectedLines) { - final StringBuilder b = new StringBuilder(); - for (String s : expectedLines) { - b.append(s).append('\n'); - } - final String expected = b.toString(); - return resultSet -> { - try { - String actual = toString(resultSet); - if (!expected.equals(actual)) { - System.out.println("Assertion failure:"); - System.out.println("\tExpected: '" + expected + "'"); - System.out.println("\tActual: '" + actual + "'"); - } - assertEquals(expected, actual); - } catch (SQLException e) { - throw TestUtil.rethrow(e); - } - return null; - }; - } - - /** Returns a function that checks the contents of a result set against an - * expected string. */ - private static Function<ResultSet, Void> expectUnordered(String... expected) { - final List<String> expectedLines = - Ordering.natural().immutableSortedCopy(Arrays.asList(expected)); - return resultSet -> { - try { - final List<String> lines = new ArrayList<>(); - SqlTest.collect(lines, resultSet); - Collections.sort(lines); - assertEquals(expectedLines, lines); - } catch (SQLException e) { - throw TestUtil.rethrow(e); - } - return null; - }; - } - - private static void collect(List<String> result, ResultSet resultSet) - throws SQLException { - final StringBuilder buf = new StringBuilder(); - while (resultSet.next()) { - buf.setLength(0); - int n = resultSet.getMetaData().getColumnCount(); - String sep = ""; - for (int i = 1; i <= n; i++) { - buf.append(sep) - .append(resultSet.getMetaData().getColumnLabel(i)) - .append("=") - .append(resultSet.getString(i)); - sep = "; "; - } - result.add(Util.toLinux(buf.toString())); - } - } - - private void checkSql(String sql, String model, Function<ResultSet, Void> fn) - throws SQLException { - Connection connection = null; - Statement statement = null; - try { - Properties info = new Properties(); - info.put("model", - Sources.of(SqlTest.class.getResource("/" + model + ".json")).path()); - connection = DriverManager.getConnection("jdbc:calcite:", info); - statement = connection.createStatement(); - final ResultSet resultSet = statement.executeQuery(sql); - fn.apply(resultSet); - } finally { - close(connection, statement); - } - } - - private static String toString(ResultSet resultSet) throws SQLException { - StringBuilder buf = new StringBuilder(); - while (resultSet.next()) { - int n = resultSet.getMetaData().getColumnCount(); - String sep = ""; - for (int i = 1; i <= n; i++) { - buf.append(sep) - .append(resultSet.getMetaData().getColumnLabel(i)) - .append("=") - .append(resultSet.getObject(i)); - sep = "; "; - } - buf.append("\n"); - } - return buf.toString(); - } - - private void close(Connection connection, Statement statement) { - if (statement != null) { - try { - statement.close(); - } catch (SQLException e) { - // ignore - } - } - if (connection != null) { - try { - connection.close(); - } catch (SQLException e) { - // ignore - } - } - } - - // tests - - /** Reads from a local file and checks the result. */ - @Test void testFileSelect() throws SQLException { - final String sql = "select H1 from T1 where H0 = 'R1C0'"; - sql("testModel", sql).returns("H1=R1C1").ok(); - } - - /** Reads from a local file without table headers <TH> and checks the - * result. */ - @Test @RequiresNetwork public void testNoThSelect() throws SQLException { - final String sql = "select \"col1\" from T1_NO_TH where \"col0\" like 'R0%'"; - sql("testModel", sql).returns("col1=R0C1").ok(); - } - - /** Reads from a local file - finds larger table even without <TH> - * elements. */ - @Test void testFindBiggerNoTh() throws SQLException { - final String sql = "select \"col4\" from TABLEX2 where \"col0\" like 'R1%'"; - sql("testModel", sql).returns("col4=R1C4").ok(); - } - - /** Reads from a URL and checks the result. */ - @Disabled("[CALCITE-1789] Wikipedia format change breaks file adapter test") - @Test @RequiresNetwork public void testUrlSelect() throws SQLException { - final String sql = "select \"State\", \"Statehood\" from \"States_as_of\"\n" - + "where \"State\" = 'California'"; - sql("wiki", sql).returns("State=California; Statehood=1850-09-09").ok(); - } - - /** Reads the EMPS table. */ - @Test void testSalesEmps() throws SQLException { - final String sql = "select * from sales.emps"; - sql("sales", sql) - .returns("EMPNO=100; NAME=Fred; DEPTNO=30", - "EMPNO=110; NAME=Eric; DEPTNO=20", - "EMPNO=110; NAME=John; DEPTNO=40", - "EMPNO=120; NAME=Wilma; DEPTNO=20", - "EMPNO=130; NAME=Alice; DEPTNO=40") - .ok(); - } - - /** Reads the DEPTS table. */ - @Test void testSalesDepts() throws SQLException { - final String sql = "select * from sales.depts"; - sql("sales", sql) - .returns("DEPTNO=10; NAME=Sales", - "DEPTNO=20; NAME=Marketing", - "DEPTNO=30; NAME=Accounts") - .ok(); - } - - /** Reads the DEPTS table from the CSV schema. */ - @Test void testCsvSalesDepts() throws SQLException { - final String sql = "select * from sales.depts"; - sql("sales-csv", sql) - .returns("DEPTNO=10; NAME=Sales", - "DEPTNO=20; NAME=Marketing", - "DEPTNO=30; NAME=Accounts") - .ok(); - } - - /** Reads the EMPS table from the CSV schema. */ - @Test void testCsvSalesEmps() throws SQLException { - final String sql = "select * from sales.emps"; - final String[] lines = { - "EMPNO=100; NAME=Fred; DEPTNO=10; GENDER=; CITY=; EMPID=30; AGE=25; SLACKER=true; MANAGER=false; JOINEDAT=1996-08-03", - "EMPNO=110; NAME=Eric; DEPTNO=20; GENDER=M; CITY=San Francisco; EMPID=3; AGE=80; SLACKER=null; MANAGER=false; JOINEDAT=2001-01-01", - "EMPNO=110; NAME=John; DEPTNO=40; GENDER=M; CITY=Vancouver; EMPID=2; AGE=null; SLACKER=false; MANAGER=true; JOINEDAT=2002-05-03", - "EMPNO=120; NAME=Wilma; DEPTNO=20; GENDER=F; CITY=; EMPID=1; AGE=5; SLACKER=null; MANAGER=true; JOINEDAT=2005-09-07", - "EMPNO=130; NAME=Alice; DEPTNO=40; GENDER=F; CITY=Vancouver; EMPID=2; AGE=null; SLACKER=false; MANAGER=true; JOINEDAT=2007-01-01", - }; - sql("sales-csv", sql).returns(lines).ok(); - } - - /** Reads the HEADER_ONLY table from the CSV schema. The CSV file has one - * line - the column headers - but no rows of data. */ - @Test void testCsvSalesHeaderOnly() throws SQLException { - final String sql = "select * from sales.header_only"; - sql("sales-csv", sql).returns().ok(); - } - - /** Reads the EMPTY table from the CSV schema. The CSV file has no lines, - * therefore the table has a system-generated column called - * "EmptyFileHasNoColumns". */ - @Test void testCsvSalesEmpty() throws SQLException { - final String sql = "select * from sales.\"EMPTY\""; - checkSql(sql, "sales-csv", resultSet -> { - try { - assertThat(resultSet.getMetaData().getColumnCount(), is(1)); - assertThat(resultSet.getMetaData().getColumnName(1), - is("EmptyFileHasNoColumns")); - assertThat(resultSet.getMetaData().getColumnType(1), - is(Types.BOOLEAN)); - String actual = toString(resultSet); - assertThat(actual, is("")); - } catch (SQLException e) { - throw TestUtil.rethrow(e); - } - return null; - }); - } - - /** Test case for - * <a href="https://issues.apache.org/jira/browse/CALCITE-1754">[CALCITE-1754] - * In Csv adapter, convert DATE and TIME values to int, and TIMESTAMP values - * to long</a>. */ - @Test void testCsvGroupByTimestampAdd() throws SQLException { - final String sql = "select count(*) as c,\n" - + " {fn timestampadd(SQL_TSI_DAY, 1, JOINEDAT) } as t\n" - + "from EMPS group by {fn timestampadd(SQL_TSI_DAY, 1, JOINEDAT ) } "; - sql("sales-csv", sql) - .returnsUnordered("C=1; T=1996-08-04", - "C=1; T=2002-05-04", - "C=1; T=2005-09-08", - "C=1; T=2007-01-02", - "C=1; T=2001-01-02") - .ok(); - final String sql2 = "select count(*) as c,\n" - + " {fn timestampadd(SQL_TSI_MONTH, 1, JOINEDAT) } as t\n" - + "from EMPS group by {fn timestampadd(SQL_TSI_MONTH, 1, JOINEDAT ) } "; - sql("sales-csv", sql2) - .returnsUnordered("C=1; T=2002-06-03", - "C=1; T=2005-10-07", - "C=1; T=2007-02-01", - "C=1; T=2001-02-01", - "C=1; T=1996-09-03").ok(); - final String sql3 = "select\n" - + " distinct {fn timestampadd(SQL_TSI_MONTH, 1, JOINEDAT) } as t\n" - + "from EMPS"; - sql("sales-csv", sql3) - .returnsUnordered("T=2002-06-03", - "T=2005-10-07", - "T=2007-02-01", - "T=2001-02-01", - "T=1996-09-03").ok(); - } - - /** Fluent API to perform test actions. */ - private class Fluent { - private final String model; - private final String sql; - private final Function<ResultSet, Void> expect; - - Fluent(String model, String sql, Function<ResultSet, Void> expect) { - this.model = model; - this.sql = sql; - this.expect = expect; - } - - /** Runs the test. */ - Fluent ok() { - try { - checkSql(sql, model, expect); - return this; - } catch (SQLException e) { - throw TestUtil.rethrow(e); - } - } - - /** Assigns a function to call to test whether output is correct. */ - Fluent checking(Function<ResultSet, Void> expect) { - return new Fluent(model, sql, expect); - } - - /** Sets the rows that are expected to be returned from the SQL query. */ - Fluent returns(String... expectedLines) { - return checking(expect(expectedLines)); - } - - /** Sets the rows that are expected to be returned from the SQL query, - * in no particular order. */ - Fluent returnsUnordered(String... expectedLines) { - return checking(expectUnordered(expectedLines)); - } - } - - /** Reads the DEPTS table from the JSON schema. */ - @Test void testJsonSalesDepts() throws SQLException { - final String sql = "select * from sales.depts"; - sql("sales-json", sql) - .returns("DEPTNO=10; NAME=Sales", - "DEPTNO=20; NAME=Marketing", - "DEPTNO=30; NAME=Accounts") - .ok(); - } - - /** Reads the EMPS table from the JSON schema. */ - @Test void testJsonSalesEmps() throws SQLException { - final String sql = "select * from sales.emps"; - final String[] lines = { - "EMPNO=100; NAME=Fred; DEPTNO=10; GENDER=; CITY=; EMPID=30; AGE=25; SLACKER=true; MANAGER=false; JOINEDAT=1996-08-03", - "EMPNO=110; NAME=Eric; DEPTNO=20; GENDER=M; CITY=San Francisco; EMPID=3; AGE=80; SLACKER=null; MANAGER=false; JOINEDAT=2001-01-01", - "EMPNO=110; NAME=John; DEPTNO=40; GENDER=M; CITY=Vancouver; EMPID=2; AGE=null; SLACKER=false; MANAGER=true; JOINEDAT=2002-05-03", - "EMPNO=120; NAME=Wilma; DEPTNO=20; GENDER=F; CITY=; EMPID=1; AGE=5; SLACKER=null; MANAGER=true; JOINEDAT=2005-09-07", - "EMPNO=130; NAME=Alice; DEPTNO=40; GENDER=F; CITY=Vancouver; EMPID=2; AGE=null; SLACKER=false; MANAGER=true; JOINEDAT=2007-01-01", - }; - sql("sales-json", sql).returns(lines).ok(); - } - - /** Reads the EMPTY table from the JSON schema. The JSON file has no lines, - * therefore the table has a system-generated column called - * "EmptyFileHasNoColumns". */ - @Test void testJsonSalesEmpty() throws SQLException { - final String sql = "select * from sales.\"EMPTY\""; - checkSql(sql, "sales-json", resultSet -> { - try { - assertThat(resultSet.getMetaData().getColumnCount(), is(1)); - assertThat(resultSet.getMetaData().getColumnName(1), - is("EmptyFileHasNoColumns")); - assertThat(resultSet.getMetaData().getColumnType(1), - is(Types.BOOLEAN)); - String actual = toString(resultSet); - assertThat(actual, is("")); - } catch (SQLException e) { - throw TestUtil.rethrow(e); - } - return null; - }); - } - - /** Test returns the result of two json file joins. */ - @Test void testJsonJoinOnString() { - final String sql = "select emps.EMPNO, emps.NAME, depts.deptno from emps\n" - + "join depts on emps.deptno = depts.deptno"; - final String[] lines = { - "EMPNO=100; NAME=Fred; DEPTNO=10", - "EMPNO=110; NAME=Eric; DEPTNO=20", - "EMPNO=120; NAME=Wilma; DEPTNO=20", - }; - sql("sales-json", sql).returns(lines).ok(); - } - - /** The folder contains both JSON files and CSV files joins. */ - @Test void testJsonWithCsvJoin() { - final String sql = "select emps.empno,\n" - + " NAME,\n" - + " \"DATE\".JOINEDAT\n" - + " from \"DATE\"\n" - + "join emps on emps.empno = \"DATE\".EMPNO\n" - + "order by empno, name, joinedat limit 3"; - final String[] lines = { - "EMPNO=100; NAME=Fred; JOINEDAT=1996-08-03", - "EMPNO=110; NAME=Eric; JOINEDAT=2001-01-01", - "EMPNO=110; NAME=Eric; JOINEDAT=2002-05-03", - }; - sql("sales-json", sql) - .returns(lines) - .ok(); - } -} diff --git a/file/build.gradle.kts b/file/src/test/resources/bug.json similarity index 69% copy from file/build.gradle.kts copy to file/src/test/resources/bug.json index 1610a03..46d0cb4 100644 --- a/file/build.gradle.kts +++ b/file/src/test/resources/bug.json @@ -14,15 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -dependencies { - api(project(":core")) - api(project(":example:csv")) - api(project(":linq4j")) - - implementation("com.google.guava:guava") - implementation("com.joestelmach:natty") - implementation("org.apache.calcite.avatica:avatica-core") - implementation("org.jsoup:jsoup") - - testImplementation(project(":core", "testClasses")) +{ + "version": "1.0", + "defaultSchema": "BUG", + "schemas": [ + { + "name": "BUG", + "type": "custom", + "factory": "org.apache.calcite.adapter.file.FileSchemaFactory", + "operand": { + "directory": "bug" + } + } + ] } diff --git a/file/src/test/resources/bug.yaml b/file/src/test/resources/bug.yaml new file mode 100644 index 0000000..fd50d5b --- /dev/null +++ b/file/src/test/resources/bug.yaml @@ -0,0 +1,24 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to you under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +version: 1.0 +defaultSchema: BUG +schemas: +- name: BUG + type: custom + factory: org.apache.calcite.adapter.csv.CsvSchemaFactory + operand: + directory: bug diff --git a/file/build.gradle.kts b/file/src/test/resources/bug/ARCHERS.json similarity index 56% copy from file/build.gradle.kts copy to file/src/test/resources/bug/ARCHERS.json index 1610a03..0e36759 100644 --- a/file/build.gradle.kts +++ b/file/src/test/resources/bug/ARCHERS.json @@ -14,15 +14,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -dependencies { - api(project(":core")) - api(project(":example:csv")) - api(project(":linq4j")) - - implementation("com.google.guava:guava") - implementation("com.joestelmach:natty") - implementation("org.apache.calcite.avatica:avatica-core") - implementation("org.jsoup:jsoup") - - testImplementation(project(":core", "testClasses")) +[ +{ + "id": "19990101", + "dow": "Friday", + "longDate": "New Years Day", + "title": "Tractor trouble.", + "characters": [ "Alice", "Bob", "Xavier" ], + "script": "Julian Hyde", + "summary": "", + "lines": [ + "Bob's tractor got stuck in a field.", + "Alice and Xavier hatch a plan to surprise Charlie." + ] +}, +{ + "id": "19990103", + "dow": "Sunday", + "longDate": "Sunday 3rd January", + "title": "Charlie's surprise.", + "characters": [ "Alice", "Zebedee", "Charlie", "Xavier" ], + "script": "William Shakespeare", + "summary": "", + "lines": [ + "Charlie is very surprised by Alice and Xavier's surprise plan." + ] } +] diff --git a/file/src/test/resources/bug/DATE.csv b/file/src/test/resources/bug/DATE.csv new file mode 100644 index 0000000..2999baf --- /dev/null +++ b/file/src/test/resources/bug/DATE.csv @@ -0,0 +1,9 @@ +EMPNO:int,JOINEDAT:date,JOINTIME:time,JOINTIMES:timestamp +100,"1996-08-03","00:01:02","1996-08-03 00:01:02" +110,"2001-01-01","00:00:00","2001-01-01 00:00:00" +110,"2002-05-03","00:00:00","2002-05-03 00:00:00" +120,"2005-09-07","00:00:00","2005-09-07 00:00:00" +130,"2007-01-01","00:00:00","2007-01-01 00:00:00" +140,"2015-12-31","07:15:56","2015-12-31 07:15:56" +150,"2015-12-31","13:31:21","2015-12-31 13:31:21" +200,,, diff --git a/file/src/test/resources/bug/LONG_EMPS.csv b/file/src/test/resources/bug/LONG_EMPS.csv new file mode 100644 index 0000000..f69e0c5 --- /dev/null +++ b/file/src/test/resources/bug/LONG_EMPS.csv @@ -0,0 +1,6 @@ +EMPNO:long,NAME:string,DEPTNO:int,GENDER:string,CITY:string,EMPID:int,AGE:int,SLACKER:boolean,MANAGER:boolean,JOINEDAT:date +100,"Fred",10,,,30,25,true,false,"1996-08-03" +110,"Eric",20,"M","San Francisco",3,80,,false,"2001-01-01" +110,"John",40,"M","Vancouver",2,,false,true,"2002-05-03" +120,"Wilma",20,"F",,1,5,,true,"2005-09-07" +130,"Alice",40,"F","Vancouver",2,,false,true,"2007-01-01" diff --git a/file/src/test/resources/bug/WACKY_COLUMN_NAMES.csv b/file/src/test/resources/bug/WACKY_COLUMN_NAMES.csv new file mode 100644 index 0000000..453d961 --- /dev/null +++ b/file/src/test/resources/bug/WACKY_COLUMN_NAMES.csv @@ -0,0 +1,6 @@ +EMPNO:int,naME:string,DEPTNO:Integer,2gender:string,CITY:string,EMPID:int,AGE:int,SLACKER:boolean,MANAGER:boolean,joined at:date +100,"Fred",10,,,30,25,true,false,"1996-08-03" +110,"Eric",20,"M","San Francisco",3,80,,false,"2001-01-01" +110,"John",40,"M","Vancouver",2,,false,true,"2002-05-03" +120,"Wilma",20,"F",,1,5,,true,"2005-09-07" +130,"Alice",40,"F","Vancouver",2,,false,true,"2007-01-01" diff --git a/file/build.gradle.kts b/file/src/test/resources/filterable-model.json similarity index 62% copy from file/build.gradle.kts copy to file/src/test/resources/filterable-model.json index 1610a03..abc2ab6 100644 --- a/file/build.gradle.kts +++ b/file/src/test/resources/filterable-model.json @@ -13,16 +13,23 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * + * A JSON model of a Calcite schema that is similar to model.json, + * except that it produces tables that implement FilterableTable. + * These tables can implement their own simple filtering. */ -dependencies { - api(project(":core")) - api(project(":example:csv")) - api(project(":linq4j")) - - implementation("com.google.guava:guava") - implementation("com.joestelmach:natty") - implementation("org.apache.calcite.avatica:avatica-core") - implementation("org.jsoup:jsoup") - - testImplementation(project(":core", "testClasses")) +{ + "version": "1.0", + "defaultSchema": "SALES", + "schemas": [ + { + "name": "SALES", + "type": "custom", + "factory": "org.apache.calcite.adapter.file.FileSchemaFactory", + "operand": { + "directory": "sales", + "flavor": "FILTERABLE" + } + } + ] } diff --git a/file/build.gradle.kts b/file/src/test/resources/model-with-custom-table.json similarity index 66% copy from file/build.gradle.kts copy to file/src/test/resources/model-with-custom-table.json index 1610a03..19aa032 100644 --- a/file/build.gradle.kts +++ b/file/src/test/resources/model-with-custom-table.json @@ -14,15 +14,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -dependencies { - api(project(":core")) - api(project(":example:csv")) - api(project(":linq4j")) - - implementation("com.google.guava:guava") - implementation("com.joestelmach:natty") - implementation("org.apache.calcite.avatica:avatica-core") - implementation("org.jsoup:jsoup") - - testImplementation(project(":core", "testClasses")) +{ + "version": "1.0", + "defaultSchema": "CUSTOM_TABLE", + "schemas": [ + { + "name": "CUSTOM_TABLE", + "tables": [ + { + "name": "EMPS", + "type": "custom", + "factory": "org.apache.calcite.adapter.file.CsvTableFactory", + "operand": { + "file": "sales/EMPS.csv.gz", + "flavor": "scannable" + } + } + ] + } + ] } diff --git a/file/build.gradle.kts b/file/src/test/resources/model.json similarity index 69% copy from file/build.gradle.kts copy to file/src/test/resources/model.json index 1610a03..826ca1a 100644 --- a/file/build.gradle.kts +++ b/file/src/test/resources/model.json @@ -13,16 +13,20 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * + * A JSON model of a simple Calcite schema. */ -dependencies { - api(project(":core")) - api(project(":example:csv")) - api(project(":linq4j")) - - implementation("com.google.guava:guava") - implementation("com.joestelmach:natty") - implementation("org.apache.calcite.avatica:avatica-core") - implementation("org.jsoup:jsoup") - - testImplementation(project(":core", "testClasses")) +{ + "version": "1.0", + "defaultSchema": "SALES", + "schemas": [ + { + "name": "SALES", + "type": "custom", + "factory": "org.apache.calcite.adapter.file.FileSchemaFactory", + "operand": { + "directory": "sales" + } + } + ] } diff --git a/file/src/test/resources/sales/DEPTS.csv b/file/src/test/resources/sales/DEPTS.csv new file mode 100644 index 0000000..628f2d8 --- /dev/null +++ b/file/src/test/resources/sales/DEPTS.csv @@ -0,0 +1,4 @@ +DEPTNO:int,NAME:string +10,"Sales" +20,"Marketing" +30,"Accounts" diff --git a/file/src/test/resources/sales/EMPS.csv.gz b/file/src/test/resources/sales/EMPS.csv.gz new file mode 100644 index 0000000..294bad4 Binary files /dev/null and b/file/src/test/resources/sales/EMPS.csv.gz differ diff --git a/file/build.gradle.kts b/file/src/test/resources/smart.json similarity index 52% copy from file/build.gradle.kts copy to file/src/test/resources/smart.json index 1610a03..9b86761 100644 --- a/file/build.gradle.kts +++ b/file/src/test/resources/smart.json @@ -13,16 +13,28 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * + * A JSON model of a Calcite schema that is similar to model.json, + * except that it produces tables that implement FilterableTable. + * These tables can implement their own simple filtering. + * + * A JSON model of a Calcite schema that is similar to model.json, + * except that it produces tables that implement + * TranslatableTable. These tables are translated to a CsvTableScan + * relational expression which participates in query planning. */ -dependencies { - api(project(":core")) - api(project(":example:csv")) - api(project(":linq4j")) - - implementation("com.google.guava:guava") - implementation("com.joestelmach:natty") - implementation("org.apache.calcite.avatica:avatica-core") - implementation("org.jsoup:jsoup") - - testImplementation(project(":core", "testClasses")) +{ + "version": "1.0", + "defaultSchema": "SALES", + "schemas": [ + { + "name": "SALES", + "type": "custom", + "factory": "org.apache.calcite.adapter.file.FileSchemaFactory", + "operand": { + "directory": "sales", + "flavor": "TRANSLATABLE" + } + } + ] }
