5353import static org .hypertrace .core .documentstore .utils .Utils .assertDocsAndSizeEqualWithoutOrder ;
5454import static org .hypertrace .core .documentstore .utils .Utils .convertJsonToMap ;
5555import static org .hypertrace .core .documentstore .utils .Utils .readFileFromResource ;
56+ import static org .junit .Assert .assertNotNull ;
5657import static org .junit .jupiter .api .Assertions .assertEquals ;
5758import static org .junit .jupiter .api .Assertions .assertFalse ;
5859import static org .junit .jupiter .api .Assertions .assertNotEquals ;
5960import static org .junit .jupiter .api .Assertions .assertThrows ;
6061import static org .junit .jupiter .api .Assertions .assertTrue ;
6162
63+ import com .fasterxml .jackson .databind .JsonNode ;
6264import com .fasterxml .jackson .databind .ObjectMapper ;
6365import com .typesafe .config .Config ;
6466import com .typesafe .config .ConfigFactory ;
9799import org .hypertrace .core .documentstore .model .options .UpdateOptions .MissingDocumentStrategy ;
98100import org .hypertrace .core .documentstore .model .subdoc .SubDocumentUpdate ;
99101import org .hypertrace .core .documentstore .model .subdoc .SubDocumentValue ;
102+ import org .hypertrace .core .documentstore .postgres .PostgresDatastore ;
100103import org .hypertrace .core .documentstore .query .Aggregation ;
101104import org .hypertrace .core .documentstore .query .Filter ;
102105import org .hypertrace .core .documentstore .query .Pagination ;
109112import org .hypertrace .core .documentstore .utils .Utils ;
110113import org .junit .jupiter .api .AfterAll ;
111114import org .junit .jupiter .api .BeforeAll ;
115+ import org .junit .jupiter .api .Disabled ;
112116import org .junit .jupiter .api .Nested ;
113117import org .junit .jupiter .api .extension .ExtensionContext ;
114118import org .junit .jupiter .params .ParameterizedTest ;
@@ -126,6 +130,9 @@ public class DocStoreQueryV1Test {
126130
127131 private static final String COLLECTION_NAME = "myTest" ;
128132 private static final String UPDATABLE_COLLECTION_NAME = "updatable_collection" ;
133+ private static final String FLAT_COLLECTION_NAME = "myTestFlat" ;
134+ private static final String PG_FLAT_COLLECTION_INSERT_STATMENTS_FILE_LOC =
135+ "query/pg_flat_collection_insert.json" ;
129136
130137 private static Map <String , Datastore > datastoreMap ;
131138
@@ -175,12 +182,79 @@ public static void init() throws IOException {
175182 createCollectionData ("query/collection_data.json" , COLLECTION_NAME );
176183 }
177184
185+ private static void createFlatCollectionSchema (
186+ Datastore postgresDatastore , String collectionName ) {
187+ String createTableSQL =
188+ String .format (
189+ "CREATE TABLE \" %s\" ("
190+ + "\" _id\" INTEGER PRIMARY KEY,"
191+ + "\" item\" TEXT,"
192+ + "\" price\" INTEGER,"
193+ + "\" quantity\" INTEGER,"
194+ + "\" date\" TIMESTAMPTZ,"
195+ + "\" props\" JSONB,"
196+ + "\" sales\" JSONB"
197+ + ");" ,
198+ collectionName );
199+
200+ // Access PostgresDatastore directly to get client connection
201+ PostgresDatastore pgDatastore = (PostgresDatastore ) postgresDatastore ;
202+
203+ try {
204+
205+ // Execute the CREATE TABLE SQL directly using the public getPostgresClient method
206+ try (java .sql .Connection connection = pgDatastore .getPostgresClient ();
207+ java .sql .PreparedStatement statement = connection .prepareStatement (createTableSQL )) {
208+ statement .execute ();
209+ System .out .println ("Created flat collection table: " + collectionName );
210+ }
211+ } catch (Exception e ) {
212+ System .err .println ("Failed to create flat collection schema: " + e .getMessage ());
213+ e .printStackTrace ();
214+ }
215+ }
216+
217+ private static void executeInsertStatements (PostgresDatastore pgDatastore ) {
218+ try {
219+ // Read JSON file from resources
220+ String jsonContent =
221+ readFileFromResource (PG_FLAT_COLLECTION_INSERT_STATMENTS_FILE_LOC ).orElseThrow ();
222+ ObjectMapper mapper = new ObjectMapper ();
223+ JsonNode rootNode = mapper .readTree (jsonContent );
224+
225+ // Extract statements array
226+ JsonNode statementsNode = rootNode .get ("statements" );
227+ if (statementsNode == null || !statementsNode .isArray ()) {
228+ throw new RuntimeException ("Invalid JSON format: 'statements' array not found" );
229+ }
230+
231+ try (java .sql .Connection connection = pgDatastore .getPostgresClient ()) {
232+ for (com .fasterxml .jackson .databind .JsonNode statementNode : statementsNode ) {
233+ String statement = statementNode .asText ().trim ();
234+ if (!statement .isEmpty ()) {
235+ try (java .sql .PreparedStatement preparedStatement =
236+ connection .prepareStatement (statement )) {
237+ preparedStatement .executeUpdate ();
238+ }
239+ }
240+ }
241+ }
242+ } catch (Exception e ) {
243+ System .err .println ("Failed to execute INSERT statements: " + e .getMessage ());
244+ }
245+ }
246+
178247 private static void createCollectionData (final String resourcePath , final String collectionName )
179248 throws IOException {
180249 final Map <Key , Document > documents = Utils .buildDocumentsFromResource (resourcePath );
181250 datastoreMap .forEach (
182251 (k , v ) -> {
183252 v .deleteCollection (collectionName );
253+ // for Postgres, we also create the flat collection
254+ if (v instanceof PostgresDatastore ) {
255+ createFlatCollectionSchema (v , FLAT_COLLECTION_NAME );
256+ executeInsertStatements ((PostgresDatastore ) v );
257+ }
184258 v .createCollection (collectionName , null );
185259 Collection collection = v .getCollection (collectionName );
186260 collection .bulkUpsert (documents );
@@ -3674,4 +3748,150 @@ private static void testCountApi(
36743748 final long expectedSize = convertJsonToMap (fileContent ).size ();
36753749 assertEquals (expectedSize , actualSize );
36763750 }
3751+
3752+ @ ParameterizedTest
3753+ @ ArgumentsSource (PostgresProvider .class )
3754+ void testFlatPostgresCollectionFindAll (String dataStoreName ) throws IOException {
3755+ Datastore datastore = datastoreMap .get (dataStoreName );
3756+ Collection flatCollection =
3757+ datastore .getCollectionForType (FLAT_COLLECTION_NAME , DocumentType .FLAT );
3758+
3759+ // Test basic query to retrieve all documents
3760+ Query query = Query .builder ().build ();
3761+ CloseableIterator <Document > iterator = flatCollection .find (query );
3762+
3763+ // Count documents
3764+ long count = 0 ;
3765+ while (iterator .hasNext ()) {
3766+ Document doc = iterator .next ();
3767+ count ++;
3768+ // Verify document has content (basic validation)
3769+ assertNotNull (doc );
3770+ assertNotNull (doc .toJson ());
3771+ assertTrue (doc .toJson ().length () > 0 );
3772+ assertEquals (DocumentType .FLAT , doc .getDocumentType ());
3773+ }
3774+ iterator .close ();
3775+
3776+ // Should have 8 documents from the INSERT statements
3777+ assertEquals (8 , count );
3778+ }
3779+
3780+ @ ParameterizedTest
3781+ @ ArgumentsSource (PostgresProvider .class )
3782+ void testFlatPostgresCollectionFilterByItem (String dataStoreName ) throws IOException {
3783+ Datastore datastore = datastoreMap .get (dataStoreName );
3784+ Collection flatCollection =
3785+ datastore .getCollectionForType (FLAT_COLLECTION_NAME , DocumentType .FLAT );
3786+
3787+ // Test filtering by item
3788+ Query itemQuery =
3789+ Query .builder ()
3790+ .setFilter (
3791+ RelationalExpression .of (
3792+ IdentifierExpression .of ("item" ), EQ , ConstantExpression .of ("Soap" )))
3793+ .build ();
3794+
3795+ CloseableIterator <Document > soapIterator = flatCollection .find (itemQuery );
3796+ long soapCount = 0 ;
3797+ while (soapIterator .hasNext ()) {
3798+ Document doc = soapIterator .next ();
3799+ // Verify it's a soap document by checking JSON contains "Soap"
3800+ assertTrue (doc .toJson ().contains ("\" Soap\" " ));
3801+ soapCount ++;
3802+ }
3803+ soapIterator .close ();
3804+
3805+ // Should have 3 soap documents (IDs 1, 5, 8)
3806+ assertEquals (3 , soapCount );
3807+ }
3808+
3809+ @ ParameterizedTest
3810+ @ ArgumentsSource (PostgresProvider .class )
3811+ void testFlatPostgresCollectionCount (String dataStoreName ) throws IOException {
3812+ Datastore datastore = datastoreMap .get (dataStoreName );
3813+ Collection flatCollection =
3814+ datastore .getCollectionForType (FLAT_COLLECTION_NAME , DocumentType .FLAT );
3815+
3816+ // Test count method - all documents
3817+ long totalCount = flatCollection .count (Query .builder ().build ());
3818+ assertEquals (8 , totalCount );
3819+
3820+ // Test count with filter - soap documents only
3821+ Query soapQuery =
3822+ Query .builder ()
3823+ .setFilter (
3824+ RelationalExpression .of (
3825+ IdentifierExpression .of ("item" ), EQ , ConstantExpression .of ("Soap" )))
3826+ .build ();
3827+ long soapCountQuery = flatCollection .count (soapQuery );
3828+ assertEquals (3 , soapCountQuery );
3829+ }
3830+
3831+ /**
3832+ * This test is disabled for now because flat collections do not support search on nested queries
3833+ * in JSONB fields (ex. props.brand)
3834+ *
3835+ * @param dataStoreName the datastore name, in this case, Postgres
3836+ * @throws IOException
3837+ */
3838+ @ ParameterizedTest
3839+ @ ArgumentsSource (PostgresProvider .class )
3840+ @ Disabled
3841+ void testFlatPostgresCollectionNestedFieldQuery (String dataStoreName ) throws IOException {
3842+ Datastore datastore = datastoreMap .get (dataStoreName );
3843+ Collection flatCollection =
3844+ datastore .getCollectionForType (FLAT_COLLECTION_NAME , DocumentType .FLAT );
3845+
3846+ // Test querying nested field in props JSONB column - filter by brand
3847+ Query brandQuery =
3848+ Query .builder ()
3849+ .setFilter (
3850+ RelationalExpression .of (
3851+ IdentifierExpression .of ("props.brand" ), EQ , ConstantExpression .of ("Dettol" )))
3852+ .build ();
3853+
3854+ CloseableIterator <Document > brandIterator = flatCollection .find (brandQuery );
3855+ long brandCount = 0 ;
3856+ while (brandIterator .hasNext ()) {
3857+ Document doc = brandIterator .next ();
3858+ // Verify it contains the expected brand
3859+ assertTrue (doc .toJson ().contains ("\" Dettol\" " ));
3860+ brandCount ++;
3861+ }
3862+ brandIterator .close ();
3863+
3864+ // Should have 1 Dettol document (ID 1)
3865+ assertEquals (1 , brandCount );
3866+
3867+ // Test querying deeply nested field - filter by seller city
3868+ Query cityQuery =
3869+ Query .builder ()
3870+ .setFilter (
3871+ RelationalExpression .of (
3872+ IdentifierExpression .of ("props.seller.address.city" ),
3873+ EQ ,
3874+ ConstantExpression .of ("Mumbai" )))
3875+ .build ();
3876+
3877+ CloseableIterator <Document > cityIterator = flatCollection .find (cityQuery );
3878+ long cityCount = 0 ;
3879+ while (cityIterator .hasNext ()) {
3880+ Document doc = cityIterator .next ();
3881+ // Verify it contains Mumbai
3882+ assertTrue (doc .toJson ().contains ("\" Mumbai\" " ));
3883+ cityCount ++;
3884+ }
3885+ cityIterator .close ();
3886+
3887+ // Should have 2 Mumbai documents (IDs 1, 3)
3888+ assertEquals (2 , cityCount );
3889+
3890+ // Test count with nested field filter
3891+ long brandCountQuery = flatCollection .count (brandQuery );
3892+ assertEquals (1 , brandCountQuery );
3893+
3894+ long cityCountQuery = flatCollection .count (cityQuery );
3895+ assertEquals (2 , cityCountQuery );
3896+ }
36773897}
0 commit comments