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

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


The following commit(s) were added to refs/heads/main by this push:
     new 541c9fb0b4 [CALCITE-6623] The MongoDB adapter throws a 
java.lang.ClassCastException when Decimal128 or Binary types are used, or when 
a primitive value is cast to a string
541c9fb0b4 is described below

commit 541c9fb0b4d918f8de9b35ca0db60529c03828c2
Author: Dmitry Sysolyatin <[email protected]>
AuthorDate: Mon Jul 29 12:45:12 2024 +0300

    [CALCITE-6623] The MongoDB adapter throws a java.lang.ClassCastException 
when Decimal128 or Binary types are used, or when a primitive value is cast to 
a string
---
 .../calcite/adapter/mongodb/MongoEnumerator.java   |  90 ++++++++++++++++--
 .../calcite/adapter/mongodb/MongoAdapterTest.java  | 102 +++++++++++----------
 2 files changed, 133 insertions(+), 59 deletions(-)

diff --git 
a/mongodb/src/main/java/org/apache/calcite/adapter/mongodb/MongoEnumerator.java 
b/mongodb/src/main/java/org/apache/calcite/adapter/mongodb/MongoEnumerator.java
index 595a9ff385..b654364b3b 100644
--- 
a/mongodb/src/main/java/org/apache/calcite/adapter/mongodb/MongoEnumerator.java
+++ 
b/mongodb/src/main/java/org/apache/calcite/adapter/mongodb/MongoEnumerator.java
@@ -16,6 +16,7 @@
  */
 package org.apache.calcite.adapter.mongodb;
 
+import org.apache.calcite.avatica.util.ByteString;
 import org.apache.calcite.avatica.util.DateTimeUtils;
 import org.apache.calcite.linq4j.Enumerator;
 import org.apache.calcite.linq4j.function.Function1;
@@ -23,14 +24,21 @@ import org.apache.calcite.linq4j.tree.Primitive;
 
 import com.mongodb.client.MongoCursor;
 
+import org.bson.BsonTimestamp;
 import org.bson.Document;
+import org.bson.types.Binary;
+import org.bson.types.Decimal128;
 import org.checkerframework.checker.nullness.qual.Nullable;
 
+import java.math.BigDecimal;
 import java.util.Date;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
+import static java.lang.String.format;
+
 /** Enumerator that reads from a MongoDB collection. */
 class MongoEnumerator implements Enumerator<Object> {
   private final Iterator<Document> cursor;
@@ -89,7 +97,7 @@ class MongoEnumerator implements Enumerator<Object> {
   /** Returns a function that projects a single field. */
   static Function1<Document, Object> singletonGetter(final String fieldName,
       final Class fieldClass) {
-    return a0 -> convert(a0.get(fieldName), fieldClass);
+    return a0 -> convert(fieldName, a0.get(fieldName), fieldClass);
   }
 
   /** Returns a function that projects fields.
@@ -103,7 +111,7 @@ class MongoEnumerator implements Enumerator<Object> {
       for (int i = 0; i < fields.size(); i++) {
         final Map.Entry<String, Class> field = fields.get(i);
         final String name = field.getKey();
-        objects[i] = convert(a0.get(name), field.getValue());
+        objects[i] = convert(name, a0.get(name), field.getValue());
       }
       return objects;
     };
@@ -119,8 +127,42 @@ class MongoEnumerator implements Enumerator<Object> {
             : (Function1) listGetter(fields);
   }
 
+  /**
+   * Converts the given object to a specific runtime type based on the 
provided class.
+   *
+   * @param fieldName The name of the field being processed, used for error 
reporting if
+   *                  conversion fails.
+   * @param o The object to be converted. If `null`, the method returns `null` 
immediately.
+   * @param clazz The target class to which the object `o` should be converted.
+   * @return The converted object as an instance of the specified `clazz`, or 
`null` if `o` is
+   * `null`.
+   *
+   * @throws IllegalArgumentException if the object `o` cannot be converted to 
the desired
+   * `clazz` type, including a message indicating the field name, expected 
data type, and the
+   * invalid value.
+   *
+   * <h3>Conversion Details:</h3>
+   *
+   * <p>If the target type is one of the following, the method performs 
specific conversions:
+   * <ul>
+   *   <li>`Long`: Converts a `Date` or `BsonTimestamp` object into the 
respective epoch time
+   *   (milliseconds).
+   *   <li>`BigDecimal`: Converts a `Decimal128` object into a `BigDecimal` 
instance.
+   *   <li>`String`: Converts arrays to string and uses `String.valueOf(o)` 
for other objects.
+   *   <li>`ByteString`: Converts a `Binary` object into a `ByteString` 
instance.
+   *   <li>`Primitive or Boxed Primitive`:
+   *       <ul>
+   *         <li>If the object is a `String`, it will be converted to the 
corresponding boxed
+   *         primitive
+   *             using {@link Primitive#parse}.</li>
+   *         <li>If the object is numeric, it will be converted to the boxed 
primitive type using
+   *             {@link Primitive#number}.</li>
+   *       </ul>
+   *   </li>
+   * </ul>
+   */
   @SuppressWarnings("JavaUtilDate")
-  private static Object convert(Object o, Class clazz) {
+  private static Object convert(String fieldName, Object o, Class clazz) {
     if (o == null) {
       return null;
     }
@@ -133,14 +175,42 @@ class MongoEnumerator implements Enumerator<Object> {
     if (clazz.isInstance(o)) {
       return o;
     }
-    if (o instanceof Date && clazz == Long.class) {
-      o = ((Date) o).getTime();
-    } else if (o instanceof Date && primitive != null) {
-      o = ((Date) o).getTime() / DateTimeUtils.MILLIS_PER_DAY;
+
+    if (clazz == Long.class) {
+      if (o instanceof Date) {
+        return ((Date) o).getTime();
+      } else if (o instanceof BsonTimestamp) {
+        return ((BsonTimestamp) o).getTime() * DateTimeUtils.MILLIS_PER_SECOND;
+      }
+    } else if (clazz == BigDecimal.class) {
+      if (o instanceof Decimal128) {
+        return new BigDecimal(((Decimal128) o).toString());
+      }
+    } else if (clazz == String.class) {
+      if (o.getClass().isArray()) {
+        return Primitive.OTHER.arrayToString(o);
+      } else {
+        return String.valueOf(o);
+      }
+    } else if (clazz == ByteString.class) {
+      if (o instanceof Binary) {
+        return new ByteString(((Binary) o).getData());
+      }
     }
-    if (o instanceof Number && primitive != null) {
-      return primitive.number((Number) o);
+
+    if (primitive != null) {
+      if (o instanceof String) {
+        return primitive.parse((String) o);
+      } else if (o instanceof Number) {
+        return primitive.number((Number) o);
+      } else if (o instanceof Date) {
+        return primitive.number(((Date) o).getTime() / 
DateTimeUtils.MILLIS_PER_DAY);
+      }
     }
-    return o;
+
+    throw new IllegalArgumentException(
+        format(Locale.ROOT, "Invalid field: '%s'. The dataType '%s' is invalid 
for '%s'.",
+            fieldName,
+            clazz.getSimpleName(), o));
   }
 }
diff --git 
a/mongodb/src/test/java/org/apache/calcite/adapter/mongodb/MongoAdapterTest.java
 
b/mongodb/src/test/java/org/apache/calcite/adapter/mongodb/MongoAdapterTest.java
index 3dbf4bfda7..3683409693 100644
--- 
a/mongodb/src/test/java/org/apache/calcite/adapter/mongodb/MongoAdapterTest.java
+++ 
b/mongodb/src/test/java/org/apache/calcite/adapter/mongodb/MongoAdapterTest.java
@@ -33,6 +33,7 @@ import com.mongodb.client.MongoDatabase;
 import net.hydromatic.foodmart.data.json.FoodmartJson;
 
 import org.bson.BsonArray;
+import org.bson.BsonBinary;
 import org.bson.BsonDateTime;
 import org.bson.BsonDocument;
 import org.bson.BsonInt32;
@@ -105,7 +106,7 @@ public class MongoAdapterTest implements SchemaFactory {
             "url"));
 
     // Manually insert data for data-time test.
-    MongoCollection<BsonDocument> datatypes =  
database.getCollection("datatypes")
+    MongoCollection<BsonDocument> datatypes = 
database.getCollection("datatypes")
         .withDocumentClass(BsonDocument.class);
     if (datatypes.countDocuments() > 0) {
       datatypes.deleteMany(new BsonDocument());
@@ -117,6 +118,7 @@ public class MongoAdapterTest implements SchemaFactory {
     doc.put("value", new BsonInt32(1231));
     doc.put("ownerId", new BsonString("531e7789e4b0853ddb861313"));
     doc.put("arr", new BsonArray(Arrays.asList(new BsonString("a"), new 
BsonString("b"))));
+    doc.put("binaryData", new 
BsonBinary("binaryData".getBytes(StandardCharsets.UTF_8)));
     datatypes.insertOne(doc);
 
     schema = new MongoSchema(database);
@@ -697,22 +699,8 @@ public class MongoAdapterTest implements SchemaFactory {
    * <a href="https://issues.apache.org/jira/browse/CALCITE-286";>[CALCITE-286]
    * Error casting MongoDB date</a>. */
   @Test void testDate() {
-    assertModel("{\n"
-        + "  version: '1.0',\n"
-        + "  defaultSchema: 'test',\n"
-        + "   schemas: [\n"
-        + "     {\n"
-        + "       type: 'custom',\n"
-        + "       name: 'test',\n"
-        + "       factory: 
'org.apache.calcite.adapter.mongodb.MongoSchemaFactory',\n"
-        + "       operand: {\n"
-        + "         host: 'localhost',\n"
-        + "         database: 'test'\n"
-        + "       }\n"
-        + "     }\n"
-        + "   ]\n"
-        + "}")
-        .query("select cast(_MAP['date'] as DATE) from \"datatypes\"")
+    assertModel(MODEL)
+        .query("select cast(_MAP['date'] as DATE) from 
\"mongo_raw\".\"datatypes\"")
         .returnsUnordered("EXPR$0=2012-09-05");
   }
 
@@ -720,22 +708,8 @@ public class MongoAdapterTest implements SchemaFactory {
    * <a 
href="https://issues.apache.org/jira/browse/CALCITE-5405";>[CALCITE-5405]
    * Error casting MongoDB dates to TIMESTAMP</a>. */
   @Test void testDateConversion() {
-    assertModel("{\n"
-        + "  version: '1.0',\n"
-        + "  defaultSchema: 'test',\n"
-        + "   schemas: [\n"
-        + "     {\n"
-        + "       type: 'custom',\n"
-        + "       name: 'test',\n"
-        + "       factory: 
'org.apache.calcite.adapter.mongodb.MongoSchemaFactory',\n"
-        + "       operand: {\n"
-        + "         host: 'localhost',\n"
-        + "         database: 'test'\n"
-        + "       }\n"
-        + "     }\n"
-        + "   ]\n"
-        + "}")
-        .query("select cast(_MAP['date'] as TIMESTAMP) from \"datatypes\"")
+    assertModel(MODEL)
+        .query("select cast(_MAP['date'] as TIMESTAMP) from 
\"mongo_raw\".\"datatypes\"")
         .returnsUnordered("EXPR$0=2012-09-05 00:00:00");
   }
 
@@ -743,22 +717,8 @@ public class MongoAdapterTest implements SchemaFactory {
    * <a 
href="https://issues.apache.org/jira/browse/CALCITE-5407";>[CALCITE-5407]
    * Error casting MongoDB array to VARCHAR ARRAY</a>. */
   @Test void testArrayConversion() {
-    assertModel("{\n"
-        + "  version: '1.0',\n"
-        + "  defaultSchema: 'test',\n"
-        + "   schemas: [\n"
-        + "     {\n"
-        + "       type: 'custom',\n"
-        + "       name: 'test',\n"
-        + "       factory: 
'org.apache.calcite.adapter.mongodb.MongoSchemaFactory',\n"
-        + "       operand: {\n"
-        + "         host: 'localhost',\n"
-        + "         database: 'test'\n"
-        + "       }\n"
-        + "     }\n"
-        + "   ]\n"
-        + "}")
-        .query("select cast(_MAP['arr'] as VARCHAR ARRAY) from \"datatypes\"")
+    assertModel(MODEL)
+        .query("select cast(_MAP['arr'] as VARCHAR ARRAY) from 
\"mongo_raw\".\"datatypes\"")
         .returnsUnordered("EXPR$0=[a, b]");
   }
 
@@ -778,6 +738,50 @@ public class MongoAdapterTest implements SchemaFactory {
         });
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6623";>[CALCITE-6623]
+   * MongoDB adapter throws a java.lang.ClassCastException when Decimal128 or 
Binary types are
+   * used, or when a primitive value is cast to a string</a>. */
+  @Test void testRuntimeTypes() {
+    assertModel(MODEL)
+        .query("select cast(_MAP['loc'] AS varchar) "
+            + "from \"mongo_raw\".\"zips\" where _MAP['_id']='99801'")
+        .returnsCount(1)
+        .returnsValue("[-134.529429, 58.362767]");
+
+    assertModel(MODEL)
+        .query("select cast(_MAP['warehouse_postal_code'] AS bigint) AS 
postal_code_as_bigint"
+            + " from \"mongo_raw\".\"warehouse\" where _MAP['warehouse_id']=1")
+        .returnsCount(1)
+        .returnsValue("55555")
+        .typeIs("[POSTAL_CODE_AS_BIGINT BIGINT]");
+
+    assertModel(MODEL)
+        .query("select cast(_MAP['warehouse_postal_code'] AS varchar) AS 
postal_code_as_varchar"
+            + " from \"mongo_raw\".\"warehouse\" where _MAP['warehouse_id']=1")
+        .returnsCount(1)
+        .returnsValue("55555")
+        .typeIs("[POSTAL_CODE_AS_VARCHAR VARCHAR]");
+
+    assertModel(MODEL)
+        .query("select cast(_MAP['binaryData'] AS binary) from 
\"mongo_raw\".\"datatypes\"")
+        .returnsCount(1)
+        .returns(resultSet -> {
+          try {
+            resultSet.next();
+            //CHECKSTYLE: IGNORE 1
+            assertThat(new String(resultSet.getBytes(1), 
StandardCharsets.UTF_8), is("binaryData"));
+          } catch (SQLException e) {
+            throw TestUtil.rethrow(e);
+          }
+        });
+
+    assertModel(MODEL)
+        .query("select cast(_MAP['loc'] AS bigint) "
+            + "from \"mongo_raw\".\"zips\" where _MAP['_id']='99801'")
+        .throws_("Invalid field:");
+  }
+
   /**
    * Returns a function that checks that a particular MongoDB query
    * has been called.

Reply via email to