Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions common/src/db/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,17 +170,17 @@ pub struct Query {
/// attributes. If it doesn't, an error will be returned
/// containing a list of the valid fields for that resource.
///
/// The value 'null' is treated specially for [Not]Equal filters:
/// it returns resources on which the field isn't set. Use the
/// LIKE operator, `~`, to match a literal "null" string. Omit the
/// value to match an empty string.
/// An ASCII value of `NUL`, percent-encoded as `%00`, may be used
/// to find resources on which a particular field isn't set. For
/// example, `name=%00` and `name!=%00` yield the WHERE clauses,
/// 'NAME IS NULL' and 'NAME IS NOT NULL', respectively.
///
/// Examples:
/// - `name=foo` - entity's _name_ matches 'foo' exactly
/// - `name~foo` - entity's _name_ contains 'foo', case-insensitive
/// - `name~foo|bar` - entity's _name_ contains either 'foo' OR 'bar', case-insensitive
/// - `name=` - entity's _name_ is the empty string, ''
/// - `name=null` - entity's _name_ isn't set
/// - `name=%00` - entity's _name_ isn't set
/// - `published>3 days ago` - date values can be "human time"
///
/// Multiple full text searches and/or filters should be
Expand Down
42 changes: 29 additions & 13 deletions common/src/db/query/columns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,18 +161,30 @@ impl Columns {
value: &str,
) -> Result<SimpleExpr, Error> {
self.for_field(field).and_then(|(lhs, ct)| {
match (value.to_lowercase().as_str(), operator, &ct) {
("null", Operator::Equal, _) => Ok(lhs.is_null()),
("null", Operator::NotEqual, _) => Ok(lhs.is_not_null()),
(v, op, ColumnType::Array(_)) => match op {
Operator::Like => Ok(array_to_string(lhs).ilike(like(v))),
Operator::NotLike => Ok(array_to_string(lhs).not_ilike(like(v))),
Operator::NotEqual => Ok(Expr::val(value).binary(op, all(lhs))),
_ => Ok(Expr::val(value).binary(op, any(lhs))),
},
(v, Operator::Like, _) => Ok(lhs.ilike(like(v))),
(v, Operator::NotLike, _) => Ok(lhs.not_ilike(like(v))),
(_, _, ct) => parse(value, ct).map(|rhs| lhs.binary(operator, rhs)),
use Operator::*;
if value == "\x00" {
// Query for NULL values
match operator {
Equal => Ok(lhs.is_null()),
NotEqual => Ok(lhs.is_not_null()),
op => Err(Error::SearchSyntax(format!(
"'{op}' is invalid for NUL values; use '{Equal}' or '{NotEqual}'"
))),
}
} else if let ColumnType::Array(_) = ct {
// Special expressions for arrays
match operator {
Like => Ok(array_to_string(lhs).ilike(like(value))),
NotLike => Ok(array_to_string(lhs).not_ilike(like(value))),
NotEqual => Ok(Expr::val(value).binary(operator, all(lhs))),
_ => Ok(Expr::val(value).binary(operator, any(lhs))),
}
} else {
match (operator, &ct) {
(Like, _) => Ok(lhs.ilike(like(value))),
(NotLike, _) => Ok(lhs.not_ilike(like(value))),
(_, ct) => parse(value, ct).map(|rhs| lhs.binary(operator, rhs)),
}
}
})
}
Expand Down Expand Up @@ -605,9 +617,13 @@ mod tests {
};

assert_eq!(
clause(q("authors=null"))?,
clause(q("authors=\x00"))?,
r#""advisory"."authors" IS NULL"#
);
assert_eq!(
clause(q("authors=null"))?,
r#"'null' = ANY("advisory"."authors")"#
);
assert_eq!(
clause(q("authors~foo"))?,
r#"array_to_string("advisory"."authors", '|') ILIKE '%foo%'"#
Expand Down
16 changes: 14 additions & 2 deletions common/src/db/query/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ pub(crate) mod tests {
Ok(_) => panic!("invalid field"),
Err(e) => log::error!("{e}"),
}
match where_clause("location>=\x00") {
Ok(_) => panic!("invalid operator"),
Err(e) => log::error!("{e}"),
}
assert_eq!(
where_clause("location=foo")?,
r#""advisory"."location" = 'foo'"#
Expand Down Expand Up @@ -269,13 +273,21 @@ pub(crate) mod tests {
r#""advisory"."published" > '2023-11-03'"#
);
assert_eq!(
where_clause("published=null")?,
where_clause("published=\x00")?,
r#""advisory"."published" IS NULL"#
);
assert_eq!(
where_clause("published!=NULL")?,
where_clause("published=null")?,
r#""advisory"."published" = 'null'"#
);
assert_eq!(
where_clause("published!=\x00")?,
r#""advisory"."published" IS NOT NULL"#
);
assert_eq!(
where_clause("published!=null")?,
r#""advisory"."published" <> 'null'"#
);
assert_eq!(
where_clause("severity=high")?,
r#""advisory"."severity" = (CAST('high' AS "Severity"))"#
Expand Down
4 changes: 2 additions & 2 deletions modules/fundamental/src/advisory/endpoints/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,8 @@ async fn rejected_cve(ctx: &TrustifyContext) -> Result<(), anyhow::Error> {
.await?;

query(2, "labels:type=cve").await;
query(1, "withdrawn=null&identifier=CVE-2024-29025").await;
query(1, "withdrawn!=null&identifier=CVE-2024-25704").await;
query(1, "withdrawn=\x00&identifier=CVE-2024-29025").await;
query(1, "withdrawn!=\x00&identifier=CVE-2024-25704").await;
query(1, "withdrawn=2024-04-25T18:21:10.150Z").await;
query(1, "withdrawn<2025-01-01").await;

Expand Down
2 changes: 1 addition & 1 deletion modules/fundamental/src/sbom_group/endpoints/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ pub async fn list_groups_with_parent(
let parent: Vec<_> = parent.into_iter().collect();

let parent = match parent.is_empty() {
true => "null".to_string(),
true => "\x00".to_string(),
false => locate_id(&ids, parent),
};

Expand Down
Loading