By default the filter parser reads PostgreSQL 18's grammar and accepts every
operator, and the sort parser accepts both ASC and DESC. You can choose
the SQL dialect, and narrow either, with functional options at construction
time.
The filter parser checks a filter in the grammar of the SQL it will run as:
PostgreSQL 18's by default, or, with WithDialect, the SQL standard's
(SQL:2023) or MySQL 8.4's, whose strings are read with MySQL's backslash
escapes. Everything about it is on its own page, SQL Dialects:
why there is one, how to choose it, how it is checked, a table of every
construct in each, and what it does not do.
p := qfv.NewFilterParser(fields, qfv.WithDialect(qfv.DialectSQLStandard))
p.Parse("id <> 1 AND name LIKE 'x!%' ESCAPE '!'") // ok
p.Parse("id != 1") // error: != is not part of the SQL:2023 dialect: write <>
m := qfv.NewFilterParser(fields, qfv.WithDialect(qfv.DialectMySQL))
m.Parse(`id != 1 AND name = 'it\'s'`) // ok: MySQL 8.4 runs it
m.Parse("name ILIKE 'x%'") // error: operator "ILIKE" is not part of the MySQL 8.4 dialectThe dialect comes before the allow-list below: an operator the dialect does not have is refused whether it is allowed or not.
Pass WithAllowedOperators and/or WithAllowedOperatorGroups to
NewFilterParser. Anything outside the allow-list is reported as a validation
error (operator "LIKE" is not allowed). When neither option is used, all
operators are allowed.
p := qfv.NewFilterParser(fields,
qfv.WithAllowedOperatorGroups(qfv.GroupComparison, qfv.GroupLogical),
qfv.WithAllowedOperators(qfv.OpIn),
)
p.Parse("age >= 18 AND status IN ('active')") // ok
p.Parse("name LIKE '%x%'") // error: operator "LIKE" is not allowedThe two options are additive — groups expand to their operators and merge with any individually listed operators.
flowchart LR
G["WithAllowedOperatorGroups(GroupComparison, GroupLogical)"] --> EXP["expand each group<br/>to its operators"]
O["WithAllowedOperators(OpIn)"] --> SET
EXP --> SET(["allowed-operator set (union)"])
SET --> CHK{"operator seen<br/>while parsing?"}
CHK -->|"in set"| OK["accept"]
CHK -->|"not in set"| ERR["error:<br/>operator is not allowed"]
Every operator the parser encounters is checked against the dialect, and then against the allowed set. A nil set (neither option supplied) is the default and permits everything the dialect has.
flowchart TD
T["operator token in input"] --> D{"in the dialect,<br/>in this spelling?"}
D -->|"no"| DERR["QFVFilterError:<br/>not part of the dialect"]
D -->|"yes"| C{"allow-list<br/>configured?"}
C -->|"no (nil) — default"| OK["allowed<br/>(every operator permitted)"]
C -->|"yes"| M{"operator in set?"}
M -->|"yes"| OK
M -->|"no"| ERR["QFVFilterError:<br/>operator is not allowed"]
| Operator | Matches |
|---|---|
OpAnd, OpOr, OpNot |
AND, OR, standalone NOT |
OpEqual, OpNotEqual |
=; <> and != |
OpLessThan, OpLessThanOrEqual |
<, <= |
OpGreaterThan, OpGreaterThanOrEqual |
>, >= |
OpLike |
LIKE, NOT LIKE, ~~, !~~ |
OpILike |
ILIKE, NOT ILIKE, ~~*, !~~* |
OpSimilarTo |
SIMILAR TO, NOT SIMILAR TO |
OpRegexMatch |
~, ~*, !~, !~* |
OpIn |
IN, NOT IN |
OpBetween |
BETWEEN, NOT BETWEEN, SYMMETRIC |
OpIsNull |
IS [NOT] NULL, ISNULL, NOTNULL |
OpBooleanTest |
IS [NOT] TRUE/FALSE/UNKNOWN |
OpIsDistinctFrom |
IS [NOT] DISTINCT FROM |
qfv.AllOperators() returns the complete list.
| Group | Operators |
|---|---|
GroupLogical |
AND, OR, NOT |
GroupComparison |
=, <>, <, <=, >, >= |
GroupPattern |
LIKE, ILIKE, SIMILAR TO, regex |
GroupMembership |
IN |
GroupRange |
BETWEEN |
GroupNull |
IS NULL |
GroupBoolean |
IS TRUE/FALSE/UNKNOWN |
GroupDistinct |
IS DISTINCT FROM |
OperatorGroup.Operators() returns the operators a group expands to.
flowchart LR
GroupLogical --> OpAnd & OpOr & OpNot
GroupComparison --> OpEqual & OpNotEqual & OpLessThan & OpLessThanOrEqual & OpGreaterThan & OpGreaterThanOrEqual
GroupPattern --> OpLike & OpILike & OpSimilarTo & OpRegexMatch
GroupMembership --> OpIn
GroupRange --> OpBetween
GroupNull --> OpIsNull
GroupBoolean --> OpBooleanTest
GroupDistinct --> OpIsDistinctFrom
A negated predicate is governed by its base operator, not by OpNot:
NOT IN,NOT LIKE,NOT BETWEEN,NOT SIMILAR TO,IS NOT DISTINCT FROMare allowed wheneverOpIn/OpLike/OpBetween/OpSimilarTo/OpIsDistinctFrom(respectively) are allowed.OpNotgoverns only the standalone logicalNOT, e.g.NOT (age > 30).IS NOT DISTINCT FROMis negated inside theISpredicate, not by a leadingNOT:age NOT DISTINCT FROM 30is refused.
flowchart TD
N["NOT appears in input"] --> K{"what follows?"}
K -->|"( expr ) or a comparison"| U["UnaryOperatorNode<br/>governed by OpNot"]
K -->|"IN / LIKE / ILIKE /<br/>BETWEEN / SIMILAR TO"| P["predicate node with IsNot=true<br/>governed by the base operator"]
U -. "allow-list check" .-> gnot{"OpNot allowed?"}
P -. "allow-list check" .-> gbase{"OpIn / OpLike / OpILike /<br/>OpBetween / OpSimilarTo allowed?"}
So allowing OpEqual but not OpNot accepts a = 1 but rejects NOT (a = 1).
Pass WithAllowedDirections to NewSortParser to forbid a direction:
p := qfv.NewSortParser(fields, qfv.WithAllowedDirections(qfv.SortAsc))
p.Parse("first_name ASC") // ok
p.Parse("first_name DESC") // error: sort direction "DESC" is not allowedWhen the option is not used, both ASC and DESC are allowed.
Field allow-lists accept dotted paths in every parser (fields, sort, and
filter), so user.profile.age works out of the box — see
Filtering › Nested field names.