@@ -4,7 +4,7 @@ use std::fmt::{Debug, Display};
44use crate :: app:: DuckDBConn ;
55use crate :: utils:: validate;
66use crate :: web:: routes:: dashboard:: GraphValue ;
7- use duckdb:: params ;
7+ use duckdb:: { params_from_iter , ToSql } ;
88use eyre:: Result ;
99use itertools:: Itertools ;
1010use poem_openapi:: { Enum , Object } ;
@@ -98,8 +98,51 @@ pub struct DimensionFilter {
9898 value : String ,
9999}
100100
101- fn filter_sql ( _filters : & [ DimensionFilter ] ) -> Result < String > {
102- Ok ( String :: new ( ) )
101+ fn filter_sql ( filters : & [ DimensionFilter ] ) -> Result < ( String , Vec < Box < dyn ToSql > > ) > {
102+ let mut params: Vec < Box < dyn ToSql > > = Vec :: new ( ) ;
103+
104+ if filters. is_empty ( ) {
105+ return Ok ( ( "" . to_owned ( ) , params) ) ;
106+ }
107+
108+ let filter_clauses = filters
109+ . iter ( )
110+ . map ( |filter| {
111+ let filter_value = match filter. filter_type {
112+ FilterType :: Equal => {
113+ params. push ( Box :: new ( filter. value . clone ( ) ) ) ;
114+ " = ?"
115+ }
116+ FilterType :: NotEqual => {
117+ params. push ( Box :: new ( filter. value . clone ( ) ) ) ;
118+ " != ?"
119+ }
120+ FilterType :: Contains => {
121+ params. push ( Box :: new ( filter. value . clone ( ) ) ) ;
122+ " like ?"
123+ }
124+ FilterType :: NotContains => {
125+ params. push ( Box :: new ( filter. value . clone ( ) ) ) ;
126+ " not like ?"
127+ }
128+ FilterType :: IsNull => " is null" ,
129+ } ;
130+
131+ match filter. dimension {
132+ Dimension :: Url => format ! ( "concat(fqdn, path) {}" , filter_value) ,
133+ Dimension :: Path => format ! ( "path {}" , filter_value) ,
134+ Dimension :: Fqdn => format ! ( "fqdn {}" , filter_value) ,
135+ Dimension :: Referrer => format ! ( "referrer {}" , filter_value) ,
136+ Dimension :: Platform => format ! ( "platform {}" , filter_value) ,
137+ Dimension :: Browser => format ! ( "browser {}" , filter_value) ,
138+ Dimension :: Mobile => format ! ( "mobile::text {}" , filter_value) ,
139+ Dimension :: Country => format ! ( "country {}" , filter_value) ,
140+ Dimension :: City => format ! ( "city {}" , filter_value) ,
141+ }
142+ } )
143+ . join ( " and " ) ;
144+
145+ Ok ( ( format ! ( "and ({})" , filter_clauses) , params) )
103146}
104147
105148fn metric_sql ( metric : & Metric ) -> Result < String > {
@@ -116,23 +159,18 @@ pub fn online_users(conn: &DuckDBConn, entities: &[String]) -> Result<u64> {
116159 return Ok ( 0 ) ;
117160 }
118161
119- // recheck the validity of the entity IDs to be super sure there's no SQL injection
120- if !entities. iter ( ) . all ( |entity| validate:: is_valid_id ( entity) ) {
121- return Err ( eyre:: eyre!( "Invalid entity ID" ) ) ;
122- }
123- let entities_list = entities. iter ( ) . map ( |entity| format ! ( "'{entity}'" ) ) . join ( ", " ) ;
124-
162+ let vars = repeat_vars ( entities. len ( ) ) ;
125163 let query = format ! (
126164 "--sql
127165 select count(distinct visitor_id) from events
128166 where
129- entity_id in ({entities_list }) and
167+ entity_id in ({vars }) and
130168 created_at >= (now()::timestamp - (interval 5 minute));
131169 "
132170 ) ;
133171
134172 let mut stmt = conn. prepare_cached ( & query) ?;
135- let rows = stmt. query_map ( [ ] , |row| row. get ( 0 ) ) ?;
173+ let rows = stmt. query_map ( params_from_iter ( entities ) , |row| row. get ( 0 ) ) ?;
136174 let online_users = rows. collect :: < Result < Vec < u64 > , duckdb:: Error > > ( ) ?;
137175 Ok ( online_users[ 0 ] )
138176}
@@ -145,7 +183,7 @@ pub fn online_users(conn: &DuckDBConn, entities: &[String]) -> Result<u64> {
145183// )]
146184pub fn overall_report (
147185 conn : & DuckDBConn ,
148- entities : & [ impl AsRef < str > + Debug ] ,
186+ entities : & [ String ] ,
149187 event : & str ,
150188 range : & DateRange ,
151189 data_points : u32 ,
@@ -156,14 +194,21 @@ pub fn overall_report(
156194 return Ok ( vec ! [ GraphValue :: U64 ( 0 ) ; data_points as usize ] ) ;
157195 }
158196
159- // recheck the validity of the entity IDs to be super sure there's no SQL injection
160- if !entities. iter ( ) . all ( |entity| validate:: is_valid_id ( entity. as_ref ( ) ) ) {
161- return Err ( eyre:: eyre!( "Invalid entity ID" ) ) ;
162- }
197+ let mut params: Vec < Box < dyn ToSql > > = Vec :: new ( ) ;
163198
164- let entities_list = entities. iter ( ) . map ( |entity| format ! ( "'{}'" , entity. as_ref( ) ) ) . join ( ", " ) ;
165- let filters_clause = filter_sql ( filters) ?;
166- let metric_column = metric_sql ( metric) ?;
199+ let ( filters_sql, filters_params) = filter_sql ( filters) ?;
200+ let metric_sql = metric_sql ( metric) ?;
201+
202+ let entity_vars = repeat_vars ( entities. len ( ) ) ;
203+
204+ params. push ( Box :: new ( range. start ( ) ) ) ;
205+ params. push ( Box :: new ( range. end ( ) ) ) ;
206+ params. push ( Box :: new ( data_points) ) ;
207+ params. push ( Box :: new ( data_points) ) ;
208+ params. push ( Box :: new ( event) ) ;
209+ params. extend ( entities. iter ( ) . map ( |entity| Box :: new ( entity. clone ( ) ) as Box < dyn ToSql > ) ) ;
210+ params. extend ( filters_params) ;
211+ params. push ( Box :: new ( range. end ( ) ) ) ;
167212
168213 let query = format ! ( "--sql
169214 with
@@ -189,13 +234,13 @@ pub fn overall_report(
189234 event = ?::text and
190235 created_at >= params.start_time and
191236 created_at <= params.end_time and
192- entity_id in ({entities_list })
193- {filters_clause }
237+ entity_id in ({entity_vars })
238+ {filters_sql }
194239 ),
195240 event_bins as (
196241 select
197242 bin_start,
198- {metric_column } as metric_value
243+ {metric_sql } as metric_value
199244 from
200245 time_bins tb
201246 left join session_data sd
@@ -214,7 +259,6 @@ pub fn overall_report(
214259 " ) ;
215260
216261 let mut stmt = conn. prepare_cached ( & query) ?;
217- let params = params ! [ range. start( ) , range. end( ) , data_points, data_points, event, range. end( ) ] ;
218262
219263 match metric {
220264 Metric :: Views | Metric :: UniqueVisitors | Metric :: Sessions => {
@@ -238,7 +282,7 @@ pub fn overall_report(
238282// )]
239283pub fn overall_stats (
240284 conn : & DuckDBConn ,
241- entities : & [ impl AsRef < str > + Debug ] ,
285+ entities : & [ String ] ,
242286 event : & str ,
243287 range : & DateRange ,
244288 filters : & [ DimensionFilter ] ,
@@ -247,18 +291,22 @@ pub fn overall_stats(
247291 return Ok ( ReportStats :: default ( ) ) ;
248292 }
249293
250- // recheck the validity of the entity IDs to be super sure there's no SQL injection
251- if !entities. iter ( ) . all ( |entity| validate:: is_valid_id ( entity. as_ref ( ) ) ) {
252- return Err ( eyre:: eyre!( "Invalid entity ID" ) ) ;
253- }
254- let entities_list = entities. iter ( ) . map ( |entity| format ! ( "'{}'" , entity. as_ref( ) ) ) . join ( ", " ) ;
255- let filters_clause = filter_sql ( filters) ?;
294+ let mut params: Vec < Box < dyn ToSql > > = Vec :: new ( ) ;
295+
296+ let entity_vars = repeat_vars ( entities. len ( ) ) ;
297+ let ( filters_sql, filters_params) = filter_sql ( filters) ?;
256298
257299 let metric_total = metric_sql ( & Metric :: Views ) ?;
258300 let metric_sessions = metric_sql ( & Metric :: Sessions ) ?;
259301 let metric_unique_visitors = metric_sql ( & Metric :: UniqueVisitors ) ?;
260302 let metric_avg_views_per_visitor = metric_sql ( & Metric :: AvgViewsPerSession ) ?;
261303
304+ params. push ( Box :: new ( range. start ( ) ) ) ;
305+ params. push ( Box :: new ( range. end ( ) ) ) ;
306+ params. push ( Box :: new ( event) ) ;
307+ params. extend ( entities. iter ( ) . map ( |entity| Box :: new ( entity) as Box < dyn ToSql > ) ) ;
308+ params. extend ( filters_params) ;
309+
262310 let query = format ! ( "--sql
263311 with
264312 params as (
@@ -276,9 +324,9 @@ pub fn overall_stats(
276324 event = ?::text and
277325 created_at >= params.start_time and
278326 created_at <= params.end_time and
279- entity_id in ({entities_list })
280- {filters_clause }
281- )
327+ entity_id in ({entity_vars })
328+ {filters_sql }
329+ )
282330 select
283331 {metric_total} as total_views,
284332 {metric_sessions} as total_sessions,
@@ -289,8 +337,6 @@ pub fn overall_stats(
289337 " ) ;
290338
291339 let mut stmt = conn. prepare_cached ( & query) ?;
292- let params = params ! [ range. start( ) , range. end( ) , event] ;
293-
294340 let result = stmt. query_row ( duckdb:: params_from_iter ( params) , |row| {
295341 Ok ( ReportStats {
296342 total_views : row. get ( 0 ) ?,
@@ -323,8 +369,10 @@ pub fn dimension_report(
323369 return Err ( eyre:: eyre!( "Invalid entity ID" ) ) ;
324370 }
325371
326- let entities_list = entities. iter ( ) . map ( |entity| format ! ( "'{}'" , entity. as_ref( ) ) ) . join ( ", " ) ;
327- let filters_clause = filter_sql ( filters) ?;
372+ let mut params: Vec < Box < dyn ToSql > > = Vec :: new ( ) ;
373+ let entity_vars = repeat_vars ( entities. len ( ) ) ;
374+ let ( filters_sql, filters_params) = filter_sql ( filters) ?;
375+
328376 let metric_column = metric_sql ( metric) ?;
329377 let ( dimension_column, group_by_columns) = match dimension {
330378 Dimension :: Url => ( "concat(fqdn, path)" , "fqdn, path" ) ,
@@ -338,6 +386,12 @@ pub fn dimension_report(
338386 Dimension :: City => ( "concat(country, city)" , "country, city" ) ,
339387 } ;
340388
389+ params. push ( Box :: new ( range. start ( ) ) ) ;
390+ params. push ( Box :: new ( range. end ( ) ) ) ;
391+ params. push ( Box :: new ( event) ) ;
392+ params. extend ( entities. iter ( ) . map ( |entity| Box :: new ( entity. as_ref ( ) ) as Box < dyn ToSql > ) ) ;
393+ params. extend ( filters_params) ;
394+
341395 let query = format ! ( "--sql
342396 with
343397 params as (
@@ -356,8 +410,8 @@ pub fn dimension_report(
356410 sd.event = ?::text and
357411 sd.created_at >= params.start_time and
358412 sd.created_at <= params.end_time and
359- sd.entity_id in ({entities_list })
360- {filters_clause }
413+ sd.entity_id in ({entity_vars })
414+ {filters_sql }
361415 group by
362416 {group_by_columns}, visitor_id, created_at
363417 )
@@ -374,11 +428,10 @@ pub fn dimension_report(
374428 ) ;
375429
376430 let mut stmt = conn. prepare_cached ( & query) ?;
377- let params = params ! [ range. start( ) , range. end( ) , event] ;
378431
379432 match metric {
380433 Metric :: Views | Metric :: UniqueVisitors | Metric :: Sessions => {
381- let rows = stmt. query_map ( params, |row| {
434+ let rows = stmt. query_map ( params_from_iter ( params) , |row| {
382435 let dimension_value: String = row. get ( 0 ) ?;
383436 let total_metric: u64 = row. get ( 1 ) ?;
384437 Ok ( ( dimension_value, total_metric) )
@@ -387,7 +440,7 @@ pub fn dimension_report(
387440 Ok ( report_table)
388441 }
389442 Metric :: AvgViewsPerSession => {
390- let rows = stmt. query_map ( params, |row| {
443+ let rows = stmt. query_map ( params_from_iter ( params) , |row| {
391444 let dimension_value: String = row. get ( 0 ) ?;
392445 let total_metric: f64 = row. get ( 1 ) ?;
393446 Ok ( ( dimension_value, ( total_metric * 1000.0 ) . round ( ) as u64 ) )
@@ -397,3 +450,11 @@ pub fn dimension_report(
397450 }
398451 }
399452}
453+
454+ fn repeat_vars ( count : usize ) -> String {
455+ assert_ne ! ( count, 0 ) ;
456+ let mut s = "?," . repeat ( count) ;
457+ // Remove trailing comma
458+ s. pop ( ) ;
459+ s
460+ }
0 commit comments