Skip to content

Commit cbb08fd

Browse files
committed
WIP
1 parent 67c169a commit cbb08fd

File tree

4 files changed

+268
-17
lines changed

4 files changed

+268
-17
lines changed

document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java

Lines changed: 200 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,20 @@
6262
import static org.junit.jupiter.api.Assertions.assertThrows;
6363
import static org.junit.jupiter.api.Assertions.assertTrue;
6464

65+
import com.fasterxml.jackson.core.JsonProcessingException;
6566
import com.fasterxml.jackson.databind.JsonNode;
6667
import com.fasterxml.jackson.databind.ObjectMapper;
6768
import com.typesafe.config.Config;
6869
import com.typesafe.config.ConfigFactory;
6970
import java.io.IOException;
7071
import java.util.HashMap;
72+
import java.util.HashSet;
7173
import java.util.Iterator;
7274
import java.util.List;
7375
import java.util.Map;
7476
import java.util.Optional;
7577
import java.util.Random;
78+
import java.util.Set;
7679
import java.util.Spliterator;
7780
import java.util.UUID;
7881
import java.util.concurrent.Callable;
@@ -85,10 +88,12 @@
8588
import org.hypertrace.core.documentstore.commons.DocStoreConstants;
8689
import org.hypertrace.core.documentstore.expression.impl.AggregateExpression;
8790
import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression;
91+
import org.hypertrace.core.documentstore.expression.impl.ArrayIdentifierExpression;
8892
import org.hypertrace.core.documentstore.expression.impl.ArrayRelationalFilterExpression;
8993
import org.hypertrace.core.documentstore.expression.impl.ConstantExpression;
9094
import org.hypertrace.core.documentstore.expression.impl.FunctionExpression;
9195
import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression;
96+
import org.hypertrace.core.documentstore.expression.impl.JsonArrayIdentifierExpression;
9297
import org.hypertrace.core.documentstore.expression.impl.JsonIdentifierExpression;
9398
import org.hypertrace.core.documentstore.expression.impl.KeyExpression;
9499
import org.hypertrace.core.documentstore.expression.impl.LogicalExpression;
@@ -206,7 +211,8 @@ private static void createFlatCollectionSchema(
206211
+ "\"sales\" JSONB,"
207212
+ "\"numbers\" INTEGER[],"
208213
+ "\"scores\" DOUBLE PRECISION[],"
209-
+ "\"flags\" BOOLEAN[]"
214+
+ "\"flags\" BOOLEAN[],"
215+
+ "\"attributes\" JSONB"
210216
+ ");",
211217
collectionName);
212218

@@ -4362,6 +4368,199 @@ void testJsonbNumericComparisonOperators(String dataStoreName) {
43624368
}
43634369
}
43644370

4371+
@Nested
4372+
class FlatCollectionArrayBehaviourTest {
4373+
4374+
/**
4375+
* Test EXISTS filter on top-level arrays. It should only return arrays that are non-empty (have
4376+
* at-least one element)
4377+
*/
4378+
@ParameterizedTest
4379+
@ArgumentsSource(PostgresProvider.class)
4380+
void testExistsFilterOnArray(String dataStoreName) throws JsonProcessingException {
4381+
Datastore datastore = datastoreMap.get(dataStoreName);
4382+
Collection flatCollection =
4383+
datastore.getCollectionForType(FLAT_COLLECTION_NAME, DocumentType.FLAT);
4384+
4385+
// Query using EXISTS on array field (simulating ArrayIdentifierExpression behavior)
4386+
// tags column has: NULL (row 9), empty '{}' (rows 10, 11, 13), non-empty (rows 1-8, 12, 14)
4387+
// Using EXISTS with 'null' parameter (matching entity-service pattern)
4388+
Query query =
4389+
Query.builder()
4390+
.addSelection(IdentifierExpression.of("item"))
4391+
.addSelection(IdentifierExpression.of("tags"))
4392+
.setFilter(
4393+
RelationalExpression.of(
4394+
ArrayIdentifierExpression.of("tags"), EXISTS, ConstantExpression.of("null")))
4395+
.build();
4396+
4397+
Iterator<Document> results = flatCollection.find(query);
4398+
4399+
int count = 0;
4400+
while (results.hasNext()) {
4401+
Document doc = results.next();
4402+
JsonNode json = new ObjectMapper().readTree(doc.toJson());
4403+
count++;
4404+
// Verify that ALL returned documents have non-empty arrays
4405+
JsonNode tags = json.get("tags");
4406+
assertTrue(
4407+
tags.isArray() && !tags.isEmpty(), "tags should be non-empty array, but was: " + tags);
4408+
}
4409+
4410+
// Should return only documents with non-empty arrays
4411+
// From test data: rows 1-8 have non-empty arrays (8 docs)
4412+
// Plus rows 12, 14 have non-empty arrays (2 docs)
4413+
// Total: 10 documents
4414+
assertEquals(10, count, "Should return a total of 10 docs that have non-empty tags");
4415+
}
4416+
4417+
/**
4418+
* Test NOT_EXISTS filter with ArrayIdentifierExpression. This validates that NOT_EXISTS on
4419+
* array fields returns both NULL and empty arrays, excluding only non-empty arrays.
4420+
*/
4421+
@ParameterizedTest
4422+
@ArgumentsSource(PostgresProvider.class)
4423+
void testNotExistsFilterOnArrays(String dataStoreName) throws JsonProcessingException {
4424+
Datastore datastore = datastoreMap.get(dataStoreName);
4425+
Collection flatCollection =
4426+
datastore.getCollectionForType(FLAT_COLLECTION_NAME, DocumentType.FLAT);
4427+
4428+
// Query using NOT_EXISTS on array field (simulating ArrayIdentifierExpression behavior)
4429+
// Using NOT_EXISTS with 'null' parameter (matching entity-service pattern)
4430+
Query query =
4431+
Query.builder()
4432+
.addSelection(IdentifierExpression.of("item"))
4433+
.addSelection(IdentifierExpression.of("tags"))
4434+
.setFilter(
4435+
RelationalExpression.of(
4436+
ArrayIdentifierExpression.of("tags"),
4437+
NOT_EXISTS,
4438+
ConstantExpression.of("null")))
4439+
.build();
4440+
4441+
Iterator<Document> results = flatCollection.find(query);
4442+
4443+
int count = 0;
4444+
while (results.hasNext()) {
4445+
Document doc = results.next();
4446+
JsonNode json = new ObjectMapper().readTree(doc.toJson());
4447+
count++;
4448+
// Verify that ALL returned documents have NULL or empty arrays
4449+
JsonNode tags = json.get("tags");
4450+
assertTrue(
4451+
tags == null || !tags.isArray() || tags.isEmpty(),
4452+
"tags should be NULL or empty array, but was: " + tags);
4453+
}
4454+
4455+
// Should return documents with NULL or empty arrays
4456+
// From test data: row 9 (NULL), rows 10, 11, 13 (empty arrays)
4457+
// Total: 4 documents
4458+
assertEquals(4, count, "Should return at 4 documents with NULL or empty tags");
4459+
}
4460+
4461+
/**
4462+
* Test EXISTS filter on JSONB arrays. Should only return non-empty arrays (with at-least one
4463+
* element).
4464+
*/
4465+
@ParameterizedTest
4466+
@ArgumentsSource(PostgresProvider.class)
4467+
void testExistsFilterOnJsonArrays(String dataStoreName) throws JsonProcessingException {
4468+
Datastore datastore = datastoreMap.get(dataStoreName);
4469+
Collection flatCollection =
4470+
datastore.getCollectionForType(FLAT_COLLECTION_NAME, DocumentType.FLAT);
4471+
4472+
// Query using EXISTS on JSONB array field
4473+
// attributes.certifications has: non-empty (row 1), empty (rows 2, 10, 11), NULL (rest)
4474+
Query query =
4475+
Query.builder()
4476+
.addSelection(IdentifierExpression.of("item"))
4477+
.addSelection(JsonIdentifierExpression.of("attributes", "certifications"))
4478+
.setFilter(
4479+
RelationalExpression.of(
4480+
JsonArrayIdentifierExpression.of("attributes", "certifications"),
4481+
EXISTS,
4482+
ConstantExpression.of("null")))
4483+
.build();
4484+
4485+
Iterator<Document> results = flatCollection.find(query);
4486+
4487+
int count = 0;
4488+
while (results.hasNext()) {
4489+
Document doc = results.next();
4490+
JsonNode json = new ObjectMapper().readTree(doc.toJson());
4491+
count++;
4492+
4493+
// Verify that ALL returned documents have non-empty arrays in attributes.certifications
4494+
JsonNode attributes = json.get("attributes");
4495+
assertTrue(attributes.isObject(), "attributes should be a JSON object");
4496+
4497+
JsonNode certifications = attributes.get("certifications");
4498+
assertTrue(
4499+
certifications.isArray() && !certifications.isEmpty(),
4500+
"certifications should be non-empty array, but was: " + certifications);
4501+
}
4502+
4503+
// Should return only row 1 which has non-empty certifications array
4504+
assertEquals(1, count, "Should return exactly 1 document with non-empty certifications");
4505+
}
4506+
4507+
/**
4508+
* Test NOT_EXISTS filter on JSONB arrays. This validates that NOT_EXISTS on array fields inside
4509+
* JSONB returns documents where the field is NULL, the parent object is NULL, or the array is
4510+
* empty.
4511+
*/
4512+
@ParameterizedTest
4513+
@ArgumentsSource(PostgresProvider.class)
4514+
void testNotExistsFilterOnJsonArrays(String dataStoreName) throws JsonProcessingException {
4515+
Datastore datastore = datastoreMap.get(dataStoreName);
4516+
Collection flatCollection =
4517+
datastore.getCollectionForType(FLAT_COLLECTION_NAME, DocumentType.FLAT);
4518+
4519+
// Query using NOT_EXISTS on JSONB array field
4520+
// Test with attributes.colors field
4521+
Query query =
4522+
Query.builder()
4523+
.addSelection(IdentifierExpression.of("item"))
4524+
.addSelection(JsonIdentifierExpression.of("attributes", "colors"))
4525+
.setFilter(
4526+
RelationalExpression.of(
4527+
JsonArrayIdentifierExpression.of("attributes", "colors"),
4528+
NOT_EXISTS,
4529+
ConstantExpression.of("null")))
4530+
.build();
4531+
4532+
Iterator<Document> results = flatCollection.find(query);
4533+
4534+
int count = 0;
4535+
Set<String> returnedItems = new HashSet<>();
4536+
while (results.hasNext()) {
4537+
Document doc = results.next();
4538+
JsonNode json = new ObjectMapper().readTree(doc.toJson());
4539+
count++;
4540+
4541+
String item = json.get("item").asText();
4542+
returnedItems.add(item);
4543+
4544+
// Verify that returned documents have NULL parent, missing field, or empty arrays
4545+
JsonNode attributes = json.get("attributes");
4546+
if (attributes != null && attributes.isObject()) {
4547+
JsonNode colors = attributes.get("colors");
4548+
assertTrue(
4549+
colors == null || !colors.isArray() || colors.isEmpty(),
4550+
"colors should be NULL or empty array for item: " + item + ", but was: " + colors);
4551+
}
4552+
// NULL attributes is also valid
4553+
}
4554+
4555+
// Should include documents where attributes is NULL or attributes.colors is NULL/empty
4556+
// Row 11 (Pencil) and other rows with empty/NULL colors
4557+
assertTrue(count > 0, "Should return at least some documents");
4558+
assertTrue(
4559+
returnedItems.contains("Pencil"),
4560+
"Should include Pencil (has empty colors array in attributes)");
4561+
}
4562+
}
4563+
43654564
@Nested
43664565
class BulkUpdateTest {
43674566

0 commit comments

Comments
 (0)