11import dayjs from 'dayjs' ;
2- import { AdminForthResource , IAdminForthSingleFilter , IAdminForthAndOrFilter , IAdminForthDataSourceConnector , AdminForthConfig } from '../types/Back.js' ;
2+ import { AdminForthResource , IAdminForthSingleFilter , IAdminForthAndOrFilter , IAdminForthDataSourceConnector , AdminForthConfig , IAggregationRule , IGroupByRule , IGroupByDateTrunc , IGroupByField } from '../types/Back.js' ;
33import { AdminForthDataTypes , AdminForthFilterOperators , AdminForthSortDirections , } from '../types/Common.js' ;
44import AdminForthBaseConnector from './baseConnector.js' ;
55import mysql from 'mysql2/promise' ;
@@ -338,13 +338,128 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
338338 } : { sql : '' , values : [ ] } ;
339339 }
340340
341+ async getAggregateWithOriginalTypes ( { resource, filters, aggregations, groupBy } : {
342+ resource : AdminForthResource ;
343+ filters : IAdminForthAndOrFilter ;
344+ aggregations : { [ alias : string ] : IAggregationRule } ;
345+ groupBy ?: IGroupByRule ;
346+ } ) : Promise < Array < { group ?: string , [ key : string ] : any } > > {
347+ const tableName = resource . table ;
348+ const selectParts : string [ ] = [ ] ;
349+ const medianFields : { alias : string ; field : string } [ ] = [ ] ;
350+ let groupExpr : string | null = null ;
351+
352+ if ( groupBy ?. type === 'field' ) {
353+ groupExpr = `\`${ groupBy . field } \`` ;
354+ selectParts . push ( `${ groupExpr } AS \`group\`` ) ;
355+ } else if ( groupBy ?. type === 'date_trunc' ) {
356+ const g = groupBy as IGroupByDateTrunc ;
357+ const tz = g . timezone ?? 'UTC' ;
358+ if ( ! / ^ [ A - Z a - z 0 - 9 / _ + \- ] + $ / . test ( tz ) ) {
359+ throw new Error ( `Invalid timezone value: ${ tz } ` ) ;
360+ }
361+ const innerExpr = `COALESCE(CONVERT_TZ(\`${ g . field } \`, 'UTC', '${ tz } '), \`${ g . field } \`)` ;
362+ switch ( g . truncation ) {
363+ case 'day' : groupExpr = `DATE_FORMAT(${ innerExpr } , '%Y-%m-%d')` ; break ;
364+ case 'month' : groupExpr = `DATE_FORMAT(${ innerExpr } , '%Y-%m-01')` ; break ;
365+ case 'year' : groupExpr = `DATE_FORMAT(${ innerExpr } , '%Y-01-01')` ; break ;
366+ case 'week' : groupExpr = `DATE_FORMAT(DATE_SUB(${ innerExpr } , INTERVAL WEEKDAY(${ innerExpr } ) DAY), '%Y-%m-%d')` ; break ;
367+ }
368+ selectParts . push ( `${ groupExpr } AS \`group\`` ) ;
369+ }
370+
371+ for ( const [ alias , rule ] of Object . entries ( aggregations ) ) {
372+ const f = `\`${ rule . field } \`` ;
373+ switch ( rule . operation ) {
374+ case 'sum' : selectParts . push ( `SUM(${ f } ) AS \`${ alias } \`` ) ; break ;
375+ case 'count' : selectParts . push ( `COUNT(*) AS \`${ alias } \`` ) ; break ;
376+ case 'avg' : selectParts . push ( `AVG(${ f } ) AS \`${ alias } \`` ) ; break ;
377+ case 'min' : selectParts . push ( `MIN(${ f } ) AS \`${ alias } \`` ) ; break ;
378+ case 'max' : selectParts . push ( `MAX(${ f } ) AS \`${ alias } \`` ) ; break ;
379+ case 'median' : medianFields . push ( { alias, field : rule . field } ) ; break ;
380+ }
381+ }
382+
383+ const { sql : where , values : filterValues } = this . whereClauseAndValues ( filters ) ;
384+
385+ type AggRow = { group ?: string } & Record < string , number | string | null > ;
386+
387+ // Run non-median aggregations
388+ let rows : AggRow [ ] = [ ] ;
389+ const hasNonMedian = selectParts . length > ( groupExpr ? 1 : 0 ) ;
390+ if ( hasNonMedian ) {
391+ let query = `SELECT ${ selectParts . join ( ', ' ) } FROM \`${ tableName } \` ${ where } ` ;
392+ if ( groupExpr ) query += ` GROUP BY ${ groupExpr } ORDER BY ${ groupExpr } ASC` ;
393+ dbLogger . trace ( `🪲📜 MySQL AGG Q: ${ query } values: ${ JSON . stringify ( filterValues ) } ` ) ;
394+ const [ result ] = await this . client . execute ( query , filterValues ) ;
395+ rows = result as AggRow [ ] ;
396+ }
397+
398+ // Run each median via window functions (MySQL 8+) — no session variables, no memory pressure
399+ for ( const { alias, field } of medianFields ) {
400+ const f = `\`${ field } \`` ;
401+ const nullGuard = where ? `${ where } AND ${ f } IS NOT NULL` : `WHERE ${ f } IS NOT NULL` ;
402+
403+ let medianQuery : string ;
404+ if ( groupExpr ) {
405+ medianQuery = `
406+ SELECT \`group\`, AVG(${ f } ) AS \`${ alias } \`
407+ FROM (
408+ SELECT ${ groupExpr } AS \`group\`, ${ f } ,
409+ ROW_NUMBER() OVER (PARTITION BY ${ groupExpr } ORDER BY ${ f } ) AS rn,
410+ COUNT(*) OVER (PARTITION BY ${ groupExpr } ) AS cnt
411+ FROM \`${ tableName } \` ${ nullGuard }
412+ ) t
413+ WHERE rn IN (FLOOR((cnt + 1) / 2.0), CEIL((cnt + 1) / 2.0))
414+ GROUP BY \`group\`
415+ ORDER BY \`group\` ASC
416+ ` ;
417+ } else {
418+ medianQuery = `
419+ SELECT AVG(${ f } ) AS \`${ alias } \`
420+ FROM (
421+ SELECT ${ f } ,
422+ ROW_NUMBER() OVER (ORDER BY ${ f } ) AS rn,
423+ COUNT(*) OVER () AS cnt
424+ FROM \`${ tableName } \` ${ nullGuard }
425+ ) t
426+ WHERE rn IN (FLOOR((cnt + 1) / 2.0), CEIL((cnt + 1) / 2.0))
427+ ` ;
428+ }
429+
430+ dbLogger . trace ( `🪲📜 MySQL MEDIAN Q: ${ medianQuery } values: ${ JSON . stringify ( filterValues ) } ` ) ;
431+ const [ medianResult ] = await this . client . execute ( medianQuery , filterValues ) ;
432+ const medianRows = medianResult as AggRow [ ] ;
433+
434+ if ( groupExpr ) {
435+ if ( rows . length === 0 ) {
436+ rows = medianRows . map ( ( r ) => ( { group : r . group , [ alias ] : r [ alias ] } ) ) ;
437+ } else {
438+ const byGroup = new Map ( medianRows . map ( ( r ) => [ String ( r . group ) , r [ alias ] ] ) ) ;
439+ for ( const row of rows ) {
440+ row [ alias ] = byGroup . get ( String ( row . group ) ) ?? null ;
441+ }
442+ }
443+ } else {
444+ const medianVal = medianRows [ 0 ] ?. [ alias ] ?? null ;
445+ if ( rows . length === 0 ) {
446+ rows = [ { [ alias ] : medianVal } ] ;
447+ } else {
448+ rows [ 0 ] [ alias ] = medianVal ;
449+ }
450+ }
451+ }
452+
453+ return rows ;
454+ }
455+
341456 async getDataWithOriginalTypes ( { resource, limit, offset, sort, filters } ) : Promise < any [ ] > {
342- const columns = resource . dataSourceColumns . map ( ( col ) => `${ col . name } ` ) . join ( ', ' ) ;
457+ const columns = resource . dataSourceColumns . map ( ( col : { name : string } ) => `${ col . name } ` ) . join ( ', ' ) ;
343458 const tableName = resource . table ;
344459
345460 const { sql : where , values : filterValues } = this . whereClauseAndValues ( filters ) ;
346461
347- const orderBy = sort . length ? `ORDER BY ${ sort . map ( ( s ) => `${ s . field } ${ this . SortDirectionsMap [ s . direction ] } ` ) . join ( ', ' ) } ` : '' ;
462+ const orderBy = sort . length ? `ORDER BY ${ sort . map ( ( s : { field : string ; direction : AdminForthSortDirections } ) => `${ s . field } ${ this . SortDirectionsMap [ s . direction ] } ` ) . join ( ', ' ) } ` : '' ;
348463 let selectQuery = `SELECT ${ columns } FROM ${ tableName } ` ;
349464 if ( where ) selectQuery += ` ${ where } ` ;
350465 if ( orderBy ) selectQuery += ` ${ orderBy } ` ;
@@ -385,7 +500,7 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
385500 async getMinMaxForColumnsWithOriginalTypes ( { resource, columns } ) {
386501 const tableName = resource . table ;
387502 const result = { } ;
388- await Promise . all ( columns . map ( async ( col ) => {
503+ await Promise . all ( columns . map ( async ( col : { name : string } ) => {
389504 const q = `SELECT MIN(${ col . name } ) as min, MAX(${ col . name } ) as max FROM ${ tableName } ` ;
390505 dbLogger . trace ( `🪲📜 MySQL Q: ${ q } ` ) ;
391506 const [ results ] = await this . client . execute ( q ) ;
@@ -410,7 +525,7 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
410525
411526 async updateRecordOriginalValues ( { resource, recordId, newValues } ) {
412527 const values = [ ...Object . values ( newValues ) , recordId ] ;
413- const columnsWithPlaceholders = Object . keys ( newValues ) . map ( ( col , i ) => `${ col } = ?` ) . join ( ', ' ) ;
528+ const columnsWithPlaceholders = Object . keys ( newValues ) . map ( ( col ) => `${ col } = ?` ) . join ( ', ' ) ;
414529 const q = `UPDATE ${ resource . table } SET ${ columnsWithPlaceholders } WHERE ${ this . getPrimaryKey ( resource ) } = ?` ;
415530 dbLogger . trace ( `🪲📜 MySQL Q: ${ q } values: ${ JSON . stringify ( values ) } ` ) ;
416531 await this . client . execute ( q , values ) ;
0 commit comments