Skip to content

Instantly share code, notes, and snippets.

@lxfontes
Last active August 29, 2015 14:23
Show Gist options
  • Select an option

  • Save lxfontes/c86971c9df6350e871f6 to your computer and use it in GitHub Desktop.

Select an option

Save lxfontes/c86971c9df6350e871f6 to your computer and use it in GitHub Desktop.
parslet example
require 'parslet'
require 'pp'
class QueryParser < Parslet::Parser
rule(:space) { match('\s').repeat(1) }
rule(:space?) { space.maybe }
rule(:lparen) { str('(') >> space? }
rule(:lparen?) { lparen.maybe }
rule(:rparen) { str(')') >> space? }
rule(:rparen?) { rparen.maybe }
rule(:quote) { str('"').repeat(1) | str('\'').repeat(1) }
rule(:number) { match('[0-9\.]').repeat(1).as(:number) >> space? }
rule(:string) { quote >> match['^\'"'].repeat(1).as(:string) >> quote >> space? }
rule(:boolean) { (str('true') | str('TRUE') | str('false') | str('FALSE')).repeat(1).as(:boolean) >> space? }
rule(:null) { (str('null') | str('NULL')).repeat(1).as(:null) >> space? }
rule(:list) { str('(') >> (rhs_basic >> str(',').maybe).repeat(1).as(:list) >> str(')') >> space? }
rule(:lhs) { match['a-z_A-Z'].repeat(1) >> space? }
rule(:rhs_basic) { number | boolean | null | string }
rule(:rhs) { (list | rhs_basic) >> space? }
rule(:ne) { str('!=').repeat(1) }
rule(:eq) { str('=').repeat(1) }
rule(:lt) { str('<').repeat(1) }
rule(:lteq) { str('<=').repeat(1) }
rule(:gt) { str('>').repeat(1) }
rule(:gteq) { str('>=').repeat(1) }
rule(:incl) { str('in').repeat(1) }
rule(:not_incl) { str('not in').repeat(1) }
rule(:operator) { (not_incl | incl | ne | eq | lteq | lt | gteq | gt).as(:op) >> space? }
rule(:agg_and) { str('and') | str('AND') }
rule(:agg_or) { str('or') | str('OR') }
rule(:agg) { (agg_and | agg_or).as(:agg) >> space? }
rule(:agg?) { agg.maybe }
rule(:operation) { (lhs.as(:left) >> operator >> rhs.as(:right)) }
rule(:expression) { (agg? >> operation).repeat(1).as(:exp) }
rule(:body) { (lparen? >> expression >> rparen? >> agg?).repeat(1).as(:body) }
root :body
end
class Query < Struct.new(:explist)
def to_s
explist.map(&:to_s).join
end
end
class QueryExpressionList < Struct.new(:explist, :agg)
def agg
self[:agg].to_s.downcase
end
def to_s
"(#{explist.map(&:to_s).join}) #{agg}"
end
end
class QueryExpression < Struct.new(:left, :op, :right, :agg)
def right_type
unwrap_type(self[:right])
end
def agg
self[:agg].to_s.downcase
end
def right_value
unwrap_value(self[:right])
end
def unwrap_type(atom)
atom.keys.first
end
def unwrap_value(atom)
val = atom.values.first
case unwrap_type(atom)
when :number
val.to_f
when :list
val.map { |v| unwrap_value(v) }
else
val.to_s
end
end
def to_s
"#{agg} #{left}#{op}#{right} "
end
end
class ElasticQuery < Query
end
class ElasticExpressionList < QueryExpressionList
def agg
case super
when 'or'
'OR '
when 'and'
'AND '
end
end
end
class ElasticExpression < QueryExpression
def to_s
"#{agg} #{expr} "
end
def expr
case self[:op]
when '!='
return "_exists_:#{left}" if right_type == :null
"NOT #{left}:#{right}"
when '='
return "_missing_:#{left}" if right_type == :null
"#{left}:#{right}"
when '>'
"#{left}:>#{right}"
when '>='
"#{left}:>=#{right}"
when '<'
"#{left}:<#{right}"
when '<='
"#{left}:<=#{right}"
when 'in'
"#{left}: (#{right.join(' ')})"
else
"#{left}:#{right}"
end
end
def agg
case super
when 'or'
'OR'
when 'and'
'AND'
end
end
def right
case right_type
when :string
"\"#{right_value}\""
else
right_value
end
end
end
class QueryTransform
def initialize(query_klass, list_klass, expr_klass)
@query_klass = query_klass
@list_klass = list_klass
@expr_klass = expr_klass
@transform = Parslet::Transform.new do
rule(exp: subtree(:exp)) { builder.new_list(exp, '') }
rule(exp: subtree(:exp), agg: simple(:agg)) { builder.new_list(exp, agg) }
rule(body: subtree(:body)) { builder.new_query(body) }
rule(left: simple(:left),
op: simple(:op),
right: subtree(:right),
agg: simple(:agg)) { builder.new_expr(left, op, right, agg) }
rule(left: simple(:left),
op: simple(:op),
right: subtree(:right)) { builder.new_expr(left, op, right, '') }
end
end
def apply(parsed)
@transform.apply(parsed, builder: self)
end
def new_query(*args)
@query_klass.new(*args)
end
def new_list(*args)
@list_klass.new(*args)
end
def new_expr(*args)
@expr_klass.new(*args)
end
end
def time_method(method=nil, *args)
beginning_time = Time.now
if block_given?
yield
else
self.send(method, args)
end
end_time = Time.now
puts "Time elapsed #{(end_time - beginning_time)*1000} milliseconds"
end
parser = QueryParser.new
transf = QueryTransform.new(ElasticQuery, ElasticExpressionList, ElasticExpression)
queries = [
"event_name='app' and event_type='z'",
"event_name='app' AND event_type='z'",
"_key='logs' and event_name='status_update' and (event_type='package_manager')",
"(_key='logs' and event_name='status_update' ) and (event_type='package_manager') and user_id != null",
"(_key='logs' and event_name='status_update' ) and (event_type='package_manager') and user_id = null",
"_key='logs' and event_name='open' and (event_type<=4)",
"_key='logs' and event_name='open' and (event_type>4.4)",
"_key='logs' and event_name='open' and (event_type in (1,2,3))"
].each do |q|
time_method do
parsed = parser.parse(q)
# pp parsed
ast = transf.apply(parsed)
puts(ast.to_s)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment