Skip to content

Commit 1db2047

Browse files
authored
Refactor table filtering using a FilterTrait trait (#11389)
1 parent 42ac01a commit 1db2047

File tree

193 files changed

+870
-799
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

193 files changed

+870
-799
lines changed

crates/viewer/re_dataframe_ui/src/datafusion_adapter.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@ use re_log_types::Timestamp;
1111
use re_sorbet::{BatchType, SorbetBatch};
1212
use re_viewer_context::AsyncRuntimeHandle;
1313

14-
use crate::RequestedObject;
15-
use crate::filters::Filter;
1614
use crate::table_blueprint::{EntryLinksSpec, PartitionLinksSpec, SortBy, TableBlueprint};
15+
use crate::{ColumnFilter, RequestedObject};
1716

1817
/// Make sure we escape column names correctly for datafusion.
1918
///
@@ -38,7 +37,7 @@ struct DataFusionQueryData {
3837
pub partition_links: Option<PartitionLinksSpec>,
3938
pub entry_links: Option<EntryLinksSpec>,
4039
pub prefilter: Option<datafusion::prelude::Expr>,
41-
pub filters: Vec<Filter>,
40+
pub column_filters: Vec<ColumnFilter>,
4241
}
4342

4443
impl From<&TableBlueprint> for DataFusionQueryData {
@@ -48,15 +47,15 @@ impl From<&TableBlueprint> for DataFusionQueryData {
4847
partition_links,
4948
entry_links,
5049
prefilter,
51-
filters,
50+
column_filters,
5251
} = value;
5352

5453
Self {
5554
sort_by: sort_by.clone(),
5655
partition_links: partition_links.clone(),
5756
entry_links: entry_links.clone(),
5857
prefilter: prefilter.clone(),
59-
filters: filters.clone(),
58+
column_filters: column_filters.clone(),
6059
}
6160
}
6261
}
@@ -108,7 +107,7 @@ impl DataFusionQuery {
108107
partition_links,
109108
entry_links,
110109
prefilter,
111-
filters,
110+
column_filters,
112111
} = &self.query_data;
113112

114113
//
@@ -162,13 +161,11 @@ impl DataFusionQuery {
162161
// Filters
163162
//
164163

165-
let schema = dataframe.schema();
166-
167-
let filter_exprs = filters
164+
let filter_exprs = column_filters
168165
.iter()
169166
.filter_map(|filter| {
170167
filter
171-
.as_filter_expression(schema)
168+
.as_filter_expression()
172169
.inspect_err(|err| {
173170
// TODO(ab): error handling will need to be improved once we introduce non-
174171
// UI means of setting up filters.

crates/viewer/re_dataframe_ui/src/datafusion_table_widget.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use re_viewer_context::{
2323

2424
use crate::datafusion_adapter::{DataFusionAdapter, DataFusionQueryResult};
2525
use crate::display_record_batch::DisplayColumn;
26-
use crate::filters::{Filter, FilterKind, FilterState};
26+
use crate::filters::{ColumnFilter, FilterState};
2727
use crate::header_tooltip::column_header_tooltip_ui;
2828
use crate::table_blueprint::{
2929
ColumnBlueprint, EntryLinksSpec, PartitionLinksSpec, SortBy, SortDirection, TableBlueprint,
@@ -715,8 +715,10 @@ impl egui_table::TableDelegate for DataFusionTableDelegate<'_> {
715715
// In the future, we'll probably need to be more fine-grained.
716716
#[expect(clippy::collapsible_if)]
717717
if column.blueprint.variant_ui.is_none()
718-
&& let Some(filter_op) =
719-
FilterKind::default_for_column(column_field)
718+
&& let Some(column_filter) =
719+
ColumnFilter::default_for_column(Arc::clone(
720+
column_field,
721+
))
720722
{
721723
if ui
722724
.icon_and_text_menu_item(
@@ -725,10 +727,7 @@ impl egui_table::TableDelegate for DataFusionTableDelegate<'_> {
725727
)
726728
.clicked()
727729
{
728-
self.filter_state.push_new_filter(Filter::new(
729-
column_physical_name.clone(),
730-
filter_op,
731-
));
730+
self.filter_state.push_new_filter(column_filter);
732731
}
733732
}
734733
});

crates/viewer/re_dataframe_ui/src/filters/boolean.rs

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ use datafusion::logical_expr::{Expr, col, lit, not};
66
use datafusion::prelude::{array_element, array_has, array_sort};
77
use strum::VariantArray as _;
88

9-
use re_ui::UiExt as _;
109
use re_ui::syntax_highlighting::SyntaxHighlightedBuilder;
10+
use re_ui::{SyntaxHighlighting, UiExt as _};
1111

12-
use super::{FilterError, FilterUiAction};
12+
use super::{Filter, FilterError, FilterUiAction};
1313

1414
/// Filter for non-nullable boolean columns.
1515
///
@@ -28,20 +28,18 @@ impl NonNullableBooleanFilter {
2828
Self::IsFalse => false,
2929
}
3030
}
31+
}
3132

32-
pub fn as_filter_expression(
33-
&self,
34-
column: &Column,
35-
field: &Field,
36-
) -> Result<Expr, FilterError> {
33+
impl Filter for NonNullableBooleanFilter {
34+
fn as_filter_expression(&self, field: &Field) -> Result<Expr, FilterError> {
3735
match field.data_type() {
38-
DataType::Boolean => Ok(col(column.clone()).eq(lit(self.as_bool()))),
36+
DataType::Boolean => Ok(col(field.name().clone()).eq(lit(self.as_bool()))),
3937

4038
DataType::List(field) | DataType::ListView(field)
4139
if field.data_type() == &DataType::Boolean =>
4240
{
4341
// `ANY` semantics
44-
Ok(array_has(col(column.clone()), lit(self.as_bool())))
42+
Ok(array_has(col(field.name().clone()), lit(self.as_bool())))
4543
}
4644

4745
_ => Err(FilterError::InvalidNonNullableBooleanFilter(
@@ -51,11 +49,13 @@ impl NonNullableBooleanFilter {
5149
}
5250
}
5351

54-
pub fn operand_text(&self) -> String {
55-
self.as_bool().to_string()
56-
}
57-
58-
pub fn popup_ui(&mut self, ui: &mut egui::Ui, column_name: &str) -> FilterUiAction {
52+
fn popup_ui(
53+
&mut self,
54+
ui: &mut egui::Ui,
55+
_timestamp_format: re_log_types::TimestampFormat,
56+
column_name: &str,
57+
_popup_just_opened: bool,
58+
) -> FilterUiAction {
5959
ui.label(
6060
SyntaxHighlightedBuilder::body_default(column_name)
6161
.with_keyword(" is")
@@ -80,6 +80,13 @@ impl NonNullableBooleanFilter {
8080
}
8181
}
8282

83+
impl SyntaxHighlighting for NonNullableBooleanFilter {
84+
fn syntax_highlight_into(&self, builder: &mut SyntaxHighlightedBuilder) {
85+
builder.append_keyword("is ");
86+
builder.append_primitive(&self.as_bool().to_string());
87+
}
88+
}
89+
8390
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, strum::VariantArray)]
8491
#[expect(clippy::enum_variant_names)]
8592
pub enum NullableBooleanValue {
@@ -163,17 +170,25 @@ impl NullableBooleanFilter {
163170
self
164171
}
165172

166-
pub fn as_filter_expression(
167-
&self,
168-
column: &Column,
169-
field: &Field,
170-
) -> Result<Expr, FilterError> {
173+
pub fn operand_text(&self) -> String {
174+
if let Some(value) = self.value.as_bool() {
175+
value.to_string()
176+
} else {
177+
"null".to_owned()
178+
}
179+
}
180+
}
181+
182+
impl Filter for NullableBooleanFilter {
183+
fn as_filter_expression(&self, field: &Field) -> Result<Expr, FilterError> {
184+
let column = Column::from(field.name().clone());
185+
171186
let expr = match field.data_type() {
172187
DataType::Boolean => {
173188
if let Some(value) = self.value.as_bool() {
174-
col(column.clone()).eq(lit(value))
189+
col(column).eq(lit(value))
175190
} else {
176-
col(column.clone()).is_null()
191+
col(column).is_null()
177192
}
178193
}
179194

@@ -182,10 +197,10 @@ impl NullableBooleanFilter {
182197
{
183198
// `ANY` semantics
184199
if let Some(value) = self.value.as_bool() {
185-
array_has(col(column.clone()), lit(value))
200+
array_has(col(column), lit(value))
186201
} else {
187202
col(column.clone()).is_null().or(array_element(
188-
array_sort(col(column.clone()), lit("ASC"), lit("NULLS FIRST")),
203+
array_sort(col(column), lit("ASC"), lit("NULLS FIRST")),
189204
lit(1),
190205
)
191206
.is_null())
@@ -206,19 +221,13 @@ impl NullableBooleanFilter {
206221
}
207222
}
208223

209-
pub fn operand_text(&self) -> String {
210-
if let Some(value) = self.value.as_bool() {
211-
value.to_string()
212-
} else {
213-
"null".to_owned()
214-
}
215-
}
216-
217-
pub fn operator(&self) -> NullableBooleanOperator {
218-
self.operator
219-
}
220-
221-
pub fn popup_ui(&mut self, ui: &mut egui::Ui, column_name: &str) -> FilterUiAction {
224+
fn popup_ui(
225+
&mut self,
226+
ui: &mut egui::Ui,
227+
_timestamp_format: re_log_types::TimestampFormat,
228+
column_name: &str,
229+
_popup_just_opened: bool,
230+
) -> FilterUiAction {
222231
ui.horizontal(|ui| {
223232
ui.label(
224233
SyntaxHighlightedBuilder::body_default(column_name).into_widget_text(ui.style()),
@@ -276,6 +285,14 @@ impl NullableBooleanFilter {
276285
}
277286
}
278287

288+
impl SyntaxHighlighting for NullableBooleanFilter {
289+
fn syntax_highlight_into(&self, builder: &mut SyntaxHighlightedBuilder) {
290+
builder.append_keyword(&self.operator.to_string());
291+
builder.append_keyword(" ");
292+
builder.append_primitive(&self.operand_text());
293+
}
294+
}
295+
279296
fn primitive_widget_text(ui: &egui::Ui, s: &str) -> egui::WidgetText {
280297
SyntaxHighlightedBuilder::primitive(s).into_widget_text(ui.style())
281298
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use std::fmt::Formatter;
2+
3+
use arrow::datatypes::{DataType, Field, FieldRef};
4+
use datafusion::prelude::Expr;
5+
6+
use super::{Filter as _, FilterError, TypedFilter};
7+
8+
/// The nullability of a nested arrow datatype.
9+
#[derive(Clone, Copy, PartialEq, Eq)]
10+
pub struct Nullability {
11+
/// The inner datatype is nullable (e.g. for a list array, one row's array may contain nulls).
12+
pub inner: bool,
13+
14+
/// The outer datatype is nullable (e.g, the a list array, one row may have a null instead of an
15+
/// array).
16+
pub outer: bool,
17+
}
18+
19+
// for test snapshot naming
20+
impl std::fmt::Debug for Nullability {
21+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
22+
match (self.inner, self.outer) {
23+
(false, false) => write!(f, "no_null"),
24+
(false, true) => write!(f, "outer_null"),
25+
(true, false) => write!(f, "inner_null"),
26+
(true, true) => write!(f, "both_null"),
27+
}
28+
}
29+
}
30+
31+
impl Nullability {
32+
pub const NONE: Self = Self {
33+
inner: false,
34+
outer: false,
35+
};
36+
37+
pub const BOTH: Self = Self {
38+
inner: true,
39+
outer: true,
40+
};
41+
42+
pub const INNER: Self = Self {
43+
inner: true,
44+
outer: false,
45+
};
46+
47+
pub const OUTER: Self = Self {
48+
inner: false,
49+
outer: true,
50+
};
51+
52+
pub const ALL: &'static [Self] = &[Self::NONE, Self::INNER, Self::OUTER, Self::BOTH];
53+
54+
pub fn from_field(field: &Field) -> Self {
55+
match field.data_type() {
56+
DataType::List(inner_field) | DataType::ListView(inner_field) => Self {
57+
inner: inner_field.is_nullable(),
58+
outer: field.is_nullable(),
59+
},
60+
61+
//TODO(ab): support other containers
62+
_ => Self {
63+
inner: field.is_nullable(),
64+
outer: false,
65+
},
66+
}
67+
}
68+
69+
pub fn is_either(&self) -> bool {
70+
self.inner || self.outer
71+
}
72+
}
73+
74+
/// A filter applied to a table's column.
75+
#[derive(Debug, Clone, PartialEq)]
76+
pub struct ColumnFilter {
77+
pub field: FieldRef,
78+
pub filter: TypedFilter,
79+
}
80+
81+
impl ColumnFilter {
82+
pub fn new(field: FieldRef, filter: impl Into<TypedFilter>) -> Self {
83+
Self {
84+
field,
85+
filter: filter.into(),
86+
}
87+
}
88+
89+
pub fn default_for_column(field: FieldRef) -> Option<Self> {
90+
let filter = TypedFilter::default_for_column(&field)?;
91+
Some(Self::new(field, filter))
92+
}
93+
94+
/// Convert to an [`Expr`].
95+
///
96+
/// The expression is used for filtering and should thus evaluate to a boolean.
97+
pub fn as_filter_expression(&self) -> Result<Expr, FilterError> {
98+
self.filter.as_filter_expression(&self.field)
99+
}
100+
}

0 commit comments

Comments
 (0)