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.