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_needsconfiguration.n.is_import: True if the need originated from aneedimportdirective.
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.
trueorfalse(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.
Parentheses¶
You can use parentheses to group expressions, for example:
(n.attribute1 = "value1" or n.attribute2 = "value2") and n.attribute3 = "value3"
not can be used to negate an entire group, for example:
not (n.attribute1 = "value1" or n.attribute2 = "value2")
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 | this_doc_check | var_field_op_expr | literal_in_var_field_expr }
not_expr = { not_keyword ~ ws+ ~ expr }
not_keyword = _{ "not" }
paren_expr = { "(" ~ ws* ~ or_expr ~ ws* ~ ")" }
literal_in_var_field_expr = { literal_single ~ ws+ ~ in_keyword ~ ws+ ~ var_field_with_func }
var_field_op_expr = {
var_field_with_func ~
(in_list_expr | not_in_list_expr | is_null_expr | is_not_null_expr | comparison_expr | str_predicate_method)?
}
comparison_expr = {
ws* ~ equals_keyword ~ ws* ~ (literal | var_field_with_func) |
ws* ~ not_equals_keyword ~ ws* ~ (literal | var_field_with_func) |
ws* ~ less_than_keyword ~ ws* ~ (number_literal | var_field_with_func) |
ws* ~ greater_than_keyword ~ ws* ~ (number_literal | var_field_with_func) |
ws* ~ less_than_or_equals_keyword ~ ws* ~ (number_literal | var_field_with_func) |
ws* ~ greater_than_or_equals_keyword ~ ws* ~ (number_literal | var_field_with_func)
}
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 }
var_field_with_func = { var_field_with_len | var_field_with_upper | var_field_with_lower | var_field }
var_field_with_len = { ("len(") ~ var_field ~ (")") }
var_field_with_lower = { var_field ~ (".lower()") }
var_field_with_upper = { var_field ~ (".upper()") }
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*)*)? ~ "]"
}
str_predicate_method = {"." ~ str_predicate_method_name ~ "(" ~ string_literal ~ ")"}
str_predicate_method_name = { "startswith" | "endswith" }
this_doc_check = { "c.this_doc()" }
in_keyword = _{ "in" }
is_keyword = _{ "is" }
null_keyword = _{ "None" }
ws = _{ " " | "\t" | "\n" }
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_needsconfiguration.is_import: True if the need originated from aneedimportdirective.
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.
TrueorFalse.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 mutators and comparators are available:
attribute.lower(): Convert string attribute to lower-case before comparison.attribute.upper(): Convert string attribute to upper-case before comparison."value" in attribute: Contains the string.attribute.startswith("value"): Starts with the string.attribute.endsswith("value"): Ends with the string.
Note these can be combined, e.g. attribute.lower().startswith("value").
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.
Parentheses¶
You can use parentheses to group expressions, for example:
(attribute1 == "value1" or attribute2 == "value2") and attribute3 == "value3"
not can be used to negate an entire group, for example:
not (attribute1 == "value1" or attribute2 == "value2")
Special functions¶
The following special functions are available:
c.this_doc(): True if the need is in the same document as the query originates from.