Skip to content

Commit dbe5715

Browse files
feat: filter sql
Signed-off-by: Henry Gressmann <[email protected]>
1 parent 11ed9b2 commit dbe5715

File tree

3 files changed

+106
-45
lines changed

3 files changed

+106
-45
lines changed

src/app/core/reports.rs

Lines changed: 103 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::fmt::{Debug, Display};
44
use crate::app::DuckDBConn;
55
use crate::utils::validate;
66
use crate::web::routes::dashboard::GraphValue;
7-
use duckdb::params;
7+
use duckdb::{params_from_iter, ToSql};
88
use eyre::Result;
99
use itertools::Itertools;
1010
use 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

105148
fn 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
// )]
146184
pub 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
// )]
239283
pub 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+
}

web/src/api/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ export const dimensionNames: Record<Dimension, string> = {
2121

2222
export const filterNames: Record<DimensionFilter["filterType"], string> = {
2323
contains: "contains",
24-
equal: "equals",
24+
equal: "is",
2525
is_null: "is null",
2626
not_contains: "does not contain",
27-
not_equal: "does not equal",
27+
not_equal: "is not",
2828
};
2929

3030
export const filterNamesCapitalized: Record<DimensionFilter["filterType"], string> = {

web/src/components/dimensions/dimensions.module.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
opacity: 0.09;
5959
z-index: -1;
6060
transition: width 0.3s ease-in-out, opacity 0.1s ease-in-out;
61-
border-radius: 10px;
61+
border-radius: 1rem;
6262
}
6363
}
6464

0 commit comments

Comments
 (0)