Skip to content

Commit 81fddfe

Browse files
committed
Add $null and $some filter operators. #76
1 parent 3a92a6e commit 81fddfe

File tree

5 files changed

+117
-21
lines changed

5 files changed

+117
-21
lines changed

docs/src/content/docs/documentation/APIs/record_apis.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,8 @@ Parameters:
341341
* **$gt**: greater-than
342342
* **$lte**: less-than-equal
343343
* **$lt**: less-than
344+
* **$null**: SQL `IS NULL` unary operator
345+
* **$some**: SQL `IS NOT NULL` unary operator
344346
* **$like**: SQL `LIKE` operator
345347
* **$re**: SQL `REGEXP` operator
346348
* Parent records, i.e. records pointed to by foreign key columns, can be

trailbase-core/src/records/list_records.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,17 @@ mod tests {
830830
.records;
831831
assert_eq!(limited_arr.len(), 1);
832832

833+
// unary filter
834+
let unary_filtered_arr = list_records(
835+
&state,
836+
Some(&user_y_token.auth_token),
837+
Some(format!("filter[mid][$some]")),
838+
)
839+
.await
840+
.unwrap()
841+
.records;
842+
assert_eq!(unary_filtered_arr.len(), 3);
843+
833844
// Composite filter
834845
let messages: Vec<Message> = arr.into_iter().map(to_message).collect();
835846
let first = &messages[0].mid;

trailbase-qs/src/column_rel_value.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ pub enum CompareOp {
1010
GreaterThan,
1111
LessThanEqual,
1212
LessThan,
13+
Null,
14+
NotNull,
1315
Like,
1416
Regexp,
1517
}
@@ -23,19 +25,27 @@ impl CompareOp {
2325
"$gt" => Some(Self::GreaterThan),
2426
"$lte" => Some(Self::LessThanEqual),
2527
"$lt" => Some(Self::LessThan),
28+
"$null" => Some(Self::Null),
29+
"$some" => Some(Self::NotNull),
2630
"$like" => Some(Self::Like),
2731
"$re" => Some(Self::Regexp),
2832
_ => None,
2933
};
3034
}
3135

36+
pub fn is_unary(&self) -> bool {
37+
return matches!(self, Self::Null | Self::NotNull);
38+
}
39+
3240
pub fn to_sql(self) -> &'static str {
3341
return match self {
3442
Self::GreaterThanEqual => ">=",
3543
Self::GreaterThan => ">",
3644
Self::LessThanEqual => "<=",
3745
Self::LessThan => "<",
3846
Self::NotEqual => "<>",
47+
Self::Null => "IS NULL",
48+
Self::NotNull => "IS NOT NULL",
3949
Self::Like => "LIKE",
4050
Self::Regexp => "REGEXP",
4151
Self::Equal => "=",

trailbase-qs/src/filter.rs

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,40 +27,50 @@ pub enum ValueOrComposite {
2727
impl ValueOrComposite {
2828
pub fn into_sql<E>(
2929
self,
30-
prefix: Option<&str>,
30+
column_prefix: Option<&str>,
3131
validator: &dyn Fn(&str) -> Result<(), E>,
3232
) -> Result<(String, Vec<(String, Value)>), E> {
3333
let mut index: usize = 0;
34-
return self.into_sql_impl(prefix, validator, &mut index);
34+
return self.into_sql_impl(column_prefix, validator, &mut index);
3535
}
3636

3737
fn into_sql_impl<E>(
3838
self,
39-
prefix: Option<&str>,
39+
column_prefix: Option<&str>,
4040
validator: &dyn Fn(&str) -> Result<(), E>,
4141
index: &mut usize,
4242
) -> Result<(String, Vec<(String, Value)>), E> {
4343
match self {
4444
Self::Value(v) => {
4545
validator(&v.column)?;
4646

47-
let param = param_name(*index);
48-
*index += 1;
47+
return Ok(if v.op.is_unary() {
48+
(
49+
match column_prefix {
50+
Some(p) => format!(r#"{p}."{c}" {o}"#, c = v.column, o = v.op.to_sql()),
51+
None => format!(r#""{c}" {o}"#, c = v.column, o = v.op.to_sql()),
52+
},
53+
vec![],
54+
)
55+
} else {
56+
let param = param_name(*index);
57+
*index += 1;
4958

50-
return Ok((
51-
match prefix {
52-
Some(p) => format!(r#"{p}."{c}" {o} {param}"#, c = v.column, o = v.op.to_sql()),
53-
None => format!(r#""{c}" {o} {param}"#, c = v.column, o = v.op.to_sql()),
54-
},
55-
vec![(param, v.value)],
56-
));
59+
(
60+
match column_prefix {
61+
Some(p) => format!(r#"{p}."{c}" {o} {param}"#, c = v.column, o = v.op.to_sql()),
62+
None => format!(r#""{c}" {o} {param}"#, c = v.column, o = v.op.to_sql()),
63+
},
64+
vec![(param, v.value)],
65+
)
66+
});
5767
}
5868
Self::Composite(combiner, vec) => {
5969
let mut fragments = Vec::<String>::with_capacity(vec.len());
6070
let mut params = Vec::<(String, Value)>::with_capacity(vec.len());
6171

6272
for value_or_composite in vec {
63-
let (f, p) = value_or_composite.into_sql_impl::<E>(prefix, validator, index)?;
73+
let (f, p) = value_or_composite.into_sql_impl::<E>(column_prefix, validator, index)?;
6474
fragments.push(f);
6575
params.extend(p);
6676
}
@@ -270,4 +280,46 @@ mod tests {
270280
qs.deserialize_str("filter[col0]=val0&filter[$and][0][col0]=val0&filter[col1]=val1");
271281
assert!(m3.is_err(), "{m3:?}");
272282
}
283+
284+
#[test]
285+
fn test_filter_to_sql() {
286+
let v0 = ValueOrComposite::Value(ColumnOpValue {
287+
column: "col0".to_string(),
288+
op: CompareOp::Equal,
289+
value: Value::String("val0".to_string()),
290+
});
291+
292+
let validator = |_: &str| -> Result<(), String> {
293+
return Ok(());
294+
};
295+
let sql0 = v0
296+
.clone()
297+
.into_sql(/* column_prefix= */ None, &validator)
298+
.unwrap();
299+
assert_eq!(sql0.0, r#""col0" = :__p0"#);
300+
let sql0 = v0
301+
.into_sql(/* column_prefix= */ Some("p"), &validator)
302+
.unwrap();
303+
assert_eq!(sql0.0, r#"p."col0" = :__p0"#);
304+
305+
let v1 = ValueOrComposite::Value(ColumnOpValue {
306+
column: "col0".to_string(),
307+
op: CompareOp::Null,
308+
value: Value::String("".to_string()),
309+
});
310+
assert_eq!(
311+
v1.into_sql(None, &validator).unwrap().0,
312+
r#""col0" IS NULL"#
313+
);
314+
315+
let v2 = ValueOrComposite::Value(ColumnOpValue {
316+
column: "col0".to_string(),
317+
op: CompareOp::NotNull,
318+
value: Value::String("ignored".to_string()),
319+
});
320+
assert_eq!(
321+
v2.into_sql(Some("p"), &validator).unwrap().0,
322+
r#"p."col0" IS NOT NULL"#
323+
);
324+
}
273325
}

trailbase-qs/src/value.rs

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,43 +109,64 @@ mod tests {
109109
fn test_value() {
110110
let qs = Config::new(5, false);
111111

112-
let v0: Query = qs.deserialize_str("filter[col0][$eq]=val0").unwrap();
113112
assert_eq!(
114-
v0.filter.unwrap(),
113+
qs.deserialize_str::<Query>("filter[col0][$eq]=val0")
114+
.unwrap()
115+
.filter
116+
.unwrap(),
115117
ValueOrComposite::Value(ColumnOpValue {
116118
column: "col0".to_string(),
117119
op: CompareOp::Equal,
118120
value: Value::String("val0".to_string()),
119121
})
120122
);
121-
let v1: Query = qs.deserialize_str("filter[col0][$ne]=TRUE").unwrap();
123+
122124
assert_eq!(
123-
v1.filter.unwrap(),
125+
qs.deserialize_str::<Query>("filter[col0][$ne]=TRUE")
126+
.unwrap()
127+
.filter
128+
.unwrap(),
124129
ValueOrComposite::Value(ColumnOpValue {
125130
column: "col0".to_string(),
126131
op: CompareOp::NotEqual,
127132
value: Value::Bool(true),
128133
})
129134
);
130135

131-
let v2: Query = qs.deserialize_str("filter[col0][$ne]=0").unwrap();
132136
assert_eq!(
133-
v2.filter.unwrap(),
137+
qs.deserialize_str::<Query>("filter[col0][$ne]=0")
138+
.unwrap()
139+
.filter
140+
.unwrap(),
134141
ValueOrComposite::Value(ColumnOpValue {
135142
column: "col0".to_string(),
136143
op: CompareOp::NotEqual,
137144
value: Value::Integer(0),
138145
})
139146
);
140147

141-
let v3: Query = qs.deserialize_str("filter[col0][$ne]=0.0").unwrap();
142148
assert_eq!(
143-
v3.filter.unwrap(),
149+
qs.deserialize_str::<Query>("filter[col0][$ne]=0.0")
150+
.unwrap()
151+
.filter
152+
.unwrap(),
144153
ValueOrComposite::Value(ColumnOpValue {
145154
column: "col0".to_string(),
146155
op: CompareOp::NotEqual,
147156
value: Value::Double(0.0),
148157
})
149158
);
159+
160+
assert_eq!(
161+
qs.deserialize_str::<Query>("filter[col0][$some]")
162+
.unwrap()
163+
.filter
164+
.unwrap(),
165+
ValueOrComposite::Value(ColumnOpValue {
166+
column: "col0".to_string(),
167+
op: CompareOp::NotNull,
168+
value: Value::String("".to_string()),
169+
})
170+
);
150171
}
151172
}

0 commit comments

Comments
 (0)