2025-01-16 18:15:22 +00:00
import isPlainObject from 'lodash/isPlainObject' ;
import * as SQLParser from 'node-sql-parser' ;
import { ChSql , chSql , concatChSql , wrapChSqlIfNotEmpty } from '@/clickhouse' ;
2025-01-21 18:44:14 +00:00
import { Metadata } from '@/metadata' ;
2025-01-16 18:15:22 +00:00
import { CustomSchemaSQLSerializerV2 , SearchQueryBuilder } from '@/queryParser' ;
import {
AggregateFunction ,
AggregateFunctionWithCombinators ,
2025-01-24 01:52:54 +00:00
ChartConfigWithDateRange ,
ChartConfigWithOptDateRange ,
2025-02-26 00:00:48 +00:00
MetricsDataType ,
2025-01-16 18:15:22 +00:00
SearchCondition ,
SearchConditionLanguage ,
SelectList ,
2025-02-26 00:00:48 +00:00
SelectSQLStatement ,
2025-01-16 18:15:22 +00:00
SortSpecificationList ,
2025-01-24 01:52:54 +00:00
SqlAstFilter ,
2025-01-16 18:15:22 +00:00
SQLInterval ,
} from '@/types' ;
import {
convertDateRangeToGranularityString ,
getFirstTimestampValueExpression ,
} from '@/utils' ;
// FIXME: SQLParser.ColumnRef is incomplete
type ColumnRef = SQLParser . ColumnRef & {
array_index ? : {
index : { type : string ; value : string } ;
} [ ] ;
} ;
2025-02-26 00:00:48 +00:00
function determineTableName ( select : SelectSQLStatement ) : string {
if ( 'metricTables' in select . from ) {
return select . from . tableName ;
}
return '' ;
}
2025-03-06 00:06:57 +00:00
const DEFAULT_METRIC_TABLE_TIME_COLUMN = 'TimeUnix' ;
2025-01-16 18:15:22 +00:00
export const FIXED_TIME_BUCKET_EXPR_ALIAS = '__hdx_time_bucket' ;
export function isUsingGroupBy (
chartConfig : ChartConfigWithOptDateRange ,
) : chartConfig is Omit < ChartConfigWithDateRange , ' groupBy ' > & {
groupBy : NonNullable < ChartConfigWithDateRange [ ' groupBy ' ] > ;
} {
return chartConfig . groupBy != null && chartConfig . groupBy . length > 0 ;
}
function isUsingGranularity (
chartConfig : ChartConfigWithOptDateRange ,
) : chartConfig is Omit <
Omit < Omit < ChartConfigWithDateRange , ' granularity ' > , 'dateRange' > ,
'timestampValueExpression'
> & {
granularity : NonNullable < ChartConfigWithDateRange [ ' granularity ' ] > ;
dateRange : NonNullable < ChartConfigWithDateRange [ ' dateRange ' ] > ;
timestampValueExpression : NonNullable <
ChartConfigWithDateRange [ 'timestampValueExpression' ]
> ;
} {
return (
chartConfig . timestampValueExpression != null &&
chartConfig . granularity != null
) ;
}
const INVERSE_OPERATOR_MAP = {
'=' : '!=' ,
'>' : '<=' ,
'<' : '>=' ,
'!=' : '=' ,
'<=' : '>' ,
'>=' : '<' ,
} as const ;
export function inverseSqlAstFilter ( filter : SqlAstFilter ) : SqlAstFilter {
return {
. . . filter ,
operator :
INVERSE_OPERATOR_MAP [
filter . operator as keyof typeof INVERSE_OPERATOR_MAP
] ,
} ;
}
export function isNonEmptyWhereExpr ( where? : string ) : where is string {
return where != null && where . trim ( ) != '' ;
}
const fastifySQL = ( {
materializedFields ,
rawSQL ,
} : {
materializedFields : Map < string , string > ;
rawSQL : string ;
} ) = > {
// Parse the SQL AST
try {
const parser = new SQLParser . Parser ( ) ;
const ast = parser . astify ( rawSQL , {
database : 'Postgresql' ,
} ) as SQLParser . Select ;
// traveral ast and replace the left node with the materialized field
// FIXME: type node (AST type is incomplete): https://github.com/taozhi8833998/node-sql-parser/blob/42ea0b1800c5d425acb8c5ca708a1cee731aada8/types.d.ts#L474
const traverse = (
node :
| SQLParser . Expr
| SQLParser . ExpressionValue
| SQLParser . ExprList
| SQLParser . Function
| null ,
) = > {
if ( node == null ) {
return ;
}
let colExpr ;
switch ( node . type ) {
case 'column_ref' : {
// FIXME: handle 'Value' type?
const _n = node as ColumnRef ;
// @ts-ignore
if ( typeof _n . column !== 'string' ) {
// @ts-ignore
colExpr = ` ${ _n . column ? . expr . value } [' ${ _n . array_index ? . [ 0 ] ? . index . value } '] ` ;
}
break ;
}
case 'binary_expr' : {
const _n = node as SQLParser . Expr ;
if ( Array . isArray ( _n . left ) ) {
for ( const left of _n . left ) {
traverse ( left ) ;
}
} else {
traverse ( _n . left ) ;
}
if ( Array . isArray ( _n . right ) ) {
for ( const right of _n . right ) {
traverse ( right ) ;
}
} else {
traverse ( _n . right ) ;
}
break ;
}
case 'function' : {
const _n = node as SQLParser . Function ;
if ( _n . args ? . type === 'expr_list' ) {
if ( Array . isArray ( _n . args ? . value ) ) {
for ( const arg of _n . args . value ) {
traverse ( arg ) ;
}
// ex: JSONExtractString(Body, 'message')
if (
_n . args ? . value ? . [ 0 ] ? . type === 'column_ref' &&
_n . args ? . value ? . [ 1 ] ? . type === 'single_quote_string'
) {
colExpr = ` ${ _n . name ? . name ? . [ 0 ] ? . value } ( ${ ( _n . args ? . value ? . [ 0 ] as any ) ? . column . expr . value } , ' ${ _n . args ? . value ? . [ 1 ] ? . value } ') ` ;
}
}
// when _n.args?.value is Expr
else if ( isPlainObject ( _n . args ? . value ) ) {
traverse ( _n . args . value ) ;
}
}
break ;
}
default :
// ignore other types
break ;
}
if ( colExpr ) {
const materializedField = materializedFields . get ( colExpr ) ;
if ( materializedField ) {
const _n = node as ColumnRef ;
// reset the node ref
for ( const key in _n ) {
// eslint-disable-next-line no-prototype-builtins
if ( _n . hasOwnProperty ( key ) ) {
// @ts-ignore
delete _n [ key ] ;
}
}
_n . type = 'column_ref' ;
// @ts-ignore
_n . table = null ;
// @ts-ignore
_n . column = { expr : { type : 'default' , value : materializedField } } ;
}
}
} ;
if ( Array . isArray ( ast . columns ) ) {
for ( const col of ast . columns ) {
traverse ( col . expr ) ;
}
}
traverse ( ast . where ) ;
return parser . sqlify ( ast ) ;
} catch ( e ) {
console . error ( '[renderWhereExpression]feat: Failed to parse SQL AST' , e ) ;
return rawSQL ;
}
} ;
const aggFnExpr = ( {
fn ,
expr ,
quantileLevel ,
where ,
} : {
fn : AggregateFunction | AggregateFunctionWithCombinators ;
expr? : string ;
quantileLevel? : number ;
where? : string ;
} ) = > {
const isCount = fn . startsWith ( 'count' ) ;
const isWhereUsed = isNonEmptyWhereExpr ( where ) ;
// Cast to float64 because the expr might not be a number
const unsafeExpr = { UNSAFE_RAW_SQL : ` toFloat64OrNull(toString( ${ expr } )) ` } ;
const whereWithExtraNullCheck = ` ${ where } AND ${ unsafeExpr . UNSAFE_RAW_SQL } IS NOT NULL ` ;
if ( fn . endsWith ( 'Merge' ) ) {
return chSql ` ${ fn } ( ${ {
UNSAFE_RAW_SQL : expr ? ? '' ,
} } ) ` ;
}
// TODO: merge this chunk with the rest of logics
else if ( fn . endsWith ( 'State' ) ) {
if ( expr == null || isCount ) {
return isWhereUsed
? chSql ` ${ fn } ( ${ { UNSAFE_RAW_SQL : where } }) `
: chSql ` ${ fn } () ` ;
}
return chSql ` ${ fn } ( ${ unsafeExpr } ${
isWhereUsed ? chSql ` , ${ { UNSAFE_RAW_SQL : whereWithExtraNullCheck } } ` : ''
} ) ` ;
}
if ( fn === 'count' ) {
if ( isWhereUsed ) {
return chSql ` ${ fn } If( ${ { UNSAFE_RAW_SQL : where } }) ` ;
}
return {
sql : ` ${ fn } () ` ,
params : { } ,
} ;
}
if ( expr != null ) {
if ( fn === 'count_distinct' ) {
return chSql ` count ${ isWhereUsed ? 'If' : '' } (DISTINCT ${ {
UNSAFE_RAW_SQL : expr ,
} } $ { isWhereUsed ? chSql ` , ${ { UNSAFE_RAW_SQL : where } } ` : '' } ) ` ;
}
if ( quantileLevel != null ) {
return chSql ` quantile ${ isWhereUsed ? 'If' : '' } ( ${ {
// Using Float64 param leads to an added coersion, but we don't need to
// escape number values anyways
UNSAFE_RAW_SQL : Number.isFinite ( quantileLevel )
? ` ${ quantileLevel } `
: '0' ,
} } ) ( $ { unsafeExpr } $ {
isWhereUsed
? chSql ` , ${ { UNSAFE_RAW_SQL : whereWithExtraNullCheck } } `
: ''
} ) ` ;
}
// TODO: Verify fn is a safe/valid function
return chSql ` ${ { UNSAFE_RAW_SQL : fn } } ${ isWhereUsed ? 'If' : '' } (
$ { unsafeExpr } $ { isWhereUsed ? chSql ` , ${ { UNSAFE_RAW_SQL : whereWithExtraNullCheck } } ` : '' }
) ` ;
} else {
throw new Error (
'Column is required for all non-count aggregation functions' ,
) ;
}
} ;
async function renderSelectList (
selectList : SelectList ,
2025-02-27 16:55:36 +00:00
chartConfig : ChartConfigWithOptDateRangeEx ,
2025-01-16 18:15:22 +00:00
metadata : Metadata ,
) {
if ( typeof selectList === 'string' ) {
return chSql ` ${ { UNSAFE_RAW_SQL : selectList } } ` ;
}
2025-02-26 00:00:48 +00:00
// This metadata query is executed in an attempt tp optimize the selects by favoring materialized fields
// on a view/table that already perform the computation in select. This optimization is not currently
// supported for queries using CTEs so skip the metadata fetch if there are CTE objects in the config.
const materializedFields = chartConfig . with ? . length
? undefined
: await metadata . getMaterializedColumnsLookupTable ( {
connectionId : chartConfig.connection ,
databaseName : chartConfig.from.databaseName ,
tableName : chartConfig.from.tableName ,
} ) ;
2025-01-16 18:15:22 +00:00
return Promise . all (
selectList . map ( async select = > {
const whereClause = await renderWhereExpression ( {
condition : select.aggCondition ? ? '' ,
from : chartConfig . from ,
language : select.aggConditionLanguage ? ? 'lucene' ,
implicitColumnExpression : chartConfig.implicitColumnExpression ,
metadata ,
connectionId : chartConfig.connection ,
2025-02-26 00:00:48 +00:00
with : chartConfig . with ,
2025-01-16 18:15:22 +00:00
} ) ;
let expr : ChSql ;
if ( select . aggFn == null ) {
expr = chSql ` ${ { UNSAFE_RAW_SQL : select.valueExpression } } ` ;
} else if ( select . aggFn === 'quantile' ) {
expr = aggFnExpr ( {
fn : select.aggFn ,
expr : select.valueExpression ,
// @ts-ignore (TS doesn't know that we've already checked for quantile)
quantileLevel : select.level ,
where : whereClause.sql ,
} ) ;
} else {
expr = aggFnExpr ( {
fn : select.aggFn ,
expr : select.valueExpression ,
where : whereClause.sql ,
} ) ;
}
const rawSQL = ` SELECT ${ expr . sql } FROM \` t \` ` ;
2025-02-26 00:00:48 +00:00
if ( materializedFields ) {
expr . sql = fastifySQL ( { materializedFields , rawSQL } )
. replace ( /^SELECT\s+/i , '' ) // Remove 'SELECT ' from the start
. replace ( /\s+FROM `t`$/i , '' ) ; // Remove ' FROM t' from the end
}
2025-01-16 18:15:22 +00:00
return chSql ` ${ expr } ${
select . alias != null
2025-03-13 06:19:26 +00:00
? chSql ` AS " ${ { UNSAFE_RAW_SQL : select.alias } }" `
2025-01-16 18:15:22 +00:00
: [ ]
} ` ;
} ) ,
) ;
}
function renderSortSpecificationList (
sortSpecificationList : SortSpecificationList ,
) {
if ( typeof sortSpecificationList === 'string' ) {
return chSql ` ${ { UNSAFE_RAW_SQL : sortSpecificationList } } ` ;
}
return sortSpecificationList . map ( sortSpecification = > {
return chSql ` ${ { UNSAFE_RAW_SQL : sortSpecification.valueExpression } } ${
sortSpecification . ordering === 'DESC' ? 'DESC' : 'ASC'
} ` ;
} ) ;
}
function timeBucketExpr ( {
interval ,
timestampValueExpression ,
dateRange ,
alias = FIXED_TIME_BUCKET_EXPR_ALIAS ,
} : {
interval : SQLInterval | 'auto' ;
timestampValueExpression : string ;
dateRange ? : [ Date , Date ] ;
alias? : string ;
} ) {
const unsafeTimestampValueExpression = {
UNSAFE_RAW_SQL : getFirstTimestampValueExpression ( timestampValueExpression ) ,
} ;
const unsafeInterval = {
UNSAFE_RAW_SQL :
interval === 'auto' && Array . isArray ( dateRange )
? convertDateRangeToGranularityString ( dateRange , 60 )
: interval ,
} ;
return chSql ` toStartOfInterval(toDateTime( ${ unsafeTimestampValueExpression } ), INTERVAL ${ unsafeInterval } ) AS \` ${ {
UNSAFE_RAW_SQL : alias ,
} } \ ` ` ;
}
async function timeFilterExpr ( {
timestampValueExpression ,
dateRange ,
dateRangeStartInclusive ,
databaseName ,
tableName ,
metadata ,
connectionId ,
2025-02-26 00:00:48 +00:00
with : withClauses ,
2025-03-07 07:03:03 +00:00
includedDataInterval ,
2025-01-16 18:15:22 +00:00
} : {
timestampValueExpression : string ;
dateRange : [ Date , Date ] ;
dateRangeStartInclusive : boolean ;
metadata : Metadata ;
connectionId : string ;
databaseName : string ;
tableName : string ;
2025-02-26 00:00:48 +00:00
with ? : { name : string ; sql : ChSql } [ ] ;
2025-03-07 07:03:03 +00:00
includedDataInterval? : string ;
2025-01-16 18:15:22 +00:00
} ) {
const valueExpressions = timestampValueExpression . split ( ',' ) ;
const startTime = dateRange [ 0 ] . getTime ( ) ;
const endTime = dateRange [ 1 ] . getTime ( ) ;
const whereExprs = await Promise . all (
valueExpressions . map ( async expr = > {
const col = expr . trim ( ) ;
2025-02-26 00:00:48 +00:00
const columnMeta = withClauses ? . length
? null
: await metadata . getColumn ( {
databaseName ,
tableName ,
column : col ,
connectionId ,
} ) ;
2025-01-16 18:15:22 +00:00
const unsafeTimestampValueExpression = {
UNSAFE_RAW_SQL : col ,
} ;
2025-02-26 00:00:48 +00:00
if ( columnMeta == null && ! withClauses ? . length ) {
2025-01-16 18:15:22 +00:00
console . warn (
` Column ${ col } not found in ${ databaseName } . ${ tableName } while inferring type for time filter ` ,
) ;
}
2025-03-07 07:03:03 +00:00
const startTimeCond = includedDataInterval
? chSql ` toStartOfInterval(fromUnixTimestamp64Milli( ${ { Int64 : startTime } }), INTERVAL ${ includedDataInterval } ) - INTERVAL ${ includedDataInterval } `
: chSql ` fromUnixTimestamp64Milli( ${ { Int64 : startTime } }) ` ;
const endTimeCond = includedDataInterval
? chSql ` toStartOfInterval(fromUnixTimestamp64Milli( ${ { Int64 : endTime } }), INTERVAL ${ includedDataInterval } ) + INTERVAL ${ includedDataInterval } `
: chSql ` fromUnixTimestamp64Milli( ${ { Int64 : endTime } }) ` ;
2025-01-16 18:15:22 +00:00
// If it's a date type
if ( columnMeta ? . type === 'Date' ) {
return chSql ` ( ${ unsafeTimestampValueExpression } ${
dateRangeStartInclusive ? '>=' : '>'
2025-03-07 07:03:03 +00:00
} toDate ( $ { startTimeCond } ) AND $ { unsafeTimestampValueExpression } <= toDate ( $ { endTimeCond } ) ) ` ;
2025-01-16 18:15:22 +00:00
} else {
return chSql ` ( ${ unsafeTimestampValueExpression } ${
dateRangeStartInclusive ? '>=' : '>'
2025-03-07 07:03:03 +00:00
} $ { startTimeCond } AND $ { unsafeTimestampValueExpression } <= $ { endTimeCond } ) ` ;
2025-01-16 18:15:22 +00:00
}
} ) ,
) ;
return concatChSql ( 'AND' , . . . whereExprs ) ;
}
async function renderSelect (
2025-02-27 16:55:36 +00:00
chartConfig : ChartConfigWithOptDateRangeEx ,
2025-01-16 18:15:22 +00:00
metadata : Metadata ,
) : Promise < ChSql > {
/ * *
* SELECT
* if granularity : toStartOfInterval ,
* if groupBy : groupBy ,
* select
* /
const isIncludingTimeBucket = isUsingGranularity ( chartConfig ) ;
const isIncludingGroupBy = isUsingGroupBy ( chartConfig ) ;
// TODO: clean up these await mess
return concatChSql (
',' ,
await renderSelectList ( chartConfig . select , chartConfig , metadata ) ,
isIncludingGroupBy && chartConfig . selectGroupBy !== false
? await renderSelectList ( chartConfig . groupBy , chartConfig , metadata )
: [ ] ,
isIncludingTimeBucket
? timeBucketExpr ( {
interval : chartConfig.granularity ,
timestampValueExpression : chartConfig.timestampValueExpression ,
dateRange : chartConfig.dateRange ,
} )
: [ ] ,
) ;
}
function renderFrom ( {
from ,
} : {
from : ChartConfigWithDateRange [ 'from' ] ;
} ) : ChSql {
2025-02-26 00:00:48 +00:00
return concatChSql (
'.' ,
chSql ` ${ from . databaseName === '' ? '' : { Identifier : from.databaseName } } ` ,
chSql ` ${ {
Identifier : from.tableName ,
} } ` ,
) ;
2025-01-16 18:15:22 +00:00
}
async function renderWhereExpression ( {
condition ,
language ,
metadata ,
from ,
implicitColumnExpression ,
connectionId ,
2025-02-26 00:00:48 +00:00
with : withClauses ,
2025-01-16 18:15:22 +00:00
} : {
condition : SearchCondition ;
language : SearchConditionLanguage ;
metadata : Metadata ;
from : ChartConfigWithDateRange [ 'from' ] ;
implicitColumnExpression? : string ;
connectionId : string ;
2025-02-26 00:00:48 +00:00
with ? : { name : string ; sql : ChSql } [ ] ;
2025-01-16 18:15:22 +00:00
} ) : Promise < ChSql > {
let _condition = condition ;
if ( language === 'lucene' ) {
const serializer = new CustomSchemaSQLSerializerV2 ( {
metadata ,
databaseName : from.databaseName ,
tableName : from.tableName ,
implicitColumnExpression ,
connectionId : connectionId ,
} ) ;
const builder = new SearchQueryBuilder ( condition , serializer ) ;
_condition = await builder . build ( ) ;
}
2025-02-26 00:00:48 +00:00
// This metadata query is executed in an attempt tp optimize the selects by favoring materialized fields
// on a view/table that already perform the computation in select. This optimization is not currently
// supported for queries using CTEs so skip the metadata fetch if there are CTE objects in the config.
const materializedFields = withClauses ? . length
? undefined
: await metadata . getMaterializedColumnsLookupTable ( {
connectionId ,
databaseName : from.databaseName ,
tableName : from.tableName ,
} ) ;
2025-01-16 18:15:22 +00:00
const _sqlPrefix = 'SELECT * FROM `t` WHERE ' ;
const rawSQL = ` ${ _sqlPrefix } ${ _condition } ` ;
// strip 'SELECT * FROM `t` WHERE ' from the sql
2025-02-26 00:00:48 +00:00
if ( materializedFields ) {
_condition = fastifySQL ( { materializedFields , rawSQL } ) . replace (
_sqlPrefix ,
'' ,
) ;
}
2025-01-16 18:15:22 +00:00
return chSql ` ${ { UNSAFE_RAW_SQL : _condition } } ` ;
}
async function renderWhere (
2025-02-27 16:55:36 +00:00
chartConfig : ChartConfigWithOptDateRangeEx ,
2025-01-16 18:15:22 +00:00
metadata : Metadata ,
) : Promise < ChSql > {
let whereSearchCondition : ChSql | [ ] = [ ] ;
if ( isNonEmptyWhereExpr ( chartConfig . where ) ) {
whereSearchCondition = wrapChSqlIfNotEmpty (
await renderWhereExpression ( {
condition : chartConfig.where ,
from : chartConfig . from ,
language : chartConfig.whereLanguage ? ? 'sql' ,
implicitColumnExpression : chartConfig.implicitColumnExpression ,
metadata ,
connectionId : chartConfig.connection ,
2025-02-26 00:00:48 +00:00
with : chartConfig . with ,
2025-01-16 18:15:22 +00:00
} ) ,
'(' ,
')' ,
) ;
}
let selectSearchConditions : ChSql [ ] = [ ] ;
if (
typeof chartConfig . select != 'string' &&
// Only if every select has an aggCondition, add to where clause
// otherwise we'll scan all rows anyways
chartConfig . select . every ( select = > isNonEmptyWhereExpr ( select . aggCondition ) )
) {
selectSearchConditions = (
await Promise . all (
chartConfig . select . map ( async select = > {
if ( isNonEmptyWhereExpr ( select . aggCondition ) ) {
return await renderWhereExpression ( {
condition : select.aggCondition ,
from : chartConfig . from ,
language : select.aggConditionLanguage ? ? 'sql' ,
implicitColumnExpression : chartConfig.implicitColumnExpression ,
metadata ,
connectionId : chartConfig.connection ,
2025-02-26 00:00:48 +00:00
with : chartConfig . with ,
2025-01-16 18:15:22 +00:00
} ) ;
}
return null ;
} ) ,
)
) . filter ( v = > v !== null ) as ChSql [ ] ;
}
const filterConditions = await Promise . all (
( chartConfig . filters ? ? [ ] ) . map ( async filter = > {
if ( filter . type === 'sql_ast' ) {
return wrapChSqlIfNotEmpty (
chSql ` ${ { UNSAFE_RAW_SQL : filter.left } } ${ filter . operator } ${ { UNSAFE_RAW_SQL : filter.right } } ` ,
'(' ,
')' ,
) ;
} else if ( filter . type === 'lucene' || filter . type === 'sql' ) {
return wrapChSqlIfNotEmpty (
await renderWhereExpression ( {
condition : filter.condition ,
from : chartConfig . from ,
language : filter.type ,
implicitColumnExpression : chartConfig.implicitColumnExpression ,
metadata ,
connectionId : chartConfig.connection ,
2025-02-26 00:00:48 +00:00
with : chartConfig . with ,
2025-01-16 18:15:22 +00:00
} ) ,
'(' ,
')' ,
) ;
}
throw new Error ( ` Unknown filter type: ${ filter . type } ` ) ;
} ) ,
) ;
return concatChSql (
' AND ' ,
chartConfig . dateRange != null &&
chartConfig . timestampValueExpression != null
? await timeFilterExpr ( {
timestampValueExpression : chartConfig.timestampValueExpression ,
dateRange : chartConfig.dateRange ,
dateRangeStartInclusive : chartConfig.dateRangeStartInclusive ? ? true ,
metadata ,
connectionId : chartConfig.connection ,
databaseName : chartConfig.from.databaseName ,
tableName : chartConfig.from.tableName ,
2025-02-26 00:00:48 +00:00
with : chartConfig . with ,
2025-03-07 07:03:03 +00:00
includedDataInterval : chartConfig.includedDataInterval ,
2025-01-16 18:15:22 +00:00
} )
: [ ] ,
whereSearchCondition ,
// Add aggConditions to where clause to utilize index
wrapChSqlIfNotEmpty ( concatChSql ( ' OR ' , selectSearchConditions ) , '(' , ')' ) ,
wrapChSqlIfNotEmpty (
concatChSql (
chartConfig . filtersLogicalOperator === 'OR' ? ' OR ' : ' AND ' ,
. . . filterConditions ,
) ,
'(' ,
')' ,
) ,
) ;
}
async function renderGroupBy (
chartConfig : ChartConfigWithOptDateRange ,
metadata : Metadata ,
) : Promise < ChSql | undefined > {
return concatChSql (
',' ,
isUsingGroupBy ( chartConfig )
? await renderSelectList ( chartConfig . groupBy , chartConfig , metadata )
: [ ] ,
isUsingGranularity ( chartConfig )
? timeBucketExpr ( {
interval : chartConfig.granularity ,
timestampValueExpression : chartConfig.timestampValueExpression ,
dateRange : chartConfig.dateRange ,
} )
: [ ] ,
) ;
}
function renderOrderBy (
chartConfig : ChartConfigWithOptDateRange ,
) : ChSql | undefined {
const isIncludingTimeBucket = isUsingGranularity ( chartConfig ) ;
if ( chartConfig . orderBy == null && ! isIncludingTimeBucket ) {
return undefined ;
}
return concatChSql (
',' ,
isIncludingTimeBucket
? timeBucketExpr ( {
interval : chartConfig.granularity ,
timestampValueExpression : chartConfig.timestampValueExpression ,
dateRange : chartConfig.dateRange ,
} )
: [ ] ,
chartConfig . orderBy != null
? renderSortSpecificationList ( chartConfig . orderBy )
: [ ] ,
) ;
}
function renderLimit (
chartConfig : ChartConfigWithOptDateRange ,
) : ChSql | undefined {
if ( chartConfig . limit == null || chartConfig . limit . limit == null ) {
return undefined ;
}
const offset =
chartConfig . limit . offset != null
? chSql ` OFFSET ${ { Int32 : chartConfig.limit.offset } } `
: [ ] ;
return chSql ` ${ { Int32 : chartConfig.limit.limit } } ${ offset } ` ;
}
2025-03-13 01:20:52 +00:00
// includedDataInterval isn't exported at this time. It's only used internally
2025-02-26 00:00:48 +00:00
// for metric SQL generation.
2025-02-27 16:55:36 +00:00
type ChartConfigWithOptDateRangeEx = ChartConfigWithOptDateRange & {
2025-03-07 07:03:03 +00:00
includedDataInterval? : string ;
2025-02-26 00:00:48 +00:00
} ;
function renderWith (
2025-02-27 16:55:36 +00:00
chartConfig : ChartConfigWithOptDateRangeEx ,
2025-02-26 00:00:48 +00:00
metadata : Metadata ,
) : ChSql | undefined {
const { with : withClauses } = chartConfig ;
if ( withClauses ) {
return concatChSql (
2025-02-27 16:55:36 +00:00
',' ,
2025-03-13 01:20:52 +00:00
withClauses . map ( clause = > {
if ( clause . isSubquery === false ) {
return chSql ` ( ${ clause . sql } ) AS ${ { Identifier : clause.name } } ` ;
}
// Can not use identifier here
return chSql ` ${ clause . name } AS ( ${ clause . sql } ) ` ;
} ) ,
2025-02-26 00:00:48 +00:00
) ;
}
return undefined ;
}
2025-02-27 16:55:36 +00:00
function intervalToSeconds ( interval : SQLInterval ) : number {
// Parse interval string like "15 second" into number of seconds
const [ amount , unit ] = interval . split ( ' ' ) ;
const value = parseInt ( amount , 10 ) ;
switch ( unit ) {
case 'second' :
return value ;
case 'minute' :
return value * 60 ;
case 'hour' :
return value * 60 * 60 ;
case 'day' :
return value * 24 * 60 * 60 ;
default :
throw new Error ( ` Invalid interval unit ${ unit } in interval ${ interval } ` ) ;
}
}
function renderFill (
chartConfig : ChartConfigWithOptDateRangeEx ,
) : ChSql | undefined {
const { granularity , dateRange } = chartConfig ;
if ( dateRange && granularity && granularity !== 'auto' ) {
const [ start , end ] = dateRange ;
const step = intervalToSeconds ( granularity ) ;
return concatChSql ( ' ' , [
chSql ` FROM toUnixTimestamp(toStartOfInterval(fromUnixTimestamp64Milli( ${ { Int64 : start.getTime ( ) } }), INTERVAL ${ granularity } ))
TO toUnixTimestamp ( toStartOfInterval ( fromUnixTimestamp64Milli ( $ { { Int64 : end.getTime ( ) } } ) , INTERVAL $ { granularity } ) )
STEP $ { { Int32 : step } } ` ,
] ) ;
}
return undefined ;
}
2025-03-06 00:06:57 +00:00
async function translateMetricChartConfig (
2025-01-16 18:15:22 +00:00
chartConfig : ChartConfigWithOptDateRange ,
2025-03-06 00:06:57 +00:00
metadata : Metadata ,
) : Promise < ChartConfigWithOptDateRangeEx > {
2025-02-26 00:00:48 +00:00
const metricTables = chartConfig . metricTables ;
if ( ! metricTables ) {
return chartConfig ;
}
// assumes all the selects are from a single metric type, for now
2025-03-11 07:25:00 +00:00
const { select , from , filters , where , . . . restChartConfig } = chartConfig ;
2025-02-26 00:00:48 +00:00
if ( ! select || ! Array . isArray ( select ) ) {
throw new Error ( 'multi select or string select on metrics not supported' ) ;
}
const { metricType , metricName , . . . _select } = select [ 0 ] ; // Initial impl only supports one metric select per chart config
if ( metricType === MetricsDataType . Gauge && metricName ) {
2025-03-06 00:06:57 +00:00
const timeBucketCol = '__hdx_time_bucket2' ;
const timeExpr = timeBucketExpr ( {
interval : chartConfig.granularity || 'auto' ,
timestampValueExpression :
chartConfig . timestampValueExpression ||
DEFAULT_METRIC_TABLE_TIME_COLUMN ,
dateRange : chartConfig.dateRange ,
alias : timeBucketCol ,
} ) ;
const where = await renderWhere (
{
. . . chartConfig ,
from : {
. . . from ,
tableName : metricTables [ MetricsDataType . Gauge ] ,
} ,
filters : [
2025-03-11 07:25:00 +00:00
. . . ( filters ? ? [ ] ) ,
2025-03-06 00:06:57 +00:00
{
type : 'sql' ,
condition : ` MetricName = ' ${ metricName } ' ` ,
} ,
] ,
} ,
metadata ,
) ;
2025-02-26 00:00:48 +00:00
return {
. . . restChartConfig ,
2025-03-06 00:06:57 +00:00
with : [
2025-03-12 02:16:15 +00:00
{
name : 'Source' ,
sql : chSql `
SELECT
* ,
cityHash64 ( mapConcat ( ScopeAttributes , ResourceAttributes , Attributes ) ) AS AttributesHash
FROM $ { renderFrom ( { from : { . . . from , tableName : metricTables [ MetricsDataType . Gauge ] } } ) }
WHERE $ { where }
` ,
} ,
2025-03-06 00:06:57 +00:00
{
name : 'Bucketed' ,
sql : chSql `
SELECT
$ { timeExpr } ,
2025-03-12 02:16:15 +00:00
AttributesHash ,
2025-03-06 00:06:57 +00:00
last_value ( Value ) AS LastValue ,
2025-03-12 02:16:15 +00:00
any ( ScopeAttributes ) AS ScopeAttributes ,
any ( ResourceAttributes ) AS ResourceAttributes ,
any ( Attributes ) AS Attributes ,
2025-03-06 00:06:57 +00:00
any ( ResourceSchemaUrl ) AS ResourceSchemaUrl ,
any ( ScopeName ) AS ScopeName ,
any ( ScopeVersion ) AS ScopeVersion ,
any ( ScopeDroppedAttrCount ) AS ScopeDroppedAttrCount ,
any ( ScopeSchemaUrl ) AS ScopeSchemaUrl ,
any ( ServiceName ) AS ServiceName ,
any ( MetricDescription ) AS MetricDescription ,
any ( MetricUnit ) AS MetricUnit ,
any ( StartTimeUnix ) AS StartTimeUnix ,
any ( Flags ) AS Flags
2025-03-12 02:16:15 +00:00
FROM Source
GROUP BY AttributesHash , $ { timeBucketCol }
2025-03-06 00:06:57 +00:00
ORDER BY AttributesHash , $ { timeBucketCol }
` ,
} ,
] ,
2025-02-26 00:00:48 +00:00
select : [
{
. . . _select ,
2025-03-06 00:06:57 +00:00
valueExpression : 'LastValue' ,
2025-03-10 23:38:55 +00:00
aggCondition : '' , // clear up the condition since the where clause is already applied at the upstream CTE
2025-02-26 00:00:48 +00:00
} ,
] ,
from : {
2025-03-06 00:06:57 +00:00
databaseName : '' ,
tableName : 'Bucketed' ,
2025-02-26 00:00:48 +00:00
} ,
2025-03-11 07:25:00 +00:00
where : '' , // clear up the condition since the where clause is already applied at the upstream CTE
2025-03-06 00:06:57 +00:00
timestampValueExpression : timeBucketCol ,
2025-02-26 00:00:48 +00:00
} ;
} else if ( metricType === MetricsDataType . Sum && metricName ) {
2025-03-07 07:03:03 +00:00
const timeBucketCol = '__hdx_time_bucket2' ;
const valueHighCol = '`__hdx_value_high`' ;
const valueHighPrevCol = '`__hdx_value_high_prev`' ;
const timeExpr = timeBucketExpr ( {
interval : chartConfig.granularity || 'auto' ,
timestampValueExpression :
chartConfig . timestampValueExpression || 'TimeUnix' ,
dateRange : chartConfig.dateRange ,
alias : timeBucketCol ,
} ) ;
// Render the where clause to limit data selection on the source CTE but also search forward/back one
// bucket window to ensure that there is enough data to compute a reasonable value on the ends of the
// series.
const where = await renderWhere (
{
. . . chartConfig ,
from : {
. . . from ,
tableName : metricTables [ MetricsDataType . Gauge ] ,
} ,
filters : [
2025-03-11 07:25:00 +00:00
. . . ( filters ? ? [ ] ) ,
2025-03-07 07:03:03 +00:00
{
type : 'sql' ,
condition : ` MetricName = ' ${ metricName } ' ` ,
} ,
] ,
includedDataInterval :
chartConfig . granularity === 'auto' &&
Array . isArray ( chartConfig . dateRange )
? convertDateRangeToGranularityString ( chartConfig . dateRange , 60 )
: chartConfig . granularity ,
} ,
metadata ,
) ;
2025-02-26 00:00:48 +00:00
return {
. . . restChartConfig ,
with : [
{
2025-03-07 07:03:03 +00:00
name : 'Source' ,
sql : chSql `
SELECT
* ,
cityHash64 ( mapConcat ( ScopeAttributes , ResourceAttributes , Attributes ) ) AS AttributesHash ,
IF ( AggregationTemporality = 1 ,
SUM ( Value ) OVER ( PARTITION BY AttributesHash ORDER BY AttributesHash , TimeUnix ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) ,
deltaSum ( Value ) OVER ( PARTITION BY AttributesHash ORDER BY AttributesHash , TimeUnix ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW )
) AS Value
2025-02-26 00:00:48 +00:00
FROM $ { renderFrom ( { from : { . . . from , tableName : metricTables [ MetricsDataType . Sum ] } } ) }
2025-03-07 07:03:03 +00:00
WHERE $ { where } ` ,
2025-02-26 00:00:48 +00:00
} ,
{
2025-03-07 07:03:03 +00:00
name : 'Bucketed' ,
sql : chSql `
SELECT
$ { timeExpr } ,
AttributesHash ,
last_value ( Source . Value ) AS $ { valueHighCol } ,
any ( $ { valueHighCol } ) OVER ( PARTITION BY AttributesHash ORDER BY \ ` ${ timeBucketCol } \` ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS ${ valueHighPrevCol } ,
$ { valueHighCol } - $ { valueHighPrevCol } AS Value ,
any ( ResourceAttributes ) AS ResourceAttributes ,
any ( ResourceSchemaUrl ) AS ResourceSchemaUrl ,
any ( ScopeName ) AS ScopeName ,
any ( ScopeVersion ) AS ScopeVersion ,
any ( ScopeAttributes ) AS ScopeAttributes ,
any ( ScopeDroppedAttrCount ) AS ScopeDroppedAttrCount ,
any ( ScopeSchemaUrl ) AS ScopeSchemaUrl ,
any ( ServiceName ) AS ServiceName ,
any ( MetricName ) AS MetricName ,
any ( MetricDescription ) AS MetricDescription ,
any ( MetricUnit ) AS MetricUnit ,
any ( Attributes ) AS Attributes ,
any ( StartTimeUnix ) AS StartTimeUnix ,
any ( Flags ) AS Flags ,
any ( AggregationTemporality ) AS AggregationTemporality ,
any ( IsMonotonic ) AS IsMonotonic
FROM Source
GROUP BY AttributesHash , \ ` ${ timeBucketCol } \`
ORDER BY AttributesHash , \ ` ${ timeBucketCol } \`
` ,
2025-02-26 00:00:48 +00:00
} ,
] ,
2025-03-10 23:38:55 +00:00
select : [
{
. . . _select ,
valueExpression : 'Value' ,
aggCondition : '' , // clear up the condition since the where clause is already applied at the upstream CTE
} ,
] ,
2025-02-26 00:00:48 +00:00
from : {
databaseName : '' ,
2025-03-07 07:03:03 +00:00
tableName : 'Bucketed' ,
2025-02-26 00:00:48 +00:00
} ,
2025-03-11 07:25:00 +00:00
where : '' , // clear up the condition since the where clause is already applied at the upstream CTE
2025-03-07 07:03:03 +00:00
timestampValueExpression : ` \` ${ timeBucketCol } \` ` ,
2025-02-27 16:55:36 +00:00
} ;
} else if ( metricType === MetricsDataType . Histogram && metricName ) {
// histograms are only valid for quantile selections
const { aggFn , level , . . . _selectRest } = _select as {
aggFn : string ;
level? : number ;
} ;
if ( aggFn !== 'quantile' || level == null ) {
throw new Error ( 'quantile must be specified for histogram metrics' ) ;
}
2025-03-10 23:38:55 +00:00
// Render the where clause to limit data selection on the source CTE but also search forward/back one
// bucket window to ensure that there is enough data to compute a reasonable value on the ends of the
// series.
const where = await renderWhere (
{
. . . chartConfig ,
from : {
. . . from ,
tableName : metricTables [ MetricsDataType . Histogram ] ,
} ,
filters : [
2025-03-11 07:25:00 +00:00
. . . ( filters ? ? [ ] ) ,
2025-03-10 23:38:55 +00:00
{
type : 'sql' ,
condition : ` MetricName = ' ${ metricName } ' ` ,
} ,
] ,
includedDataInterval :
chartConfig . granularity === 'auto' &&
Array . isArray ( chartConfig . dateRange )
? convertDateRangeToGranularityString ( chartConfig . dateRange , 60 )
: chartConfig . granularity ,
} ,
metadata ,
) ;
2025-02-27 16:55:36 +00:00
return {
. . . restChartConfig ,
with : [
{
name : 'HistRate' ,
2025-03-12 14:12:21 +00:00
sql : chSql `
SELECT
* ,
cityHash64 ( mapConcat ( ScopeAttributes , ResourceAttributes , Attributes ) ) AS AttributesHash ,
length ( BucketCounts ) as CountLength ,
any ( BucketCounts ) OVER ( ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING ) AS PrevBucketCounts ,
2025-02-27 16:55:36 +00:00
any ( CountLength ) OVER ( ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING ) AS PrevCountLength ,
any ( AttributesHash ) OVER ( ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING ) AS PrevAttributesHash ,
IF ( AggregationTemporality = 1 ,
BucketCounts ,
IF ( AttributesHash = PrevAttributesHash AND CountLength = PrevCountLength ,
arrayMap ( ( prev , curr ) - > IF ( curr < prev , curr , toUInt64 ( toInt64 ( curr ) - toInt64 ( prev ) ) ) , PrevBucketCounts , BucketCounts ) ,
BucketCounts ) ) as BucketRates
2025-03-12 14:12:21 +00:00
FROM $ { renderFrom ( { from : { . . . from , tableName : metricTables [ MetricsDataType . Histogram ] } } ) }
WHERE $ { where }
ORDER BY Attributes , TimeUnix ASC
2025-02-27 16:55:36 +00:00
` ,
} ,
{
name : 'RawHist' ,
sql : chSql `
2025-03-12 14:12:21 +00:00
SELECT
* ,
toUInt64 ( $ { { Float64 : level } } * arraySum ( BucketRates ) ) AS Rank ,
arrayCumSum ( BucketRates ) as CumRates ,
arrayFirstIndex ( x - > if ( x > Rank , 1 , 0 ) , CumRates ) AS BucketLowIdx ,
IF ( BucketLowIdx = length ( BucketRates ) ,
arrayElement ( ExplicitBounds , length ( ExplicitBounds ) ) ,
IF ( BucketLowIdx > 1 ,
arrayElement ( ExplicitBounds , BucketLowIdx - 1 ) + ( arrayElement ( ExplicitBounds , BucketLowIdx ) - arrayElement ( ExplicitBounds , BucketLowIdx - 1 ) ) *
IF ( arrayElement ( CumRates , BucketLowIdx ) > arrayElement ( CumRates , BucketLowIdx - 1 ) ,
( Rank - arrayElement ( CumRates , BucketLowIdx - 1 ) ) / ( arrayElement ( CumRates , BucketLowIdx ) - arrayElement ( CumRates , BucketLowIdx - 1 ) ) , 0 ) ,
IF ( arrayElement ( CumRates , 1 ) > 0 , arrayElement ( ExplicitBounds , BucketLowIdx + 1 ) * ( Rank / arrayElement ( CumRates , BucketLowIdx ) ) , 0 )
) ) as Rate
2025-02-27 16:55:36 +00:00
FROM HistRate ` ,
} ,
] ,
select : [
{
. . . _selectRest ,
aggFn : 'sum' ,
2025-03-10 23:38:55 +00:00
aggCondition : '' , // clear up the condition since the where clause is already applied at the upstream CTE
2025-02-27 16:55:36 +00:00
valueExpression : 'Rate' ,
} ,
] ,
from : {
databaseName : '' ,
tableName : 'RawHist' ,
} ,
2025-03-11 07:25:00 +00:00
where : '' , // clear up the condition since the where clause is already applied at the upstream CTE
2025-02-26 00:00:48 +00:00
} ;
}
throw new Error ( ` no query support for metric type= ${ metricType } ` ) ;
}
export async function renderChartConfig (
rawChartConfig : ChartConfigWithOptDateRange ,
2025-01-21 18:44:14 +00:00
metadata : Metadata ,
2025-01-16 18:15:22 +00:00
) : Promise < ChSql > {
2025-02-26 00:00:48 +00:00
// metric types require more rewriting since we know more about the schema
// but goes through the same generation process
const chartConfig =
rawChartConfig . metricTables != null
2025-03-06 00:06:57 +00:00
? await translateMetricChartConfig ( rawChartConfig , metadata )
2025-02-26 00:00:48 +00:00
: rawChartConfig ;
const withClauses = renderWith ( chartConfig , metadata ) ;
2025-01-16 18:15:22 +00:00
const select = await renderSelect ( chartConfig , metadata ) ;
const from = renderFrom ( chartConfig ) ;
const where = await renderWhere ( chartConfig , metadata ) ;
const groupBy = await renderGroupBy ( chartConfig , metadata ) ;
const orderBy = renderOrderBy ( chartConfig ) ;
2025-03-13 01:30:50 +00:00
//const fill = renderFill(chartConfig); //TODO: Fill breaks heatmaps and some charts
2025-01-16 18:15:22 +00:00
const limit = renderLimit ( chartConfig ) ;
2025-02-27 16:55:36 +00:00
return concatChSql ( ' ' , [
chSql ` ${ withClauses ? . sql ? chSql ` WITH ${ withClauses } ` : '' } ` ,
chSql ` SELECT ${ select } ` ,
chSql ` FROM ${ from } ` ,
chSql ` ${ where . sql ? chSql ` WHERE ${ where } ` : '' } ` ,
chSql ` ${ groupBy ? . sql ? chSql ` GROUP BY ${ groupBy } ` : '' } ` ,
chSql ` ${ orderBy ? . sql ? chSql ` ORDER BY ${ orderBy } ` : '' } ` ,
2025-03-13 01:30:50 +00:00
//chSql`${fill?.sql ? chSql`WITH FILL ${fill}` : ''}`,
2025-02-27 16:55:36 +00:00
chSql ` ${ limit ? . sql ? chSql ` LIMIT ${ limit } ` : '' } ` ,
] ) ;
2025-01-16 18:15:22 +00:00
}
// EditForm -> translateToQueriedChartConfig -> QueriedChartConfig
// renderFn(QueriedChartConfig) -> sql
// query(sql) -> data
// formatter(data) -> displayspecificDs
// displaySettings(QueriedChartConfig) -> displaySepcificDs
// chartComponent(displayspecificDs) -> React.Node