Skip to content

Commit a73e3c7

Browse files
author
Bruce Irschick
authored
AD-204 Fixed comparisons against NULL/undefined (#5)
### Summary AD-204 Fixed comparisons against NULL/undefined ### Description Comparison operators (<, >, !=) now return null in expressions where a field is compared to null. Fixes three types of queries: 1. Queries with multiple conditions on a single field eg. `SELECT * FROM table WHERE field <> 3 AND field <> 5` 2. Queries did not remove null values in not-equals filters. For example in the above query a row containing `field:null` would be included in the result set. 3. Queries involving CASE would incorrectly consider some comparisons to null to be true/false, instead of unknown. eg. `SELECT CASE WHEN field < 2 THEN 'A' ELSE 'B' END FROM table` would result in 'A' if `field==null` instead of 'B'. The same is true if the field does not exist at all in the document. ### Related Issue https://bitquill.atlassian.net/browse/AD-204
1 parent 4d85069 commit a73e3c7

File tree

4 files changed

+226
-3
lines changed

4 files changed

+226
-3
lines changed

calcite-adapter/src/main/java/software/amazon/documentdb/jdbc/calcite/adapter/DocumentDbFilter.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,25 @@ private Map<String, Object> translateAnd(final RexNode node0) {
152152
return map;
153153
}
154154

155+
@SuppressWarnings("unchecked")
155156
private static void addPredicate(final Map<String, Object> map, final String op,
156157
final Object v) {
157158
if (map.containsKey(op) && stronger(op, map.get(op), v)) {
158159
return;
159160
}
160-
map.put(op, v);
161+
if ("$ne".equals(op)) {
162+
if (map.containsKey("$nin") && map.get("$nin") instanceof List) {
163+
final List<Object> vars = (List<Object>) map.get("$nin");
164+
vars.add(v);
165+
} else {
166+
final List<Object> vars = new ArrayList<>();
167+
vars.add(null);
168+
vars.add(v);
169+
map.put("$nin", vars);
170+
}
171+
} else {
172+
map.put(op, v);
173+
}
161174
}
162175

163176
/** Returns whether {@code v0} is a stronger value for operator {@code key}

calcite-adapter/src/main/java/software/amazon/documentdb/jdbc/calcite/adapter/DocumentDbRules.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,16 @@ protected RexToMongoTranslator(final JavaTypeFactory typeFactory,
240240
}
241241
final String stdOperator = MONGO_OPERATORS.get(call.getOperator());
242242
if (stdOperator != null) {
243-
return "{" + stdOperator + ": [" + Util.commaList(strings) + "]}";
243+
// For comparisons other than equals we must check it exists and is not null.
244+
final String op = "{" + stdOperator + ": [" + Util.commaList(strings) + "]}";
245+
if (MONGO_OPERATORS.get(SqlStdOperatorTable.LESS_THAN).equals(stdOperator) ||
246+
MONGO_OPERATORS.get(SqlStdOperatorTable.LESS_THAN_OR_EQUAL).equals(stdOperator) ||
247+
MONGO_OPERATORS.get(SqlStdOperatorTable.NOT_EQUALS).equals(stdOperator) ||
248+
MONGO_OPERATORS.get(SqlStdOperatorTable.GREATER_THAN).equals(stdOperator) ||
249+
MONGO_OPERATORS.get(SqlStdOperatorTable.GREATER_THAN_OR_EQUAL).equals(stdOperator)) {
250+
return addNullChecksToQuery(strings, op);
251+
}
252+
return op;
244253
}
245254
if (call.getOperator() == SqlStdOperatorTable.ITEM) {
246255
final RexNode op1 = call.operands.get(1);
@@ -283,6 +292,21 @@ protected RexToMongoTranslator(final JavaTypeFactory typeFactory,
283292
+ " is not supported by MongoProject");
284293
}
285294

295+
private String addNullChecksToQuery(final List<String> strings, final String op) {
296+
final StringBuilder sb = new StringBuilder("{$and: [");
297+
sb.append(op);
298+
for (int i = 0; i < 2; i++) {
299+
if (!strings.get(i).equals("null")) {
300+
// The operator {$gt null} filters out any values that are null or undefined.
301+
sb.append("{$gt: [");
302+
sb.append(strings.get(i));
303+
sb.append(", null]}");
304+
}
305+
}
306+
sb.append("]}");
307+
return sb.toString();
308+
}
309+
286310
private static String stripQuotes(final String s) {
287311
return s.startsWith("'") && s.endsWith("'")
288312
? s.substring(1, s.length() - 1)

calcite-adapter/src/test/java/software/amazon/documentdb/jdbc/query/DocumentDbQueryMappingServiceTest.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,4 +1251,47 @@ void testQueryWithSumOne() throws SQLException {
12511251
"{\"$group\": {\"_id\": {}, \"EXPR$0\": {\"$sum\": \"$_f0\"}}}"),
12521252
result.getAggregateOperations().get(1));
12531253
}
1254+
1255+
@Test
1256+
@DisplayName("Tests CASE with one field, and three sections.")
1257+
void testQueryWithCASE() throws SQLException {
1258+
final String query =
1259+
String.format(
1260+
"SELECT CASE " +
1261+
"WHEN \"field\" > 10 THEN 'A' " +
1262+
"WHEN \"field\" > 5 THEN 'B' " +
1263+
"ELSE 'C' END FROM \"%s\".\"%s\"", DATABASE_NAME, COLLECTION_NAME + "_array");
1264+
final DocumentDbMqlQueryContext result = queryMapper.get(query);
1265+
Assertions.assertNotNull(result);
1266+
Assertions.assertEquals(COLLECTION_NAME, result.getCollectionName());
1267+
Assertions.assertEquals(1, result.getColumnMetaData().size());
1268+
Assertions.assertEquals(3, result.getAggregateOperations().size());
1269+
Assertions.assertEquals(
1270+
BsonDocument.parse(
1271+
"{\"$match\": " +
1272+
"{\"$or\": " +
1273+
"[{\"array.field\": {\"$exists\": true}}, {\"array.field1\": {\"$exists\": true}}, " +
1274+
"{\"array.field2\": {\"$exists\": true}}]}}"),
1275+
result.getAggregateOperations().get(0));
1276+
Assertions.assertEquals(
1277+
BsonDocument.parse(
1278+
"{\"$unwind\": " +
1279+
"{\"path\": \"$array\", " +
1280+
"\"preserveNullAndEmptyArrays\": true, " +
1281+
"\"includeArrayIndex\": \"array_index_lvl_0\"}}"),
1282+
result.getAggregateOperations().get(1));
1283+
Assertions.assertEquals(
1284+
BsonDocument.parse(
1285+
"{\"$addFields\": {\"EXPR$0\": " +
1286+
"{\"$cond\": [{\"$and\": [" +
1287+
"{\"$gt\": [\"$array.field\", {\"$literal\": 10}]}, " +
1288+
"{\"$gt\": [\"$array.field\", null]}, " +
1289+
"{\"$gt\": [{\"$literal\": 10}, null]}]}, {\"$literal\": \"A\"}, " +
1290+
"{\"$cond\": [{\"$and\": [" +
1291+
"{\"$gt\": [\"$array.field\", {\"$literal\": 5}]}, " +
1292+
"{\"$gt\": [\"$array.field\", null]}, " +
1293+
"{\"$gt\": [{\"$literal\": 5}, null]}]}, " +
1294+
"{\"$literal\": \"B\"}, {\"$literal\": \"C\"}]}]}}}\n"),
1295+
result.getAggregateOperations().get(2));
1296+
}
12541297
}

src/test/java/software/amazon/documentdb/jdbc/DocumentDbStatementTest.java

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package software.amazon.documentdb.jdbc;
1818

1919
import org.bson.BsonDocument;
20+
import org.bson.BsonMinKey;
2021
import org.junit.jupiter.api.AfterEach;
2122
import org.junit.jupiter.api.Assertions;
2223
import org.junit.jupiter.api.BeforeAll;
@@ -1267,7 +1268,7 @@ void testCountColumnName() throws SQLException {
12671268
@Test
12681269
@DisplayName("Tests query with SUM(1).")
12691270
void testQuerySumOne() throws SQLException {
1270-
final String tableName = "testSumOfNulls";
1271+
final String tableName = "testQuerySumOne";
12711272
final BsonDocument doc1 = BsonDocument.parse("{\"_id\": 101,\n" +
12721273
"\"field\": 4}");
12731274
final BsonDocument doc2 = BsonDocument.parse("{\"_id\": 102}");
@@ -1283,6 +1284,148 @@ void testQuerySumOne() throws SQLException {
12831284
Assertions.assertFalse(resultSet.next());
12841285
}
12851286

1287+
/**
1288+
* Tests that queries with not-equals do not return null or undefined values.
1289+
* @throws SQLException occurs if query fails.
1290+
*/
1291+
@Test
1292+
@DisplayName("Tests that comparisons to null do not return a value.")
1293+
void testComparisonToNull() throws SQLException {
1294+
final String tableName = "testComparisonsToNull";
1295+
final BsonDocument doc1 = BsonDocument.parse("{\"_id\": 101,\n" +
1296+
"\"field\": 4}");
1297+
final BsonDocument doc2 = BsonDocument.parse("{\"_id\": 102, \n}" +
1298+
"\"field\": null");
1299+
final BsonDocument doc3 = BsonDocument.parse("{\"_id\": 103}");
1300+
insertBsonDocuments(tableName, DATABASE_NAME, USER, PASSWORD,
1301+
new BsonDocument[]{doc1, doc2, doc3});
1302+
final Statement statement = getDocumentDbStatement();
1303+
final ResultSet resultSet = statement.executeQuery(
1304+
String.format("SELECT * from \"%s\".\"%s\" WHERE \"field\" <> 5", DATABASE_NAME, tableName));
1305+
Assertions.assertNotNull(resultSet);
1306+
Assertions.assertTrue(resultSet.next());
1307+
Assertions.assertFalse(resultSet.next());
1308+
}
1309+
1310+
/**
1311+
* Tests that queries with multiple not-equals clauses are correct.
1312+
* @throws SQLException occurs if query fails.
1313+
*/
1314+
@Test
1315+
@DisplayName("Tests that multiple != conditions can be used.")
1316+
void testMultipleNotEquals() throws SQLException {
1317+
final String tableName = "testMultipleNotEquals";
1318+
final BsonDocument doc1 = BsonDocument.parse("{\"_id\": 101,\n" +
1319+
"\"field\": 4}");
1320+
final BsonDocument doc2 = BsonDocument.parse("{\"_id\": 102, \n" +
1321+
"\"field\": 3}");
1322+
final BsonDocument doc3 = BsonDocument.parse("{\"_id\": 103, \n" +
1323+
"\"field\": 2}");
1324+
final BsonDocument doc4 = BsonDocument.parse("{\"_id\": 104, \n" +
1325+
"\"field\": null}");
1326+
insertBsonDocuments(tableName, DATABASE_NAME, USER, PASSWORD,
1327+
new BsonDocument[]{doc1, doc2, doc3, doc4});
1328+
final Statement statement = getDocumentDbStatement();
1329+
final ResultSet resultSet = statement.executeQuery(
1330+
String.format("SELECT * from \"%s\".\"%s\" WHERE \"field\" <> 4 AND \"field\" <> 3", DATABASE_NAME, tableName));
1331+
Assertions.assertNotNull(resultSet);
1332+
Assertions.assertTrue(resultSet.next());
1333+
Assertions.assertEquals(2, resultSet.getInt(2));
1334+
Assertions.assertFalse(resultSet.next());
1335+
}
1336+
1337+
/**
1338+
* Tests that queries CASE are correct, particularly where null or undefined values are involved.
1339+
* @throws SQLException occurs if query fails.
1340+
*/
1341+
@Test
1342+
@DisplayName("Tests queries with CASE and null values are correct.")
1343+
void testCASE() throws SQLException {
1344+
final String tableName = "testCASE";
1345+
final BsonDocument doc1 = BsonDocument.parse("{\"_id\": 101,\n" +
1346+
"\"field\": 1}");
1347+
final BsonDocument doc2 = BsonDocument.parse("{\"_id\": 102,\n" +
1348+
"\"field\": 2}");
1349+
final BsonDocument doc3 = BsonDocument.parse("{\"_id\": 103,\n" +
1350+
"\"field\": 5}");
1351+
final BsonDocument doc4 = BsonDocument.parse("{\"_id\": 104,\n" +
1352+
"\"field\": 4}");
1353+
final BsonDocument doc5 = BsonDocument.parse("{\"_id\": 105,\n" +
1354+
"\"field\": 3}");
1355+
final BsonDocument doc6 = BsonDocument.parse("{\"_id\": 106,\n" +
1356+
"\"field\": null}");
1357+
final BsonDocument doc7 = BsonDocument.parse("{\"_id\": 107}");
1358+
final BsonDocument doc8 = BsonDocument.parse("{\"_id\": 108}");
1359+
doc8.append("field", new BsonMinKey());
1360+
insertBsonDocuments(tableName, DATABASE_NAME, USER, PASSWORD,
1361+
new BsonDocument[]{doc1, doc2, doc3, doc4, doc5, doc6, doc7, doc8});
1362+
final Statement statement = getDocumentDbStatement();
1363+
final ResultSet resultSet = statement.executeQuery(
1364+
String.format(
1365+
"SELECT CASE " +
1366+
"WHEN \"field\" < 2 THEN 'A' " +
1367+
"WHEN \"field\" <= 2 THEN 'B' " +
1368+
"WHEN \"field\" > 4 THEN 'C' " +
1369+
"WHEN \"field\" >= 4 THEN 'D' " +
1370+
"WHEN \"field\" <> 7 THEN 'E' " +
1371+
"ELSE 'F' END FROM \"%s\".\"%s\"",
1372+
DATABASE_NAME, tableName));
1373+
Assertions.assertNotNull(resultSet);
1374+
Assertions.assertTrue(resultSet.next());
1375+
Assertions.assertEquals("A", resultSet.getString(1));
1376+
Assertions.assertTrue(resultSet.next());
1377+
Assertions.assertEquals("B", resultSet.getString(1));
1378+
Assertions.assertTrue(resultSet.next());
1379+
Assertions.assertEquals("C", resultSet.getString(1));
1380+
Assertions.assertTrue(resultSet.next());
1381+
Assertions.assertEquals("D", resultSet.getString(1));
1382+
Assertions.assertTrue(resultSet.next());
1383+
Assertions.assertEquals("E", resultSet.getString(1));
1384+
Assertions.assertTrue(resultSet.next());
1385+
Assertions.assertEquals("F", resultSet.getString(1));
1386+
Assertions.assertTrue(resultSet.next());
1387+
Assertions.assertEquals("F", resultSet.getString(1));
1388+
Assertions.assertTrue(resultSet.next());
1389+
Assertions.assertEquals("F", resultSet.getString(1));
1390+
Assertions.assertFalse(resultSet.next());
1391+
}
1392+
1393+
/**
1394+
* Tests that queries of CASE are correct with two different fields involved.
1395+
* @throws SQLException occurs if query fails.
1396+
*/
1397+
@Test
1398+
@DisplayName("Tests queries with two field CASE.")
1399+
void testCASETwoFields() throws SQLException {
1400+
final String tableName = "testCASETwoFields";
1401+
final BsonDocument doc1 = BsonDocument.parse("{\"_id\": 101,\n" +
1402+
"\"fieldA\": 1,\n" +
1403+
"\"fieldB\": 2}");
1404+
final BsonDocument doc2 = BsonDocument.parse("{\"_id\": 102,\n" +
1405+
"\"fieldA\": 2,\n" +
1406+
"\"fieldB\": 1}");
1407+
final BsonDocument doc3 = BsonDocument.parse("{\"_id\": 103,\n" +
1408+
"\"fieldA\": 1}");
1409+
insertBsonDocuments(tableName, DATABASE_NAME, USER, PASSWORD,
1410+
new BsonDocument[]{doc1, doc2, doc3});
1411+
final Statement statement = getDocumentDbStatement();
1412+
final ResultSet resultSet = statement.executeQuery(
1413+
String.format(
1414+
"SELECT CASE " +
1415+
"WHEN \"fieldA\" < \"fieldB\" THEN 'A' " +
1416+
"WHEN \"fieldA\" > \"fieldB\" THEN 'B' " +
1417+
"ELSE 'C' END FROM \"%s\".\"%s\"",
1418+
DATABASE_NAME, tableName));
1419+
Assertions.assertNotNull(resultSet);
1420+
Assertions.assertTrue(resultSet.next());
1421+
Assertions.assertEquals("A", resultSet.getString(1));
1422+
Assertions.assertTrue(resultSet.next());
1423+
Assertions.assertEquals("B", resultSet.getString(1));
1424+
Assertions.assertTrue(resultSet.next());
1425+
Assertions.assertEquals("C", resultSet.getString(1));
1426+
Assertions.assertFalse(resultSet.next());
1427+
}
1428+
12861429
protected static DocumentDbStatement getDocumentDbStatement() throws SQLException {
12871430
return getDocumentDbStatement(DocumentDbMetadataScanMethod.RANDOM);
12881431
}

0 commit comments

Comments
 (0)