@@ -345,37 +345,42 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
345345 resource : AdminForthResource ;
346346 filters : IAdminForthAndOrFilter ;
347347 aggregations : { [ alias : string ] : IAggregationRule } ;
348- groupBy ?: IGroupByRule ;
348+ groupBy ?: IGroupByRule | IGroupByRule [ ] ;
349349 } ) : Promise < Array < { group ?: string , [ key : string ] : any } > > {
350350 const tableName = resource . table ;
351351 const selectParts : string [ ] = [ ] ;
352352 const medianFields : { alias : string ; field : string } [ ] = [ ] ;
353- let groupExpr : string | null = null ;
354-
355- if ( groupBy ?. type === 'field' ) {
356- groupExpr = `\`${ groupBy . field } \`` ;
357- selectParts . push ( `${ groupExpr } AS \`group\`` ) ;
358- } else if ( groupBy ?. type === 'date_trunc' ) {
359- const g = groupBy as IGroupByDateTrunc ;
360- const tz = g . timezone ?? 'UTC' ;
361- if ( ! / ^ [ A - Z a - z 0 - 9 / _ + \- ] + $ / . test ( tz ) ) {
362- throw new Error ( `Invalid timezone value: ${ tz } ` ) ;
363- }
364- const innerExpr = `COALESCE(CONVERT_TZ(\`${ g . field } \`, 'UTC', '${ tz } '), \`${ g . field } \`)` ;
365- switch ( g . truncation ) {
366- case 'day' : groupExpr = `DATE_FORMAT(${ innerExpr } , '%Y-%m-%d')` ; break ;
367- case 'month' : groupExpr = `DATE_FORMAT(${ innerExpr } , '%Y-%m-01')` ; break ;
368- case 'year' : groupExpr = `DATE_FORMAT(${ innerExpr } , '%Y-01-01')` ; break ;
369- case 'week' : groupExpr = `DATE_FORMAT(DATE_SUB(${ innerExpr } , INTERVAL WEEKDAY(${ innerExpr } ) DAY), '%Y-%m-%d')` ; break ;
353+ const groupExprs : string [ ] = [ ] ;
354+ const groupAliases : string [ ] = [ ] ;
355+ const groupByRules = this . normalizeGroupByRules ( groupBy ) ;
356+
357+ for ( const [ index , groupByRule ] of groupByRules . entries ( ) ) {
358+ let groupExpr : string ;
359+ if ( groupByRule . type === 'field' ) {
360+ groupExpr = `\`${ groupByRule . field } \`` ;
361+ } else {
362+ const g = groupByRule as IGroupByDateTrunc ;
363+ const tz = g . timezone ?? 'UTC' ;
364+ const innerExpr = `COALESCE(CONVERT_TZ(\`${ g . field } \`, 'UTC', '${ tz } '), \`${ g . field } \`)` ;
365+ switch ( g . truncation ) {
366+ case 'day' : groupExpr = `DATE_FORMAT(${ innerExpr } , '%Y-%m-%d')` ; break ;
367+ case 'month' : groupExpr = `DATE_FORMAT(${ innerExpr } , '%Y-%m-01')` ; break ;
368+ case 'year' : groupExpr = `DATE_FORMAT(${ innerExpr } , '%Y-01-01')` ; break ;
369+ case 'week' : groupExpr = `DATE_FORMAT(DATE_SUB(${ innerExpr } , INTERVAL WEEKDAY(${ innerExpr } ) DAY), '%Y-%m-%d')` ; break ;
370+ }
370371 }
371- selectParts . push ( `${ groupExpr } AS \`group\`` ) ;
372+ const groupAlias = this . getGroupByResultAlias ( groupByRule , index , groupByRules . length ) ;
373+ groupExprs . push ( groupExpr ) ;
374+ groupAliases . push ( groupAlias ) ;
375+ selectParts . push ( `${ groupExpr } AS \`${ groupAlias } \`` ) ;
372376 }
373377
374378 for ( const [ alias , rule ] of Object . entries ( aggregations ) ) {
375379 const f = `\`${ rule . field } \`` ;
376380 switch ( rule . operation ) {
377381 case 'sum' : selectParts . push ( `SUM(${ f } ) AS \`${ alias } \`` ) ; break ;
378382 case 'count' : selectParts . push ( `COUNT(*) AS \`${ alias } \`` ) ; break ;
383+ case 'count_distinct' : selectParts . push ( `COUNT(DISTINCT ${ f } ) AS \`${ alias } \`` ) ; break ;
379384 case 'avg' : selectParts . push ( `AVG(${ f } ) AS \`${ alias } \`` ) ; break ;
380385 case 'min' : selectParts . push ( `MIN(${ f } ) AS \`${ alias } \`` ) ; break ;
381386 case 'max' : selectParts . push ( `MAX(${ f } ) AS \`${ alias } \`` ) ; break ;
@@ -389,10 +394,10 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
389394
390395 // Run non-median aggregations
391396 let rows : AggRow [ ] = [ ] ;
392- const hasNonMedian = selectParts . length > ( groupExpr ? 1 : 0 ) ;
397+ const hasNonMedian = selectParts . length > groupExprs . length ;
393398 if ( hasNonMedian ) {
394399 let query = `SELECT ${ selectParts . join ( ', ' ) } FROM \`${ tableName } \` ${ where } ` ;
395- if ( groupExpr ) query += ` GROUP BY ${ groupExpr } ORDER BY ${ groupExpr } ASC` ;
400+ if ( groupExprs . length ) query += ` GROUP BY ${ groupExprs . join ( ', ' ) } ORDER BY ${ groupExprs . join ( ', ' ) } ASC` ;
396401 dbLogger . trace ( `🪲📜 MySQL AGG Q: ${ query } values: ${ JSON . stringify ( filterValues ) } ` ) ;
397402 const [ result ] = await this . client . execute ( query , filterValues ) ;
398403 rows = result as AggRow [ ] ;
@@ -404,18 +409,20 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
404409 const nullGuard = where ? `${ where } AND ${ f } IS NOT NULL` : `WHERE ${ f } IS NOT NULL` ;
405410
406411 let medianQuery : string ;
407- if ( groupExpr ) {
412+ if ( groupExprs . length ) {
413+ const groupSelect = groupExprs . map ( ( expr , index ) => `${ expr } AS \`${ groupAliases [ index ] } \`` ) . join ( ', ' ) ;
414+ const groupColumns = groupAliases . map ( alias => `\`${ alias } \`` ) . join ( ', ' ) ;
408415 medianQuery = `
409- SELECT \`group\` , AVG(${ f } ) AS \`${ alias } \`
416+ SELECT ${ groupColumns } , AVG(${ f } ) AS \`${ alias } \`
410417 FROM (
411- SELECT ${ groupExpr } AS \`group\` , ${ f } ,
412- ROW_NUMBER() OVER (PARTITION BY ${ groupExpr } ORDER BY ${ f } ) AS rn,
413- COUNT(*) OVER (PARTITION BY ${ groupExpr } ) AS cnt
418+ SELECT ${ groupSelect } , ${ f } ,
419+ ROW_NUMBER() OVER (PARTITION BY ${ groupExprs . join ( ', ' ) } ORDER BY ${ f } ) AS rn,
420+ COUNT(*) OVER (PARTITION BY ${ groupExprs . join ( ', ' ) } ) AS cnt
414421 FROM \`${ tableName } \` ${ nullGuard }
415422 ) t
416423 WHERE rn IN (FLOOR((cnt + 1) / 2.0), CEIL((cnt + 1) / 2.0))
417- GROUP BY \`group\`
418- ORDER BY \`group\` ASC
424+ GROUP BY ${ groupColumns }
425+ ORDER BY ${ groupColumns } ASC
419426 ` ;
420427 } else {
421428 medianQuery = `
@@ -434,13 +441,14 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
434441 const [ medianResult ] = await this . client . execute ( medianQuery , filterValues ) ;
435442 const medianRows = medianResult as AggRow [ ] ;
436443
437- if ( groupExpr ) {
444+ if ( groupExprs . length ) {
445+ const groupKey = ( row : AggRow ) => groupAliases . map ( alias => String ( row [ alias ] ) ) . join ( '\u0000' ) ;
438446 if ( rows . length === 0 ) {
439- rows = medianRows . map ( ( r ) => ( { group : r . group , [ alias ] : r [ alias ] } ) ) ;
447+ rows = medianRows . map ( ( r ) => ( { ... Object . fromEntries ( groupAliases . map ( groupAlias => [ groupAlias , r [ groupAlias ] ] ) ) , [ alias ] : r [ alias ] } ) ) ;
440448 } else {
441- const byGroup = new Map ( medianRows . map ( ( r ) => [ String ( r . group ) , r [ alias ] ] ) ) ;
449+ const byGroup = new Map ( medianRows . map ( ( r ) => [ groupKey ( r ) , r [ alias ] ] ) ) ;
442450 for ( const row of rows ) {
443- row [ alias ] = byGroup . get ( String ( row . group ) ) ?? null ;
451+ row [ alias ] = byGroup . get ( groupKey ( row ) ) ?? null ;
444452 }
445453 }
446454 } else {
@@ -557,4 +565,4 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
557565 }
558566}
559567
560- export default MysqlConnector ;
568+ export default MysqlConnector ;
0 commit comments