11import { type FilterState } from "../../../types" ;
2+ import { InvalidRequestError } from "../../../errors" ;
23import { greptimeQuery } from "../../greptime/client" ;
34import { createGreptimeFilterFromFilterState } from "../../greptime/sql/factory" ;
45import { USAGE_COST_KNOWN_KEYS } from "../../greptime/sql/fragments" ;
@@ -111,7 +112,20 @@ export const getScoreAggregateGreptime = async (
111112 dashboardGreptimeColumnDefinitions ,
112113 ) ,
113114 ) ;
114- const restRes = restList . apply ( ) ;
115+ // This aggregation reads `scores s` (optionally JOIN traces); it has no `observations o` alias.
116+ // Observation-scoped dashboard columns (level / token / cost / tool-name filters) compile to
117+ // predicates correlated to `o`, so they cannot be expressed here -- reject them loudly instead of
118+ // emitting SQL that references a non-existent alias.
119+ if ( restList . some ( ( f ) => f . table === "observations" ) ) {
120+ throw new InvalidRequestError (
121+ "Observation-scoped filters are not supported for score aggregation on GreptimeDB" ,
122+ ) ;
123+ }
124+ // Split by physical table so each side pre-filters inside its own subquery: a flat `scores JOIN
125+ // traces` defeats GreptimeDB pushdown of the scores TIME INDEX / trace_id bloom and the traces TIME
126+ // INDEX to the region scans.
127+ const scoreRestRes = restList . filter ( ( f ) => f . table !== "traces" ) . apply ( ) ;
128+ const traceRestRes = restList . filter ( ( f ) => f . table === "traces" ) . apply ( ) ;
115129
116130 const hasTraceFilter = restList . some ( ( f ) => f . table === "traces" ) ;
117131 const timeFilter = restList . find (
@@ -122,7 +136,8 @@ export const getScoreAggregateGreptime = async (
122136 const useLookback = Boolean ( timeFilter && hasTraceFilter ) ;
123137 const params : Record < string , unknown > = {
124138 projectId,
125- ...restRes . params ,
139+ ...scoreRestRes . params ,
140+ ...traceRestRes . params ,
126141 ...env . params ,
127142 } ;
128143 if ( useLookback && timeFilter ) {
@@ -143,12 +158,22 @@ export const getScoreAggregateGreptime = async (
143158 query : `
144159 SELECT s.name AS name, count(*) AS count, avg(s.value) AS avg_value,
145160 s.source AS source, s.data_type AS data_type
146- FROM scores s
147- ${ hasTraceFilter ? "JOIN traces t ON t.id = s.trace_id AND t.project_id = s.project_id AND " + notDeleted ( "t" ) : "" }
148- WHERE s.project_id = :projectId AND ${ notDeleted ( "s" ) }
149- ${ restRes . query ? `AND ${ restRes . query } ` : "" }
150- ${ env . query ? `AND ${ env . query } ` : "" }
151- ${ useLookback ? "AND t.timestamp >= :tracesTimestamp" : "" }
161+ FROM (
162+ SELECT * FROM scores s
163+ WHERE s.project_id = :projectId AND ${ notDeleted ( "s" ) }
164+ ${ scoreRestRes . query ? `AND ${ scoreRestRes . query } ` : "" }
165+ ${ env . query ? `AND ${ env . query } ` : "" }
166+ ) s
167+ ${
168+ hasTraceFilter
169+ ? `JOIN (
170+ SELECT * FROM traces t
171+ WHERE t.project_id = :projectId AND ${ notDeleted ( "t" ) }
172+ ${ traceRestRes . query ? `AND ${ traceRestRes . query } ` : "" }
173+ ${ useLookback ? "AND t.timestamp >= :tracesTimestamp" : "" }
174+ ) t ON t.id = s.trace_id AND t.project_id = s.project_id`
175+ : ""
176+ }
152177 GROUP BY s.name, s.source, s.data_type
153178 ORDER BY count(*) DESC` ,
154179 params,
@@ -188,7 +213,6 @@ const getObservationDetailByTypeByTime = async (opts: {
188213 dashboardGreptimeColumnDefinitions ,
189214 ) ,
190215 ) ;
191- const restRes = restList . apply ( ) ;
192216
193217 const hasTraceFilter = restList . some ( ( f ) => f . table === "traces" ) ;
194218 // CH derived the trace lookback from an observation start_time lower bound, only when a trace
@@ -212,11 +236,20 @@ const getObservationDetailByTypeByTime = async (opts: {
212236 Boolean ( env . query ) ||
213237 restList . some ( ( f ) => f . table === "observations" ) ;
214238
239+ // Split predicates by physical table so each base table pre-filters inside its own subquery: a flat
240+ // `uc JOIN observations JOIN traces ... WHERE` defeats GreptimeDB pushdown of the uc/observations TIME
241+ // INDEX window to the region scans. Score-grain predicates correlate to `o`, so they stay obs-side.
242+ const obsRestRes = restList . filter ( ( f ) => f . table !== "traces" ) . apply ( ) ;
243+ const traceRestRes = restList . filter ( ( f ) => f . table === "traces" ) . apply ( ) ;
244+ const obsRestClause = obsRestRes . query ? `AND ${ obsRestRes . query } ` : "" ;
245+ const traceRestClause = traceRestRes . query ? `AND ${ traceRestRes . query } ` : "" ;
246+
215247 const params : Record < string , unknown > = {
216248 projectId,
217249 winFrom : greptimeTsParam ( new Date ( fromTime ) ) ,
218250 winTo : greptimeTsParam ( new Date ( toTime ) ) ,
219- ...restRes . params ,
251+ ...obsRestRes . params ,
252+ ...traceRestRes . params ,
220253 ...env . params ,
221254 } ;
222255 if ( useLookback && obsStartLowerBound ) {
@@ -231,20 +264,30 @@ const getObservationDetailByTypeByTime = async (opts: {
231264 const kind = jsonColumn === "cost_details" ? "cost" : "usage" ;
232265 params . kind = kind ;
233266
234- const traceJoin = hasTraceFilter
235- ? `LEFT JOIN traces t ON o.trace_id = t.id AND o.project_id = t.project_id AND ${ notDeleted ( "t" ) } `
236- : "" ;
237- // Conditional in Q2 (see needsObservationJoin); Q1 always reads observations directly so it keeps
238- // its own FROM.
239- const obsJoin = needsObservationJoin
240- ? `JOIN observations o ON uc.entity_id = o.id AND uc.project_id = o.project_id AND ${ notDeleted ( "o" ) } `
241- : "" ;
242- const restClause = restRes . query ? `AND ${ restRes . query } ` : "" ;
243267 const envClause = env . query ? `AND ${ env . query } ` : "" ;
244268 const lookbackClause = useLookback
245269 ? "AND t.timestamp >= :traceTimestamp"
246270 : "" ;
247271
272+ // Pre-filtered traces subquery. INNER join: a trace filter means the row must match a passing trace
273+ // -- the same effect the old `LEFT JOIN traces ... WHERE t.<pred>` produced.
274+ const tracesJoinSql = hasTraceFilter
275+ ? `JOIN (
276+ SELECT * FROM traces t
277+ WHERE t.project_id = :projectId AND ${ notDeleted ( "t" ) } ${ traceRestClause } ${ lookbackClause }
278+ ) t ON o.trace_id = t.id AND o.project_id = t.project_id`
279+ : "" ;
280+ // Pre-filtered observations subquery for Q2, bounded by the same window (the EAV row's timestamp is
281+ // the observation start_time) so the scan prunes via the TIME INDEX before the EAV join.
282+ const obsJoinSql = needsObservationJoin
283+ ? `JOIN (
284+ SELECT * FROM observations o
285+ WHERE o.project_id = :projectId AND ${ notDeleted ( "o" ) }
286+ AND o.start_time >= :winFrom AND o.start_time < :winTo
287+ ${ obsRestClause } ${ envClause }
288+ ) o ON uc.entity_id = o.id AND uc.project_id = o.project_id`
289+ : "" ;
290+
248291 const byBucket = new Map < number , Record < string , number > > ( ) ;
249292 const allKeys = new Set < string > ( ) ;
250293 const setSum = ( bucketMsKey : number , key : string , value : number ) => {
@@ -269,13 +312,14 @@ const getObservationDetailByTypeByTime = async (opts: {
269312 query : `
270313 SELECT date_bin(INTERVAL '${ bucketSizeSeconds } ' second, o.start_time) AS bucket,
271314 ${ knownSums }
272- FROM observations o
273- ${ traceJoin }
274- WHERE o.project_id = :projectId AND ${ notDeleted ( "o" ) }
275- AND o.start_time >= :winFrom AND o.start_time < :winTo
276- ${ restClause }
277- ${ envClause }
278- ${ lookbackClause }
315+ FROM (
316+ SELECT * FROM observations o
317+ WHERE o.project_id = :projectId AND ${ notDeleted ( "o" ) }
318+ AND o.start_time >= :winFrom AND o.start_time < :winTo
319+ ${ obsRestClause }
320+ ${ envClause }
321+ ) o
322+ ${ tracesJoinSql }
279323 GROUP BY bucket
280324 ORDER BY bucket ASC` ,
281325 params,
@@ -295,16 +339,15 @@ const getObservationDetailByTypeByTime = async (opts: {
295339 SELECT date_bin(INTERVAL '${ bucketSizeSeconds } ' second, uc.${ quoteIdent ( "timestamp" ) } ) AS bucket,
296340 uc.${ quoteIdent ( "key" ) } AS detail_key,
297341 sum(uc.${ quoteIdent ( "value" ) } ) AS sum
298- FROM observations_usage_cost uc
299- ${ obsJoin }
300- ${ traceJoin }
301- WHERE uc.${ quoteIdent ( "kind" ) } = :kind
302- AND uc.project_id = :projectId AND ${ notDeleted ( "uc" ) }
303- AND uc.${ quoteIdent ( "timestamp" ) } >= :winFrom AND uc.${ quoteIdent ( "timestamp" ) } < :winTo
304- AND uc.${ quoteIdent ( "key" ) } NOT IN (${ KNOWN_DETAIL_KEYS_SQL } )
305- ${ restClause }
306- ${ envClause }
307- ${ lookbackClause }
342+ FROM (
343+ SELECT * FROM observations_usage_cost uc
344+ WHERE uc.${ quoteIdent ( "kind" ) } = :kind
345+ AND uc.project_id = :projectId AND ${ notDeleted ( "uc" ) }
346+ AND uc.${ quoteIdent ( "timestamp" ) } >= :winFrom AND uc.${ quoteIdent ( "timestamp" ) } < :winTo
347+ AND uc.${ quoteIdent ( "key" ) } NOT IN (${ KNOWN_DETAIL_KEYS_SQL } )
348+ ) uc
349+ ${ obsJoinSql }
350+ ${ tracesJoinSql }
308351 GROUP BY bucket, uc.${ quoteIdent ( "key" ) }
309352 ORDER BY bucket ASC` ,
310353 params,
0 commit comments