Skip to content

Instantly share code, notes, and snippets.

@snopoke

snopoke/ccql.py Secret

Last active February 24, 2022 09:48
Show Gist options
  • Save snopoke/b67352a911ccc96b8c1ef4080a27d04a to your computer and use it in GitHub Desktop.
Save snopoke/b67352a911ccc96b8c1ef4080a27d04a to your computer and use it in GitHub Desktop.
Grammar for CommCare Query Language
import dataclasses
from typing import List, Iterable
from lark import Lark, Transformer, v_args
ccql_grammar = r"""
?start: b_expr+
?b_expr: expression (BOP expression)*
?expression: c_expr
|group
|set_expr
|function
c_expr: (NAME|ancestor|set_expr) OP (value|function)
?group: "(" b_expr+ ")"
?set_expr: set "." set_ops
set_ops: function ("." function)*
?set: "{" b_expr "}"
ancestor: NAME ("/" NAME)+
function: NAME "(" [args] ")"
args: arg ("," arg)*
?arg: value|b_expr
?value: STRING|INT|FLOAT|NAME
BOP.1: "and"i|"or"i
NAME: LETTER ("_"|"-"|LETTER|DIGIT)*
SET_RESOLVER: "count"|"exists"
SET_JOIN: "subcase"|"parent"
OP: ">"|">="|"<"|"<="|"="|"!="
STRING: ("\""|"'") _STRING_ESC_INNER ("\""|"'")
%import common._STRING_ESC_INNER
%import common.FLOAT
%import common.INT
%import common.DIGIT
%import common.LETTER
%import common.WS
%ignore WS
"""
@dataclasses.dataclass
class BinaryExpression:
left: object
op: str
right: object
@dataclasses.dataclass
class SetExpression:
expr: BinaryExpression
ops: Iterable["Function"]
@dataclasses.dataclass
class Function:
name: str
args: list
@dataclasses.dataclass
class Ancestor:
lineage: Iterable[str]
field: str
def _value(self, token):
return token.value
def _values(self, *args):
return args
@v_args(inline=True)
class CCQLTransformer(Transformer):
INT = int
FLOAT = float
NAME = _value
OP = _value
STRING = _value
BOP = _value
FUNC = _value
args = _values
set_eval = _values
set_ops = _values
def b_expr(self, left, op, right):
return BinaryExpression(left, op, right)
def c_expr(self, left, op, right):
return BinaryExpression(left, op, right)
def function(self, name, args):
return Function(name, args)
def set_expr(self, expr, *ops):
return SetExpression(expr, ops)
def ancestor(self, *args):
return Ancestor(args[:-1], args[-1])
parser = Lark(ccql_grammar, start='start', parser='lalr')
tests = [
"age > 3",
"age = 3 and name = 'bob'",
"(age > 3 and age < 10) and region = 'chamonix'",
"dob > date('2018-01-03')",
"not(age > 10)",
"age < 3 or (age > 10 and not(danger = 1))",
"name = 'bob' and {age > 3 and name != 'bob'}.subcase('parent').exists()",
"parent/parent/age = 4"
]
for t in tests:
tree = parser.parse(t)
print(t)
# print(tree)
print(tree.pretty())
print(CCQLTransformer().transform(tree))
@snopoke
Copy link
Author

snopoke commented Feb 24, 2022

"name = 'bob' and {age > 3 and name != 'bob'}.subcase('parent').exists()"

BinaryExpression(
  left=BinaryExpression(left='name', op='=', right="'bob'"),
  op='and',
  right=SetExpression(
    expr=BinaryExpression(
      left=BinaryExpression(left='age', op='>', right=3),
      op='and',
      right=BinaryExpression(left='name', op='!=', right="'bob'")
    ),
    ops=((Function(name='subcase', args=("'parent'",)), Function(name='exists', args=None)),)
  )
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment