diff --git a/cassandra/src/main/java/org/apache/calcite/adapter/cassandra/CassandraFilter.java b/cassandra/src/main/java/org/apache/calcite/adapter/cassandra/CassandraFilter.java index f357079d61f1..9bca4419a5f4 100644 --- a/cassandra/src/main/java/org/apache/calcite/adapter/cassandra/CassandraFilter.java +++ b/cassandra/src/main/java/org/apache/calcite/adapter/cassandra/CassandraFilter.java @@ -302,7 +302,7 @@ private String translateOp2(String op, String name, RexLiteral right) { requireNonNull(rowType.getField(name, true, false)); SqlTypeName typeName = field.getType().getSqlTypeName(); if (typeName != SqlTypeName.CHAR) { - valueString = "'" + valueString + "'"; + valueString = "'" + valueString.replace("'", "''") + "'"; } } return name + " " + op + " " + valueString; diff --git a/cassandra/src/test/java/org/apache/calcite/test/CassandraAdapterTest.java b/cassandra/src/test/java/org/apache/calcite/test/CassandraAdapterTest.java index 411ffdbb65be..88898e6a0455 100644 --- a/cassandra/src/test/java/org/apache/calcite/test/CassandraAdapterTest.java +++ b/cassandra/src/test/java/org/apache/calcite/test/CassandraAdapterTest.java @@ -71,6 +71,15 @@ static void load(CqlSession session) { + " CassandraTableScan(table=[[twissandra, userline]]"); } + @Test void testFilterWithSingleQuote() { + // A string literal containing a single quote must be escaped so it does not + // break out of the CQL string literal in the generated query. + CalciteAssert.that() + .with(TWISSANDRA) + .query("select * from \"userline\" where \"username\" = 'a''b'") + .returnsCount(0); + } + @Test void testFilterUUID() { CalciteAssert.that() .with(TWISSANDRA) diff --git a/geode/src/main/java/org/apache/calcite/adapter/geode/rel/GeodeFilter.java b/geode/src/main/java/org/apache/calcite/adapter/geode/rel/GeodeFilter.java index dafc0122d139..27b85dd72f91 100644 --- a/geode/src/main/java/org/apache/calcite/adapter/geode/rel/GeodeFilter.java +++ b/geode/src/main/java/org/apache/calcite/adapter/geode/rel/GeodeFilter.java @@ -395,7 +395,7 @@ private String translateBinary2(String op, RexNode left, private static String quoteCharLiteral(RexLiteral literal) { String value = literalValue(literal); if (literal.getTypeName() == CHAR) { - value = "'" + value + "'"; + value = "'" + value.replace("'", "''") + "'"; } return value; } diff --git a/geode/src/test/java/org/apache/calcite/adapter/geode/rel/GeodeZipsTest.java b/geode/src/test/java/org/apache/calcite/adapter/geode/rel/GeodeZipsTest.java index bf42258edabc..94fd3a41e4f6 100644 --- a/geode/src/test/java/org/apache/calcite/adapter/geode/rel/GeodeZipsTest.java +++ b/geode/src/test/java/org/apache/calcite/adapter/geode/rel/GeodeZipsTest.java @@ -202,6 +202,31 @@ public void testJoin() { GeodeAssertions.query(expectedQuery)); } + @Test void testFilterWithSingleQuoteLiteral() { + String expectedQuery = "SELECT city AS city FROM /zips " + + "WHERE city = 'a''b'"; + calciteAssert() + .query("SELECT city as city " + + "FROM view WHERE city = 'a''b'") + .returnsCount(0) + .queryContains( + GeodeAssertions.query(expectedQuery)); + } + + /** Value that already contains adjacent single quotes: the literal {@code 'I''''m fine'} + * denotes the runtime value {@code I''m fine}, and every embedded quote is doubled so the + * OQL round-trips back to that same value. */ + @Test void testFilterWithAdjacentSingleQuoteLiteral() { + String expectedQuery = "SELECT city AS city FROM /zips " + + "WHERE city = 'I''''m fine'"; + calciteAssert() + .query("SELECT city as city " + + "FROM view WHERE city = 'I''''m fine'") + .returnsCount(0) + .queryContains( + GeodeAssertions.query(expectedQuery)); + } + @Test void testWhereWithOrForNumericField() { calciteAssert() .query("SELECT pop as pop "