diff --git a/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java b/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java index 1f2831da7..640cb7ccd 100644 --- a/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java +++ b/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java @@ -5972,6 +5972,332 @@ private static Collection getFlatCollection(String dataStoreName) { return datastore.getCollectionForType(FLAT_COLLECTION_NAME, DocumentType.FLAT); } + @Nested + class FlatCollectionLegacySearchMethod { + + @ParameterizedTest + @ArgumentsSource(PostgresProvider.class) + void testSearchWithNoFilter(String dataStoreName) { + Collection flatCollection = getFlatCollection(dataStoreName); + + // Test legacy search() method with no filter - should return all documents + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query(); + + Iterator results = flatCollection.search(legacyQuery); + int count = 0; + while (results.hasNext()) { + results.next(); + count++; + } + assertEquals(10, count); // All 10 documents in flat collection + } + + @ParameterizedTest + @ArgumentsSource(PostgresProvider.class) + void testSearchWithEqFilter(String dataStoreName) throws JsonProcessingException { + Collection flatCollection = getFlatCollection(dataStoreName); + + // Test legacy search() with EQ filter on scalar column + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query() + .withFilter( + new org.hypertrace.core.documentstore.Filter( + org.hypertrace.core.documentstore.Filter.Op.EQ, "item", "Soap")); + + Iterator results = flatCollection.search(legacyQuery); + int count = 0; + while (results.hasNext()) { + Document doc = results.next(); + JsonNode json = new ObjectMapper().readTree(doc.toJson()); + assertEquals("Soap", json.get("item").asText()); + count++; + } + assertEquals(3, count); // 3 Soap items (IDs 1, 5, 8) + } + + @ParameterizedTest + @ArgumentsSource(PostgresProvider.class) + void testSearchWithInFilter(String dataStoreName) { + Collection flatCollection = getFlatCollection(dataStoreName); + + // Test legacy search() with IN filter + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query() + .withFilter( + new org.hypertrace.core.documentstore.Filter( + org.hypertrace.core.documentstore.Filter.Op.IN, + "item", + List.of("Soap", "Mirror"))); + + Iterator results = flatCollection.search(legacyQuery); + int count = 0; + while (results.hasNext()) { + results.next(); + count++; + } + assertEquals(4, count); // 3 Soap + 1 Mirror = 4 + } + + @ParameterizedTest + @ArgumentsSource(PostgresProvider.class) + void testSearchWithNumericFilter(String dataStoreName) throws JsonProcessingException { + Collection flatCollection = getFlatCollection(dataStoreName); + + // Test legacy search() with GT filter on integer column + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query() + .withFilter( + new org.hypertrace.core.documentstore.Filter( + org.hypertrace.core.documentstore.Filter.Op.GT, "price", 15)); + + Iterator results = flatCollection.search(legacyQuery); + int count = 0; + while (results.hasNext()) { + Document doc = results.next(); + JsonNode json = new ObjectMapper().readTree(doc.toJson()); + assertTrue(json.get("price").asInt() > 15); + count++; + } + assertTrue(count > 0); + } + + @ParameterizedTest + @ArgumentsSource(PostgresProvider.class) + void testSearchWithCompositeAndFilter(String dataStoreName) throws JsonProcessingException { + Collection flatCollection = getFlatCollection(dataStoreName); + + // Test legacy search() with composite AND filter + org.hypertrace.core.documentstore.Filter itemFilter = + new org.hypertrace.core.documentstore.Filter( + org.hypertrace.core.documentstore.Filter.Op.EQ, "item", "Soap"); + org.hypertrace.core.documentstore.Filter priceFilter = + new org.hypertrace.core.documentstore.Filter( + org.hypertrace.core.documentstore.Filter.Op.GTE, "price", 10); + + org.hypertrace.core.documentstore.Filter compositeFilter = + new org.hypertrace.core.documentstore.Filter(); + compositeFilter.setOp(org.hypertrace.core.documentstore.Filter.Op.AND); + compositeFilter.setChildFilters( + new org.hypertrace.core.documentstore.Filter[] {itemFilter, priceFilter}); + + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query().withFilter(compositeFilter); + + Iterator results = flatCollection.search(legacyQuery); + int count = 0; + while (results.hasNext()) { + Document doc = results.next(); + JsonNode json = new ObjectMapper().readTree(doc.toJson()); + assertEquals("Soap", json.get("item").asText()); + assertTrue(json.get("price").asInt() >= 10); + count++; + } + assertTrue(count > 0); + } + + @ParameterizedTest + @ArgumentsSource(PostgresProvider.class) + void testSearchWithOrderBy(String dataStoreName) throws JsonProcessingException { + Collection flatCollection = getFlatCollection(dataStoreName); + + // Test legacy search() with ORDER BY + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query() + .withOrderBy(new OrderBy("price", true)); // ASC + + Iterator results = flatCollection.search(legacyQuery); + int previousPrice = Integer.MIN_VALUE; + int count = 0; + while (results.hasNext()) { + Document doc = results.next(); + JsonNode json = new ObjectMapper().readTree(doc.toJson()); + int currentPrice = json.get("price").asInt(); + assertTrue(currentPrice >= previousPrice, "Results should be sorted by price ASC"); + previousPrice = currentPrice; + count++; + } + assertEquals(10, count); + } + + @ParameterizedTest + @ArgumentsSource(PostgresProvider.class) + void testSearchWithPagination(String dataStoreName) { + Collection flatCollection = getFlatCollection(dataStoreName); + + // Test legacy search() with LIMIT and OFFSET + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query().withLimit(3).withOffset(2); + + Iterator results = flatCollection.search(legacyQuery); + int count = 0; + while (results.hasNext()) { + results.next(); + count++; + } + assertEquals(3, count); // Should return exactly 3 documents + } + + @ParameterizedTest + @ArgumentsSource(PostgresProvider.class) + void testSearchWithSelections(String dataStoreName) throws JsonProcessingException { + Collection flatCollection = getFlatCollection(dataStoreName); + + // Test legacy search() with selections + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query() + .withSelection("item") + .withSelection("price") + .withLimit(5); + + Iterator results = flatCollection.search(legacyQuery); + int count = 0; + while (results.hasNext()) { + Document doc = results.next(); + JsonNode json = new ObjectMapper().readTree(doc.toJson()); + // Should have item and price fields + assertNotNull(json.get("item")); + assertNotNull(json.get("price")); + count++; + } + assertEquals(5, count); + } + + @ParameterizedTest + @ArgumentsSource(PostgresProvider.class) + void testSearchWithJsonbFilter(String dataStoreName) throws JsonProcessingException { + Collection flatCollection = getFlatCollection(dataStoreName); + + // Test legacy search() with filter on JSONB nested field (props.brand) + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query() + .withFilter( + new org.hypertrace.core.documentstore.Filter( + org.hypertrace.core.documentstore.Filter.Op.EQ, "props.brand", "Dettol")); + + Iterator results = flatCollection.search(legacyQuery); + int count = 0; + while (results.hasNext()) { + Document doc = results.next(); + JsonNode json = new ObjectMapper().readTree(doc.toJson()); + JsonNode props = json.get("props"); + assertNotNull(props); + assertEquals("Dettol", props.get("brand").asText()); + count++; + } + assertEquals(1, count); // Only 1 document with brand=Dettol + } + + @ParameterizedTest + @ArgumentsSource(PostgresProvider.class) + void testSearchCompleteQuery(String dataStoreName) throws JsonProcessingException { + Collection flatCollection = getFlatCollection(dataStoreName); + + // Test legacy search() with filter, orderBy, selections, and pagination + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query() + .withFilter( + new org.hypertrace.core.documentstore.Filter( + org.hypertrace.core.documentstore.Filter.Op.GTE, "price", 5)) + .withSelection("item") + .withSelection("price") + .withOrderBy(new OrderBy("price", false)) // DESC + .withLimit(5) + .withOffset(0); + + Iterator results = flatCollection.search(legacyQuery); + int previousPrice = Integer.MAX_VALUE; + int count = 0; + while (results.hasNext()) { + Document doc = results.next(); + JsonNode json = new ObjectMapper().readTree(doc.toJson()); + int currentPrice = json.get("price").asInt(); + assertTrue(currentPrice >= 5, "Price should be >= 5"); + assertTrue(currentPrice <= previousPrice, "Results should be sorted by price DESC"); + previousPrice = currentPrice; + count++; + } + assertEquals(5, count); + } + + @ParameterizedTest + @ArgumentsSource(PostgresProvider.class) + void testSearchWithFilterSelectionsOrderByAndOffset(String dataStoreName) + throws JsonProcessingException { + Collection flatCollection = getFlatCollection(dataStoreName); + + // Test covering: filter, selections, orderBy, and offset (pagination) + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query() + .withFilter( + new org.hypertrace.core.documentstore.Filter( + org.hypertrace.core.documentstore.Filter.Op.GTE, "price", 5)) + .withSelection("item") + .withSelection("price") + .withOrderBy(new OrderBy("price", true)) // ASC + .withLimit(3) + .withOffset(1); // Skip first result + + Iterator results = flatCollection.search(legacyQuery); + int count = 0; + int previousPrice = Integer.MIN_VALUE; + while (results.hasNext()) { + Document doc = results.next(); + JsonNode json = new ObjectMapper().readTree(doc.toJson()); + assertTrue(json.has("item")); + assertTrue(json.has("price")); + int price = json.get("price").asInt(); + assertTrue(price >= previousPrice); // ASC order + previousPrice = price; + count++; + } + assertEquals(3, count); + } + + @ParameterizedTest + @ArgumentsSource(PostgresProvider.class) + void testSearchWithNullFilterEmptySelectionsNoOrderBy(String dataStoreName) { + Collection flatCollection = getFlatCollection(dataStoreName); + + // Test covering null/empty branches: + // - No filter (null filter path) + // - No selections (empty selections path) + // - No orderBy (empty orderBys path) + // - Limit without offset (offset defaults to 0) + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query().withLimit(5); + + Iterator results = flatCollection.search(legacyQuery); + int count = 0; + while (results.hasNext()) { + results.next(); + count++; + } + assertEquals(5, count); + } + + @ParameterizedTest + @ArgumentsSource(PostgresProvider.class) + void testSearchWithUnknownFieldFallback(String dataStoreName) { + Collection flatCollection = getFlatCollection(dataStoreName); + + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query() + .withSelection("nonexistent_field") + .withOrderBy(new OrderBy("another_unknown", true)) + .withLimit(1); + + assertThrows( + Exception.class, + () -> { + Iterator results = flatCollection.search(legacyQuery); + while (results.hasNext()) { + results.next(); + } + }); + } + } + @Nested class BulkUpdateTest { diff --git a/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/LegacyToQueryFilterTransformationTest.java b/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/LegacyToQueryFilterTransformationTest.java index f4a5c0876..173fc8cfd 100644 --- a/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/LegacyToQueryFilterTransformationTest.java +++ b/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/LegacyToQueryFilterTransformationTest.java @@ -97,29 +97,12 @@ public static void init() throws IOException { pgDatastore.getSchemaRegistry(), FLAT_COLLECTION_NAME); } + private static final String FLAT_COLLECTION_SCHEMA_PATH = + "schema/flat_collection_test_schema.sql"; + private static void createFlatCollectionSchema() { - String createTableSQL = - String.format( - "CREATE TABLE \"%s\" (" - + "\"id\" TEXT PRIMARY KEY," - + "\"item\" TEXT," - + "\"price\" INTEGER," - + "\"quantity\" INTEGER," - + "\"date\" TIMESTAMPTZ," - + "\"in_stock\" BOOLEAN," - + "\"tags\" TEXT[]," - + "\"categoryTags\" TEXT[]," - + "\"props\" JSONB," - + "\"sales\" JSONB," - + "\"numbers\" INTEGER[]," - + "\"scores\" DOUBLE PRECISION[]," - + "\"flags\" BOOLEAN[]," - + "\"big_number\" BIGINT," - + "\"rating\" REAL," - + "\"created_date\" DATE," - + "\"weight\" DOUBLE PRECISION" - + ");", - FLAT_COLLECTION_NAME); + String schemaTemplate = loadSchemaFromResource(); + String createTableSQL = String.format(schemaTemplate, FLAT_COLLECTION_NAME); PostgresDatastore pgDatastore = (PostgresDatastore) postgresDatastore; @@ -132,6 +115,23 @@ private static void createFlatCollectionSchema() { } } + private static String loadSchemaFromResource() { + try (var is = + LegacyToQueryFilterTransformationTest.class + .getClassLoader() + .getResourceAsStream(FLAT_COLLECTION_SCHEMA_PATH); + var reader = new java.io.BufferedReader(new java.io.InputStreamReader(is))) { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append(" "); + } + return sb.toString().trim(); + } catch (Exception e) { + throw new RuntimeException("Failed to load schema from " + FLAT_COLLECTION_SCHEMA_PATH, e); + } + } + private static void executeInsertStatements() { PostgresDatastore pgDatastore = (PostgresDatastore) postgresDatastore; try { @@ -202,7 +202,7 @@ void testEqOperator() throws Exception { List newResults = collectResults(flatCollection.find(query)); assertNotNull(newResults); - assertFalse(newResults.isEmpty(), "Should find at least one document with item='Soap'"); + assertFalse(newResults.isEmpty()); for (Document doc : newResults) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); @@ -223,9 +223,7 @@ void testNeqOperator() throws Exception { assertNotNull(results); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); - assertTrue( - !node.has("item") || !node.get("item").asText().equals("Soap"), - "Should not contain item='Soap'"); + assertTrue(!node.has("item") || !node.get("item").asText().equals("Soap")); } } } @@ -245,11 +243,11 @@ void testGtOperator() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse(results.isEmpty(), "Should find documents with price > 10"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); if (node.has("price") && !node.get("price").isNull()) { - assertTrue(node.get("price").asInt() > 10, "Price should be > 10"); + assertTrue(node.get("price").asInt() > 10); } } } @@ -265,11 +263,11 @@ void testGteOperator() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse(results.isEmpty(), "Should find documents with price >= 10"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); if (node.has("price") && !node.get("price").isNull()) { - assertTrue(node.get("price").asInt() >= 10, "Price should be >= 10"); + assertTrue(node.get("price").asInt() >= 10); } } } @@ -285,11 +283,11 @@ void testLtOperator() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse(results.isEmpty(), "Should find documents with price < 10"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); if (node.has("price") && !node.get("price").isNull()) { - assertTrue(node.get("price").asInt() < 10, "Price should be < 10"); + assertTrue(node.get("price").asInt() < 10); } } } @@ -305,11 +303,11 @@ void testLteOperator() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse(results.isEmpty(), "Should find documents with price <= 10"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); if (node.has("price") && !node.get("price").isNull()) { - assertTrue(node.get("price").asInt() <= 10, "Price should be <= 10"); + assertTrue(node.get("price").asInt() <= 10); } } } @@ -331,11 +329,11 @@ void testInOperator() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse(results.isEmpty(), "Should find documents with item in list"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); String item = node.get("item").asText(); - assertTrue(items.contains(item), "Item should be in the list: " + item); + assertTrue(items.contains(item)); } } @@ -351,12 +349,12 @@ void testNotInOperator() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse(results.isEmpty(), "Should find documents with item not in list"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); if (node.has("item") && !node.get("item").isNull()) { String item = node.get("item").asText(); - assertFalse(items.contains(item), "Item should NOT be in the list: " + item); + assertFalse(items.contains(item)); } } } @@ -373,12 +371,12 @@ void testInOperatorWithNumbers() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse(results.isEmpty(), "Should find documents with price in list"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); if (node.has("price") && !node.get("price").isNull()) { int price = node.get("price").asInt(); - assertTrue(prices.contains(price), "Price should be in the list: " + price); + assertTrue(prices.contains(price)); } } } @@ -402,11 +400,11 @@ void testAndOperator() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse(results.isEmpty(), "Should find documents matching AND condition"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); assertEquals("Soap", node.get("item").asText()); - assertTrue(node.get("price").asInt() > 10, "Price should be > 10"); + assertTrue(node.get("price").asInt() > 10); } } @@ -424,13 +422,11 @@ void testOrOperator() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse(results.isEmpty(), "Should find documents matching OR condition"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); String item = node.get("item").asText(); - assertTrue( - item.equals("Soap") || item.equals("Mirror"), - "Item should be 'Soap' or 'Mirror', got: " + item); + assertTrue(item.equals("Soap") || item.equals("Mirror")); } } @@ -451,7 +447,7 @@ void testNestedAndOr() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse(results.isEmpty(), "Should find documents matching complex condition"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); String item = node.get("item").asText(); @@ -461,9 +457,7 @@ void testNestedAndOr() throws Exception { boolean matchesSoapAndPrice = item.equals("Soap") && price > 10; boolean matchesComb = item.equals("Comb"); - assertTrue( - matchesSoapAndPrice || matchesComb, - "Document should match (Soap AND price>10) OR Comb. Got: " + item + ", " + price); + assertTrue(matchesSoapAndPrice || matchesComb); } } } @@ -485,9 +479,7 @@ void testExistsOperator() throws Exception { assertNotNull(results); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); - assertTrue( - node.has("in_stock") && !node.get("in_stock").isNull(), - "Document should have in_stock field"); + assertTrue(node.has("in_stock") && !node.get("in_stock").isNull()); } } @@ -504,9 +496,7 @@ void testNotExistsOperator() throws Exception { assertNotNull(results); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); - assertTrue( - !node.has("in_stock") || node.get("in_stock").isNull(), - "Document should not have in_stock field or it should be null"); + assertTrue(!node.has("in_stock") || node.get("in_stock").isNull()); } } } @@ -526,11 +516,11 @@ void testLikeOperator() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse(results.isEmpty(), "Should find documents matching LIKE pattern"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); String item = node.get("item").asText(); - assertTrue(item.startsWith("Sha"), "Item should start with 'Sha': " + item); + assertTrue(item.startsWith("Sha")); } } } @@ -550,11 +540,11 @@ void testNestedStringEq() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse(results.isEmpty(), "Should find documents with props.brand='Dettol'"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); JsonNode props = node.get("props"); - assertNotNull(props, "props should exist"); + assertNotNull(props); assertEquals("Dettol", props.get("brand").asText()); } } @@ -591,13 +581,13 @@ void testNestedStringIn() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse(results.isEmpty(), "Should find documents with props.brand in list"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); JsonNode props = node.get("props"); - assertNotNull(props, "props should exist"); + assertNotNull(props); String brand = props.get("brand").asText(); - assertTrue(brands.contains(brand), "Brand should be in list: " + brand); + assertTrue(brands.contains(brand)); } } @@ -613,13 +603,11 @@ void testDeeplyNestedStringEq() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse( - results.isEmpty(), - "Should find documents with props.seller.name='Metro Chemicals Pvt. Ltd.'"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); JsonNode seller = node.path("props").path("seller"); - assertNotNull(seller, "seller should exist"); + assertNotNull(seller); assertEquals("Metro Chemicals Pvt. Ltd.", seller.get("name").asText()); } } @@ -635,12 +623,11 @@ void testTripleNestedStringEq() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse( - results.isEmpty(), "Should find documents with props.seller.address.city='Kolkata'"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); JsonNode address = node.path("props").path("seller").path("address"); - assertNotNull(address, "address should exist"); + assertNotNull(address); assertEquals("Kolkata", address.get("city").asText()); } } @@ -656,11 +643,11 @@ void testNestedStringArrayContains() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse(results.isEmpty(), "Should find documents with props.colors containing 'Blue'"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); JsonNode colors = node.path("props").path("colors"); - assertTrue(colors.isArray(), "colors should be an array"); + assertTrue(colors.isArray()); boolean containsBlue = false; for (JsonNode color : colors) { if ("Blue".equals(color.asText())) { @@ -668,7 +655,7 @@ void testNestedStringArrayContains() throws Exception { break; } } - assertTrue(containsBlue, "colors should contain 'Blue'"); + assertTrue(containsBlue); } } @@ -683,13 +670,11 @@ void testNestedStringArraySourceLocContains() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse( - results.isEmpty(), - "Should find documents with props.source-loc containing 'warehouse-A'"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); JsonNode sourceLoc = node.path("props").path("source-loc"); - assertTrue(sourceLoc.isArray(), "source-loc should be an array"); + assertTrue(sourceLoc.isArray()); boolean containsWarehouseA = false; for (JsonNode loc : sourceLoc) { if ("warehouse-A".equals(loc.asText())) { @@ -697,7 +682,7 @@ void testNestedStringArraySourceLocContains() throws Exception { break; } } - assertTrue(containsWarehouseA, "source-loc should contain 'warehouse-A'"); + assertTrue(containsWarehouseA); } } @@ -715,7 +700,7 @@ void testNestedJsonbAnd() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse(results.isEmpty(), "Should find documents matching nested AND condition"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); JsonNode props = node.get("props"); @@ -738,13 +723,11 @@ void testNestedJsonbOr() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse(results.isEmpty(), "Should find documents matching nested OR condition"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); String brand = node.path("props").get("brand").asText(); - assertTrue( - brand.equals("Dettol") || brand.equals("Sunsilk"), - "Brand should be 'Dettol' or 'Sunsilk', got: " + brand); + assertTrue(brand.equals("Dettol") || brand.equals("Sunsilk")); } } @@ -759,14 +742,11 @@ void testNestedStringLike() throws Exception { List results = collectResults(flatCollection.find(query)); assertNotNull(results); - assertFalse( - results.isEmpty(), "Should find documents with props.product-code like 'SOAP-.*'"); + assertFalse(results.isEmpty()); for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); String productCode = node.path("props").get("product-code").asText(); - assertTrue( - productCode.startsWith("SOAP-"), - "product-code should start with 'SOAP-': " + productCode); + assertTrue(productCode.startsWith("SOAP-")); } } @@ -784,8 +764,7 @@ void testNestedStringExists() throws Exception { for (Document doc : results) { JsonNode node = OBJECT_MAPPER.readTree(doc.toJson()); JsonNode brand = node.path("props").path("brand"); - assertTrue( - !brand.isMissingNode() && !brand.isNull(), "props.brand should exist and not be null"); + assertTrue(!brand.isMissingNode() && !brand.isNull()); } } @@ -805,9 +784,7 @@ void testNestedStringNotExists() throws Exception { JsonNode props = node.get("props"); if (props != null && !props.isNull()) { JsonNode brand = props.get("brand"); - assertTrue( - brand == null || brand.isNull() || brand.isMissingNode(), - "props.brand should not exist or be null"); + assertTrue(brand == null || brand.isNull() || brand.isMissingNode()); } } } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/FlatPostgresCollection.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/FlatPostgresCollection.java index aa64fa445..27bb1e255 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/FlatPostgresCollection.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/FlatPostgresCollection.java @@ -56,6 +56,7 @@ import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field.PostgresDataType; import org.hypertrace.core.documentstore.postgres.query.v1.transformer.FlatPostgresFieldTransformer; import org.hypertrace.core.documentstore.postgres.query.v1.transformer.LegacyFilterToQueryFilterTransformer; +import org.hypertrace.core.documentstore.postgres.query.v1.transformer.LegacyQueryToV2QueryTransformer; import org.hypertrace.core.documentstore.postgres.update.parser.PostgresAddToListIfAbsentParser; import org.hypertrace.core.documentstore.postgres.update.parser.PostgresAddValueParser; import org.hypertrace.core.documentstore.postgres.update.parser.PostgresAppendToListParser; @@ -173,6 +174,14 @@ public CloseableIterator find( return queryWithParser(query, queryParser); } + @Override + public CloseableIterator search(org.hypertrace.core.documentstore.Query query) { + LegacyQueryToV2QueryTransformer transformer = + new LegacyQueryToV2QueryTransformer(schemaRegistry, tableIdentifier.getTableName()); + Query v2Query = transformer.transform(query); + return query(v2Query, QueryOptions.builder().build()); + } + @Override public long count( org.hypertrace.core.documentstore.query.Query query, QueryOptions queryOptions) { diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/LegacyQueryToV2QueryTransformer.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/LegacyQueryToV2QueryTransformer.java new file mode 100644 index 000000000..92002e47c --- /dev/null +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/LegacyQueryToV2QueryTransformer.java @@ -0,0 +1,187 @@ +package org.hypertrace.core.documentstore.postgres.query.v1.transformer; + +import com.google.common.base.Preconditions; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.hypertrace.core.documentstore.OrderBy; +import org.hypertrace.core.documentstore.commons.ColumnMetadata; +import org.hypertrace.core.documentstore.commons.SchemaRegistry; +import org.hypertrace.core.documentstore.expression.impl.DataType; +import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.JsonFieldType; +import org.hypertrace.core.documentstore.expression.impl.JsonIdentifierExpression; +import org.hypertrace.core.documentstore.expression.operators.SortOrder; +import org.hypertrace.core.documentstore.query.Filter; +import org.hypertrace.core.documentstore.query.Pagination; +import org.hypertrace.core.documentstore.query.Query; + +/** + * Transforms the legacy {@link org.hypertrace.core.documentstore.Query} to the newer {@link Query} + * format. Since the legacy query does not carry any type information, this class interfaces with + * {@link SchemaRegistry} to find the type info. + * + *

This transformer handles: + * + *

    + *
  • Filter transformation (delegated to {@link LegacyFilterToQueryFilterTransformer}) + *
  • Selection transformation (field names to appropriate identifier expressions) + *
  • OrderBy transformation (field names to sort expressions) + *
  • Pagination (limit/offset) + *
+ */ +@Slf4j +public class LegacyQueryToV2QueryTransformer { + + private final SchemaRegistry schemaRegistry; + private final String tableName; + private final LegacyFilterToQueryFilterTransformer filterTransformer; + + public LegacyQueryToV2QueryTransformer( + SchemaRegistry schemaRegistry, String tableName) { + this.schemaRegistry = schemaRegistry; + this.tableName = tableName; + this.filterTransformer = new LegacyFilterToQueryFilterTransformer(schemaRegistry, tableName); + } + + /** + * Transforms a legacy Query to the new v2 Query. + * + * @param legacyQuery the legacy query to transform + * @return the transformed v2 Query + */ + public Query transform(org.hypertrace.core.documentstore.Query legacyQuery) { + if (legacyQuery == null) { + return Query.builder().build(); + } + + log.debug("Incoming v1 query: {}", legacyQuery); + + Query.QueryBuilder builder = Query.builder(); + + // Transform filter + if (legacyQuery.getFilter() != null) { + Filter v2Filter = filterTransformer.transform(legacyQuery.getFilter()); + if (v2Filter != null && v2Filter.getExpression() != null) { + builder.setFilter(v2Filter.getExpression()); + } + } + + // Transform selections + if (legacyQuery.getSelections() != null && !legacyQuery.getSelections().isEmpty()) { + for (String selection : legacyQuery.getSelections()) { + builder.addSelection(createIdentifierExpression(selection)); + } + } + + // Transform orderBy + if (legacyQuery.getOrderBys() != null && !legacyQuery.getOrderBys().isEmpty()) { + for (OrderBy orderBy : legacyQuery.getOrderBys()) { + SortOrder sortOrder = orderBy.isAsc() ? SortOrder.ASC : SortOrder.DESC; + builder.addSort(createIdentifierExpression(orderBy.getField()), sortOrder); + } + } + + // Set pagination + Integer limit = legacyQuery.getLimit(); + Integer offset = legacyQuery.getOffset(); + if (limit != null && limit >= 0) { + builder.setPagination( + Pagination.builder() + .offset(offset != null && offset >= 0 ? offset : 0) + .limit(limit) + .build()); + } + + Query v2Query = builder.build(); + + log.debug("Transformed v2 query: {}", v2Query); + + return v2Query; + } + + /** + * Creates the appropriate identifier expression based on the field name and schema. + * + *

Uses the schema registry to determine if a field is: + * + *

    + *
  • A direct column → IdentifierExpression + *
  • A JSONB nested path → JsonIdentifierExpression with STRING type (default for + * selections/orderBy since we don't have a value to infer type from) + *
+ * + *

Returns IdentifierExpression (or subclass) which implements both SelectTypeExpression and + * SortTypeExpression, allowing use in both selections and orderBy clauses. + */ + private IdentifierExpression createIdentifierExpression(String fieldName) { + + Preconditions.checkArgument( + fieldName != null && !fieldName.isEmpty(), "Field name cannot be null or empty"); + + // Check if the full path is a direct column + if (schemaRegistry.getColumnOrRefresh(tableName, fieldName).isPresent()) { + return IdentifierExpression.of(fieldName); + } + + // Try to find a JSONB column prefix + Optional jsonbColumn = findJsonbColumnPrefix(fieldName); + if (jsonbColumn.isPresent()) { + String columnName = jsonbColumn.get(); + String[] jsonPath = getNestedPath(fieldName, columnName); + // Default to STRING for selections/orderBy since we don't have a value to infer from + return JsonIdentifierExpression.of(columnName, JsonFieldType.STRING, jsonPath); + } + + // Fallback: treat as direct column (will fail at query time if column doesn't exist) + return IdentifierExpression.of(fieldName); + } + + /** + * Finds the JSONB column prefix for a given path by progressively checking prefixes. + * + *

For example, given path "props.inheritedAttributes.color": + * + *

    + *
  • If "props" is a JSONB column → returns "props" + *
  • If "props.inheritedAttributes" is a JSONB column → returns "props.inheritedAttributes" + *
  • If neither is JSONB → returns empty + *
+ */ + private Optional findJsonbColumnPrefix(String path) { + if (!path.contains(".")) { + return Optional.empty(); + } + + String[] parts = path.split("\\."); + StringBuilder columnBuilder = new StringBuilder(parts[0]); + + for (int i = 0; i < parts.length - 1; i++) { + if (i > 0) { + columnBuilder.append(".").append(parts[i]); + } + String candidateColumn = columnBuilder.toString(); + Optional colMeta = + schemaRegistry.getColumnOrRefresh(tableName, candidateColumn); + + if (colMeta.isPresent() && colMeta.get().getCanonicalType() == DataType.JSON) { + return Optional.of(candidateColumn); + } + } + + return Optional.empty(); + } + + /** + * Extracts the JSONB path portion after removing the column name prefix. + * + *

For example, if the path is "props.inheritedAttributes.color" and the column name is + * "props", then the returned path is ["inheritedAttributes", "color"]. + */ + private String[] getNestedPath(String fullPath, String jsonbColName) { + if (fullPath.equals(jsonbColName)) { + return new String[0]; + } + String nested = fullPath.substring(jsonbColName.length() + 1); + return nested.split("\\."); + } +} diff --git a/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/LegacyQueryToV2QueryTransformerTest.java b/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/LegacyQueryToV2QueryTransformerTest.java new file mode 100644 index 000000000..aa27b52ff --- /dev/null +++ b/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/LegacyQueryToV2QueryTransformerTest.java @@ -0,0 +1,312 @@ +package org.hypertrace.core.documentstore.postgres.query.v1.transformer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import org.hypertrace.core.documentstore.Filter; +import org.hypertrace.core.documentstore.OrderBy; +import org.hypertrace.core.documentstore.commons.SchemaRegistry; +import org.hypertrace.core.documentstore.expression.impl.DataType; +import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.JsonIdentifierExpression; +import org.hypertrace.core.documentstore.expression.operators.SortOrder; +import org.hypertrace.core.documentstore.postgres.model.PostgresColumnMetadata; +import org.hypertrace.core.documentstore.query.Query; +import org.hypertrace.core.documentstore.query.SelectionSpec; +import org.hypertrace.core.documentstore.query.SortingSpec; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class LegacyQueryToV2QueryTransformerTest { + + private static final String TABLE_NAME = "test_table"; + private SchemaRegistry schemaRegistry; + private LegacyQueryToV2QueryTransformer transformer; + + @BeforeEach + void setUp() { + schemaRegistry = mock(SchemaRegistry.class); + transformer = new LegacyQueryToV2QueryTransformer(schemaRegistry, TABLE_NAME); + } + + @Nested + class TransformNullOrEmptyQuery { + + @Test + void transformNullQuery_returnsEmptyV2Query() { + Query result = transformer.transform(null); + + assertNotNull(result); + assertTrue(result.getSelections().isEmpty()); + assertTrue(result.getFilter().isEmpty()); + assertTrue(result.getSorts().isEmpty()); + assertTrue(result.getPagination().isEmpty()); + } + + @Test + void transformEmptyQuery_returnsEmptyV2Query() { + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query(); + + Query result = transformer.transform(legacyQuery); + + assertNotNull(result); + assertTrue(result.getSelections().isEmpty()); + assertTrue(result.getFilter().isEmpty()); + assertTrue(result.getSorts().isEmpty()); + assertTrue(result.getPagination().isEmpty()); + } + } + + @Nested + class TransformSelections { + + @Test + void transformDirectColumnSelection_createsIdentifierExpression() { + PostgresColumnMetadata columnMeta = mock(PostgresColumnMetadata.class); + when(schemaRegistry.getColumnOrRefresh(TABLE_NAME, "status")) + .thenReturn(Optional.of(columnMeta)); + + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query().withSelection("status"); + + Query result = transformer.transform(legacyQuery); + + assertEquals(1, result.getSelections().size()); + SelectionSpec spec = result.getSelections().get(0); + assertTrue(spec.getExpression() instanceof IdentifierExpression); + assertEquals("status", ((IdentifierExpression) spec.getExpression()).getName()); + } + + @Test + void transformJsonbPathSelection_createsJsonIdentifierExpression() { + PostgresColumnMetadata jsonbColumnMeta = mock(PostgresColumnMetadata.class); + when(jsonbColumnMeta.getCanonicalType()).thenReturn(DataType.JSON); + when(schemaRegistry.getColumnOrRefresh(TABLE_NAME, "customAttr.myField")) + .thenReturn(Optional.empty()); + when(schemaRegistry.getColumnOrRefresh(TABLE_NAME, "customAttr")) + .thenReturn(Optional.of(jsonbColumnMeta)); + + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query().withSelection("customAttr.myField"); + + Query result = transformer.transform(legacyQuery); + + assertEquals(1, result.getSelections().size()); + SelectionSpec spec = result.getSelections().get(0); + assertTrue(spec.getExpression() instanceof JsonIdentifierExpression); + JsonIdentifierExpression jsonExpr = (JsonIdentifierExpression) spec.getExpression(); + assertEquals("customAttr", jsonExpr.getColumnName()); + assertEquals(1, jsonExpr.getJsonPath().size()); + assertEquals("myField", jsonExpr.getJsonPath().get(0)); + } + + @Test + void transformMultipleSelections_createsMultipleExpressions() { + PostgresColumnMetadata columnMeta = mock(PostgresColumnMetadata.class); + when(schemaRegistry.getColumnOrRefresh(TABLE_NAME, "col1")) + .thenReturn(Optional.of(columnMeta)); + when(schemaRegistry.getColumnOrRefresh(TABLE_NAME, "col2")) + .thenReturn(Optional.of(columnMeta)); + + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query().withSelection("col1").withSelection("col2"); + + Query result = transformer.transform(legacyQuery); + + assertEquals(2, result.getSelections().size()); + } + + @Test + void transformNullFieldName_throwsException() { + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query(); + legacyQuery.addSelection(null); + + assertThrows(IllegalArgumentException.class, () -> transformer.transform(legacyQuery)); + } + } + + @Nested + class TransformOrderBy { + + @Test + void transformAscendingOrderBy_createsSortWithAscOrder() { + PostgresColumnMetadata columnMeta = mock(PostgresColumnMetadata.class); + when(schemaRegistry.getColumnOrRefresh(TABLE_NAME, "createdAt")) + .thenReturn(Optional.of(columnMeta)); + + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query().withOrderBy(new OrderBy("createdAt", true)); + + Query result = transformer.transform(legacyQuery); + + assertEquals(1, result.getSorts().size()); + SortingSpec sortSpec = result.getSorts().get(0); + assertEquals(SortOrder.ASC, sortSpec.getOrder()); + assertTrue(sortSpec.getExpression() instanceof IdentifierExpression); + } + + @Test + void transformDescendingOrderBy_createsSortWithDescOrder() { + PostgresColumnMetadata columnMeta = mock(PostgresColumnMetadata.class); + when(schemaRegistry.getColumnOrRefresh(TABLE_NAME, "updatedAt")) + .thenReturn(Optional.of(columnMeta)); + + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query() + .withOrderBy(new OrderBy("updatedAt", false)); + + Query result = transformer.transform(legacyQuery); + + assertEquals(1, result.getSorts().size()); + SortingSpec sortSpec = result.getSorts().get(0); + assertEquals(SortOrder.DESC, sortSpec.getOrder()); + } + + @Test + void transformJsonbPathOrderBy_createsJsonIdentifierExpression() { + PostgresColumnMetadata jsonbColumnMeta = mock(PostgresColumnMetadata.class); + when(jsonbColumnMeta.getCanonicalType()).thenReturn(DataType.JSON); + when(schemaRegistry.getColumnOrRefresh(TABLE_NAME, "props.priority")) + .thenReturn(Optional.empty()); + when(schemaRegistry.getColumnOrRefresh(TABLE_NAME, "props")) + .thenReturn(Optional.of(jsonbColumnMeta)); + + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query() + .withOrderBy(new OrderBy("props.priority", true)); + + Query result = transformer.transform(legacyQuery); + + assertEquals(1, result.getSorts().size()); + SortingSpec sortSpec = result.getSorts().get(0); + assertTrue(sortSpec.getExpression() instanceof JsonIdentifierExpression); + } + } + + @Nested + class TransformPagination { + + @Test + void transformLimitOnly_createsPaginationWithZeroOffset() { + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query().withLimit(10); + + Query result = transformer.transform(legacyQuery); + + assertTrue(result.getPagination().isPresent()); + assertEquals(10, result.getPagination().get().getLimit()); + assertEquals(0, result.getPagination().get().getOffset()); + } + + @Test + void transformLimitAndOffset_createsPaginationWithBoth() { + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query().withLimit(20).withOffset(5); + + Query result = transformer.transform(legacyQuery); + + assertTrue(result.getPagination().isPresent()); + assertEquals(20, result.getPagination().get().getLimit()); + assertEquals(5, result.getPagination().get().getOffset()); + } + + @Test + void transformNegativeLimit_noPagination() { + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query().withLimit(-1); + + Query result = transformer.transform(legacyQuery); + + assertTrue(result.getPagination().isEmpty()); + } + + @Test + void transformNullLimit_noPagination() { + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query(); + + Query result = transformer.transform(legacyQuery); + + assertTrue(result.getPagination().isEmpty()); + } + } + + @Nested + class TransformFilter { + + @Test + void transformSimpleEqFilter_createsRelationalExpression() { + PostgresColumnMetadata columnMeta = mock(PostgresColumnMetadata.class); + when(schemaRegistry.getColumnOrRefresh(TABLE_NAME, "status")) + .thenReturn(Optional.of(columnMeta)); + + Filter legacyFilter = new Filter(Filter.Op.EQ, "status", "active"); + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query().withFilter(legacyFilter); + + Query result = transformer.transform(legacyQuery); + + assertTrue(result.getFilter().isPresent()); + } + + @Test + void transformCompositeAndFilter_createsLogicalExpression() { + PostgresColumnMetadata columnMeta = mock(PostgresColumnMetadata.class); + when(schemaRegistry.getColumnOrRefresh(anyString(), anyString())) + .thenReturn(Optional.of(columnMeta)); + + Filter filter1 = new Filter(Filter.Op.EQ, "status", "active"); + Filter filter2 = new Filter(Filter.Op.GT, "count", 10); + Filter compositeFilter = new Filter(); + compositeFilter.setOp(Filter.Op.AND); + compositeFilter.setChildFilters(new Filter[] {filter1, filter2}); + + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query().withFilter(compositeFilter); + + Query result = transformer.transform(legacyQuery); + + assertTrue(result.getFilter().isPresent()); + } + } + + @Nested + class TransformCompleteQuery { + + @Test + void transformCompleteQuery_allComponentsTransformed() { + PostgresColumnMetadata columnMeta = mock(PostgresColumnMetadata.class); + when(schemaRegistry.getColumnOrRefresh(eq(TABLE_NAME), anyString())) + .thenReturn(Optional.of(columnMeta)); + + Filter legacyFilter = new Filter(Filter.Op.EQ, "status", "active"); + org.hypertrace.core.documentstore.Query legacyQuery = + new org.hypertrace.core.documentstore.Query() + .withSelection("id") + .withSelection("name") + .withFilter(legacyFilter) + .withOrderBy(new OrderBy("createdAt", false)) + .withLimit(50) + .withOffset(10); + + Query result = transformer.transform(legacyQuery); + + assertEquals(2, result.getSelections().size()); + assertTrue(result.getFilter().isPresent()); + assertEquals(1, result.getSorts().size()); + assertTrue(result.getPagination().isPresent()); + assertEquals(50, result.getPagination().get().getLimit()); + assertEquals(10, result.getPagination().get().getOffset()); + } + } +}