Skip to content

Commit b1ffa42

Browse files
dssysolyatindssysolyatin
authored andcommitted
[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
1 parent 7ce986f commit b1ffa42

File tree

2 files changed

+123
-59
lines changed

2 files changed

+123
-59
lines changed

mongodb/src/main/java/org/apache/calcite/adapter/mongodb/MongoEnumerator.java

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,28 @@
1616
*/
1717
package org.apache.calcite.adapter.mongodb;
1818

19+
import org.apache.calcite.avatica.util.ByteString;
1920
import org.apache.calcite.avatica.util.DateTimeUtils;
2021
import org.apache.calcite.linq4j.Enumerator;
2122
import org.apache.calcite.linq4j.function.Function1;
2223
import org.apache.calcite.linq4j.tree.Primitive;
2324

2425
import com.mongodb.client.MongoCursor;
2526

27+
import org.bson.BsonTimestamp;
2628
import org.bson.Document;
29+
import org.bson.types.Binary;
30+
import org.bson.types.Decimal128;
2731
import org.checkerframework.checker.nullness.qual.Nullable;
2832

33+
import java.math.BigDecimal;
2934
import java.util.Date;
3035
import java.util.Iterator;
3136
import java.util.List;
3237
import java.util.Map;
3338

39+
import static java.lang.String.format;
40+
3441
/** Enumerator that reads from a MongoDB collection. */
3542
class MongoEnumerator implements Enumerator<Object> {
3643
private final Iterator<Document> cursor;
@@ -89,7 +96,7 @@ static Function1<Document, Map> mapGetter() {
8996
/** Returns a function that projects a single field. */
9097
static Function1<Document, Object> singletonGetter(final String fieldName,
9198
final Class fieldClass) {
92-
return a0 -> convert(a0.get(fieldName), fieldClass);
99+
return a0 -> convert(fieldName, a0.get(fieldName), fieldClass);
93100
}
94101

95102
/** Returns a function that projects fields.
@@ -103,7 +110,7 @@ static Function1<Document, Object[]> listGetter(
103110
for (int i = 0; i < fields.size(); i++) {
104111
final Map.Entry<String, Class> field = fields.get(i);
105112
final String name = field.getKey();
106-
objects[i] = convert(a0.get(name), field.getValue());
113+
objects[i] = convert(name, a0.get(name), field.getValue());
107114
}
108115
return objects;
109116
};
@@ -119,8 +126,34 @@ static Function1<Document, Object> getter(
119126
: (Function1) listGetter(fields);
120127
}
121128

129+
/**
130+
* Converts the given object to a specific runtime type based on the provided class.
131+
*
132+
* @param fieldName The name of the field being processed, used for error reporting if
133+
* conversion fails.
134+
* @param o The object to be converted. If `null`, the method returns `null` immediately.
135+
* @param clazz The target class to which the object `o` should be converted.
136+
* @return The converted object as an instance of the specified `clazz`, or `null` if `o` is
137+
* `null`.
138+
*
139+
* @throws IllegalArgumentException if the object `o` cannot be converted to the desired
140+
* `clazz` type, including a message indicating the field name, expected data type, and the
141+
* invalid value.
142+
*
143+
* <h3>Conversion Details:
144+
*
145+
* <p>If the target type is one of the following, the method performs specific conversions:
146+
* <ul>
147+
* <li>`Long`: Converts a `Date` or `BsonTimestamp` object into the respective epoch time
148+
* (milliseconds).
149+
* <li>`BigDecimal`: Converts a `Decimal128` object into a `BigDecimal` instance.
150+
* <li>`String`: Converts arrays to string and uses `String.valueOf(o)` for other objects.
151+
* <li>`ByteString`: Converts a `Binary` object into a `ByteString` instance.
152+
* </ul>
153+
*
154+
*/
122155
@SuppressWarnings("JavaUtilDate")
123-
private static Object convert(Object o, Class clazz) {
156+
private static Object convert(String fieldName, Object o, Class clazz) {
124157
if (o == null) {
125158
return null;
126159
}
@@ -133,14 +166,41 @@ private static Object convert(Object o, Class clazz) {
133166
if (clazz.isInstance(o)) {
134167
return o;
135168
}
136-
if (o instanceof Date && clazz == Long.class) {
137-
o = ((Date) o).getTime();
138-
} else if (o instanceof Date && primitive != null) {
139-
o = ((Date) o).getTime() / DateTimeUtils.MILLIS_PER_DAY;
169+
170+
if (clazz == Long.class) {
171+
if (o instanceof Date) {
172+
return ((Date) o).getTime();
173+
} else if (o instanceof BsonTimestamp) {
174+
return ((BsonTimestamp) o).getTime() * DateTimeUtils.MILLIS_PER_SECOND;
175+
}
176+
} else if (clazz == BigDecimal.class) {
177+
if (o instanceof Decimal128) {
178+
return new BigDecimal(((Decimal128) o).toString());
179+
}
180+
} else if (clazz == String.class) {
181+
if (o.getClass().isArray()) {
182+
return Primitive.OTHER.arrayToString(o);
183+
} else {
184+
return String.valueOf(o);
185+
}
186+
} else if (clazz == ByteString.class) {
187+
if (o instanceof Binary) {
188+
return new ByteString(((Binary) o).getData());
189+
}
140190
}
141-
if (o instanceof Number && primitive != null) {
142-
return primitive.number((Number) o);
191+
192+
if (primitive != null) {
193+
if (o instanceof String) {
194+
return primitive.parse((String) o);
195+
} else if (o instanceof Number) {
196+
return primitive.number((Number) o);
197+
} else if (o instanceof Date) {
198+
return primitive.number(((Date) o).getTime() / DateTimeUtils.MILLIS_PER_DAY);
199+
}
143200
}
144-
return o;
201+
202+
throw new IllegalArgumentException(
203+
format("Invalid field: '%s'. The dataType '%s' is invalid for '%s'.", fieldName,
204+
clazz.getSimpleName(), o));
145205
}
146206
}

mongodb/src/test/java/org/apache/calcite/adapter/mongodb/MongoAdapterTest.java

Lines changed: 53 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import net.hydromatic.foodmart.data.json.FoodmartJson;
3434

3535
import org.bson.BsonArray;
36+
import org.bson.BsonBinary;
3637
import org.bson.BsonDateTime;
3738
import org.bson.BsonDocument;
3839
import org.bson.BsonInt32;
@@ -105,7 +106,7 @@ public static void setUp() throws Exception {
105106
"url"));
106107

107108
// Manually insert data for data-time test.
108-
MongoCollection<BsonDocument> datatypes = database.getCollection("datatypes")
109+
MongoCollection<BsonDocument> datatypes = database.getCollection("datatypes")
109110
.withDocumentClass(BsonDocument.class);
110111
if (datatypes.countDocuments() > 0) {
111112
datatypes.deleteMany(new BsonDocument());
@@ -117,6 +118,7 @@ public static void setUp() throws Exception {
117118
doc.put("value", new BsonInt32(1231));
118119
doc.put("ownerId", new BsonString("531e7789e4b0853ddb861313"));
119120
doc.put("arr", new BsonArray(Arrays.asList(new BsonString("a"), new BsonString("b"))));
121+
doc.put("binaryData", new BsonBinary("binaryData".getBytes(StandardCharsets.UTF_8)));
120122
datatypes.insertOne(doc);
121123

122124
schema = new MongoSchema(database);
@@ -697,68 +699,26 @@ private void checkPredicate(int expected, String q) {
697699
* <a href="https://issues.apache.org/jira/browse/CALCITE-286">[CALCITE-286]
698700
* Error casting MongoDB date</a>. */
699701
@Test void testDate() {
700-
assertModel("{\n"
701-
+ " version: '1.0',\n"
702-
+ " defaultSchema: 'test',\n"
703-
+ " schemas: [\n"
704-
+ " {\n"
705-
+ " type: 'custom',\n"
706-
+ " name: 'test',\n"
707-
+ " factory: 'org.apache.calcite.adapter.mongodb.MongoSchemaFactory',\n"
708-
+ " operand: {\n"
709-
+ " host: 'localhost',\n"
710-
+ " database: 'test'\n"
711-
+ " }\n"
712-
+ " }\n"
713-
+ " ]\n"
714-
+ "}")
715-
.query("select cast(_MAP['date'] as DATE) from \"datatypes\"")
702+
assertModel(MODEL)
703+
.query("select cast(_MAP['date'] as DATE) from \"mongo_raw\".\"datatypes\"")
716704
.returnsUnordered("EXPR$0=2012-09-05");
717705
}
718706

719707
/** Test case for
720708
* <a href="https://issues.apache.org/jira/browse/CALCITE-5405">[CALCITE-5405]
721709
* Error casting MongoDB dates to TIMESTAMP</a>. */
722710
@Test void testDateConversion() {
723-
assertModel("{\n"
724-
+ " version: '1.0',\n"
725-
+ " defaultSchema: 'test',\n"
726-
+ " schemas: [\n"
727-
+ " {\n"
728-
+ " type: 'custom',\n"
729-
+ " name: 'test',\n"
730-
+ " factory: 'org.apache.calcite.adapter.mongodb.MongoSchemaFactory',\n"
731-
+ " operand: {\n"
732-
+ " host: 'localhost',\n"
733-
+ " database: 'test'\n"
734-
+ " }\n"
735-
+ " }\n"
736-
+ " ]\n"
737-
+ "}")
738-
.query("select cast(_MAP['date'] as TIMESTAMP) from \"datatypes\"")
711+
assertModel(MODEL)
712+
.query("select cast(_MAP['date'] as TIMESTAMP) from \"mongo_raw\".\"datatypes\"")
739713
.returnsUnordered("EXPR$0=2012-09-05 00:00:00");
740714
}
741715

742716
/** Test case for
743717
* <a href="https://issues.apache.org/jira/browse/CALCITE-5407">[CALCITE-5407]
744718
* Error casting MongoDB array to VARCHAR ARRAY</a>. */
745719
@Test void testArrayConversion() {
746-
assertModel("{\n"
747-
+ " version: '1.0',\n"
748-
+ " defaultSchema: 'test',\n"
749-
+ " schemas: [\n"
750-
+ " {\n"
751-
+ " type: 'custom',\n"
752-
+ " name: 'test',\n"
753-
+ " factory: 'org.apache.calcite.adapter.mongodb.MongoSchemaFactory',\n"
754-
+ " operand: {\n"
755-
+ " host: 'localhost',\n"
756-
+ " database: 'test'\n"
757-
+ " }\n"
758-
+ " }\n"
759-
+ " ]\n"
760-
+ "}")
761-
.query("select cast(_MAP['arr'] as VARCHAR ARRAY) from \"datatypes\"")
720+
assertModel(MODEL)
721+
.query("select cast(_MAP['arr'] as VARCHAR ARRAY) from \"mongo_raw\".\"datatypes\"")
762722
.returnsUnordered("EXPR$0=[a, b]");
763723
}
764724

@@ -778,6 +738,50 @@ private void checkPredicate(int expected, String q) {
778738
});
779739
}
780740

741+
/** Test case for
742+
* <a href="https://issues.apache.org/jira/browse/CALCITE-6623">[CALCITE-6623]
743+
* MongoDB adapter throws a java.lang.ClassCastException when Decimal128 or Binary types are
744+
* used, or when a primitive value is cast to a string</a>. */
745+
@Test void testRuntimeTypes() {
746+
assertModel(MODEL)
747+
.query("select cast(_MAP['loc'] AS varchar) "
748+
+ "from \"mongo_raw\".\"zips\" where _MAP['_id']='99801'")
749+
.returnsCount(1)
750+
.returnsValue("[-134.529429, 58.362767]");
751+
752+
assertModel(MODEL)
753+
.query("select cast(_MAP['warehouse_postal_code'] AS bigint) AS postal_code_as_bigint"
754+
+ " from \"mongo_raw\".\"warehouse\" where _MAP['warehouse_id']=1")
755+
.returnsCount(1)
756+
.returnsValue("55555")
757+
.typeIs("[POSTAL_CODE_AS_BIGINT BIGINT]");
758+
759+
assertModel(MODEL)
760+
.query("select cast(_MAP['warehouse_postal_code'] AS varchar) AS postal_code_as_varchar"
761+
+ " from \"mongo_raw\".\"warehouse\" where _MAP['warehouse_id']=1")
762+
.returnsCount(1)
763+
.returnsValue("55555")
764+
.typeIs("[POSTAL_CODE_AS_VARCHAR VARCHAR]");
765+
766+
assertModel(MODEL)
767+
.query("select cast(_MAP['binaryData'] AS binary) from \"mongo_raw\".\"datatypes\"")
768+
.returnsCount(1)
769+
.returns(resultSet -> {
770+
try {
771+
resultSet.next();
772+
//CHECKSTYLE: IGNORE 1
773+
assertThat(new String(resultSet.getBytes(1), StandardCharsets.UTF_8), is("binaryData"));
774+
} catch (Throwable e) {
775+
throw new RuntimeException(e);
776+
}
777+
});
778+
779+
assertModel(MODEL)
780+
.query("select cast(_MAP['loc'] AS bigint) "
781+
+ "from \"mongo_raw\".\"zips\" where _MAP['_id']='99801'")
782+
.throws_("Invalid field:");
783+
}
784+
781785
/**
782786
* Returns a function that checks that a particular MongoDB query
783787
* has been called.

0 commit comments

Comments
 (0)