@@ -1937,6 +1937,35 @@ await command.ExecuteNonQueryWithRetryAsync(
19371937 StringBuilder row = new ( ) ;
19381938 ulong rowCount = 0 ;
19391939
1940+ // Precompute nested JSON composite column mapping
1941+ // Maps column index to: (compositeColumnName, fieldName, isFirstField, isLastField, fieldCount)
1942+ Dictionary < int , ( string CompositeColumnName , string FieldName , bool IsFirstField , bool IsLastField , int FieldCount ) > ? nestedJsonColumnMap = null ;
1943+ if ( endpoint . NestedJsonForCompositeTypes == true && routine . CompositeColumnInfo is not null )
1944+ {
1945+ nestedJsonColumnMap = new ( ) ;
1946+ foreach ( var ( _, compositeInfo ) in routine . CompositeColumnInfo )
1947+ {
1948+ var indices = compositeInfo . ExpandedColumnIndices ;
1949+ var fieldCount = indices . Length ;
1950+ for ( int fieldIdx = 0 ; fieldIdx < fieldCount ; fieldIdx ++ )
1951+ {
1952+ var colIdx = indices [ fieldIdx ] ;
1953+ nestedJsonColumnMap [ colIdx ] = (
1954+ compositeInfo . ConvertedColumnName ,
1955+ compositeInfo . FieldNames [ fieldIdx ] ,
1956+ fieldIdx == 0 ,
1957+ fieldIdx == indices . Length - 1 ,
1958+ fieldCount
1959+ ) ;
1960+ }
1961+ }
1962+ }
1963+
1964+ // Buffer for composite fields to detect all-NULL composites
1965+ StringBuilder ? compositeBuffer = nestedJsonColumnMap is not null ? new ( ) : null ;
1966+ bool compositeHasNonNullValue = false ;
1967+ string ? currentCompositeName = null ;
1968+
19401969 if ( endpoint . Raw is true && endpoint . RawColumnNames is true && binary is false )
19411970 {
19421971 StringBuilder columns = new ( ) ;
@@ -2017,76 +2046,155 @@ await command.ExecuteNonQueryWithRetryAsync(
20172046 }
20182047 else
20192048 {
2049+ // Handle nested JSON composite types
2050+ ( string CompositeColumnName , string FieldName , bool IsFirstField , bool IsLastField , int FieldCount ) compositeMapping = default ;
2051+ bool isInComposite = nestedJsonColumnMap is not null && nestedJsonColumnMap . TryGetValue ( i , out compositeMapping ) ;
2052+
2053+ // Determine which buffer to write to
2054+ StringBuilder outputBuffer = ( isInComposite && compositeBuffer is not null ) ? compositeBuffer : row ;
2055+
20202056 if ( routine . ReturnsUnnamedSet == false )
20212057 {
20222058 if ( i == 0 )
20232059 {
20242060 row . Append ( Consts . OpenBrace ) ;
20252061 }
2026- row . Append ( Consts . DoubleQuote ) ;
2027- row . Append ( routine . ColumnNames [ i ] ) ;
2028- row . Append ( Consts . DoubleQuoteColon ) ;
2062+
2063+ if ( isInComposite )
2064+ {
2065+ if ( compositeMapping . IsFirstField )
2066+ {
2067+ // Start of composite: reset buffer and track column name
2068+ compositeBuffer ! . Clear ( ) ;
2069+ compositeHasNonNullValue = false ;
2070+ currentCompositeName = compositeMapping . CompositeColumnName ;
2071+
2072+ // Write field name/value to buffer
2073+ outputBuffer . Append ( Consts . DoubleQuote ) ;
2074+ outputBuffer . Append ( compositeMapping . FieldName ) ;
2075+ outputBuffer . Append ( Consts . DoubleQuoteColon ) ;
2076+ }
2077+ else
2078+ {
2079+ // Middle or end field in composite: just output field name
2080+ outputBuffer . Append ( Consts . DoubleQuote ) ;
2081+ outputBuffer . Append ( compositeMapping . FieldName ) ;
2082+ outputBuffer . Append ( Consts . DoubleQuoteColon ) ;
2083+ }
2084+ }
2085+ else
2086+ {
2087+ row . Append ( Consts . DoubleQuote ) ;
2088+ row . Append ( routine . ColumnNames [ i ] ) ;
2089+ row . Append ( Consts . DoubleQuoteColon ) ;
2090+ }
20292091 }
20302092
20312093 var descriptor = routine . ColumnsTypeDescriptor [ i ] ;
20322094 if ( value == DBNull . Value )
20332095 {
2034- row . Append ( Consts . Null ) ;
2096+ outputBuffer . Append ( Consts . Null ) ;
20352097 }
20362098 else if ( descriptor . IsArray && value is not null )
20372099 {
2038- row . Append ( PgArrayToJsonArray ( raw , descriptor ) ) ;
2100+ if ( isInComposite ) compositeHasNonNullValue = true ;
2101+ // Check if this is an array of composite types - always serialize as nested JSON objects
2102+ if ( routine . ArrayCompositeColumnInfo is not null &&
2103+ routine . ArrayCompositeColumnInfo . TryGetValue ( i , out var arrayCompositeInfo ) )
2104+ {
2105+ outputBuffer . Append ( PgCompositeArrayToJsonArray ( raw , arrayCompositeInfo . FieldNames , arrayCompositeInfo . FieldDescriptors ) ) ;
2106+ }
2107+ else
2108+ {
2109+ outputBuffer . Append ( PgArrayToJsonArray ( raw , descriptor ) ) ;
2110+ }
20392111 }
20402112 else if ( ( descriptor . IsNumeric || descriptor . IsBoolean || descriptor . IsJson ) && value is not null )
20412113 {
2114+ if ( isInComposite ) compositeHasNonNullValue = true ;
20422115 if ( descriptor . IsBoolean )
20432116 {
20442117 if ( raw . Length == 1 && raw [ 0 ] == 't' )
20452118 {
2046- row . Append ( Consts . True ) ;
2119+ outputBuffer . Append ( Consts . True ) ;
20472120 }
20482121 else if ( raw . Length == 1 && raw [ 0 ] == 'f' )
20492122 {
2050- row . Append ( Consts . False ) ;
2123+ outputBuffer . Append ( Consts . False ) ;
20512124 }
20522125 else
20532126 {
2054- row . Append ( raw ) ;
2127+ outputBuffer . Append ( raw ) ;
20552128 }
20562129 }
20572130 else
20582131 {
20592132 // numeric and json
2060- row . Append ( raw ) ;
2133+ outputBuffer . Append ( raw ) ;
20612134 }
20622135 }
20632136 else
20642137 {
2138+ if ( isInComposite ) compositeHasNonNullValue = true ;
20652139 if ( descriptor . ActualDbType == NpgsqlDbType . Unknown )
20662140 {
2067- row . Append ( PgUnknownToJsonArray ( ref raw ) ) ;
2141+ outputBuffer . Append ( PgUnknownToJsonArray ( ref raw ) ) ;
20682142 }
20692143 else if ( descriptor . NeedsEscape )
20702144 {
2071- row . Append ( SerializeString ( ref raw ) ) ;
2145+ outputBuffer . Append ( SerializeString ( ref raw ) ) ;
20722146 }
20732147 else
20742148 {
20752149 if ( descriptor . IsDateTime )
20762150 {
2077- row . Append ( QuoteDateTime ( ref raw ) ) ;
2151+ outputBuffer . Append ( QuoteDateTime ( ref raw ) ) ;
20782152 }
20792153 else
20802154 {
2081- row . Append ( Quote ( ref raw ) ) ;
2155+ outputBuffer . Append ( Quote ( ref raw ) ) ;
20822156 }
20832157 }
20842158 }
2159+
2160+ // Handle closing braces and commas for nested JSON composite types
2161+ if ( isInComposite && compositeMapping . IsLastField )
2162+ {
2163+ // End of composite: decide whether to output null or the buffered object
2164+ row . Append ( Consts . DoubleQuote ) ;
2165+ row . Append ( currentCompositeName ) ;
2166+ row . Append ( Consts . DoubleQuoteColon ) ;
2167+
2168+ if ( compositeHasNonNullValue )
2169+ {
2170+ // At least one field has a value, output as object
2171+ row . Append ( Consts . OpenBrace ) ;
2172+ row . Append ( compositeBuffer ) ;
2173+ row . Append ( Consts . CloseBrace ) ;
2174+ }
2175+ else
2176+ {
2177+ // All fields are NULL, output null
2178+ row . Append ( Consts . Null ) ;
2179+ }
2180+ }
2181+ else if ( isInComposite )
2182+ {
2183+ // Add comma between composite fields
2184+ outputBuffer . Append ( Consts . Comma ) ;
2185+ }
2186+
20852187 if ( routine . ReturnsUnnamedSet == false && i == routine . ColumnCount - 1 )
20862188 {
20872189 row . Append ( Consts . CloseBrace ) ;
20882190 }
2089- if ( i < routine . ColumnCount - 1 )
2191+
2192+ // Add comma between columns (but not within composite fields which are handled above)
2193+ if ( ! isInComposite && i < routine . ColumnCount - 1 )
2194+ {
2195+ row . Append ( Consts . Comma ) ;
2196+ }
2197+ else if ( isInComposite && compositeMapping . IsLastField && i < routine . ColumnCount - 1 )
20902198 {
20912199 row . Append ( Consts . Comma ) ;
20922200 }
0 commit comments