Skip to content

Commit a3fb4b0

Browse files
authored
feat: support $NOT, $AND, $OR nested filters (#47)
* feat: support , , nested filters * feat: support , , nested filters. add README
1 parent 772cbfb commit a3fb4b0

11 files changed

Lines changed: 531 additions & 212 deletions

File tree

README.md

Lines changed: 91 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ java-cosmos is a client for Azure CosmosDB 's SQL API (also called documentdb fo
2929
<dependency>
3030
<groupId>com.github.thunderz99</groupId>
3131
<artifactId>java-cosmos</artifactId>
32-
<version>0.4.1</version>
32+
<version>0.4.2</version>
3333
</dependency>
3434

3535
```
@@ -167,17 +167,24 @@ db.updatePartial("Container", user1.id, Map.of("lastName", "UpdatedPartially"),
167167
"tagIds ARRAY_CONTAINS", "T001", // see cosmosdb ARRAY_CONTAINS
168168
"tagIds ARRAY_CONTAINS_ANY", List.of("T001", "T002"), // see cosmosdb EXISTS
169169
"tags ARRAY_CONTAINS_ALL name", List.of("Java", "React"), // see cosmosdb EXISTS
170-
"SUB_COND_OR", List.of( // add an OR sub condition
170+
"$OR", List.of( // add an OR sub condition
171171
Condition.filter("position", "leader"), // subquery's fields/order/offset/limit will be ignored
172172
Condition.filter("organization.id", "executive_committee")
173173
),
174-
"SUB_COND_OR 2", List.of( // add another OR sub condition (name it SUB_COND_OR xxx in order to avoid the same key to a previous SUB_COND_OR )
174+
"$OR 2", List.of( // add another OR sub condition (name it $OR xxx in order to avoid the same key to a previous $OR )
175175
Condition.filter("position", "leader"), // subquery's fields/order/offset/limit will be ignored
176176
Condition.filter("organization.id", "executive_committee")
177177
),
178-
"SUB_COND_AND", List.of(
178+
"$AND", List.of(
179179
Condition.filter("tagIds ARRAY_CONTAINS_ALL", List.of("T001", "T002")).not() // A negative condition. see cosmosdb NOT
180180
Condition.filter("city", "Tokyo")
181+
),
182+
"$NOT", Map.of("lastName CONTAINS", "Willington"), // A negative query using $NOT
183+
"$NOT 2", Map.of("$OR 3", // A nested filter using $NOT and $OR
184+
List.of(
185+
Map.of("lastName", ""), // note they will do the same thing using Condition.filter or Map.of
186+
Map.of("age >=", 20)
187+
)
181188
)
182189
)
183190
.fields("id", "lastName", "age", "organization.name") // select certain fields
@@ -251,12 +258,12 @@ db.updatePartial("Container", user1.id, Map.of("lastName", "UpdatedPartially"),
251258
var params = new SqlParameterCollection(new SqlParameter("@state", "%NY%"));
252259

253260
var cond = Condition.filter(
254-
"SUB_COND_AND",
261+
"$AND",
255262
List.of(
256263
Condition.filter("gender", "female"),
257264
Condition.rawSql("c.state LIKE @state", params)
258265
),
259-
"SUB_COND_AND another", // name it SUB_COND_OR xxx in order to avoid the same key to a previous SUB_COND_AND
266+
"$AND another", // name it $OR xxx in order to avoid the same key to a previous $AND
260267
List.of(
261268
Condition.filter("age > ", "22"),
262269
Condition.rawSql("c.address != \"\" ")
@@ -268,3 +275,81 @@ db.updatePartial("Container", user1.id, Map.of("lastName", "UpdatedPartially"),
268275

269276
```
270277

278+
279+
280+
### Queries using json as a filter
281+
282+
Support the following operators in order to implement nested filter queries using JSON, especially for a rest api.
283+
284+
285+
#### $NOT example
286+
287+
```
288+
289+
var queryJson="""
290+
{
291+
"$NOT":
292+
{
293+
"address.state": "WA"
294+
},
295+
"$NOT 2": {
296+
"id": "AndersenFamily"
297+
}
298+
}
299+
"""
300+
301+
var cond = new Condition(JsonUtil.toMap(queryJson)).sort("id", "ASC");
302+
var items = db.find(conName, cond, partition);
303+
304+
```
305+
306+
#### $OR example
307+
308+
```
309+
{
310+
"$OR": [
311+
{
312+
"address.state": "WA"
313+
},
314+
{
315+
"id": "WakefieldFamily"
316+
}
317+
]
318+
}
319+
```
320+
321+
#### $AND example
322+
323+
```
324+
{
325+
"$AND": {
326+
"address.state": "WA",
327+
"lastName": "Andersen"
328+
}
329+
}
330+
```
331+
332+
#### nested example
333+
334+
```
335+
{
336+
"$AND" : [
337+
{
338+
"$OR": [
339+
{
340+
"address.state": "WA"
341+
},
342+
{
343+
"id": "WakefieldFamily"
344+
}
345+
]
346+
},
347+
{
348+
"$NOT": {
349+
"creationDate =": 1431620472
350+
}
351+
}
352+
]
353+
}
354+
355+
```

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<groupId>com.github.thunderz99</groupId>
55
<artifactId>java-cosmos</artifactId>
66
<packaging>jar</packaging>
7-
<version>0.4.1</version>
7+
<version>0.4.2</version>
88
<name>${project.groupId}:${project.artifactId}$</name>
99
<description>A lightweight Azure CosmosDB client for Java</description>
1010
<url>https://github.com/thunderz99/java-cosmos</url>

src/main/java/io/github/thunderz99/cosmos/condition/Condition.java

Lines changed: 94 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,36 @@
2323
*/
2424
public class Condition {
2525

26-
private static Logger log = LoggerFactory.getLogger(Condition.class);
26+
private static Logger log = LoggerFactory.getLogger(Condition.class);
2727

28-
/**
29-
* Default constructor
30-
*/
31-
public Condition() {
32-
}
28+
/**
29+
* Default constructor
30+
*/
31+
public Condition() {
32+
}
3333

34-
public Map<String, Object> filter = new LinkedHashMap<>();
34+
/**
35+
* A constructor accepting a map as filter
36+
*
37+
* @param filter
38+
*/
39+
public Condition(Map<String, Object> filter) {
40+
this.filter = filter;
41+
}
3542

36-
public List<String> sort = List.of();
43+
public Map<String, Object> filter = new LinkedHashMap<>();
3744

38-
public Set<String> fields = new LinkedHashSet<>();
45+
public List<String> sort = List.of();
3946

40-
public int offset = 0;
41-
public int limit = 100;
47+
public Set<String> fields = new LinkedHashSet<>();
4248

43-
/**
44-
* whether this query is cross-partition or not (default to false)
45-
*/
46-
public boolean crossPartition = false;
49+
public int offset = 0;
50+
public int limit = 100;
51+
52+
/**
53+
* whether this query is cross-partition or not (default to false)
54+
*/
55+
public boolean crossPartition = false;
4756

4857
/**
4958
* whether this query is a negative query. (default to false)
@@ -346,23 +355,30 @@ FilterQuery generateFilterQuery(String selectPart, List<SqlParameter> params,
346355

347356
var subFilterQueryToAdd = "";
348357

349-
if (entry.getKey().startsWith(SubConditionType.SUB_COND_AND.name())) {
358+
if (entry.getKey().startsWith(SubConditionType.AND)) {
350359
// sub query AND
351-
var subQueries = extractSubQueries(entry.getValue());
352-
subFilterQueryToAdd = generateFilterQuery4List(subQueries, "AND", params, conditionIndex, paramIndex);
353-
354-
} else if (entry.getKey().startsWith(SubConditionType.SUB_COND_OR.name())) {
355-
// sub query OR
356-
var subQueries = extractSubQueries(entry.getValue());
357-
subFilterQueryToAdd = generateFilterQuery4List(subQueries, "OR", params, conditionIndex, paramIndex);
358-
359-
} else {
360-
// normal expression
361-
var exp = parse(entry.getKey(), entry.getValue());
362-
var expQuerySpec = exp.toQuerySpec(paramIndex);
363-
subFilterQueryToAdd = expQuerySpec.getQueryText();
364-
params.addAll(expQuerySpec.getParameters());
365-
}
360+
var subQueries = extractSubQueries(entry.getValue());
361+
subFilterQueryToAdd = generateFilterQuery4List(subQueries, "AND", params, conditionIndex, paramIndex);
362+
363+
} else if (entry.getKey().startsWith(SubConditionType.OR)) {
364+
// sub query OR
365+
var subQueries = extractSubQueries(entry.getValue());
366+
subFilterQueryToAdd = generateFilterQuery4List(subQueries, "OR", params, conditionIndex, paramIndex);
367+
368+
} else if (entry.getKey().startsWith(SubConditionType.NOT)) {
369+
// sub query NOT
370+
var subQueries = extractSubQueries(entry.getValue());
371+
var subQueryWithNot = Condition.filter(SubConditionType.AND, subQueries).not();
372+
// recursively generate the filterQuery with negative flag true
373+
var filterQueryWithNot = subQueryWithNot.generateFilterQuery("", params, conditionIndex, paramIndex);
374+
subFilterQueryToAdd = " " + removeConnectPart(filterQueryWithNot.queryText.toString());
375+
} else {
376+
// normal expression
377+
var exp = parse(entry.getKey(), entry.getValue());
378+
var expQuerySpec = exp.toQuerySpec(paramIndex);
379+
subFilterQueryToAdd = expQuerySpec.getQueryText();
380+
params.addAll(expQuerySpec.getParameters());
381+
}
366382

367383
if (StringUtils.isNotEmpty(subFilterQueryToAdd)) {
368384
queryTexts.add(subFilterQueryToAdd);
@@ -384,32 +400,57 @@ FilterQuery generateFilterQuery(String selectPart, List<SqlParameter> params,
384400
return new FilterQuery(queryText, params, conditionIndex, paramIndex);
385401
}
386402

387-
/**
388-
* add negative NOT operator for queryText, if not empty
389-
*
390-
* @param queryText
391-
* @param negative
392-
* @return
393-
*/
394-
static String processNegativeQuery(String queryText, boolean negative) {
395-
return negative && StringUtils.isNotEmpty(queryText) ?
396-
" NOT(" + queryText + ")" : queryText;
397-
}
403+
/**
404+
* add negative NOT operator for queryText, if not empty
405+
*
406+
* @param queryText
407+
* @param negative
408+
* @return
409+
*/
410+
static String processNegativeQuery(String queryText, boolean negative) {
411+
return negative && StringUtils.isNotEmpty(queryText) ?
412+
" NOT(" + queryText + ")" : queryText;
413+
}
398414

399-
/**
400-
* extract subQueries for SUB_COND_AND / SUB_COND_OR 's filter value
401-
*
402-
* @param value
403-
*/
404-
static List<Condition> extractSubQueries(Object value) {
405-
if (value == null) {
406-
return List.of();
407-
}
415+
/**
416+
* extract subQuery for SUB_COND_AND / SUB_COND_OR 's filter value, single condition only.
417+
*
418+
* @param value
419+
*/
420+
static Condition extractSubQuery(Object value) {
421+
if (value == null) {
422+
return null;
423+
}
408424

409425
if (value instanceof Condition) {
410-
return List.of((Condition) value);
426+
// single condition
427+
return (Condition) value;
428+
} else if (value instanceof Map<?, ?>) {
429+
// single condition in the form of map
430+
return new Condition(JsonUtil.toMap(value));
431+
} else if (value instanceof Collection<?>) {
432+
throw new IllegalArgumentException("Cannot convert input to a single condition. Ensure the input is a single value(not a collection)." + value);
433+
}
434+
throw new IllegalArgumentException("Invalid input. expect a condition or a map. " + value);
435+
}
436+
437+
/**
438+
* extract subQueries for SUB_COND_AND / SUB_COND_OR 's filter value
439+
*
440+
* @param value
441+
*/
442+
static List<Condition> extractSubQueries(Object value) {
443+
if (value == null) {
444+
return List.of();
445+
}
446+
447+
if (value instanceof Condition || value instanceof Map<?, ?>) {
448+
// single condition
449+
return List.of(extractSubQuery(value));
411450
} else if (value instanceof List<?>) {
412-
return (List<Condition>) value;
451+
// multi condition
452+
var listValue = (List<Object>) value;
453+
return listValue.stream().map(v -> extractSubQuery(v)).filter(Objects::nonNull).collect(Collectors.toList());
413454
}
414455

415456
return List.of();
@@ -665,17 +706,6 @@ public static Condition rawSql(String queryText) {
665706
return cond;
666707
}
667708

668-
/**
669-
* sub query 's OR / RAW operator
670-
*
671-
* <p>
672-
* TODO SUB_COND_NOT operator
673-
* </p>
674-
*/
675-
public enum SubConditionType {
676-
SUB_COND_AND, SUB_COND_OR
677-
}
678-
679709
/**
680710
* Instead of c.key, return c["key"] or c["key1"]["key2"] for query. In order for cosmosdb reserved words
681711
*
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.github.thunderz99.cosmos.condition;
2+
3+
/**
4+
* sub query 's OR / AND / NOT operator
5+
*/
6+
public class SubConditionType {
7+
8+
public static final String AND = "$AND";
9+
public static final String OR = "$OR";
10+
public static final String NOT = "$NOT";
11+
12+
}

0 commit comments

Comments
 (0)