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 is type, 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 the external_needs configuration.

  • n.is_import: True if the need originated from a needimport 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, or 0.1e-3.

  • Booleans, e.g. true or false (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 the external_needs configuration.

  • is_import: True if the need originated from a needimport 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, or 0.1e-3.

  • Booleans, e.g. True or False.

  • 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"