diff --git a/mongodb/src/main/java/org/apache/calcite/adapter/mongodb/MongoRules.java b/mongodb/src/main/java/org/apache/calcite/adapter/mongodb/MongoRules.java index 48e46e610d55..fe7c65db1059 100644 --- a/mongodb/src/main/java/org/apache/calcite/adapter/mongodb/MongoRules.java +++ b/mongodb/src/main/java/org/apache/calcite/adapter/mongodb/MongoRules.java @@ -110,7 +110,9 @@ static String maybeQuote(String s) { } static String quote(String s) { - return "'" + s + "'"; // TODO: handle embedded quotes + // Escape backslash and the single-quote delimiter so that s cannot break + // out of the quoted token when the string is parsed by BsonDocument.parse. + return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'"; } private static boolean needsQuote(String s) { @@ -181,7 +183,7 @@ protected RexToMongoTranslator(JavaTypeFactory typeFactory, @Override public String visitCall(RexCall call) { String name = isItem(call); if (name != null) { - return "'$" + name + "'"; + return quote("$" + name); } final List strings = visitList(call.operands); if (call.getKind() == SqlKind.CAST) { 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 7c79718e7984..c4fef104f8b5 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 @@ -86,6 +86,13 @@ public class MongoAdapterTest implements SchemaFactory { /** Number of records in local file. */ protected static final int ZIPS_SIZE = 149; + /** Field of the "datatypes" collection whose name contains a single quote + * and characters that would be pipeline syntax if it were not escaped. */ + private static final String QUOTED_FIELD = "x', injected: {$literal: 1}, y: 'z"; + + /** Field of the "datatypes" collection whose name ends with a backslash. */ + private static final String BACKSLASH_FIELD = "a\\"; + @RegisterExtension public static final MongoDatabasePolicy POLICY = MongoDatabasePolicy.create(); @@ -119,6 +126,8 @@ public static void setUp() throws Exception { 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))); + doc.put(QUOTED_FIELD, new BsonString("quoted")); + doc.put(BACKSLASH_FIELD, new BsonString("backslash")); datatypes.insertOne(doc); schema = new MongoSchema(database); @@ -743,6 +752,30 @@ private void checkPredicate(int expected, String q) { .returnsUnordered("EXPR$0=[a, b]"); } + /** A field name that contains a single quote or a backslash must not be able + * to break out of the quoted token in the generated pipeline and add stage + * fields of its own. + * + *

The expected values were validated against a real MongoDB instance: + * without the escaping the first query parses as + * {@code {$project: {C: '$x', injected: {$literal: 1}, y: 'z'}}} and the + * injected field breaks the pipeline, so the query fails. */ + @Test void testItemKeyWithEmbeddedQuote() { + assertModel(MODEL) + .query("select cast(_MAP['x'', injected: {$literal: 1}, y: ''z'] as varchar) as c\n" + + "from \"mongo_raw\".\"datatypes\"") + .returnsUnordered("C=quoted") + .queryContains( + mongoChecker("{$project: {C: '$x\\', injected: {$literal: 1}, y: \\'z'}}")); + + assertModel(MODEL) + .query("select cast(_MAP['a\\'] as varchar) as c\n" + + "from \"mongo_raw\".\"datatypes\"") + .returnsUnordered("C=backslash") + .queryContains( + mongoChecker("{$project: {C: '$a\\\\'}}")); + } + /** Test case for * [CALCITE-665] * ClassCastException in MongoDB adapter. */