@@ -769,6 +769,126 @@ async fn test_mongodb_unnesting_depth_1(port: usize) {
769769 . await ;
770770}
771771
772+ /// JSON nesting (`json_object`): declared static columns (`_id`, `name`) stay
773+ /// top-level while every other document field — scalar, nested document, and
774+ /// array — folds into one sorted-key JSON `Utf8` catch-all column (`data`).
775+ /// Exercised end-to-end through the DataFusion scan path against a live MongoDB.
776+ async fn test_mongodb_json_nesting ( port : usize ) {
777+ use datafusion_table_providers:: schema_projection:: SchemaProjection ;
778+
779+ let test_docs = vec ! [
780+ doc! {
781+ "_id" : 1 ,
782+ "name" : "Alice" ,
783+ "email" : "alice@example.com" ,
784+ "age" : 30 ,
785+ "address" : { "city" : "NYC" , "zip" : "10001" } ,
786+ } ,
787+ doc! {
788+ "_id" : 2 ,
789+ "name" : "Bob" ,
790+ "email" : "bob@example.com" ,
791+ "tags" : [ "x" , "y" ] ,
792+ } ,
793+ ] ;
794+
795+ let ctx = SessionContext :: new ( ) ;
796+ let client = common:: get_mongodb_client ( port)
797+ . await
798+ . expect ( "MongoDB client should be created" ) ;
799+ let collection = client
800+ . database ( "testdb" )
801+ . collection :: < Document > ( "json_nesting_collection" ) ;
802+ let _ = collection. drop ( ) . await ;
803+ collection
804+ . insert_many ( test_docs)
805+ . await
806+ . expect ( "MongoDB documents should be inserted" ) ;
807+
808+ // `_id` and `name` are declared static; every other field folds into `data`.
809+ let projection = SchemaProjection :: nesting (
810+ vec ! [ "_id" . to_string( ) , "name" . to_string( ) ] ,
811+ "data" . to_string ( ) ,
812+ ) ;
813+
814+ let pool = common:: get_mongodb_connection_pool ( port, None )
815+ . await
816+ . expect ( "MongoDB connection pool should be created" ) ;
817+ let table = MongoDBTable :: new_with_projection (
818+ & Arc :: new ( pool) ,
819+ "json_nesting_collection" ,
820+ None ,
821+ Some ( projection) ,
822+ )
823+ . await
824+ . expect ( "Table should be created" ) ;
825+ ctx. register_table ( "json_nesting_collection" , Arc :: new ( table) )
826+ . expect ( "Table should be registered" ) ;
827+
828+ let batches = ctx
829+ . sql ( "SELECT name, data FROM json_nesting_collection ORDER BY _id" )
830+ . await
831+ . expect ( "query should plan" )
832+ . collect ( )
833+ . await
834+ . expect ( "query should execute" ) ;
835+
836+ let mut rows: Vec < ( String , serde_json:: Value ) > = Vec :: new ( ) ;
837+ for batch in & batches {
838+ let names = batch
839+ . column_by_name ( "name" )
840+ . expect ( "name column" )
841+ . as_any ( )
842+ . downcast_ref :: < StringArray > ( )
843+ . expect ( "name should be a static Utf8 column" ) ;
844+ let data = batch
845+ . column_by_name ( "data" )
846+ . expect ( "catch-all data column" )
847+ . as_any ( )
848+ . downcast_ref :: < StringArray > ( )
849+ . expect ( "catch-all should be a Utf8 JSON string" ) ;
850+ for row in 0 ..batch. num_rows ( ) {
851+ let catch_all: serde_json:: Value =
852+ serde_json:: from_str ( data. value ( row) ) . expect ( "catch-all must be valid JSON" ) ;
853+ rows. push ( ( names. value ( row) . to_string ( ) , catch_all) ) ;
854+ }
855+ }
856+
857+ assert_eq ! ( rows. len( ) , 2 , "expected two documents" ) ;
858+
859+ // Row 0 (Alice): non-declared scalar + nested-document fields fold into the
860+ // catch-all; declared static keys must not leak into it.
861+ let ( name0, data0) = & rows[ 0 ] ;
862+ assert_eq ! ( name0, "Alice" ) ;
863+ assert_eq ! ( data0[ "email" ] , serde_json:: json!( "alice@example.com" ) ) ;
864+ assert ! (
865+ data0. get( "age" ) . is_some( ) ,
866+ "scalar `age` must be in the catch-all"
867+ ) ;
868+ assert ! (
869+ data0[ "address" ] . is_object( ) ,
870+ "nested `address` must be preserved as JSON in the catch-all"
871+ ) ;
872+ assert ! (
873+ data0. get( "name" ) . is_none( ) ,
874+ "static `name` must not leak into the catch-all"
875+ ) ;
876+ assert ! (
877+ data0. get( "_id" ) . is_none( ) ,
878+ "static `_id` must not leak into the catch-all"
879+ ) ;
880+
881+ // Row 1 (Bob): an array field folds in as well.
882+ let ( name1, data1) = & rows[ 1 ] ;
883+ assert_eq ! ( name1, "Bob" ) ;
884+ assert_eq ! ( data1[ "email" ] , serde_json:: json!( "bob@example.com" ) ) ;
885+ assert ! (
886+ data1[ "tags" ] . is_array( ) ,
887+ "array `tags` must be preserved in the catch-all"
888+ ) ;
889+ assert ! ( data1. get( "name" ) . is_none( ) ) ;
890+ }
891+
772892use datafusion:: common:: Result as DFResult ;
773893fn project_record_batch ( batch : & RecordBatch , columns : & [ & str ] ) -> DFResult < RecordBatch > {
774894 let schema = batch. schema ( ) ;
@@ -812,6 +932,7 @@ async fn test_mongodb_arrow_oneway() {
812932 test_mongodb_nested_object_types ( port) . await ;
813933 test_mongodb_null_and_missing_fields ( port) . await ;
814934 test_mongodb_unnesting_depth_1 ( port) . await ;
935+ test_mongodb_json_nesting ( port) . await ;
815936 test_mongodb_sort_limit ( port) . await ;
816937
817938 mongodb_container. remove ( ) . await . expect ( "container to stop" ) ;
0 commit comments