main_query = { SOI ~ pragma* ~ features? ~ query ~ EOI }
main_tag_expr = { SOI ~ tag_expr ~ EOI }
main_simple_expr = { SOI ~ simple_expr ~ EOI }
main_ident = @{ SOI ~ ident ~ EOI }
main_func_name = @{ SOI ~ func_name ~ EOI }
main_timestamp = @{ SOI ~ isodate ~ EOI }
// pragmas
pragma = ${ "PRAGMA" ~ WHITE_SPACE+ ~ feature_word ~ ( !NEWLINE ~ WHITE_SPACE )* ~ ( pragma_multi | pragma_single ) }
pragma_multi = _{ NEWLINE ~ pragma_multi_text ~ NEWLINE ~ "ENDPRAGMA" ~ NEWLINE }
pragma_multi_text = ${ ( !( NEWLINE ~ "ENDPRAGMA" ~ NEWLINE ) ~ ANY )* }
pragma_single = _{ ":=" ~ ( !NEWLINE ~ WHITE_SPACE )* ~ pragma_single_text ~ NEWLINE }
pragma_single_text = ${ ( !NEWLINE ~ ANY )* }
// queries
query = { "FROM" ~ ( tag_expr ~ query_order? | array ) ~ query_op* ~ "END"? }
query_order = { "ORDER" ~ order }
order = { "ASC" | "DESC" | "STREAM" }
query_op = _{ filter | select | aggregate | limit | binding }
filter = { "FILTER" ~ simple_expr }
select = { "SELECT" ~ spread? ~ simple_expr ~ ( "," ~ spread? ~ simple_expr )* }
aggregate = { "AGGREGATE" ~ simple_expr }
limit = { "LIMIT" ~ positive }
binding = { "LET" ~ ident ~ ":=" ~ simple_expr }
features = { "FEATURES(" ~ feature_word* ~ ")" }
feature_word = @{ ( ASCII_ALPHANUMERIC | "ø" )+ }
// strings
single_quoted = @{ "'" ~ ( !"'" ~ ANY | "''" )+ ~ "'" }
double_quoted = @{ "\"" ~ ( !"\"" ~ ANY | "\"\"" )+ ~ "\"" }
nonempty_string = { single_quoted | double_quoted }
empty_string = { "\"\"" | "''" }
interpolation = ${ "`" ~ ( !("`"|"{") ~ ANY | "{" ~ ( unicode | simple_expr ) ~ "}" )* ~ "`" }
string = { nonempty_string | empty_string }
// literals
null = { "NULL" }
bool = { "TRUE" | "FALSE" }
decimal = @{ "-"? ~ digit+ ~ ( "." ~ digit+ )? }
natural = @{ digit+ }
positive = @{ '1'..'9' ~ digit* }
digit = { '0'..'9' }
sign = { "+" | "-" }
hexdigit = { '0'..'9' | 'a'..'f' | 'A'..'F' }
unicode = @{ "U+" ~ hexdigit{1,6} }
event_key = { natural ~ ( "/" ~ stream_id )? }
// This currently permits { a:1 b:2 }, which I’d like to try; comma is available for disambiguation
object = { "{" ~ ( ( ident | "[" ~ natural ~ "]" | "[" ~ string ~ "]" | "[" ~ simple_expr ~ "]" ) ~ ":" ~ simple_expr ~ ","? )* ~ "}" }
array = { "[" ~ "]" | "[" ~ spread? ~ simple_expr ~ ( "," ~ spread? ~ simple_expr )* ~ "]" }
app_id = @{ app_id_char+ ~ ( "." ~ app_id_char+ )* }
app_id_char = { "-" | '0'..'9' | 'a'..'z' }
stream_id = @{ crypt_char{43} ~ "-" ~ natural }
crypt_char = { "." | "/" | 'a'..'z' | 'A'..'Z' | '0'..'9' }
// identifiers
ident = @{ "_" | &( LOWERCASE | UPPERCASE ~ LOWERCASE ) ~ ( ALPHABETIC | NUMBER | "_" )+ }
func_name = @{ UPPERCASE ~ LOWERCASE ~ ( ALPHABETIC | NUMBER | "_" )* }
var = { ident }
index = _{ "." ~ ident | "[" ~ natural ~ "]" | "[" ~ string ~ "]" | "[" ~ simple_expr ~ "]" }
// ISO8601 timestamp
year = { '0'..'9'{4} }
month = { ( "0" ~ '1'..'9' ) | ("1" ~ '0'..'2') }
day = { ( "0" ~ '1'..'9' ) | ( "1" ~ '0'..'9' ) | ( "2" ~ '0'..'9' ) | "30" | "31" }
hour = { ( '0'..'1' ~ '0'..'9' ) | ( "2" ~ '0' .. '3' ) }
minute = { ( '0'..'5' ~ '0'..'9' ) }
second = { ( '0'..'5' ~ '0'..'9' ) }
millisecond = { ( '0'..'9'{3} ) }
microsecond = { ( '0'..'9'{6} ) }
nanosecond = { ( '0'..'9'{9} ) }
isodate = ${ year ~ "-" ~ month ~ "-" ~ day ~ ("T" ~ hour ~ ":" ~ minute ~ (":" ~ second ~ ("." ~ (nanosecond|microsecond|millisecond))?)?)? ~ ( "Z" | sign ~ hour ~ ":" ~ minute ) }
// some duration ago
duration_ago = ${ natural ~ duration_unit ~ WHITE_SPACE ~ "ago" }
duration_unit = { "Y" | "M" | "W" | "D" | "h" | "m" | "s" }
// metadata
meta_key = { "KEY(" ~ ( ident | event_key ) ~ ")" }
meta_time = { "TIME(" ~ ( ident | isodate ) ~ ")" }
meta_tags = { "TAGS(" ~ ident ~ ")" }
meta_app = { "APP(" ~ ident ~ ")" }
meta = _{ meta_key | meta_time | meta_tags | meta_app }
// tags
tag_expr = { tag_atom ~ ( tag_op ~ tag_atom )* }
tag_atom = _{ tag | "(" ~ tag_expr ~ ")" | all_events | is_local | tag_from | tag_to | tag_app | tag_key | tag_time }
tag_op = _{ and | or }
tag = { nonempty_string | interpolation }
all_events = { "allEvents" }
is_local = { "isLocal" }
tag_from = { "from(" ~ ( isodate | event_key ) ~ ")" }
tag_to = { "to(" ~ ( isodate | event_key ) ~ ")" }
tag_app = ${ "appId(" ~ app_id ~ ")" }
tag_key = { "KEY" ~ tag_comp ~ event_key }
tag_time = { "TIME" ~ tag_comp ~ ( isodate | duration_ago ) }
tag_comp = _{ le | lt | ge | gt }
// expressions
simple_expr = !{ simple_prefix* ~ simple_atom ~ ( binary_op ~ simple_prefix* ~ simple_atom )* }
binary_op = _{ alternative | add | sub | mul | div | modulo | pow | and | or | xor | le | lt | ge | gt | eq | ne }
var_index = { var ~ index* }
expr_index = { "(" ~ simple_expr ~ ")" ~ index* }
func_call = { func_name ~ "(" ~ ( simple_expr ~ ("," ~ simple_expr)* )? ~ ")" }
simple_atom = _{ decimal | func_call | var_index | string | interpolation | expr_index | object | array | null | bool | simple_cases | aggr_op | sub_query | meta }
sub_query = { query }
simple_cases = { ( "CASE" ~ simple_expr ~ "=>" ~ simple_expr )+ ~ "ENDCASE" }
simple_prefix = _{ not }
// aggregations
aggr_op = { aggr_sum | aggr_prod | aggr_min | aggr_max | aggr_first | aggr_last }
aggr_sum = { "SUM(" ~ simple_expr ~ ")" }
aggr_prod = { "PRODUCT(" ~ simple_expr ~ ")" }
aggr_min = { "MIN(" ~ simple_expr ~ ")" }
aggr_max = { "MAX(" ~ simple_expr ~ ")" }
aggr_first = { "FIRST(" ~ simple_expr ~ ")" }
aggr_last = { "LAST(" ~ simple_expr ~ ")" }
// operators
add = { "+" }
sub = { "-" }
mul = { "*" | "×" }
div = { "/" | "÷" | "⁄" }
modulo = { "%" }
pow = { "^" }
and = { "&" | "∧" }
or = { "|" | "∨" }
not = { "!" | "¬" }
xor = { "~" | "⊻" }
lt = { "<" }
le = { "<=" | "≤" }
gt = { ">" }
ge = { ">=" | "≥" }
eq = { "=" }
ne = { "!=" | "≠" }
alternative = { "??" }
spread = { "..." }
WHITESPACE = _{ WHITE_SPACE }
COMMENT = _{ "--" ~ ( !("\n"|"\r") ~ ANY )* }