@@ -244,9 +244,15 @@ pub fn shape_decoded_rows(decoded: &JsonValue, projection: Option<&OutputSchema>
244244 let lookup_keys: Vec < String > = s. columns . iter ( ) . map ( |c| c. lookup_key . clone ( ) ) . collect ( ) ;
245245 let display_names: Vec < String > =
246246 s. columns . iter ( ) . map ( |c| c. display_name . clone ( ) ) . collect ( ) ;
247+ // Row cells are stored under per-column unique keys, not display
248+ // names: duplicate display names (`SELECT w.id, b.id` → `id`,
249+ // `id`) would collide in the row map and collapse both wire
250+ // columns to the last value. Encoders re-derive the same keys via
251+ // `cell_keys` when reading cells.
252+ let keys = super :: project:: cell_keys ( & display_names) ;
247253 let projected_rows = rows
248254 . iter ( )
249- . map ( |row| project_row ( row, & lookup_keys, & display_names) )
255+ . map ( |row| project_row ( row, & lookup_keys, & display_names, & keys ) )
250256 . collect ( ) ;
251257 // Carry each projected column's real catalog type, aligned in
252258 // order with `display_names`. Only the pgwire encoder consumes
@@ -280,10 +286,15 @@ pub fn shape_decoded_rows(decoded: &JsonValue, projection: Option<&OutputSchema>
280286/// Select and rename one flat row's fields per the projection lists, trying
281287/// each candidate key in order: the full lookup key, then the bare
282288/// (post-dot) column name, then the SELECT alias.
289+ ///
290+ /// Cells are inserted under `cell_keys` (unique per column, see
291+ /// [`super::project::cell_keys`]) rather than the display names, which may
292+ /// repeat across columns and would otherwise collapse in the output map.
283293fn project_row (
284294 row : & Map < String , JsonValue > ,
285295 lookup_keys : & [ String ] ,
286296 display_names : & [ String ] ,
297+ cell_keys : & [ String ] ,
287298) -> Map < String , JsonValue > {
288299 let mut out = Map :: new ( ) ;
289300 for ( i, lookup_key) in lookup_keys. iter ( ) . enumerate ( ) {
@@ -313,7 +324,8 @@ fn project_row(
313324 } )
314325 . cloned ( )
315326 . unwrap_or ( JsonValue :: Null ) ;
316- out. insert ( display_name. to_string ( ) , value) ;
327+ let cell_key = cell_keys. get ( i) . map ( String :: as_str) . unwrap_or ( display_name) ;
328+ out. insert ( cell_key. to_string ( ) , value) ;
317329 }
318330 out
319331}
@@ -372,3 +384,44 @@ fn single_result_row(text: String) -> ShapedRows {
372384 notice : None ,
373385 }
374386}
387+
388+ #[ cfg( test) ]
389+ mod tests {
390+ use super :: * ;
391+
392+ /// Two projected columns sharing the display name `id` (`SELECT w.id,
393+ /// b.id`) must keep both values in the shaped row instead of collapsing
394+ /// to the last table's value.
395+ #[ test]
396+ fn project_row_keeps_both_columns_with_duplicate_display_names ( ) {
397+ let mut row = Map :: new ( ) ;
398+ row. insert ( "w.id" . to_string ( ) , JsonValue :: String ( "w1" . to_string ( ) ) ) ;
399+ row. insert ( "b.id" . to_string ( ) , JsonValue :: String ( "b1" . to_string ( ) ) ) ;
400+
401+ let lookup_keys = vec ! [ "w.id" . to_string( ) , "b.id" . to_string( ) ] ;
402+ let display_names = vec ! [ "id" . to_string( ) , "id" . to_string( ) ] ;
403+ let keys = super :: super :: project:: cell_keys ( & display_names) ;
404+
405+ let out = project_row ( & row, & lookup_keys, & display_names, & keys) ;
406+ assert_eq ! ( out. len( ) , 2 , "both cells must survive the projection" ) ;
407+ assert_eq ! ( out. get( "id" ) , Some ( & JsonValue :: String ( "w1" . to_string( ) ) ) ) ;
408+ assert_eq ! ( out. get( "id_1" ) , Some ( & JsonValue :: String ( "b1" . to_string( ) ) ) ) ;
409+ }
410+
411+ /// Duplicate-free projections still store cells under the display name
412+ /// (`cell_keys` is the identity), so existing readers are unaffected.
413+ #[ test]
414+ fn project_row_uses_display_names_when_unique ( ) {
415+ let mut row = Map :: new ( ) ;
416+ row. insert ( "w.id" . to_string ( ) , JsonValue :: String ( "w1" . to_string ( ) ) ) ;
417+ row. insert ( "b.title" . to_string ( ) , JsonValue :: String ( "t" . to_string ( ) ) ) ;
418+
419+ let lookup_keys = vec ! [ "w.id" . to_string( ) , "b.title" . to_string( ) ] ;
420+ let display_names = vec ! [ "id" . to_string( ) , "title" . to_string( ) ] ;
421+ let keys = super :: super :: project:: cell_keys ( & display_names) ;
422+
423+ let out = project_row ( & row, & lookup_keys, & display_names, & keys) ;
424+ assert_eq ! ( out. get( "id" ) , Some ( & JsonValue :: String ( "w1" . to_string( ) ) ) ) ;
425+ assert_eq ! ( out. get( "title" ) , Some ( & JsonValue :: String ( "t" . to_string( ) ) ) ) ;
426+ }
427+ }
0 commit comments