@@ -602,6 +602,7 @@ pub fn apply(
602602 parameters : & HashMap < String , ParameterValue > ,
603603 dialect : & dyn SqlDialect ,
604604 aesthetic_ctx : & AestheticContext ,
605+ domain_aesthetics : & [ & ' static str ] ,
605606) -> Result < StatResult > {
606607 let raw = match parameters. get ( "aggregate" ) {
607608 None | Some ( ParameterValue :: Null ) => return Ok ( StatResult :: Identity ) ,
@@ -656,6 +657,15 @@ pub fn apply(
656657 Some ( c) => c. to_string ( ) ,
657658 None => continue , // literals & annotation columns pass through
658659 } ;
660+ // Geom-declared domain aesthetics (e.g. `pos1` for line/area/ribbon)
661+ // always become group keys — they identify each row, never get
662+ // aggregated, never get dropped.
663+ if domain_aesthetics. contains ( & aes. as_str ( ) ) {
664+ if !kept_cols. contains ( & col) {
665+ kept_cols. push ( col) ;
666+ }
667+ continue ;
668+ }
659669 let info = schema. iter ( ) . find ( |c| c. name == col) ;
660670 let is_discrete = info. map ( |c| c. is_discrete ) . unwrap_or ( false ) ;
661671 if is_discrete {
@@ -922,11 +932,31 @@ mod tests {
922932 schema : & Schema ,
923933 group_by : & [ String ] ,
924934 dialect : & dyn SqlDialect ,
935+ ) -> Result < StatResult > {
936+ run_with_domain ( params, aes, schema, group_by, dialect, & [ ] )
937+ }
938+
939+ fn run_with_domain (
940+ params : ParameterValue ,
941+ aes : & Mappings ,
942+ schema : & Schema ,
943+ group_by : & [ String ] ,
944+ dialect : & dyn SqlDialect ,
945+ domain : & [ & ' static str ] ,
925946 ) -> Result < StatResult > {
926947 let mut p = HashMap :: new ( ) ;
927948 p. insert ( "aggregate" . to_string ( ) , params) ;
928949 let ctx = cartesian_ctx ( ) ;
929- apply ( "SELECT * FROM t" , schema, aes, group_by, & p, dialect, & ctx)
950+ apply (
951+ "SELECT * FROM t" ,
952+ schema,
953+ aes,
954+ group_by,
955+ & p,
956+ dialect,
957+ & ctx,
958+ domain,
959+ )
930960 }
931961
932962 fn arr ( items : & [ & str ] ) -> ParameterValue {
@@ -1138,6 +1168,7 @@ mod tests {
11381168 & p,
11391169 & InlineQuantileDialect ,
11401170 & ctx,
1171+ & [ ] ,
11411172 )
11421173 . unwrap ( ) ;
11431174 assert_eq ! ( result, StatResult :: Identity ) ;
@@ -1451,6 +1482,57 @@ mod tests {
14511482 }
14521483 }
14531484
1485+ #[ test]
1486+ fn domain_aesthetic_kept_as_group_key_even_when_continuous ( ) {
1487+ // Regression test for the line/area/ribbon case: the user writes
1488+ // DRAW line ... SETTING aggregate => ('y:min', 'y:max')
1489+ // and expects pos1 (the continuous time-axis column) to be a group
1490+ // key, not a dropped numeric mapping. The geom declares pos1 as a
1491+ // domain aesthetic; the stat keeps it as a group column.
1492+ let mut aes = Mappings :: new ( ) ;
1493+ aes. insert ( "pos1" , col ( "__ggsql_aes_pos1__" ) ) ;
1494+ aes. insert ( "pos2" , col ( "__ggsql_aes_pos2__" ) ) ;
1495+ let schema = schema_for ( & [
1496+ ( "__ggsql_aes_pos1__" , false ) , // continuous, would be dropped without the domain hint
1497+ ( "__ggsql_aes_pos2__" , false ) ,
1498+ ] ) ;
1499+ let result = run_with_domain (
1500+ arr ( & [ "y:min" , "y:max" ] ) ,
1501+ & aes,
1502+ & schema,
1503+ & [ ] ,
1504+ & InlineQuantileDialect ,
1505+ & [ "pos1" ] ,
1506+ )
1507+ . unwrap ( ) ;
1508+ match result {
1509+ StatResult :: Transformed {
1510+ query,
1511+ stat_columns,
1512+ consumed_aesthetics,
1513+ ..
1514+ } => {
1515+ // pos1 is in the GROUP BY, not aggregated.
1516+ assert ! (
1517+ query. contains( "GROUP BY \" __ggsql_aes_pos1__\" " ) ,
1518+ "{}" ,
1519+ query
1520+ ) ;
1521+ assert ! ( !query. contains( "MIN(\" __ggsql_aes_pos1__\" )" ) ) ;
1522+ assert ! ( !query. contains( "MAX(\" __ggsql_aes_pos1__\" )" ) ) ;
1523+ // pos2 is exploded into MIN and MAX branches.
1524+ assert ! ( query. contains( "MIN(\" __ggsql_aes_pos2__\" )" ) ) ;
1525+ assert ! ( query. contains( "MAX(\" __ggsql_aes_pos2__\" )" ) ) ;
1526+ // pos1 is NOT consumed (kept), pos2 IS consumed.
1527+ assert ! ( !consumed_aesthetics. contains( & "pos1" . to_string( ) ) ) ;
1528+ assert ! ( consumed_aesthetics. contains( & "pos2" . to_string( ) ) ) ;
1529+ // synthetic aggregate column emitted in the explosion case.
1530+ assert ! ( stat_columns. contains( & "aggregate" . to_string( ) ) ) ;
1531+ }
1532+ _ => panic ! ( "expected Transformed" ) ,
1533+ }
1534+ }
1535+
14541536 #[ test]
14551537 fn explosion_with_range_geom_two_defaults ( ) {
14561538 // For ribbon: pos1 + pos2min (lower) + pos2max (upper).
0 commit comments