Repository: googleapis/java-datastore
Pull request #993 (2023-02-15 21:40:13)

feat: OR query support
Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly:
- [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/java-datastore/issues/new/choose) before writing your code!  That way we can discuss the change, evaluate designs, and agree on the general idea
- [ ] Ensure the tests and linter pass
- [ ] Code coverage does not decrease (if any source code was changed)
- [ ] Appropriate docs were updated (if necessary)

Fixes #<issue_number_goes_here> ☕️

If you write sample code, please follow the [samples format](
https://github.com/GoogleCloudPlatform/java-docs-samples/blob/main/SAMPLE_FORMAT.md).



Patch:
diff --git a/README.md b/README.md
index 89f92fb3..650f21f7 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@ If you are using Maven without BOM, add this to your dependencies:
 <dependency>
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-datastore</artifactId>
-  <version>2.13.5</version>
+  <version>2.13.6</version>
 </dependency>
 
 ```
@@ -49,20 +49,20 @@ If you are using Maven without BOM, add this to your dependencies:
 If you are using Gradle 5.x or later, add this to your dependencies:
 
 ```Groovy
-implementation platform('com.google.cloud:libraries-bom:26.9.0')
+implementation platform('com.google.cloud:libraries-bom:26.10.0')
 
 implementation 'com.google.cloud:google-cloud-datastore'
 ```
 If you are using Gradle without BOM, add this to your dependencies:
 
 ```Groovy
-implementation 'com.google.cloud:google-cloud-datastore:2.13.5'
+implementation 'com.google.cloud:google-cloud-datastore:2.13.6'
 ```
 
 If you are using SBT, add this to your dependencies:
 
 ```Scala
-libraryDependencies += "com.google.cloud" % "google-cloud-datastore" % "2.13.5"
+libraryDependencies += "com.google.cloud" % "google-cloud-datastore" % "2.13.6"
 ```
 
 ## Authentication
diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/StructuredQuery.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/StructuredQuery.java
index b394dcd9..a6195ad2 100644
--- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/StructuredQuery.java
+++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/StructuredQuery.java
@@ -26,6 +26,7 @@ import static com.google.cloud.datastore.TimestampValue.of;
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.api.core.ApiFunction;
+import com.google.api.core.BetaApi;
 import com.google.api.core.InternalApi;
 import com.google.cloud.StringEnumType;
 import com.google.cloud.StringEnumValue;
@@ -151,6 +152,8 @@ public abstract class StructuredQuery<V> extends Query<V> implements RecordQuery
 
       static final Operator AND = type.createAndRegister("AND");
 
+      static final Operator OR = type.createAndRegister("OR");
+
       com.google.datastore.v1.CompositeFilter.Operator toPb() {
         return com.google.datastore.v1.CompositeFilter.Operator.valueOf(name());
       }
@@ -231,6 +234,11 @@ public abstract class StructuredQuery<V> extends Query<V> implements RecordQuery
       return new CompositeFilter(Operator.AND, first, other);
     }
 
+    @BetaApi
+    public static CompositeFilter or(Filter first, Filter... other) {
+      return new CompositeFilter(Operator.OR, first, other);
+    }
+
     @Override
     com.google.datastore.v1.Filter toPb() {
       com.google.datastore.v1.Filter.Builder filterPb = com.google.datastore.v1.Filter.newBuilder();
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index a381705d..bcfc25e9 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -29,7 +29,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-datastore</artifactId>
-      <version>2.13.5</version>
+      <version>2.13.6</version>
     </dependency>
     <!-- [END datastore_install_without_bom] -->
 
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index fa69d98a..a17839a0 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -28,7 +28,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-datastore</artifactId>
-      <version>2.13.5</version>
+      <version>2.13.6</version>
     </dependency>
 
     <dependency>


Test patch:
diff --git a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/SerializationTest.java b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/SerializationTest.java
index 10538651..1b8186a5 100644
--- a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/SerializationTest.java
+++ b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/SerializationTest.java
@@ -69,6 +69,19 @@ public class SerializationTest extends BaseSerializationTest {
           .addDistinctOn("p")
           .addOrderBy(OrderBy.asc("p"))
           .build();
+  private static final Query<ProjectionEntity> QUERY4 =
+      Query.newProjectionEntityQueryBuilder()
+          .setKind("k")
+          .setNamespace("ns1")
+          .addProjection("p")
+          .setLimit(100)
+          .setOffset(5)
+          .setStartCursor(CURSOR1)
+          .setEndCursor(CURSOR2)
+          .setFilter(CompositeFilter.or(PropertyFilter.gt("p1", 10), PropertyFilter.eq("a", "v")))
+          .addDistinctOn("p")
+          .addOrderBy(OrderBy.asc("p"))
+          .build();
   private static final KeyValue KEY_VALUE = KeyValue.of(KEY1);
   private static final NullValue NULL_VALUE =
       NullValue.newBuilder().setExcludeFromIndexes(true).build();
@@ -136,6 +149,7 @@ public class SerializationTest extends BaseSerializationTest {
       QUERY1,
       QUERY2,
       QUERY3,
+      QUERY4,
       NULL_VALUE,
       KEY_VALUE,
       STRING_VALUE,
diff --git a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/StructuredQueryTest.java b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/StructuredQueryTest.java
index b3f4b944..c5933758 100644
--- a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/StructuredQueryTest.java
+++ b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/StructuredQueryTest.java
@@ -37,8 +37,10 @@ public class StructuredQueryTest {
   private static final Cursor END_CURSOR = Cursor.copyFrom(new byte[] {10});
   private static final int OFFSET = 42;
   private static final Integer LIMIT = 43;
-  private static final Filter FILTER =
+  private static final Filter AND_FILTER =
       CompositeFilter.and(PropertyFilter.gt("p1", 10), PropertyFilter.eq("a", "v"));
+  private static final Filter OR_FILTER =
+      CompositeFilter.or(PropertyFilter.gt("p1", 10), PropertyFilter.eq("a", "v"));
   private static final OrderBy ORDER_BY_1 = OrderBy.asc("p2");
   private static final OrderBy ORDER_BY_2 = OrderBy.desc("p3");
   private static final List<OrderBy> ORDER_BY = ImmutableList.of(ORDER_BY_1, ORDER_BY_2);
@@ -56,7 +58,7 @@ public class StructuredQueryTest {
           .setEndCursor(END_CURSOR)
           .setOffset(OFFSET)
           .setLimit(LIMIT)
-          .setFilter(FILTER)
+          .setFilter(AND_FILTER)
           .setOrderBy(ORDER_BY_1, ORDER_BY_2)
           .build();
   private static final KeyQuery KEY_QUERY =
@@ -67,7 +69,7 @@ public class StructuredQueryTest {
           .setEndCursor(END_CURSOR)
           .setOffset(OFFSET)
           .setLimit(LIMIT)
-          .setFilter(FILTER)
+          .setFilter(OR_FILTER)
           .setOrderBy(ORDER_BY_1, ORDER_BY_2)
           .build();
   private static final ProjectionEntityQuery PROJECTION_QUERY =
@@ -78,7 +80,7 @@ public class StructuredQueryTest {
           .setEndCursor(END_CURSOR)
           .setOffset(OFFSET)
           .setLimit(LIMIT)
-          .setFilter(FILTER)
+          .setFilter(AND_FILTER)
           .setOrderBy(ORDER_BY_1, ORDER_BY_2)
           .setProjection(PROJECTION1, PROJECTION2)
           .setDistinctOn(DISTINCT_ON1, DISTINCT_ON2)
@@ -93,7 +95,14 @@ public class StructuredQueryTest {
 
   @Test
   public void testKeyQueryBuilder() {
-    compareBaseBuilderFields(KEY_QUERY);
+    assertEquals(NAMESPACE, KEY_QUERY.getNamespace());
+    assertEquals(KIND, KEY_QUERY.getKind());
+    assertEquals(START_CURSOR, KEY_QUERY.getStartCursor());
+    assertEquals(END_CURSOR, KEY_QUERY.getEndCursor());
+    assertEquals(OFFSET, KEY_QUERY.getOffset());
+    assertEquals(LIMIT, KEY_QUERY.getLimit());
+    assertEquals(OR_FILTER, KEY_QUERY.getFilter());
+    assertEquals(ORDER_BY, KEY_QUERY.getOrderBy());
     assertEquals(ImmutableList.of(StructuredQuery.KEY_PROPERTY_NAME), KEY_QUERY.getProjection());
     assertTrue(KEY_QUERY.getDistinctOn().isEmpty());
   }
@@ -112,7 +121,7 @@ public class StructuredQueryTest {
     assertEquals(END_CURSOR, query.getEndCursor());
     assertEquals(OFFSET, query.getOffset());
     assertEquals(LIMIT, query.getLimit());
-    assertEquals(FILTER, query.getFilter());
+    assertEquals(AND_FILTER, query.getFilter());
     assertEquals(ORDER_BY, query.getOrderBy());
   }
 
diff --git a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreTest.java b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreTest.java
index b8c3bb4b..f010d813 100644
--- a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreTest.java
+++ b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreTest.java
@@ -59,6 +59,7 @@ import com.google.cloud.datastore.QueryResults;
 import com.google.cloud.datastore.ReadOption;
 import com.google.cloud.datastore.StringValue;
 import com.google.cloud.datastore.StructuredQuery;
+import com.google.cloud.datastore.StructuredQuery.CompositeFilter;
 import com.google.cloud.datastore.StructuredQuery.OrderBy;
 import com.google.cloud.datastore.StructuredQuery.PropertyFilter;
 import com.google.cloud.datastore.TimestampValue;
@@ -223,6 +224,85 @@ public class ITDatastoreTest {
     return results;
   }
 
+  @Test
+  public void orQuery() {
+    Key key = Key.newBuilder(KEY1, KIND2, 2).build();
+    Entity entity3 =
+        Entity.newBuilder(ENTITY1)
+            .setKey(key)
+            .remove("str")
+            .set("name", "Dan")
+            .setNull("null")
+            .set("age", 19)
+            .build();
+    DATASTORE.put(entity3);
+
+    // age == 19 || age == 20
+    CompositeFilter orFilter =
+        CompositeFilter.or(PropertyFilter.eq("age", 19), PropertyFilter.eq("age", 20));
+    Query<Entity> simpleOrQuery =
+        Query.newEntityQueryBuilder()
+            .setNamespace(NAMESPACE)
+            .setKind(KIND2)
+            .setFilter(orFilter)
+            .build();
+    QueryResults<Entity> results = DATASTORE.run(simpleOrQuery);
+    assertTrue(results.hasNext());
+    assertEquals(ENTITY2, results.next());
+    assertTrue(results.hasNext());
+    assertEquals(entity3, results.next());
+    assertFalse(results.hasNext());
+
+    // age == 19 || age == 20 with limit of 1
+    Query<Entity> simpleOrQueryLimit =
+        Query.newEntityQueryBuilder()
+            .setNamespace(NAMESPACE)
+            .setKind(KIND2)
+            .setFilter(orFilter)
+            .setLimit(1)
+            .build();
+    QueryResults<Entity> results2 = DATASTORE.run(simpleOrQueryLimit);
+    assertTrue(results2.hasNext());
+    assertEquals(ENTITY2, results2.next());
+    assertFalse(results2.hasNext());
+
+    // (age == 18 && name == Dan) || (age == 20 && name == Dan)
+    CompositeFilter nestedOr =
+        CompositeFilter.or(
+            CompositeFilter.and(PropertyFilter.eq("age", 18), PropertyFilter.eq("name", "Dan")),
+            CompositeFilter.and(PropertyFilter.eq("age", 20), PropertyFilter.eq("name", "Dan")));
+    CompositeFilter compositeFilter =
+        CompositeFilter.and(PropertyFilter.hasAncestor(ROOT_KEY), nestedOr);
+    Query<Entity> orQueryNested =
+        Query.newEntityQueryBuilder()
+            .setNamespace(NAMESPACE)
+            .setKind(KIND2)
+            .setFilter(compositeFilter)
+            .build();
+    QueryResults<Entity> results3 = DATASTORE.run(orQueryNested);
+    assertTrue(results3.hasNext());
+    assertEquals(ENTITY2, results3.next());
+    assertFalse(results3.hasNext());
+
+    // age == 20 && (name == Bob || name == Dan)
+    CompositeFilter nestedOr2 =
+        CompositeFilter.or(PropertyFilter.eq("name", "Dan"), PropertyFilter.eq("name", "Bob"));
+    CompositeFilter andFilter = CompositeFilter.and(PropertyFilter.eq("age", 20), nestedOr2);
+    CompositeFilter ancestorAndFilter =
+        CompositeFilter.and(PropertyFilter.hasAncestor(ROOT_KEY), andFilter);
+    Query<Entity> orQueryNested2 =
+        Query.newEntityQueryBuilder()
+            .setNamespace(NAMESPACE)
+            .setKind(KIND2)
+            .setFilter(ancestorAndFilter)
+            .setLimit(1)
+            .build();
+    QueryResults<Entity> results4 = DATASTORE.run(orQueryNested2);
+    assertTrue(results4.hasNext());
+    assertEquals(ENTITY2, results4.next());
+    assertFalse(results4.hasNext());
+  }
+
   @Test
   public void testNewTransactionCommit() {
     Transaction transaction = DATASTORE.newTransaction();
@@ -946,6 +1026,21 @@ public class ITDatastoreTest {
     assertEquals(e2, resultNeq.next());
     assertFalse(resultNeq.hasNext());
 
+    Query<Entity> scQueryInEqOr =
+        Query.newEntityQueryBuilder()
+            .setKind(KIND1)
+            .setFilter(
+                CompositeFilter.or(
+                    PropertyFilter.in("v_int", ListValue.of(10, 50000)),
+                    PropertyFilter.eq("v_int", 10000)))
+            .build();
+
+    QueryResults<Entity> run = DATASTORE.run(scQueryInEqOr);
+
+    assertTrue(run.hasNext());
+    assertEquals(e1, run.next());
+    assertFalse(run.hasNext());
+
     DATASTORE.delete(e1.getKey());
     DATASTORE.delete(e2.getKey());
   }