Writing a filter¶
Filter queries are used to filter the results of need collection, for example in the Needs Index
tree view.
They can take one of two forms:
Cypher syntax¶
This syntax of a filter query uses a subset of the Cypher query language.
See full syntax grammar
Written in pest, the grammar for the filter query language is as follows:
// A grammar for parsing the ubquery language
// This query language aims to be a strict subset of open cypher (https://opencypher.org/resources/, https://neo4j.com/docs/cypher-manual/current/clauses/where/#where-introduction)
// see also https://github.com/a-poor/open-cypher/blob/main/src/cypher.pest
start = { SOI ~ ws* ~ or_expr ~ ws* ~ EOI }
or_expr = { and_expr ~ (ws+ ~ or_keyword ~ ws+ ~ and_expr)* }
or_keyword = _{ ^"OR" }
and_expr = { (expr | not_expr) ~ (ws+ ~ and_keyword ~ ws+ ~ (expr | not_expr))* }
and_keyword = _{ ^"AND" }
expr = { paren_expr | var_field_op_expr | literal_in_var_field_expr }
not_expr = { not_keyword ~ ws+ ~ (paren_expr | var_field_op_expr | literal_in_var_field_expr) }
not_keyword = _{ ^"NOT" }
paren_expr = { "(" ~ ws* ~ or_expr ~ ws* ~ ")" }
literal_in_var_field_expr = { literal_single ~ ws+ ~ in_keyword ~ ws+ ~ var_field }
var_field_op_expr = {
(var_field | var_field_function) ~
(string_predicate_expr | in_list_expr | is_null_expr | is_not_null_expr | comparison_expr)?
}
comparison_expr = {
ws* ~ equals_keyword ~ ws* ~ (literal | var_field) |
ws* ~ not_equals_keyword ~ ws* ~ (literal | var_field) |
ws* ~ less_than_keyword ~ ws* ~ (number_literal | var_field) |
ws* ~ greater_than_keyword ~ ws* ~ (number_literal | var_field) |
ws* ~ less_than_or_equals_keyword ~ ws* ~ (number_literal | var_field) |
ws* ~ greater_than_or_equals_keyword ~ ws* ~ (number_literal | var_field)
}
equals_keyword = { "=" }
not_equals_keyword = { "<>" }
less_than_keyword = { "<" }
greater_than_keyword = { ">" }
less_than_or_equals_keyword = { "<=" }
greater_than_or_equals_keyword = { ">=" }
is_null_expr = { ws+ ~ is_keyword ~ ws+ ~ null_keyword }
is_not_null_expr = { ws+ ~ is_keyword ~ ws+ ~ not_keyword ~ ws+ ~ null_keyword }
string_predicate_expr = {
(ws+ ~ starts_with_keyword | ws+ ~ ends_with_keyword | ws+ ~ contains_keyword) ~ ws* ~ string_literal
}
starts_with_keyword = { ^"STARTS WITH" }
ends_with_keyword = { ^"ENDS WITH" }
contains_keyword = { ^"CONTAINS" }
in_list_expr = { ws+ ~ in_keyword ~ ws+ ~ (list_literal | var_field) }
in_keyword = _{ ^"IN" }
var_field = { symbolic_name_simple ~ "." ~ (symbolic_name_simple | symbolic_name_quoted) }
symbolic_name_simple = @{ id_start ~ id_part* }
id_start = @{ "_" | ASCII_ALPHA }
id_part = @{ id_start | ASCII_DIGIT }
symbolic_name_quoted = @{ "`" ~ (!"`" ~ ANY)* ~ "`" }
function_name = { ^"upper" | ^"lower" | ^"size" }
var_field_function = { function_name ~ "(" ~ var_field ~ ")" }
literal_single = { null_literal | boolean_literal | number_literal | string_literal }
literal = { null_literal | boolean_literal | number_literal | string_literal | list_literal }
null_literal = { ^"NULL" }
boolean_literal = { true_literal | false_literal }
true_literal = { ^"TRUE" }
false_literal = { ^"FALSE" }
number_literal = { float_literal | decimal_literal | integer_literal }
integer_literal = @{ "-"? ~ ("0" | ASCII_NONZERO_DIGIT ~ ASCII_DIGIT*) }
decimal_literal = @{ integer_literal ~ "." ~ ASCII_DIGIT* }
float_literal = @{ integer_literal ~ exp | decimal_literal ~ exp? }
exp = @{ ^"E" ~ integer_literal }
string_literal = { string_single | string_double }
// TODO support escape sequences
string_single = @{ "'" ~ (!("'" | "\\") ~ ANY)* ~ "'" }
string_double = @{ "\"" ~ (!("\"" | "\\") ~ ANY)* ~ "\"" }
list_literal = {
"[" ~ ws* ~ (literal_single ~ ws* ~ ("," ~ ws* ~ literal_single ~ ws*)*)? ~ "]"
}
is_keyword = _{ ^"IS" }
null_keyword = _{ ^"NULL" }
ws = _{ " " | "\t" }
Variables and attributes¶
The following variables are available in the filter query:
n
: The node representing the need.l
: The node representing a link to another need. The main attribute of a link istype
, i.e.l.type
.o
: The node representing a linked need.
The graph would look like: (n)-[l]->(o)
.
You can access the attributes of a node using the dot operator, for example n.title
.
If the attribute contains spaces or other non-ascii characters, you can wrap the attribute name in backticks, for example n.`my attribute`
.
In addition to node attributes, you can query by the source type of the node:
n.is_directive
: True if the need originated from a directive.n.is_external
: True if the need originated from theexternal_needs
configuration.n.is_import
: True if the need originated from aneedimport
directive.
You can also query if the need is modified by one or more needextend
directives using the n.is_modified
attribute.
Values¶
Values can be:
Strings (in single or double quotes), e.g.
"my value"
or'my value'
.Numbers, e.g.
42
,3.14
, or0.1e-3
.Booleans, e.g.
true
orfalse
(case-insensitive).Null, e.g.
null
(case-insensitive).Other attributes, e.g.
n.title
.Lists of values, e.g.
["value1", "value2"]
.
Comparison operators¶
The following boolean operators are available:
n.attribute = value
: Equal to.n.attribute <> value
: Not equal to.n.attribute < value
: Less than.n.attribute <= value
: Less than or equal to.n.attribute > value
: Greater than.n.attribute >= value
: Greater than or equal to.
String operators¶
The following string operators are available:
n.attribute contains "value"
: Contains the string.n.attribute starts with "value"
: Starts with the string.n.attribute ends with "value"
: Ends with the string.
Null operators¶
The following null operators are available:
n.attribute is null
: Is null.n.attribute is not null
: Is not null.
List operators¶
The following list operators are available:
n.attribute in ["value1", "value2"]
: Attribute is in the list."value" in n.attribute
: Value is in a list type attribute.
Attribute functions¶
Some functions are available to modify attributes before comparison:
lower(n.attribute)
: Convert string attribute to lower-case.upper(n.attribute)
: Convert string attribute to upper-case.size(n.attribute)
: Get the length of a string or list attribute.
Logical operators¶
The following logical operators are available:
not
: Negate the following expression.and
: Logical AND.or
: Logical OR (not yet supported).
Parentheses¶
You can use parentheses to group expressions, for example:
(n.attribute1 = "value1" and n.attribute2 = "value2") and n.attribute3 = "value3"
Examples¶
To filter for all needs that start with a case-insensitive string:
lower(n.title) starts with "my string"
To filter for all needs that are linked to a need of type requirement
by a link of type needs
:
l.type = "needs" and o.type = "requirement"
Python syntax¶
This syntax of a filter query uses a subset of the Python expression syntax.
See full syntax grammar
Written in pest, the grammar for the filter query language is as follows:
// A grammar for supporting a subset of python expressions
start = { SOI ~ ws* ~ or_expr ~ ws* ~ EOI }
or_expr = { and_expr ~ (ws+ ~ or_keyword ~ ws+ ~ and_expr)* }
or_keyword = _{ "or" }
and_expr = { (expr | not_expr) ~ (ws+ ~ and_keyword ~ ws+ ~ (expr | not_expr))* }
and_keyword = _{ "and" }
expr = { paren_expr | var_field_op_expr | literal_in_var_field_expr }
not_expr = { not_keyword ~ ws+ ~ (paren_expr | var_field_op_expr | literal_in_var_field_expr) }
not_keyword = _{ "not" }
paren_expr = { "(" ~ ws* ~ or_expr ~ ws* ~ ")" }
literal_in_var_field_expr = { literal_single ~ ws+ ~ in_keyword ~ ws+ ~ var_field }
var_field_op_expr = {
(var_field) ~
(in_list_expr | not_in_list_expr | is_null_expr | is_not_null_expr | comparison_expr | string_method_expr)?
}
comparison_expr = {
ws* ~ equals_keyword ~ ws* ~ (literal | var_field) |
ws* ~ not_equals_keyword ~ ws* ~ (literal | var_field) |
ws* ~ less_than_keyword ~ ws* ~ (number_literal | var_field) |
ws* ~ greater_than_keyword ~ ws* ~ (number_literal | var_field) |
ws* ~ less_than_or_equals_keyword ~ ws* ~ (number_literal | var_field) |
ws* ~ greater_than_or_equals_keyword ~ ws* ~ (number_literal | var_field)
}
equals_keyword = { "==" }
not_equals_keyword = { "!=" }
less_than_keyword = { "<" }
greater_than_keyword = { ">" }
less_than_or_equals_keyword = { "<=" }
greater_than_or_equals_keyword = { ">=" }
is_null_expr = { ws+ ~ is_keyword ~ ws+ ~ null_keyword }
is_not_null_expr = { ws+ ~ is_keyword ~ ws+ ~ not_keyword ~ ws+ ~ null_keyword }
in_list_expr = { ws+ ~ in_keyword ~ ws+ ~ (list_literal | var_field) }
not_in_list_expr = { ws+ ~ "not" ~ ws+ ~ in_keyword ~ ws+ ~ (list_literal | var_field) }
var_field = { symbolic_name_simple }
reserved = { "None" | "True" | "False" | "and" | "or" | "not" | "in" | "is" }
symbolic_name_simple = @{ !(reserved ~ (ws | EOI)) ~ id_start ~ id_part* }
id_start = @{ "_" | ASCII_ALPHA }
id_part = @{ id_start | ASCII_DIGIT }
literal_single = { null_literal | boolean_literal | number_literal | string_literal }
literal = { null_literal | boolean_literal | number_literal | string_literal | list_literal }
boolean_literal = { true_literal | false_literal }
null_literal = { "None" }
true_literal = { "True" }
false_literal = { "False" }
number_literal = { float_literal | decimal_literal | integer_literal }
integer_literal = @{ "-"? ~ ("0" | ASCII_NONZERO_DIGIT ~ ASCII_DIGIT*) }
decimal_literal = @{ integer_literal ~ "." ~ ASCII_DIGIT* }
float_literal = @{ integer_literal ~ exp | decimal_literal ~ exp? }
exp = @{ ^"E" ~ integer_literal }
string_literal = { string_single | string_double }
// TODO support escape sequences
string_single = @{ "'" ~ (!("'" | "\\") ~ ANY)* ~ "'" }
string_double = @{ "\"" ~ (!("\"" | "\\") ~ ANY)* ~ "\"" }
list_literal = {
"[" ~ ws* ~ (literal_single ~ ws* ~ ("," ~ ws* ~ literal_single ~ ws*)*)? ~ "]"
}
string_method_expr = {"." ~ (startswith_method | endswith_method) ~ "(" ~ string_literal ~ ")"}
startswith_method = { "startswith" }
endswith_method = { "endswith" }
in_keyword = _{ "in" }
is_keyword = _{ "is" }
null_keyword = _{ "None" }
ws = _{ " " | "\t" }
Variables¶
Variables relate to attributes of the need, e.g. id
or title
In addition to node attributes, you can query by the source type of the node:
is_directive
: True if the need originated from a directive.is_external
: True if the need originated from theexternal_needs
configuration.is_import
: True if the need originated from aneedimport
directive.
You can also query if the need is modified by one or more needextend
directives using the is_modified
attribute.
Values¶
Values can be:
Strings (in single or double quotes), e.g.
"my value"
or'my value'
.Numbers, e.g.
42
,3.14
, or0.1e-3
.Booleans, e.g.
True
orFalse
.Null, e.g.
None
.Other attributes, e.g.
title
.Lists of values, e.g.
["value1", "value2"]
.
Comparison operators¶
The following boolean operators are available:
attribute == value
: Equal to.attribute != value
: Not equal to.attribute < value
: Less than.attribute <= value
: Less than or equal to.attribute > value
: Greater than.attribute >= value
: Greater than or equal to.
String operators¶
The following string operators are available:
"value" in attribute
: Contains the string.attribute.startswith("value")
: Starts with the string.attribute.endsswith("value")
: Ends with the string.
Null operators¶
The following null operators are available:
attribute is None
: Is null.attribute is not None
: Is not null.
List operators¶
The following list operators are available:
attribute in ["value1", "value2"]
: Attribute is in the list."value" in attribute
: Value is in a list type attribute."value" not in attribute
: Value is not in a list type attribute.
Logical operators¶
The following logical operators are available:
not
: Negate the following expression.and
: Logical AND.or
: Logical OR (not yet supported).
Parentheses¶
You can use parentheses to group expressions, for example:
(attribute1 == "value1" and attribute2 == "value2") and attribute3 == "value3"