Skip to content

Commit 157a1a4

Browse files
chitalianJustin Torreclaude
authored
fix: Remove ARRAY JOIN from property queries to fix cost calculation discrepancy (#5519)
The property page queries (totalCost, totalRequests, averageLatency) were using ARRAY JOIN which required the search_properties filter. This change: - Adds transformSearchPropertiesToPropertyKey() helper to convert search_properties filters to property_key filters - Removes ARRAY JOIN from all three property queries - Uses has(mapKeys(properties), 'key') instead of ARRAY JOIN + key column This aligns the property queries with the dashboard query pattern and eliminates potential cost calculation discrepancies. Fixes ENG-3213 Co-authored-by: Justin Torre <justin@Justins-MacBook-Pro.local> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a54122f commit 157a1a4

File tree

4 files changed

+106
-31
lines changed

4 files changed

+106
-31
lines changed

web/lib/api/property/averageLatency.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,41 @@
11
import { FilterNode } from "@helicone-package/filters/filterDefs";
22
import { timeFilterToFilterNode } from "@helicone-package/filters/helpers";
3-
import { buildFilterWithAuthClickHousePropertiesV2 } from "@helicone-package/filters/filters";
3+
import { buildFilterWithAuthClickHouse } from "@helicone-package/filters/filters";
44
import { Result, resultMap } from "@/packages/common/result";
55
import { dbQueryClickhouse } from "../db/dbExecute";
6+
import { transformSearchPropertiesToPropertyKey } from "./propertyFilterHelpers";
67

78
export async function getAverageLatency(
89
filter: FilterNode,
910
timeFilter: {
1011
start: Date;
1112
end: Date;
1213
},
13-
org_id: string,
14+
org_id: string
1415
): Promise<Result<number, string>> {
15-
const { filter: filterString, argsAcc } =
16-
await buildFilterWithAuthClickHousePropertiesV2({
16+
// Transform search_properties to property_key to avoid ARRAY JOIN
17+
const transformedFilter = transformSearchPropertiesToPropertyKey({
18+
left: timeFilterToFilterNode(timeFilter, "request_response_rmt"),
19+
right: filter,
20+
operator: "and",
21+
});
22+
23+
const { filter: filterString, argsAcc } = await buildFilterWithAuthClickHouse(
24+
{
1725
org_id,
18-
filter: {
19-
left: timeFilterToFilterNode(timeFilter, "request_response_rmt"),
20-
right: filter,
21-
operator: "and",
22-
},
26+
filter: transformedFilter,
2327
argsAcc: [],
24-
});
28+
}
29+
);
30+
31+
// Query without ARRAY JOIN - uses property_key filter which generates
32+
// has(mapKeys(properties), 'key') instead of requiring ARRAY JOIN
2533
const query = `
2634
WITH total_count AS (
27-
SELECT
35+
SELECT
2836
count(*) as count,
2937
sum(request_response_rmt.latency) as total_latency
3038
FROM request_response_rmt
31-
ARRAY JOIN mapKeys(properties) AS key
3239
WHERE (
3340
(${filterString})
3441
)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {
2+
FilterBranch,
3+
FilterLeaf,
4+
FilterNode,
5+
} from "@helicone-package/filters/filterDefs";
6+
7+
/**
8+
* Transforms search_properties filters to property_key filters.
9+
* This allows queries to work without ARRAY JOIN.
10+
*
11+
* search_properties: { "prop_name": { equals: "prop_name" } }
12+
* becomes: property_key: { equals: "prop_name" }
13+
*
14+
* The search_properties filter requires ARRAY JOIN to create a `key` column,
15+
* while property_key uses has(mapKeys(properties), 'key') which doesn't need ARRAY JOIN.
16+
*/
17+
export function transformSearchPropertiesToPropertyKey(
18+
filter: FilterNode
19+
): FilterNode {
20+
if (filter === "all") {
21+
return filter;
22+
}
23+
24+
// Check if it's a leaf node with search_properties
25+
if ("request_response_rmt" in filter) {
26+
const leaf = filter as FilterLeaf;
27+
const rmt = leaf.request_response_rmt;
28+
if (rmt && "search_properties" in rmt && rmt.search_properties) {
29+
// Extract the property key from search_properties
30+
const propKey = Object.keys(rmt.search_properties)[0];
31+
if (propKey) {
32+
// Transform to property_key filter
33+
return {
34+
request_response_rmt: {
35+
property_key: {
36+
equals: propKey,
37+
},
38+
},
39+
} as FilterLeaf;
40+
}
41+
}
42+
return filter;
43+
}
44+
45+
// Check if it's a branch node
46+
if ("left" in filter && "right" in filter && "operator" in filter) {
47+
const branch = filter as FilterBranch;
48+
return {
49+
left: transformSearchPropertiesToPropertyKey(branch.left),
50+
right: transformSearchPropertiesToPropertyKey(branch.right),
51+
operator: branch.operator,
52+
};
53+
}
54+
55+
return filter;
56+
}

web/lib/api/property/totalCosts.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { FilterNode } from "@helicone-package/filters/filterDefs";
22
import { timeFilterToFilterNode } from "@helicone-package/filters/helpers";
3-
import { buildFilterWithAuthClickHousePropertiesV2 } from "@helicone-package/filters/filters";
3+
import { buildFilterWithAuthClickHouse } from "@helicone-package/filters/filters";
44
import { Result, resultMap } from "@/packages/common/result";
55
import { dbQueryClickhouse } from "../db/dbExecute";
66
import { COST_PRECISION_MULTIPLIER } from "@helicone-package/cost/costCalc";
7+
import { transformSearchPropertiesToPropertyKey } from "./propertyFilterHelpers";
78

89
export interface TotalCost {
910
cost: number;
@@ -12,20 +13,25 @@ export interface TotalCost {
1213

1314
export async function getTotalCostRaw(
1415
filter: FilterNode,
15-
org_id: string,
16+
org_id: string
1617
): Promise<Result<number, string>> {
17-
const { filter: filterString, argsAcc } =
18-
await buildFilterWithAuthClickHousePropertiesV2({
18+
// Transform search_properties to property_key to avoid ARRAY JOIN
19+
const transformedFilter = transformSearchPropertiesToPropertyKey(filter);
20+
21+
const { filter: filterString, argsAcc } = await buildFilterWithAuthClickHouse(
22+
{
1923
org_id,
20-
filter: filter,
24+
filter: transformedFilter,
2125
argsAcc: [],
22-
});
26+
}
27+
);
2328

29+
// Query without ARRAY JOIN - uses property_key filter which generates
30+
// has(mapKeys(properties), 'key') instead of requiring ARRAY JOIN
2431
const query = `
2532
WITH total_cost AS (
2633
SELECT sum(cost) / ${COST_PRECISION_MULTIPLIER} as cost
2734
FROM request_response_rmt
28-
ARRAY JOIN mapKeys(properties) AS key
2935
WHERE (
3036
(${filterString})
3137
)

web/lib/api/property/totalRequests.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,39 @@
11
import { FilterNode } from "@helicone-package/filters/filterDefs";
22
import { timeFilterToFilterNode } from "@helicone-package/filters/helpers";
3-
import { buildFilterWithAuthClickHousePropertiesV2 } from "@helicone-package/filters/filters";
3+
import { buildFilterWithAuthClickHouse } from "@helicone-package/filters/filters";
44
import { Result, resultMap } from "@/packages/common/result";
55
import { dbQueryClickhouse } from "../db/dbExecute";
6+
import { transformSearchPropertiesToPropertyKey } from "./propertyFilterHelpers";
67

78
export async function getTotalRequests(
89
filter: FilterNode,
910
timeFilter: {
1011
start: Date;
1112
end: Date;
1213
},
13-
org_id: string,
14+
org_id: string
1415
): Promise<Result<number, string>> {
15-
const { filter: filterString, argsAcc } =
16-
await buildFilterWithAuthClickHousePropertiesV2({
16+
// Transform search_properties to property_key to avoid ARRAY JOIN
17+
const transformedFilter = transformSearchPropertiesToPropertyKey({
18+
left: timeFilterToFilterNode(timeFilter, "request_response_rmt"),
19+
right: filter,
20+
operator: "and",
21+
});
22+
23+
const { filter: filterString, argsAcc } = await buildFilterWithAuthClickHouse(
24+
{
1725
org_id,
18-
filter: {
19-
left: timeFilterToFilterNode(timeFilter, "request_response_rmt"),
20-
right: filter,
21-
operator: "and",
22-
},
26+
filter: transformedFilter,
2327
argsAcc: [],
24-
});
25-
const query = `
28+
}
29+
);
2630

31+
// Query without ARRAY JOIN - uses property_key filter which generates
32+
// has(mapKeys(properties), 'key') instead of requiring ARRAY JOIN
33+
const query = `
2734
WITH total_count AS (
2835
SELECT count(*) as count
2936
FROM request_response_rmt
30-
ARRAY JOIN mapKeys(properties) AS key
3137
WHERE (
3238
(${filterString})
3339
)

0 commit comments

Comments
 (0)