@@ -5,8 +5,9 @@ use std::collections::HashMap;
55use async_trait:: async_trait;
66
77use super :: ClickHouseConnectionInfo ;
8- use super :: select_queries:: parse_count;
8+ use super :: select_queries:: { parse_count, parse_json_rows } ;
99use crate :: db:: evaluation_queries:: EvaluationQueries ;
10+ use crate :: db:: evaluation_queries:: EvaluationRunInfoRow ;
1011use crate :: error:: Error ;
1112
1213#[ async_trait]
@@ -21,4 +22,223 @@ impl EvaluationQueries for ClickHouseConnectionInfo {
2122let response =self . run_query_synchronous ( query, & HashMap :: new ( ) ) . await ?;
2223parse_count ( & response. response )
2324}
25+
26+ async fn list_evaluation_runs (
27+ & self ,
28+ limit : u32 ,
29+ offset : u32 ,
30+ ) ->Result < Vec < EvaluationRunInfoRow > , Error > {
31+ let query =r"
32+ SELECT
33+ evaluation_run_id,
34+ any(evaluation_name) AS evaluation_name,
35+ any(inference_function_name) AS function_name,
36+ any(variant_name) AS variant_name,
37+ any(dataset_name) AS dataset_name,
38+ formatDateTime(UUIDv7ToDateTime(uint_to_uuid(max(max_inference_id))), '%Y-%m-%dT%H:%i:%SZ') AS last_inference_timestamp
39+ FROM (
40+ SELECT
41+ maxIf(value, key = 'tensorzero::evaluation_run_id') AS evaluation_run_id,
42+ maxIf(value, key = 'tensorzero::evaluation_name') AS evaluation_name,
43+ maxIf(value, key = 'tensorzero::dataset_name') AS dataset_name,
44+ any(function_name) AS inference_function_name,
45+ any(variant_name) AS variant_name,
46+ max(toUInt128(inference_id)) AS max_inference_id
47+ FROM TagInference FINAL
48+ WHERE key IN ('tensorzero::evaluation_run_id', 'tensorzero::evaluation_name', 'tensorzero::dataset_name')
49+ GROUP BY inference_id
50+ )
51+ WHERE NOT startsWith(inference_function_name, 'tensorzero::')
52+ GROUP BY evaluation_run_id
53+ ORDER BY toUInt128(toUUID(evaluation_run_id)) DESC
54+ LIMIT {limit:UInt32}
55+ OFFSET {offset:UInt32}
56+ FORMAT JSONEachRow
57+ "
58+ . to_string ( ) ;
59+
60+ let limit_str = limit. to_string ( ) ;
61+ let offset_str = offset. to_string ( ) ;
62+ let mut params =HashMap :: new ( ) ;
63+ params. insert ( "limit" , limit_str. as_str ( ) ) ;
64+ params. insert ( "offset" , offset_str. as_str ( ) ) ;
65+
66+ let response =self . run_query_synchronous ( query, & params) . await ?;
67+
68+ parse_json_rows ( response. response . as_str ( ) )
69+ }
70+ }
71+
72+ #[ cfg( test) ]
73+ mod tests{
74+ use std:: sync:: Arc ;
75+
76+ use crate :: db:: {
77+ clickhouse:: {
78+ ClickHouseConnectionInfo , ClickHouseResponse , ClickHouseResponseMetadata ,
79+ clickhouse_client:: MockClickHouseClient ,
80+ query_builder:: test_util:: assert_query_contains,
81+ } ,
82+ evaluation_queries:: EvaluationQueries ,
83+ } ;
84+
85+ #[ tokio:: test]
86+ async fn test_count_total_evaluation_runs ( ) {
87+ let mut mock_clickhouse_client =MockClickHouseClient :: new ( ) ;
88+
89+ mock_clickhouse_client
90+ . expect_run_query_synchronous ( )
91+ . withf ( |query, params|{
92+ assert_query_contains (
93+ query,
94+ "SELECT toUInt32(uniqExact(value)) as count
95+ FROM TagInference
96+ WHERE key = 'tensorzero::evaluation_run_id'
97+ FORMAT JSONEachRow" ,
98+ ) ;
99+ assert_eq ! ( params. len( ) , 0 , "Should have no parameters" ) ;
100+ true
101+ } )
102+ . returning ( |_, _|{
103+ Ok ( ClickHouseResponse {
104+ response : r#"{"count":42}"# . to_string ( ) ,
105+ metadata : ClickHouseResponseMetadata {
106+ read_rows : 1 ,
107+ written_rows : 0 ,
108+ } ,
109+ } )
110+ } ) ;
111+
112+ let conn =ClickHouseConnectionInfo :: new_mock ( Arc :: new ( mock_clickhouse_client) ) ;
113+
114+ let result = conn. count_total_evaluation_runs ( ) . await . unwrap ( ) ;
115+
116+ assert_eq ! ( result, 42 , "Should return count of 42" ) ;
117+ }
118+
119+ #[ tokio:: test]
120+ async fn test_list_evaluation_runs_with_defaults ( ) {
121+ let mut mock_clickhouse_client =MockClickHouseClient :: new ( ) ;
122+
123+ mock_clickhouse_client
124+ . expect_run_query_synchronous ( )
125+ . withf ( |query, params|{
126+ // Verify the query contains the expected structure
127+ assert_query_contains ( query, "SELECT" ) ;
128+ assert_query_contains ( query, "evaluation_run_id" ) ;
129+ assert_query_contains ( query, "FROM TagInference FINAL" ) ;
130+ assert_query_contains ( query, "LIMIT {limit:UInt32}" ) ;
131+ assert_query_contains ( query, "OFFSET {offset:UInt32}" ) ;
132+
133+ // Verify parameters
134+ assert_eq ! ( params. get( "limit" ) , Some ( & "100" ) ) ;
135+ assert_eq ! ( params. get( "offset" ) , Some ( & "0" ) ) ;
136+ true
137+ } )
138+ . returning ( |_, _|{
139+ Ok ( ClickHouseResponse {
140+ response : r#"{"evaluation_run_id":"0196ee9c-d808-74f3-8000-02ec7409b95d","evaluation_name":"test_eval","function_name":"test_func","variant_name":"test_variant","dataset_name":"test_dataset","last_inference_timestamp":"2025-05-20T16:52:58Z"}"# . to_string ( ) ,
141+ metadata : ClickHouseResponseMetadata {
142+ read_rows : 1 ,
143+ written_rows : 0 ,
144+ } ,
145+ } )
146+ } ) ;
147+
148+ let conn =ClickHouseConnectionInfo :: new_mock ( Arc :: new ( mock_clickhouse_client) ) ;
149+
150+ let result = conn. list_evaluation_runs ( 100 , 0 ) . await . unwrap ( ) ;
151+
152+ assert_eq ! ( result. len( ) , 1 , "Should return one evaluation run" ) ;
153+ assert_eq ! ( result[ 0 ] . evaluation_name, "test_eval" ) ;
154+ assert_eq ! ( result[ 0 ] . function_name, "test_func" ) ;
155+ assert_eq ! ( result[ 0 ] . variant_name, "test_variant" ) ;
156+ assert_eq ! ( result[ 0 ] . dataset_name, "test_dataset" ) ;
157+ }
158+
159+ #[ tokio:: test]
160+ async fn test_list_evaluation_runs_with_custom_pagination ( ) {
161+ let mut mock_clickhouse_client =MockClickHouseClient :: new ( ) ;
162+
163+ mock_clickhouse_client
164+ . expect_run_query_synchronous ( )
165+ . withf ( |_query, params|{
166+ // Verify custom pagination parameters
167+ assert_eq ! ( params. get( "limit" ) , Some ( & "50" ) ) ;
168+ assert_eq ! ( params. get( "offset" ) , Some ( & "100" ) ) ;
169+ true
170+ } )
171+ . returning ( |_, _|{
172+ Ok ( ClickHouseResponse {
173+ response : String :: new ( ) ,
174+ metadata : ClickHouseResponseMetadata {
175+ read_rows : 0 ,
176+ written_rows : 0 ,
177+ } ,
178+ } )
179+ } ) ;
180+
181+ let conn =ClickHouseConnectionInfo :: new_mock ( Arc :: new ( mock_clickhouse_client) ) ;
182+
183+ let result = conn. list_evaluation_runs ( 50 , 100 ) . await . unwrap ( ) ;
184+
185+ assert_eq ! ( result. len( ) , 0 , "Should return empty results" ) ;
186+ }
187+
188+ #[ tokio:: test]
189+ async fn test_list_evaluation_runs_multiple_results ( ) {
190+ let mut mock_clickhouse_client =MockClickHouseClient :: new ( ) ;
191+
192+ mock_clickhouse_client
193+ . expect_run_query_synchronous ( )
194+ . returning ( |_, _|{
195+ Ok ( ClickHouseResponse {
196+ response : r#"{"evaluation_run_id":"0196ee9c-d808-74f3-8000-02ec7409b95d","evaluation_name":"eval1","function_name":"func1","variant_name":"variant1","dataset_name":"dataset1","last_inference_timestamp":"2025-05-20T16:52:58Z"}
197+ {"evaluation_run_id":"0196ee9c-d808-74f3-8000-02ec7409b95e","evaluation_name":"eval2","function_name":"func2","variant_name":"variant2","dataset_name":"dataset2","last_inference_timestamp":"2025-05-20T17:52:58Z"}
198+ {"evaluation_run_id":"0196ee9c-d808-74f3-8000-02ec7409b95f","evaluation_name":"eval3","function_name":"func3","variant_name":"variant3","dataset_name":"dataset3","last_inference_timestamp":"2025-05-20T18:52:58Z"}"# . to_string ( ) ,
199+ metadata : ClickHouseResponseMetadata {
200+ read_rows : 3 ,
201+ written_rows : 0 ,
202+ } ,
203+ } )
204+ } ) ;
205+
206+ let conn =ClickHouseConnectionInfo :: new_mock ( Arc :: new ( mock_clickhouse_client) ) ;
207+
208+ let result = conn. list_evaluation_runs ( 100 , 0 ) . await . unwrap ( ) ;
209+
210+ assert_eq ! ( result. len( ) , 3 , "Should return three evaluation runs" ) ;
211+ assert_eq ! ( result[ 0 ] . evaluation_name, "eval1" ) ;
212+ assert_eq ! ( result[ 1 ] . evaluation_name, "eval2" ) ;
213+ assert_eq ! ( result[ 2 ] . evaluation_name, "eval3" ) ;
214+ }
215+
216+ #[ tokio:: test]
217+ async fn test_list_evaluation_runs_filters_out_tensorzero_functions ( ) {
218+ let mut mock_clickhouse_client =MockClickHouseClient :: new ( ) ;
219+
220+ mock_clickhouse_client
221+ . expect_run_query_synchronous ( )
222+ . withf ( |query, _params|{
223+ // Verify the query filters out tensorzero:: functions
224+ assert_query_contains (
225+ query,
226+ "NOT startsWith(inference_function_name, 'tensorzero::')" ,
227+ ) ;
228+ true
229+ } )
230+ . returning ( |_, _|{
231+ Ok ( ClickHouseResponse {
232+ response : String :: new ( ) ,
233+ metadata : ClickHouseResponseMetadata {
234+ read_rows : 0 ,
235+ written_rows : 0 ,
236+ } ,
237+ } )
238+ } ) ;
239+
240+ let conn =ClickHouseConnectionInfo :: new_mock ( Arc :: new ( mock_clickhouse_client) ) ;
241+
242+ let _result = conn. list_evaluation_runs ( 100 , 0 ) . await . unwrap ( ) ;
243+ }
24244}