This is an automated email from the ASF dual-hosted git repository.

jacques pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/master by this push:
     new 065e323  [CALCITE-4841] Support decimal column type in CSV file format
065e323 is described below

commit 065e323da89ca73a46bec195efc7bd4019dc4f2f
Author: xwkuang5 <[email protected]>
AuthorDate: Mon Nov 1 09:09:49 2021 -0400

    [CALCITE-4841] Support decimal column type in CSV file format
    
    - Introduce a new `decimal` type in the CSV file format for
      more accurate representation of decimal data.
    - Implement the `decimal` SQL type using the BigDecimal JAVA type.
---
 .../calcite/adapter/csv/CsvFilterableTable.java    |   4 +-
 .../calcite/adapter/csv/CsvScannableTable.java     |   4 +-
 .../adapter/csv/CsvStreamScannableTable.java       |   4 +-
 .../org/apache/calcite/adapter/csv/CsvTable.java   |   5 +-
 .../apache/calcite/adapter/file/CsvEnumerator.java | 166 ++++++++++++++++-----
 .../apache/calcite/adapter/file/CsvFieldType.java  |  78 ----------
 .../org/apache/calcite/adapter/file/CsvTable.java  |   4 +-
 .../apache/calcite/adapter/file/FileFieldType.java |   2 -
 .../calcite/adapter/file/CsvEnumeratorTest.java    |  58 +++++++
 .../calcite/adapter/file/FileAdapterTest.java      |  68 +++++++++
 file/src/test/resources/sales-csv/DECIMAL.csv      |   4 +
 11 files changed, 269 insertions(+), 128 deletions(-)

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 c61a04c..aa96bfa 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
@@ -18,11 +18,11 @@ 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.adapter.java.JavaTypeFactory;
 import org.apache.calcite.linq4j.AbstractEnumerable;
 import org.apache.calcite.linq4j.Enumerable;
 import org.apache.calcite.linq4j.Enumerator;
+import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelProtoDataType;
 import org.apache.calcite.rex.RexCall;
 import org.apache.calcite.rex.RexInputRef;
@@ -57,7 +57,7 @@ public class CsvFilterableTable extends CsvTable
 
   @Override public Enumerable<@Nullable Object[]> scan(DataContext root, 
List<RexNode> filters) {
     JavaTypeFactory typeFactory = root.getTypeFactory();
-    final List<CsvFieldType> fieldTypes = getFieldTypes(typeFactory);
+    final List<RelDataType> fieldTypes = getFieldTypes(typeFactory);
     final @Nullable String[] filterValues = new String[fieldTypes.size()];
     filters.removeIf(filter -> addFilter(filter, filterValues));
     final List<Integer> fields = ImmutableIntList.identity(fieldTypes.size());
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 dd24e21..6d456fb 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
@@ -18,11 +18,11 @@ 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.adapter.java.JavaTypeFactory;
 import org.apache.calcite.linq4j.AbstractEnumerable;
 import org.apache.calcite.linq4j.Enumerable;
 import org.apache.calcite.linq4j.Enumerator;
+import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelProtoDataType;
 import org.apache.calcite.schema.ScannableTable;
 import org.apache.calcite.util.ImmutableIntList;
@@ -52,7 +52,7 @@ public class CsvScannableTable extends CsvTable
 
   @Override public Enumerable<@Nullable Object[]> scan(DataContext root) {
     JavaTypeFactory typeFactory = root.getTypeFactory();
-    final List<CsvFieldType> fieldTypes = getFieldTypes(typeFactory);
+    final List<RelDataType> fieldTypes = getFieldTypes(typeFactory);
     final List<Integer> fields = ImmutableIntList.identity(fieldTypes.size());
     final AtomicBoolean cancelFlag = 
DataContext.Variable.CANCEL_FLAG.get(root);
     return new AbstractEnumerable<@Nullable Object[]>() {
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 2c01cd4..683c4c4 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
@@ -18,11 +18,11 @@ 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.adapter.java.JavaTypeFactory;
 import org.apache.calcite.linq4j.AbstractEnumerable;
 import org.apache.calcite.linq4j.Enumerable;
 import org.apache.calcite.linq4j.Enumerator;
+import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelProtoDataType;
 import org.apache.calcite.schema.ScannableTable;
 import org.apache.calcite.schema.StreamableTable;
@@ -58,7 +58,7 @@ public class CsvStreamScannableTable extends CsvScannableTable
 
   @Override public Enumerable<@Nullable Object[]> scan(DataContext root) {
     JavaTypeFactory typeFactory = root.getTypeFactory();
-    final List<CsvFieldType> fieldTypes = getFieldTypes(typeFactory);
+    final List<RelDataType> fieldTypes = getFieldTypes(typeFactory);
     final List<Integer> fields = ImmutableIntList.identity(fieldTypes.size());
     final AtomicBoolean cancelFlag = 
DataContext.Variable.CANCEL_FLAG.get(root);
     return new AbstractEnumerable<@Nullable Object[]>() {
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 9af670f..17ad898 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
@@ -17,7 +17,6 @@
 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;
@@ -37,7 +36,7 @@ public abstract class CsvTable extends AbstractTable {
   protected final Source source;
   protected final @Nullable RelProtoDataType protoRowType;
   private @Nullable RelDataType rowType;
-  private @Nullable List<CsvFieldType> fieldTypes;
+  private @Nullable List<RelDataType> fieldTypes;
 
   /** Creates a CsvTable. */
   CsvTable(Source source, @Nullable RelProtoDataType protoRowType) {
@@ -57,7 +56,7 @@ public abstract class CsvTable extends AbstractTable {
   }
 
   /** Returns the field types of this CSV table. */
-  public List<CsvFieldType> getFieldTypes(RelDataTypeFactory typeFactory) {
+  public List<RelDataType> getFieldTypes(RelDataTypeFactory typeFactory) {
     if (fieldTypes == null) {
       fieldTypes = new ArrayList<>();
       CsvEnumerator.deduceRowType((JavaTypeFactory) typeFactory, source,
diff --git 
a/file/src/main/java/org/apache/calcite/adapter/file/CsvEnumerator.java 
b/file/src/main/java/org/apache/calcite/adapter/file/CsvEnumerator.java
index a2f97af..600ec89 100644
--- a/file/src/main/java/org/apache/calcite/adapter/file/CsvEnumerator.java
+++ b/file/src/main/java/org/apache/calcite/adapter/file/CsvEnumerator.java
@@ -25,21 +25,32 @@ import org.apache.calcite.util.ImmutableIntList;
 import org.apache.calcite.util.ImmutableNullableList;
 import org.apache.calcite.util.Pair;
 import org.apache.calcite.util.Source;
+import org.apache.calcite.util.trace.CalciteLogger;
 
 import org.apache.commons.lang3.time.FastDateFormat;
 
 import au.com.bytecode.opencsv.CSVReader;
 
+import com.google.common.annotations.VisibleForTesting;
+
 import org.checkerframework.checker.nullness.qual.Nullable;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
+import java.util.Locale;
 import java.util.Objects;
 import java.util.TimeZone;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static com.google.common.base.Preconditions.checkArgument;
 
 import static org.apache.calcite.linq4j.Nullness.castNonNull;
 
@@ -48,6 +59,9 @@ import static org.apache.calcite.linq4j.Nullness.castNonNull;
  * @param <E> Row type
  */
 public class CsvEnumerator<E> implements Enumerator<E> {
+
+  private static final CalciteLogger LOGGER = new CalciteLogger(
+      LoggerFactory.getLogger(CsvEnumerator.class));
   private final CSVReader reader;
   private final @Nullable List<@Nullable String> filterValues;
   private final AtomicBoolean cancelFlag;
@@ -57,6 +71,8 @@ public class CsvEnumerator<E> implements Enumerator<E> {
   private static final FastDateFormat TIME_FORMAT_DATE;
   private static final FastDateFormat TIME_FORMAT_TIME;
   private static final FastDateFormat TIME_FORMAT_TIMESTAMP;
+  private static final Pattern DECIMAL_TYPE_PATTERN = Pattern
+      .compile("\"decimal\\(([0-9]+),([0-9]+)\\)");
 
   static {
     final TimeZone gmt = TimeZone.getTimeZone("GMT");
@@ -67,7 +83,7 @@ public class CsvEnumerator<E> implements Enumerator<E> {
   }
 
   public CsvEnumerator(Source source, AtomicBoolean cancelFlag,
-      List<CsvFieldType> fieldTypes, List<Integer> fields) {
+      List<RelDataType> fieldTypes, List<Integer> fields) {
     //noinspection unchecked
     this(source, cancelFlag, false, null,
         (RowConverter<E>) converter(fieldTypes, fields));
@@ -91,7 +107,7 @@ public class CsvEnumerator<E> implements Enumerator<E> {
     }
   }
 
-  private static RowConverter<?> converter(List<CsvFieldType> fieldTypes,
+  private static RowConverter<?> converter(List<RelDataType> fieldTypes,
       List<Integer> fields) {
     if (fields.size() == 1) {
       final int field = fields.get(0);
@@ -102,21 +118,14 @@ public class CsvEnumerator<E> implements Enumerator<E> {
   }
 
   public static RowConverter<@Nullable Object[]> arrayConverter(
-      List<CsvFieldType> fieldTypes, List<Integer> fields, boolean stream) {
+      List<RelDataType> 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,
-      List<CsvFieldType> fieldTypes) {
-    return deduceRowType(typeFactory, source, fieldTypes, false);
-  }
-
-  /** Deduces the names and types of a table's columns by reading the first 
line
-  * of a CSV file. */
   public static RelDataType deduceRowType(JavaTypeFactory typeFactory,
-      Source source, @Nullable List<CsvFieldType> fieldTypes, Boolean stream) {
+      Source source, @Nullable List<RelDataType> fieldTypes, Boolean stream) {
     final List<RelDataType> types = new ArrayList<>();
     final List<String> names = new ArrayList<>();
     if (stream) {
@@ -130,30 +139,69 @@ public class CsvEnumerator<E> implements Enumerator<E> {
       }
       for (String string : strings) {
         final String name;
-        final CsvFieldType fieldType;
+        final RelDataType fieldType;
         final int colon = string.indexOf(':');
         if (colon >= 0) {
           name = string.substring(0, colon);
           String typeString = string.substring(colon + 1);
-          fieldType = CsvFieldType.of(typeString);
-          if (fieldType == null) {
-            System.out.println("WARNING: Found unknown type: "
-                + typeString + " in file: " + source.path()
-                + " for column: " + name
-                + ". Will assume the type of column is string");
+          Matcher decimalMatcher = DECIMAL_TYPE_PATTERN.matcher(typeString);
+          if (decimalMatcher.matches()) {
+            int precision = Integer.parseInt(decimalMatcher.group(1));
+            int scale = Integer.parseInt(decimalMatcher.group(2));
+            fieldType = parseDecimalSqlType(typeFactory, precision, scale);
+          } else {
+            switch (typeString) {
+            case "string":
+              fieldType = toNullableRelDataType(typeFactory, 
SqlTypeName.VARCHAR);
+              break;
+            case "boolean":
+              fieldType = toNullableRelDataType(typeFactory, 
SqlTypeName.BOOLEAN);
+              break;
+            case "byte":
+              fieldType = toNullableRelDataType(typeFactory, 
SqlTypeName.TINYINT);
+              break;
+            case "char":
+              fieldType = toNullableRelDataType(typeFactory, SqlTypeName.CHAR);
+              break;
+            case "short":
+              fieldType = toNullableRelDataType(typeFactory, 
SqlTypeName.SMALLINT);
+              break;
+            case "int":
+              fieldType = toNullableRelDataType(typeFactory, 
SqlTypeName.INTEGER);
+              break;
+            case "long":
+              fieldType = toNullableRelDataType(typeFactory, 
SqlTypeName.BIGINT);
+              break;
+            case "float":
+              fieldType = toNullableRelDataType(typeFactory, SqlTypeName.REAL);
+              break;
+            case "double":
+              fieldType = toNullableRelDataType(typeFactory, 
SqlTypeName.DOUBLE);
+              break;
+            case "date":
+              fieldType = toNullableRelDataType(typeFactory, SqlTypeName.DATE);
+              break;
+            case "timestamp":
+              fieldType = toNullableRelDataType(typeFactory, 
SqlTypeName.TIMESTAMP);
+              break;
+            case "time":
+              fieldType = toNullableRelDataType(typeFactory, SqlTypeName.TIME);
+              break;
+            default:
+              LOGGER.warn(
+                  "Found unknown type: {} in file: {} for column: {}. Will 
assume the type of "
+                      + "column is string.",
+                  typeString, source.path(), name);
+              fieldType = toNullableRelDataType(typeFactory, 
SqlTypeName.VARCHAR);
+              break;
+            }
           }
         } else {
           name = string;
-          fieldType = null;
-        }
-        final RelDataType type;
-        if (fieldType == null) {
-          type = typeFactory.createSqlType(SqlTypeName.VARCHAR);
-        } else {
-          type = fieldType.toType(typeFactory);
+          fieldType = typeFactory.createSqlType(SqlTypeName.VARCHAR);
         }
         names.add(name);
-        types.add(type);
+        types.add(fieldType);
         if (fieldTypes != null) {
           fieldTypes.add(fieldType);
         }
@@ -237,6 +285,11 @@ public class CsvEnumerator<E> implements Enumerator<E> {
     return integers;
   }
 
+  private static RelDataType toNullableRelDataType(JavaTypeFactory typeFactory,
+      SqlTypeName sqlTypeName) {
+    return 
typeFactory.createTypeWithNullability(typeFactory.createSqlType(sqlTypeName), 
true);
+  }
+
   /** Row converter.
    *
    * @param <E> element type */
@@ -244,32 +297,32 @@ public class CsvEnumerator<E> implements Enumerator<E> {
     abstract E convertRow(@Nullable String[] rows);
 
     @SuppressWarnings("JavaUtilDate")
-    protected @Nullable Object convert(@Nullable CsvFieldType fieldType, 
@Nullable String string) {
+    protected @Nullable Object convert(@Nullable RelDataType fieldType, 
@Nullable String string) {
       if (fieldType == null || string == null) {
         return string;
       }
-      switch (fieldType) {
+      switch (fieldType.getSqlTypeName()) {
       case BOOLEAN:
         if (string.length() == 0) {
           return null;
         }
         return Boolean.parseBoolean(string);
-      case BYTE:
+      case TINYINT:
         if (string.length() == 0) {
           return null;
         }
         return Byte.parseByte(string);
-      case SHORT:
+      case SMALLINT:
         if (string.length() == 0) {
           return null;
         }
         return Short.parseShort(string);
-      case INT:
+      case INTEGER:
         if (string.length() == 0) {
           return null;
         }
         return Integer.parseInt(string);
-      case LONG:
+      case BIGINT:
         if (string.length() == 0) {
           return null;
         }
@@ -284,6 +337,11 @@ public class CsvEnumerator<E> implements Enumerator<E> {
           return null;
         }
         return Double.parseDouble(string);
+      case DECIMAL:
+        if (string.length() == 0) {
+          return null;
+        }
+        return parseDecimal(fieldType.getPrecision(), fieldType.getScale(), 
string);
       case DATE:
         if (string.length() == 0) {
           return null;
@@ -314,22 +372,56 @@ public class CsvEnumerator<E> implements Enumerator<E> {
         } catch (ParseException e) {
           return null;
         }
-      case STRING:
+      case VARCHAR:
       default:
         return string;
       }
     }
   }
 
+  private static RelDataType parseDecimalSqlType(JavaTypeFactory typeFactory, 
int precision,
+      int scale) {
+    checkArgument(precision > 0, "DECIMAL type must have precision > 0. Found 
%s", precision);
+    checkArgument(scale >= 0, "DECIMAL type must have scale >= 0. Found %s", 
scale);
+    checkArgument(precision >= scale,
+        "DECIMAL type must have precision >= scale. Found precision (%s) and 
scale (%s).",
+        precision, scale);
+    return typeFactory.createTypeWithNullability(
+        typeFactory.createSqlType(SqlTypeName.DECIMAL, precision, scale), 
true);
+  }
+
+  @VisibleForTesting
+  protected static BigDecimal parseDecimal(int precision, int scale, String 
string) {
+    BigDecimal result = new BigDecimal(string);
+    // If the parsed value has more fractional digits than the specified 
scale, round ties away
+    // from 0.
+    if (result.scale() > scale) {
+      LOGGER.warn(
+          "Decimal value {} exceeds declared scale ({}). Performing rounding 
to keep the "
+              + "first {} fractional digits.",
+          result, scale, scale);
+      result = result.setScale(scale, RoundingMode.HALF_UP);
+    }
+    // Throws an exception if the parsed value has more digits to the left of 
the decimal point
+    // than the specified value.
+    if (result.precision() - result.scale() > precision - scale) {
+      throw new IllegalArgumentException(String
+          .format(Locale.ROOT, "Decimal value %s exceeds declared precision 
(%d) and scale (%d).",
+              result, precision, scale));
+    }
+    return result;
+  }
+
   /** Array row converter. */
   static class ArrayRowConverter extends RowConverter<@Nullable Object[]> {
+
     /** Field types. List must not be null, but any element may be null. */
-    private final List<CsvFieldType> fieldTypes;
+    private final List<RelDataType> fieldTypes;
     private final ImmutableIntList fields;
     /** Whether the row to convert is from a stream. */
     private final boolean stream;
 
-    ArrayRowConverter(List<CsvFieldType> fieldTypes, List<Integer> fields,
+    ArrayRowConverter(List<RelDataType> fieldTypes, List<Integer> fields,
         boolean stream) {
       this.fieldTypes = ImmutableNullableList.copyOf(fieldTypes);
       this.fields = ImmutableIntList.copyOf(fields);
@@ -366,10 +458,10 @@ public class CsvEnumerator<E> implements Enumerator<E> {
 
   /** Single column row converter. */
   private static class SingleColumnRowConverter extends RowConverter<Object> {
-    private final CsvFieldType fieldType;
+    private final RelDataType fieldType;
     private final int fieldIndex;
 
-    private SingleColumnRowConverter(CsvFieldType fieldType, int fieldIndex) {
+    private SingleColumnRowConverter(RelDataType fieldType, int fieldIndex) {
       this.fieldType = fieldType;
       this.fieldIndex = fieldIndex;
     }
diff --git 
a/file/src/main/java/org/apache/calcite/adapter/file/CsvFieldType.java 
b/file/src/main/java/org/apache/calcite/adapter/file/CsvFieldType.java
deleted file mode 100644
index ff11225..0000000
--- a/file/src/main/java/org/apache/calcite/adapter/file/CsvFieldType.java
+++ /dev/null
@@ -1,78 +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.adapter.java.JavaTypeFactory;
-import org.apache.calcite.linq4j.tree.Primitive;
-import org.apache.calcite.rel.type.RelDataType;
-
-import org.checkerframework.checker.nullness.qual.Nullable;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Type of a field in a CSV file.
- *
- * <p>Usually, and unless specified explicitly in the header row, a field is
- * of type {@link #STRING}. But specifying the field type in the header row
- * makes it easier to write SQL.</p>
- */
-public enum CsvFieldType {
-  STRING(String.class, "string"),
-  BOOLEAN(Primitive.BOOLEAN),
-  BYTE(Primitive.BYTE),
-  CHAR(Primitive.CHAR),
-  SHORT(Primitive.SHORT),
-  INT(Primitive.INT),
-  LONG(Primitive.LONG),
-  FLOAT(Primitive.FLOAT),
-  DOUBLE(Primitive.DOUBLE),
-  DATE(java.sql.Date.class, "date"),
-  TIME(java.sql.Time.class, "time"),
-  TIMESTAMP(java.sql.Timestamp.class, "timestamp");
-
-  private final Class clazz;
-  private final String simpleName;
-
-  private static final Map<String, CsvFieldType> MAP = new HashMap<>();
-
-  static {
-    for (CsvFieldType value : values()) {
-      MAP.put(value.simpleName, value);
-    }
-  }
-
-  CsvFieldType(Primitive primitive) {
-    this(primitive.getBoxClass(), primitive.getPrimitiveName());
-  }
-
-  CsvFieldType(Class clazz, String simpleName) {
-    this.clazz = clazz;
-    this.simpleName = simpleName;
-  }
-
-  public RelDataType toType(JavaTypeFactory typeFactory) {
-    RelDataType javaType = typeFactory.createJavaType(clazz);
-    RelDataType sqlType = typeFactory.createSqlType(javaType.getSqlTypeName());
-    return typeFactory.createTypeWithNullability(sqlType, true);
-  }
-
-  public static @Nullable CsvFieldType of(String typeString) {
-    return MAP.get(typeString);
-  }
-}
diff --git a/file/src/main/java/org/apache/calcite/adapter/file/CsvTable.java 
b/file/src/main/java/org/apache/calcite/adapter/file/CsvTable.java
index 1ab4e70..eb704ae 100644
--- a/file/src/main/java/org/apache/calcite/adapter/file/CsvTable.java
+++ b/file/src/main/java/org/apache/calcite/adapter/file/CsvTable.java
@@ -36,7 +36,7 @@ public abstract class CsvTable extends AbstractTable {
   protected final Source source;
   protected final RelProtoDataType protoRowType;
   private RelDataType rowType;
-  private List<CsvFieldType> fieldTypes;
+  private List<RelDataType> fieldTypes;
 
   /** Creates a CsvTable. */
   CsvTable(Source source, RelProtoDataType protoRowType) {
@@ -56,7 +56,7 @@ public abstract class CsvTable extends AbstractTable {
   }
 
   /** Returns the field types of this CSV table. */
-  public List<CsvFieldType> getFieldTypes(RelDataTypeFactory typeFactory) {
+  public List<RelDataType> getFieldTypes(RelDataTypeFactory typeFactory) {
     if (fieldTypes == null) {
       fieldTypes = new ArrayList<>();
       CsvEnumerator.deduceRowType((JavaTypeFactory) typeFactory, source,
diff --git 
a/file/src/main/java/org/apache/calcite/adapter/file/FileFieldType.java 
b/file/src/main/java/org/apache/calcite/adapter/file/FileFieldType.java
index 85ed9bd..822966a 100644
--- a/file/src/main/java/org/apache/calcite/adapter/file/FileFieldType.java
+++ b/file/src/main/java/org/apache/calcite/adapter/file/FileFieldType.java
@@ -30,8 +30,6 @@ import java.util.Map;
  * <p>Usually, and unless specified explicitly in the header row, a field is
  * of type {@link #STRING}. But specifying the field type in the fields
  * makes it easier to write SQL.
- *
- * <p>Trivially modified from CsvFieldType.
  */
 enum FileFieldType {
   STRING(null, String.class),
diff --git 
a/file/src/test/java/org/apache/calcite/adapter/file/CsvEnumeratorTest.java 
b/file/src/test/java/org/apache/calcite/adapter/file/CsvEnumeratorTest.java
new file mode 100644
index 0000000..0a60329
--- /dev/null
+++ b/file/src/test/java/org/apache/calcite/adapter/file/CsvEnumeratorTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Test for the {@link CsvEnumerator}.
+ */
+class CsvEnumeratorTest {
+
+  @Test void testParseDecimalScaleRounding() {
+    assertEquals(new BigDecimal("123.45"),
+        CsvEnumerator.parseDecimal(5, 2, "123.45"));
+    assertEquals(new BigDecimal("123.46"),
+        CsvEnumerator.parseDecimal(5, 2, "123.455"));
+    assertEquals(new BigDecimal("-123.46"),
+        CsvEnumerator.parseDecimal(5, 2, "-123.455"));
+    assertEquals(new BigDecimal("123.45"),
+        CsvEnumerator.parseDecimal(5, 2, "123.454"));
+    assertEquals(new BigDecimal("-123.45"),
+        CsvEnumerator.parseDecimal(5, 2, "-123.454"));
+  }
+
+  @Test void testParseDecimalPrecisionExceeded() {
+    assertThrows(IllegalArgumentException.class,
+        () -> CsvEnumerator.parseDecimal(4, 0, "1e+5"));
+    assertThrows(IllegalArgumentException.class,
+        () -> CsvEnumerator.parseDecimal(4, 0, "-1e+5"));
+    assertThrows(IllegalArgumentException.class,
+        () -> CsvEnumerator.parseDecimal(4, 0, "12345"));
+    assertThrows(IllegalArgumentException.class,
+        () -> CsvEnumerator.parseDecimal(4, 0, "-12345"));
+    assertThrows(IllegalArgumentException.class,
+        () -> CsvEnumerator.parseDecimal(4, 2, "123.45"));
+    assertThrows(IllegalArgumentException.class,
+        () -> CsvEnumerator.parseDecimal(4, 2, "-123.45"));
+  }
+}
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
index 6abb040..bb0a41f 100644
--- a/file/src/test/java/org/apache/calcite/adapter/file/FileAdapterTest.java
+++ b/file/src/test/java/org/apache/calcite/adapter/file/FileAdapterTest.java
@@ -29,6 +29,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 
+import java.math.BigDecimal;
 import java.sql.Connection;
 import java.sql.Date;
 import java.sql.DriverManager;
@@ -51,6 +52,7 @@ 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;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 
 /**
  * System test of the Calcite file adapter, which can read and parse
@@ -619,6 +621,72 @@ class FileAdapterTest {
     return sb.append(')').toString();
   }
 
+  @Test void testDecimalType() {
+    sql("sales-csv", "select BUDGET from sales.\"DECIMAL\"")
+        .checking(resultSet -> {
+          try {
+            ResultSetMetaData metaData = resultSet.getMetaData();
+            assertEquals("DECIMAL", metaData.getColumnTypeName(1));
+            assertEquals(18, metaData.getPrecision(1));
+            assertEquals(2, metaData.getScale(1));
+          } catch (SQLException e) {
+            throw TestUtil.rethrow(e);
+          }
+        })
+        .ok();
+  }
+
+  @Test void testDecimalTypeArithmeticOperations() {
+    sql("sales-csv", "select BUDGET + 100.0 from sales.\"DECIMAL\" where 
DEPTNO = 10")
+        .checking(resultSet -> {
+          try {
+            resultSet.next();
+            assertEquals(0,
+                resultSet.getBigDecimal(1).compareTo(new BigDecimal("200")));
+            assertFalse(resultSet.next());
+          } catch (SQLException e) {
+            throw TestUtil.rethrow(e);
+          }
+        })
+        .ok();
+    sql("sales-csv", "select BUDGET - 100.0 from sales.\"DECIMAL\" where 
DEPTNO = 10")
+        .checking(resultSet -> {
+          try {
+            resultSet.next();
+            assertEquals(0,
+                resultSet.getBigDecimal(1).compareTo(new BigDecimal("0")));
+            assertFalse(resultSet.next());
+          } catch (SQLException e) {
+            throw TestUtil.rethrow(e);
+          }
+        })
+        .ok();
+    sql("sales-csv", "select BUDGET * 0.01 from sales.\"DECIMAL\" where DEPTNO 
= 10")
+        .checking(resultSet -> {
+          try {
+            resultSet.next();
+            assertEquals(0,
+                resultSet.getBigDecimal(1).compareTo(new BigDecimal("1")));
+            assertFalse(resultSet.next());
+          } catch (SQLException e) {
+            throw TestUtil.rethrow(e);
+          }
+        })
+        .ok();
+    sql("sales-csv", "select BUDGET / 100 from sales.\"DECIMAL\" where DEPTNO 
= 10")
+        .checking(resultSet -> {
+          try {
+            resultSet.next();
+            assertEquals(0,
+                resultSet.getBigDecimal(1).compareTo(new BigDecimal("1")));
+            assertFalse(resultSet.next());
+          } catch (SQLException e) {
+            throw TestUtil.rethrow(e);
+          }
+        })
+        .ok();
+  }
+
   @Test void testDateType() throws SQLException {
     Properties info = new Properties();
     info.put("model", FileAdapterTests.jsonPath("bug"));
diff --git a/file/src/test/resources/sales-csv/DECIMAL.csv 
b/file/src/test/resources/sales-csv/DECIMAL.csv
new file mode 100644
index 0000000..03247cb
--- /dev/null
+++ b/file/src/test/resources/sales-csv/DECIMAL.csv
@@ -0,0 +1,4 @@
+DEPTNO:int,BUDGET:"decimal(18,2)"
+10,100.00
+20,100.01
+30,-100.01

Reply via email to