cql2 0.6.0

Parse, validate, and convert Common Query Language (CQL2) text and JSON
Documentation
WHITESPACE = _{ SEPARATOR | "\n" | "\r" | "\t" }
COLON      = _{ ":" }
LPAREN     = _{ "(" }
RPAREN     = _{ ")" }
DQUOTE     = _{ "\"" }
PERIOD     = _{ "." }
UNDERSCORE = _{ "_" }
COMMA      = _{ "," }
COMMADELIM = _{ WHITESPACE* ~ COMMA ~ WHITESPACE* }

// A date or timestamp is read as a `SingleQuotedString` and interpreted in `temporal.rs`, so
// `DATE('2020-01-01')` and a bare literal reach the same code and agree.

// wkt
PADDED_DECIMAL     = { WHITESPACE* ~ DECIMAL ~ WHITESPACE* }
COORD              = { #four_d = PADDED_DECIMAL{4} | #three_d = PADDED_DECIMAL{3} | #two_d = PADDED_DECIMAL{2} }
PCOORD             = { WHITESPACE* ~ LPAREN ~ COORD ~ RPAREN ~ WHITESPACE* }
COORDLIST          = { WHITESPACE* ~ COORD ~ (COMMADELIM ~ COORD)* ~ WHITESPACE* }
PCOORDLIST         = { WHITESPACE* ~ LPAREN ~ COORDLIST ~ RPAREN ~ WHITESPACE* }
PCOORDLISTLIST     = { WHITESPACE* ~ LPAREN ~ PCOORDLIST ~ (COMMADELIM ~ PCOORDLIST)* ~ RPAREN ~ WHITESPACE* }
PCOORDLISTLISTLIST = { WHITESPACE* ~ LPAREN ~ PCOORDLISTLIST ~ (COMMADELIM ~ PCOORDLISTLIST)* ~ RPAREN ~ WHITESPACE* }

ZM = { WHITESPACE* ~ (^"ZM" | ^"Z" | ^"M")? ~ WHITESPACE* }

// A geometry with no coordinates at all, written `POLYGON EMPTY` rather than with an empty
// coordinate list. Every geometry type has one, and this crate emits them: GeoJSON coordinates of
// `[]` render as `POLYGON EMPTY`, so without this production its own output would not parse.
//
// `!NameChar` ends the keyword where a name would, so `POINT EMPTYISH` is not an empty point
// followed by nonsense.
EMPTYSET = { ^"EMPTY" ~ !NameChar }

POINT      = ${ ^"POINT" ~ ZM ~ (PCOORD | EMPTYSET) }
LINESTRING = ${ ^"LINESTRING" ~ ZM ~ (PCOORDLIST | EMPTYSET) }
POLYGON    = ${ ^"POLYGON" ~ ZM ~ (PCOORDLISTLIST | EMPTYSET) }

MULTIPOINT_1     = ${ ^"MULTIPOINT" ~ ZM ~ PCOORDLIST }
MULTIPOINT_2     = ${ ^"MULTIPOINT" ~ ZM ~ PCOORDLISTLIST }
MULTIPOINT_EMPTY = ${ ^"MULTIPOINT" ~ ZM ~ EMPTYSET }
MULTIPOINT       = ${ MULTIPOINT_1 | MULTIPOINT_2 | MULTIPOINT_EMPTY }

MULTILINESTRING = ${ ^"MULTILINESTRING" ~ ZM ~ (PCOORDLISTLIST | EMPTYSET) }
MULTIPOLYGON    = ${ ^"MULTIPOLYGON" ~ ZM ~ (PCOORDLISTLISTLIST | EMPTYSET) }

GEOMETRY_SINGLE = ${ WHITESPACE* ~ (POINT | LINESTRING | POLYGON | MULTIPOINT | MULTILINESTRING | MULTIPOLYGON) ~ WHITESPACE* }

// A member is any geometry, a collection included. WKT allows the nesting, and matching it here is
// what keeps `GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(...), POINT(...))` from falling through to the
// `Function` production and becoming an operation named `GEOMETRYCOLLECTION`. The cql2-json schema
// admits only the six non-collection types as members, so `parse_expr` rejects a nested one with a
// message that says so — an error the author can act on, rather than a silent reinterpretation.
GEOMETRY_MEMBER     = ${ GEOMETRY_SINGLE | GEOMETRY_COLLECTION }
GEOMETRY_COLLECTION = ${ WHITESPACE* ~ ^"GEOMETRYCOLLECTION" ~ ZM ~ (GEOMETRY_MEMBERS | EMPTYSET) ~ WHITESPACE* }
GEOMETRY_MEMBERS    = _{ LPAREN ~ GEOMETRY_MEMBER ~ (COMMADELIM ~ GEOMETRY_MEMBER)* ~ RPAREN }

GEOMETRY = ${ GEOMETRY_SINGLE | GEOMETRY_COLLECTION }

// A keyword only ends where a name would: `notes` is a property, not `not` applied to `es`.
NameChar = _{ ALPHABETIC | NUMBER | UNDERSCORE | PERIOD | COLON }

IdentifierInner = _{
    ALPHABETIC ~ NameChar*
}

IdentifierQuoted = { DQUOTE ~ (DQUOTE ~ DQUOTE | !DQUOTE ~ ANY)* ~ DQUOTE }

Identifier = @{ IdentifierInner | IdentifierQuoted }

NotFlag                   = @{ ^"not" ~ !NameChar }

// Anchored so that input the grammar cannot fully consume is an error. Matching `Expr` alone lets
// pest stop at the last complete atom and report success, discarding the remainder.
ExprRoot                  = _{ SOI ~ Expr ~ EOI }
Expr                      =  { (Negative* ~ ExprAtomValue ~ (ExprInfixOp ~ Negative* ~ ExprAtomValue)*) }
ExprInfixOp = _{ ArithInfixOp | CmpInfixOp | And | Or }

// `BETWEEN` is the three-operand `isBetweenPredicate` rather than an infix operator, because the
// `AND` inside it delimits the bounds rather than being the boolean connective.
//
// A bound is any comparable expression: a number, a string, a date or timestamp, a property, a
// function call, an arithmetic expression, or a parenthesised one. The spec's numeric requirement
// (Requirement 6 -- functions and properties SHALL evaluate to a numericLiteral) is semantic, so
// operands of other types parse here and are left to the validator.
//
// The one thing a bound cannot contain is a bare `AND` or `OR`, which is what makes
// `x BETWEEN 1 AND 2 AND y = 3` unambiguous -- the second `AND` can only be the connective.
BetweenOperand =  { Negative* ~ AtomicExpr ~ (ArithInfixOp ~ Negative* ~ AtomicExpr)* }
BetweenPostfix =  { NotFlag? ~ ^"between" ~ BetweenOperand ~ ^"and" ~ BetweenOperand }
And                       =  { ^"and" }
Or                        =  { ^"or" }
ArithInfixOp              = _{ Add | Subtract | Multiply | Divide | Modulo | Power}
Add                       =  { "+" }
Subtract                  =  { "-" }
Multiply                  =  { "*" }
Divide                    =  { "/" | ^"div" }
Modulo                    =  { "%" }
Power                    =  { "^" }
Negative                   = { "-" }
// `IS NULL` is the postfix `IsNullPostfix` predicate, matched as part of `ExprAtomValue` before any
// infix operator is looked for.
CmpInfixOp                = _{ NotEq | GtEq | Gt | LtEq | Lt | Eq | In | Like }
Eq                        =  { "=" }
Gt                        =  { ">" }
GtEq                      =  { ">=" }
Lt                        =  { "<" }
LtEq                      =  { "<=" }
Like                      =  { NotFlag? ~ ^"like" }
NotEq                     =  { "<>" | "!=" }
In                        =  { NotFlag? ~ ^"in" }
ExprAtomValue             = _{ UnaryNot* ~ AtomicExpr ~ (IsNullPostfix | BetweenPostfix)? }
UnaryNot                  = @{ NotFlag }
IsNullPostfix             =  { ^"is" ~ NotFlag? ~ ^"null" }
AtomicExpr                = _{ Literal | Function | Identifier | ExpressionInParentheses | Array }
// `DECIMAL` matches an integer with no fractional part, so it subsumes a bare integer alternative:
// anything `Integer` would match here, `DECIMAL` already matched.
Literal                   = _{ GEOMETRY | True | False | Null | Double | DECIMAL | SingleQuotedString }
True                      = @{ ^"true" ~ !NameChar }
False                     = @{ ^"false" ~ !NameChar }
Null                      = @{ ^"null" ~ !NameChar }
DECIMAL                   = @{ Integer ~ ("." ~ ASCII_DIGIT*)? }
Double                    = @{ Integer ~ ("." ~ ASCII_DIGIT*)? ~ (^"e" ~ Integer) }
Integer                   = @{ ("+" | "-")? ~ ASCII_DIGIT+ }
SingleQuotedString        = @{ "'" ~ ("''" | (!"'" ~ ANY))* ~ "'" }
FunctionArgs              = _{ (Expr ~ ("," ~ Expr)*)? }
Function                  =  { Identifier ~ LPAREN ~ FunctionArgs ~ RPAREN }
ExpressionInParentheses   =  { "(" ~ Expr ~ ")" }
Array = { "(" ~ Expr ~ ("," ~ Expr)* ~ ")" }