Repository navigation
fix(common-utils): rewrite groupBy, aggregates, and orderBy to use materialized columns #3562
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@hyperdx/common-utils': patch | ||
| --- | ||
|
|
||
| fix(common-utils): rewrite string groupBy, orderBy, and SQL aggregate functions to use materialized columns |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -336,6 +336,12 @@ const fastifySQL = ({ | |
|
|
||
| break; | ||
| } | ||
| case 'aggr_func': { | ||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | ||
| const _n = node as SQLParser.AggrFunc; | ||
| traverse(_n.args?.expr); | ||
| break; | ||
| } | ||
| default: | ||
| // ignore other types | ||
| break; | ||
|
|
@@ -716,16 +722,29 @@ type RenderSelectListOptions = { | |
| mergeRatio: boolean; | ||
| }; | ||
|
|
||
| function rewriteSelectExprWithMaterializedFields( | ||
| expr: string, | ||
| materializedFields: Map<string, string> | undefined, | ||
| ): string { | ||
| if ( | ||
| !materializedFields || | ||
| materializedFields.size === 0 || | ||
| expr.trim() === '' | ||
| ) { | ||
| return expr; | ||
| } | ||
| const rawSQL = `SELECT ${expr} FROM \`t\``; | ||
| return fastifySQL({ materializedFields, rawSQL }) | ||
| .replace(/^SELECT\s+/i, '') // Remove 'SELECT ' from the start | ||
| .replace(/\s+FROM `t`$/i, ''); // Remove ' FROM t' from the end | ||
| } | ||
|
|
||
| async function renderSelectList( | ||
| selectList: SelectList, | ||
| chartConfig: BuilderChartConfigWithOptDateRangeEx, | ||
| metadata: Metadata, | ||
| { mergeRatio }: RenderSelectListOptions, | ||
| ) { | ||
| if (typeof selectList === 'string') { | ||
| return chSql`${{ UNSAFE_RAW_SQL: selectList }}`; | ||
| } | ||
|
|
||
| // 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 subquery CTEs so skip the metadata fetch if there are subquery CTE | ||
|
|
@@ -746,6 +765,14 @@ async function renderSelectList( | |
| // ignore | ||
| } | ||
|
|
||
| if (typeof selectList === 'string') { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 major — String groupBy is rewritten as one unit, but ORDER BY and the series-limit tuple are rewritten one column at a time
Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond |
||
| const resolvedSelectList = rewriteSelectExprWithMaterializedFields( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 major — Rewriting string selects renames search result columns to internal materialized column names The string-select branch is also the search path: Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond |
||
| selectList, | ||
| materializedFields, | ||
| ); | ||
| return chSql`${{ UNSAFE_RAW_SQL: resolvedSelectList }}`; | ||
| } | ||
|
|
||
| const isRatio = mergeRatio && isRatioChartConfig(selectList, chartConfig); | ||
|
|
||
| const selectsSQL = await Promise.all( | ||
|
|
@@ -765,6 +792,21 @@ async function renderSelectList( | |
| }) | ||
| : chSql``; | ||
|
|
||
| const needsPreAggRewrite = | ||
| select.aggFn != null && | ||
| select.valueExpressionLanguage !== 'lucene' && | ||
| (select.aggFn.startsWith('quantile') || | ||
| select.aggFn.startsWith('histogram') || | ||
| (select.aggFn === 'count_distinct' && | ||
| isNonEmptyWhereExpr(select.aggCondition))); | ||
|
|
||
| const valueExpression = needsPreAggRewrite | ||
| ? rewriteSelectExprWithMaterializedFields( | ||
| select.valueExpression, | ||
| materializedFields, | ||
| ) | ||
| : select.valueExpression; | ||
|
|
||
| let expr: ChSql; | ||
| if (select.aggFn == null) { | ||
| expr = | ||
|
|
@@ -781,14 +823,14 @@ async function renderSelectList( | |
| connectionId: chartConfig.connection, | ||
| with: chartConfig.with, | ||
| }) | ||
| : chSql`${{ UNSAFE_RAW_SQL: select.valueExpression }}`; | ||
| : chSql`${{ UNSAFE_RAW_SQL: valueExpression }}`; | ||
| } else if ( | ||
| select.aggFn.startsWith('quantile') || | ||
| select.aggFn.startsWith('histogram') | ||
| ) { | ||
| expr = aggFnExpr({ | ||
| fn: select.aggFn, | ||
| expr: select.valueExpression, | ||
| expr: valueExpression, | ||
| // @ts-expect-error (TS doesn't know that we've already checked for quantile) | ||
| level: select.level, | ||
| where: whereClause.sql, | ||
|
|
@@ -797,14 +839,14 @@ async function renderSelectList( | |
| } else { | ||
| expr = aggFnExpr({ | ||
| fn: select.aggFn, | ||
| expr: select.valueExpression, | ||
| expr: valueExpression, | ||
| where: whereClause.sql, | ||
| sampleWeightExpression: chartConfig.sampleWeightExpression, | ||
| }); | ||
| } | ||
|
|
||
| const rawSQL = `SELECT ${expr.sql} FROM \`t\``; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — The new rewrite helper isn't used for the existing inline copy of the same logic Lines 848-853 still build Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond |
||
| if (materializedFields) { | ||
| if (materializedFields && !needsPreAggRewrite) { | ||
| 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 | ||
|
|
@@ -825,13 +867,41 @@ async function renderSelectList( | |
|
|
||
| function renderSortSpecificationList( | ||
| sortSpecificationList: SortSpecificationList, | ||
| materializedFields?: Map<string, string>, | ||
| ) { | ||
| if (typeof sortSpecificationList === 'string') { | ||
| return chSql`${{ UNSAFE_RAW_SQL: sortSpecificationList }}`; | ||
| if ( | ||
| !materializedFields || | ||
| materializedFields.size === 0 || | ||
| sortSpecificationList.trim() === '' | ||
| ) { | ||
| return chSql`${{ UNSAFE_RAW_SQL: sortSpecificationList }}`; | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 major — ORDER BY items with NULLS FIRST/LAST or other modifiers are never rewritten, while the GROUP BY now is
Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond |
||
| const resolvedSortList = splitAndTrimWithBracket(sortSpecificationList) | ||
| .map(item => { | ||
| const match = item.match(/^(.*?)\s+(ASC|DESC)$/i); | ||
| if (match) { | ||
| const rewritten = rewriteSelectExprWithMaterializedFields( | ||
| match[1], | ||
| materializedFields, | ||
| ); | ||
| return `${rewritten} ${match[2]}`; | ||
| } | ||
| return rewriteSelectExprWithMaterializedFields( | ||
| item, | ||
| materializedFields, | ||
| ); | ||
| }) | ||
| .join(', '); | ||
| return chSql`${{ UNSAFE_RAW_SQL: resolvedSortList }}`; | ||
| } | ||
|
|
||
| return sortSpecificationList.map(sortSpecification => { | ||
| return chSql`${{ UNSAFE_RAW_SQL: sortSpecification.valueExpression }} ${ | ||
| const valueExpression = rewriteSelectExprWithMaterializedFields( | ||
| sortSpecification.valueExpression, | ||
| materializedFields, | ||
| ); | ||
| return chSql`${{ UNSAFE_RAW_SQL: valueExpression }} ${ | ||
| sortSpecification.ordering === 'DESC' ? 'DESC' : 'ASC' | ||
| }`; | ||
| }); | ||
|
|
@@ -1594,8 +1664,14 @@ async function renderSeriesLimitCte( | |
| // inside Map['a,b']; the per-column null filter below needs them separated. | ||
| let groupByCols: ChSql[]; | ||
| if (typeof chartConfig.groupBy === 'string') { | ||
| groupByCols = splitAndTrimWithBracket(chartConfig.groupBy).map( | ||
| col => chSql`${{ UNSAFE_RAW_SQL: col }}`, | ||
| groupByCols = await Promise.all( | ||
| splitAndTrimWithBracket(chartConfig.groupBy).map( | ||
| async col => | ||
| // renderSelectList returns a single ChSql when given a string | ||
| (await renderSelectList(col, chartConfig, metadata, { | ||
| mergeRatio: false, | ||
| })) as ChSql, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — The Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond |
||
| ), | ||
| ); | ||
| } else { | ||
| // Strip aliases: these go inside tuple(...)/`IS NOT NULL`, where an | ||
|
|
@@ -1686,15 +1762,32 @@ async function renderHaving( | |
| }); | ||
| } | ||
|
|
||
| function renderOrderBy( | ||
| async function renderOrderBy( | ||
| chartConfig: BuilderChartConfigWithOptDateRangeEx, | ||
| ): ChSql | undefined { | ||
| metadata: Metadata, | ||
| ): Promise<ChSql | undefined> { | ||
| const isIncludingTimeBucket = isUsingGranularity(chartConfig); | ||
|
|
||
| if (chartConfig.orderBy == null && !isIncludingTimeBucket) { | ||
| return undefined; | ||
| } | ||
|
|
||
| let materializedFields: Map<string, string> | undefined; | ||
| if (chartConfig.orderBy != null) { | ||
| try { | ||
| materializedFields = | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — Materialized-fields lookup block copied a third time This try/catch with the Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond |
||
| hasSubqueryCte(chartConfig.with) || !chartConfig.from.databaseName | ||
| ? undefined | ||
| : await metadata.getMaterializedColumnsLookupTable({ | ||
| connectionId: chartConfig.connection, | ||
| databaseName: chartConfig.from.databaseName, | ||
| tableName: chartConfig.from.tableName, | ||
| }); | ||
| } catch { | ||
| // ignore | ||
| } | ||
| } | ||
|
|
||
| return concatChSql( | ||
| ',', | ||
| isIncludingTimeBucket | ||
|
|
@@ -1709,7 +1802,7 @@ function renderOrderBy( | |
| }) | ||
| : [], | ||
| chartConfig.orderBy != null | ||
| ? renderSortSpecificationList(chartConfig.orderBy) | ||
| ? renderSortSpecificationList(chartConfig.orderBy, materializedFields) | ||
| : [], | ||
| ); | ||
| } | ||
|
|
@@ -3335,7 +3428,7 @@ export async function renderChartConfig( | |
| let where = await renderWhere(chartConfig, metadata); | ||
| const groupBy = await renderGroupBy(chartConfig, metadata); | ||
| const having = await renderHaving(chartConfig, metadata); | ||
| const orderBy = renderOrderBy(chartConfig); | ||
| const orderBy = await renderOrderBy(chartConfig, metadata); | ||
| // TODO: WITH FILL (gap-filling for time buckets) was removed as dead code; it | ||
| // broke heatmaps and some charts. Reintroduce a fill renderer if we revisit this. | ||
| const limit = renderLimit(chartConfig); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔵 minor — Tests miss the pre-aggregate rewrite branches and the fallback paths
No test covers
count_distinctwith anaggCondition(countIf(DISTINCT …)),histogram, an orderBy item withNULLS LAST, or a string groupBy where one column doesn't parse. Those are the paths whereneedsPreAggRewriteand the per-item and whole-string rewrites diverge. Add cases that assert GROUP BY, ORDER BY, and the CTE tuple use the same expressions.Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond