Skip to content

Commit 5618214

Browse files
authored
feat: add type check functions support (#43)
1 parent 7971718 commit 5618214

6 files changed

Lines changed: 184 additions & 144 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
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.2.23</version>
32+
<version>0.2.24</version>
3333
</dependency>
3434

3535
```
@@ -163,6 +163,7 @@ db.updatePartial("Collection", user1.id, Map.of("lastName", "UpdatedPartially"),
163163
"desciption CONTAINS", "Project manager",// see cosmosdb CONTAINS
164164
"certificates IS_DEFINED", true, // see cosmosdb IS_DEFINED
165165
"families.villa IS_DEFINED", false, // see cosmosdb IS_DEFINED
166+
"age IS_NUMBER", true, // see cosmosdb type check functions
166167
"tagIds ARRAY_CONTAINS", "T001", // see cosmosdb ARRAY_CONTAINS
167168
"tagIds ARRAY_CONTAINS_ANY", List.of("T001", "T002"), // see cosmosdb EXISTS
168169
"tags ARRAY_CONTAINS_ALL name", List.of("Java", "React"), // see cosmosdb EXISTS

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.2.23</version>
7+
<version>0.2.24</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: 71 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package io.github.thunderz99.cosmos.condition;
22

3+
import java.util.*;
4+
import java.util.concurrent.atomic.AtomicInteger;
5+
import java.util.regex.Pattern;
6+
import java.util.stream.Collectors;
7+
38
import com.fasterxml.jackson.annotation.JsonGetter;
49
import com.fasterxml.jackson.annotation.JsonSetter;
510
import com.google.common.collect.Maps;
@@ -14,11 +19,6 @@
1419
import org.slf4j.Logger;
1520
import org.slf4j.LoggerFactory;
1621

17-
import java.util.*;
18-
import java.util.concurrent.atomic.AtomicInteger;
19-
import java.util.regex.Pattern;
20-
import java.util.stream.Collectors;
21-
2222
/**
2323
* Condition for find. (e.g. filter / sort / offset / limit)
2424
*/
@@ -46,66 +46,72 @@ public Condition() {
4646
*/
4747
public boolean crossPartition = false;
4848

49-
/**
50-
* whether this query is a negative query. (default to false)
51-
* <p>
52-
* this field represents the NOT operator in cosmos db
53-
* </p>
54-
* <p>
55-
* attention. this field will have no effect when this condition is a complete rawSql
56-
* </p>
57-
*/
58-
public boolean negative = false;
59-
60-
/**
61-
* a raw query spec which can use raw sql
62-
*/
63-
public SqlQuerySpec rawQuerySpec = null;
64-
65-
public static final String COND_SQL_TRUE = "1=1";
66-
public static final String COND_SQL_FALSE = "1=0";
67-
68-
/**
69-
* OperatorType for WHERE clause
70-
*
71-
* {@code
72-
* BINARY_OPERATORの例:{@code =, !=, >, >=, <, <= }
73-
* BINARY_FUCTIONの例: STARTSWITH, ENDSWITH, CONTAINS, ARRAY_CONTAINS
74-
* }
75-
*
76-
*/
77-
public enum OperatorType {
78-
BINARY_OPERATOR, BINARY_FUNCTION
79-
}
80-
81-
public static final Pattern simpleExpressionPattern = Pattern
82-
.compile("(.+)\\s(STARTSWITH|ENDSWITH|CONTAINS|ARRAY_CONTAINS|LIKE|IN|IS_DEFINED|=|!=|<|<=|>|>=)\\s*$");
83-
84-
public static final Pattern subQueryExpressionPattern = Pattern
85-
.compile("(.+)\\s(ARRAY_CONTAINS_ANY|ARRAY_CONTAINS_ALL)\\s*(.*)$");
86-
87-
/**
88-
* add filters
89-
* @param filters search filters using key / value pair.
90-
* @return condition
91-
*/
92-
public static Condition filter(Object... filters) {
93-
94-
Condition cond = new Condition();
95-
if (filters == null || filters.length == 0) {
96-
return cond;
97-
}
98-
99-
Checker.check(filters.length % 2 == 0, "filters must be key/value pairs like: \"lastName\", \"Banks\"");
100-
101-
for (int i = 0; i < filters.length; i++) {
102-
if (i % 2 == 0) {
103-
cond.filter.put(filters[i].toString(), filters[i + 1]);
104-
}
105-
}
106-
107-
return cond;
108-
}
49+
/**
50+
* whether this query is a negative query. (default to false)
51+
* <p>
52+
* this field represents the NOT operator in cosmos db
53+
* </p>
54+
* <p>
55+
* attention. this field will have no effect when this condition is a complete rawSql
56+
* </p>
57+
*/
58+
public boolean negative = false;
59+
60+
/**
61+
* a raw query spec which can use raw sql
62+
*/
63+
public SqlQuerySpec rawQuerySpec = null;
64+
65+
public static final String COND_SQL_TRUE = "1=1";
66+
public static final String COND_SQL_FALSE = "1=0";
67+
68+
/**
69+
* OperatorType for WHERE clause
70+
* <p>
71+
* {@code
72+
* BINARY_OPERATORの例:{@code =, !=, >, >=, <, <= }
73+
* BINARY_FUCTIONの例: STARTSWITH, ENDSWITH, CONTAINS, ARRAY_CONTAINS
74+
* }
75+
*/
76+
public enum OperatorType {
77+
BINARY_OPERATOR, BINARY_FUNCTION
78+
}
79+
80+
public static final String TYPE_CHECK_FUNCTIONS = "IS_ARRAY|IS_BOOL|IS_DEFINED|IS_NULL|IS_NUMBER|IS_OBJECT|IS_PRIMITIVE|IS_STRING";
81+
82+
83+
public static final Pattern simpleExpressionPattern = Pattern
84+
.compile("(.+)\\s(STARTSWITH|ENDSWITH|CONTAINS|ARRAY_CONTAINS|LIKE|IN|" + TYPE_CHECK_FUNCTIONS + "|=|!=|<|<=|>|>=)\\s*$");
85+
86+
public static final Pattern typeCheckFunctionPattern = Pattern
87+
.compile(TYPE_CHECK_FUNCTIONS);
88+
89+
public static final Pattern subQueryExpressionPattern = Pattern
90+
.compile("(.+)\\s(ARRAY_CONTAINS_ANY|ARRAY_CONTAINS_ALL)\\s*(.*)$");
91+
92+
/**
93+
* add filters
94+
*
95+
* @param filters search filters using key / value pair.
96+
* @return condition
97+
*/
98+
public static Condition filter(Object... filters) {
99+
100+
Condition cond = new Condition();
101+
if (filters == null || filters.length == 0) {
102+
return cond;
103+
}
104+
105+
Checker.check(filters.length % 2 == 0, "filters must be key/value pairs like: \"lastName\", \"Banks\"");
106+
107+
for (int i = 0; i < filters.length; i++) {
108+
if (i % 2 == 0) {
109+
cond.filter.put(filters[i].toString(), filters[i + 1]);
110+
}
111+
}
112+
113+
return cond;
114+
}
109115

110116
/**
111117
* set Orders in the following way. Overwrite previous orders.

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

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
package io.github.thunderz99.cosmos.condition;
22

3-
import com.microsoft.azure.documentdb.SqlParameterCollection;
4-
import com.microsoft.azure.documentdb.SqlQuerySpec;
5-
import io.github.thunderz99.cosmos.condition.Condition.OperatorType;
6-
import io.github.thunderz99.cosmos.util.Checker;
7-
import io.github.thunderz99.cosmos.util.JsonUtil;
8-
import org.apache.commons.lang3.RandomStringUtils;
9-
import org.apache.commons.lang3.StringUtils;
10-
113
import java.util.ArrayList;
124
import java.util.Collection;
135
import java.util.Set;
@@ -16,6 +8,14 @@
168
import java.util.stream.Collectors;
179
import java.util.stream.Stream;
1810

11+
import com.microsoft.azure.documentdb.SqlParameterCollection;
12+
import com.microsoft.azure.documentdb.SqlQuerySpec;
13+
import io.github.thunderz99.cosmos.condition.Condition.OperatorType;
14+
import io.github.thunderz99.cosmos.util.Checker;
15+
import io.github.thunderz99.cosmos.util.JsonUtil;
16+
import org.apache.commons.lang3.RandomStringUtils;
17+
import org.apache.commons.lang3.StringUtils;
18+
1919
/**
2020
* A class representing simple expression
2121
* <p>
@@ -117,14 +117,14 @@ public SqlQuerySpec toQuerySpec(AtomicInteger paramIndex) {
117117
paramIndex.getAndIncrement();
118118
// other types
119119
var formattedKey = Condition.getFormattedKey(this.key);
120-
if (this.type == OperatorType.BINARY_OPERATOR) { // operators, e.g. =, !=, <, >, LIKE
121-
//use c["key"] for cosmosdb reserved words
122-
ret.setQueryText(String.format(" (%s %s %s)", formattedKey, this.operator, paramName));
123-
} else if ("IS_DEFINED".equals(this.operator)) { // special func IS_DEFINED
124-
ret.setQueryText(String.format(" (%s(%s) = %s)", this.operator, formattedKey, paramName));
125-
} else { // other binary funcs. e.g. STARTSWITH, CONTAINS, ARRAY_CONTAINS
126-
ret.setQueryText(String.format(" (%s(%s, %s))", this.operator, formattedKey, paramName));
127-
}
120+
if (this.type == OperatorType.BINARY_OPERATOR) { // operators, e.g. =, !=, <, >, LIKE
121+
//use c["key"] for cosmosdb reserved words
122+
ret.setQueryText(String.format(" (%s %s %s)", formattedKey, this.operator, paramName));
123+
} else if (Condition.typeCheckFunctionPattern.asMatchPredicate().test(this.operator)) { // type check funcs: IS_DEFINED|IS_NUMBER|IS_PRIMITIVE, etc
124+
ret.setQueryText(String.format(" (%s(%s) = %s)", this.operator, formattedKey, paramName));
125+
} else { // other binary funcs. e.g. STARTSWITH, CONTAINS, ARRAY_CONTAINS
126+
ret.setQueryText(String.format(" (%s(%s, %s))", this.operator, formattedKey, paramName));
127+
}
128128

129129
params.add(Condition.createSqlParameter(paramName, paramValue));
130130
}

src/test/java/io/github/thunderz99/cosmos/CosmosDatabaseTest.java

Lines changed: 79 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package io.github.thunderz99.cosmos;
22

3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.Map;
6+
import java.util.Set;
7+
38
import com.microsoft.azure.documentdb.SqlParameter;
49
import com.microsoft.azure.documentdb.SqlParameterCollection;
510
import io.github.thunderz99.cosmos.condition.Aggregate;
@@ -12,11 +17,6 @@
1217
import org.slf4j.Logger;
1318
import org.slf4j.LoggerFactory;
1419

15-
import java.util.ArrayList;
16-
import java.util.List;
17-
import java.util.Map;
18-
import java.util.Set;
19-
2020
import static io.github.thunderz99.cosmos.condition.Condition.SubConditionType;
2121
import static org.assertj.core.api.Assertions.assertThat;
2222
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -662,22 +662,23 @@ void get_database_name_should_work() throws Exception {
662662

663663
@Test
664664
void dynamic_field_with_hyphen_should_work() throws Exception {
665-
var partition = "SheetConents";
665+
var partition = "SheetConents";
666666

667-
var id = "D001"; // form with content
668-
var formId = "829cc727-2d49-4d60-8f91-b30f50560af7"; //uuid
669-
var formContent = Map.of("name", "Tom", "sex", "Male", "address", "NY");
670-
var data = Map.of("id", id, formId, formContent, "sheet-2", Map.of("skills", Set.of("Java", "Python")));
667+
var id = "D001"; // form with content
668+
var age = 20;
669+
var formId = "829cc727-2d49-4d60-8f91-b30f50560af7"; //uuid
670+
var formContent = Map.of("name", "Tom", "sex", "Male", "address", "NY");
671+
var data = Map.of("id", id, "age", age, formId, formContent, "sheet-2", Map.of("skills", Set.of("Java", "Python")));
671672

672-
var id2 = "D002"; // form is empty
673-
var formContent2 = Map.of("name", "", "sex", "", "empty", true);
674-
var data2 = Map.of("id", id2, formId, formContent2);
673+
var id2 = "D002"; // form is empty
674+
var formContent2 = Map.of("name", "", "sex", "", "empty", true);
675+
var data2 = Map.of("id", id2, formId, formContent2);
675676

676-
var id3 = "D003"; // form is undefined
677-
var data3 = Map.of("id", id3);
677+
var id3 = "D003"; // form is undefined
678+
var data3 = Map.of("id", id3);
678679

679680

680-
try {
681+
try {
681682
db.upsert(coll, data, partition);
682683
db.upsert(coll, data2, partition);
683684
db.upsert(coll, data3, partition);
@@ -690,43 +691,68 @@ void dynamic_field_with_hyphen_should_work() throws Exception {
690691
assertThat(items).hasSize(1);
691692
var map = JsonUtil.toMap(JsonUtil.toJson(items.get(0).get(formId)));
692693
assertThat(map).containsEntry("name", "Tom").containsEntry("sex", "Male").containsEntry("address", "NY");
693-
}
694-
695-
{
696-
// IS_DEFINED = true
697-
var cond = Condition.filter("id", id, String.format("%s IS_DEFINED", formId), true);
698-
var items = db.find(coll, cond, partition).toMap();
699-
assertThat(items).hasSize(1);
700-
assertThat(items.get(0).get("id")).isEqualTo(id);
701-
}
702-
703-
{
704-
// IS_DEFINED = false
705-
var cond = Condition.filter("id", id, "test IS_DEFINED", false);
706-
var items = db.find(coll, cond, partition).toMap();
707-
assertThat(items).hasSize(1);
708-
assertThat(items.get(0).get("id")).isEqualTo(id);
709-
}
710-
711-
{
712-
// IS_DEFINED = false in OR condition. result: 2 item
713-
var cond = Condition.filter("id LIKE", "D00%", SubConditionType.SUB_COND_OR, List.of(
714-
Condition.filter(String.format("%s IS_DEFINED", formId), false),
715-
Condition.filter(String.format("%s.empty", formId), true)
716-
)).sort("id", "ASC");
717-
var items = db.find(coll, cond, partition).toMap();
718-
assertThat(items).hasSize(2);
719-
assertThat(items.get(0).get("id")).isEqualTo(id2);
720-
assertThat(items.get(1).get("id")).isEqualTo(id3);
721-
}
722-
723-
{
724-
// IS_DEFINED = false in OR condition. result: 1 item
725-
var cond = Condition.filter("id LIKE", "D00%", SubConditionType.SUB_COND_AND, List.of(
726-
Condition.filter(String.format("%s.name IS_DEFINED", formId), true),
727-
Condition.filter(String.format("%s.empty IS_DEFINED", formId), false)
728-
)).sort("id", "ASC");
729-
var items = db.find(coll, cond, partition).toMap();
694+
}
695+
696+
{
697+
// IS_DEFINED = true
698+
var cond = Condition.filter("id", id, String.format("%s IS_DEFINED", formId), true);
699+
var items = db.find(coll, cond, partition).toMap();
700+
assertThat(items).hasSize(1);
701+
assertThat(items.get(0).get("id")).isEqualTo(id);
702+
}
703+
704+
{
705+
// IS_DEFINED = false
706+
var cond = Condition.filter("id", id, "test IS_DEFINED", false);
707+
var items = db.find(coll, cond, partition).toMap();
708+
assertThat(items).hasSize(1);
709+
assertThat(items.get(0).get("id")).isEqualTo(id);
710+
}
711+
712+
{
713+
// IS_NUMBER = true
714+
var cond = Condition.filter("id", id, "age IS_NUMBER", true);
715+
var items = db.find(coll, cond, partition).toMap();
716+
assertThat(items).hasSize(1);
717+
assertThat(items.get(0).get("id")).isEqualTo(id);
718+
}
719+
{
720+
// IS_NUMBER = false
721+
var cond = Condition.filter("id", id, String.format("%s IS_NUMBER", formId), false);
722+
var items = db.find(coll, cond, partition).toMap();
723+
assertThat(items).hasSize(1);
724+
assertThat(items.get(0).get("id")).isEqualTo(id);
725+
}
726+
727+
{
728+
// use rawSql to implement IS_NUMBER
729+
var cond1 = Condition.filter("id", id);
730+
var cond2 = Condition.rawSql("IS_NUMBER(c.test) = false");
731+
var cond = Condition.filter(SubConditionType.SUB_COND_AND, List.of(cond1, cond2));
732+
var items = db.find(coll, cond, partition).toMap();
733+
assertThat(items).hasSize(1);
734+
assertThat(items.get(0).get("id")).isEqualTo(id);
735+
}
736+
737+
{
738+
// IS_DEFINED = false in OR condition. result: 2 item
739+
var cond = Condition.filter("id LIKE", "D00%", SubConditionType.SUB_COND_OR, List.of(
740+
Condition.filter(String.format("%s IS_DEFINED", formId), false),
741+
Condition.filter(String.format("%s.empty", formId), true)
742+
)).sort("id", "ASC");
743+
var items = db.find(coll, cond, partition).toMap();
744+
assertThat(items).hasSize(2);
745+
assertThat(items.get(0).get("id")).isEqualTo(id2);
746+
assertThat(items.get(1).get("id")).isEqualTo(id3);
747+
}
748+
749+
{
750+
// IS_DEFINED = false in OR condition. result: 1 item
751+
var cond = Condition.filter("id LIKE", "D00%", SubConditionType.SUB_COND_AND, List.of(
752+
Condition.filter(String.format("%s.name IS_DEFINED", formId), true),
753+
Condition.filter(String.format("%s.empty IS_DEFINED", formId), false)
754+
)).sort("id", "ASC");
755+
var items = db.find(coll, cond, partition).toMap();
730756
assertThat(items).hasSize(1);
731757
assertThat(items.get(0).get("id")).isEqualTo(id);
732758
}

0 commit comments

Comments
 (0)