Skip to content

Instantly share code, notes, and snippets.

@bigfleet
Last active October 12, 2018 06:15
Show Gist options
  • Save bigfleet/4625050 to your computer and use it in GitHub Desktop.
Save bigfleet/4625050 to your computer and use it in GitHub Desktop.
My citrus effort
grammar Querying
rule statement
(compound_predicate | predicate)
end
rule compound_predicate
(predicate space* operator_and_predicate){
Wither::CompoundPredicate.new(predicate.value,
operator_and_predicate.group_operator.value,
operator_and_predicate.predicate.value)
}
end
rule operator_and_predicate
(group_operator space* predicate)
end
rule predicate
(group | within_phrase | in_phrase | binary_phrase)
end
rule group
(lparen statement rparen){
statement.value
}
end
rule within_phrase
('within' space* number space* 'of' space* keyword){
Wither::WithinPhrase.new(number.value, keyword.value)
}
end
rule in_phrase
(keyword space* 'in' space* '[' value_list ']') {
Wither::InPhrase.new(keyword.value, value_list.value)
}
end
rule binary_phrase
(keyword space* binary_operator space* avalue){
Wither::BinaryPhrase.new(keyword.value, binary_operator.value, avalue.value)
}
end
rule term
(keyword | value)
end
rule value_list
(avalue ',' value_list | avalue)
end
rule avalue
(literal | number)
end
rule keyword
(unreserved 1*)
end
rule literal
('"'ALPHA 1*'"')
end
rule number
(DIGIT 1*)
end
## Lexical syntax
# unreserved = ALPHA / DIGIT / "-" / "." / "_"
rule unreserved
(ALPHA | "_")
end
# reserved = gen-delims / sub-delims
rule reserved
group_operator | unary_operator | grouper
end
rule group_operator
"&" | "|" | "-"
end
rule binary_operator
"<=" | ">=" | "=" | "~" | ">" | "<"
end
rule DIGIT
[0-9]
end
rule ALPHA
[a-zA-Z]
end
rule lparen '(' space* end
rule rparen ')' space* end
rule space [ \t\n\r] end
end
module Wither
class Engine
class << self
def activate
Citrus.require("#{::Rails.root}/config/wither.citrus")
end
def config(args={})
Wither::Config.instance(args)
end
def parse(filter)
parse_text(filter.text)
end
def parse_text(text)
Querying.parse(text).value
end
end
end
class Walker
attr_accessor :ast
def initialize(ast)
@ast = ast
end
def walk
@ast.keywords
end
end
class Config
attr_accessor :args
def initialize(args)
@args = HashWithIndifferentAccess.new(args)
end
def load
self
end
def self.keywords(ast)
ast.keywords
end
def self.instance(args)
Wither::Config.new(args).load
end
end
class Statement
attr_accessor :captures
def initialize(captures)
@captures = captures
end
end
class CompoundPredicate
attr_accessor :lhs, :operator, :rhs
def initialize(p1, o, p2)
@lhs, @operator, @rhs = p1, o, p2
end
def keywords
(@lhs.keywords + @rhs.keywords).sort.uniq
end
end
class BinaryPhrase
attr_accessor :keyword, :operator, :value
def initialize(k, o, v)
@keyword, @operator, @value = k,o,v
end
def keywords
[@keyword]
end
end
class InPhrase
attr_accessor :keyword, :value_list
def initialize(k, vl)
@keyword, @value_list = k,vl
end
def keywords
[@keyword]
end
end
class WithinPhrase
attr_accessor :magnitude, :origin
def initialize(m,o)
@magnitude, @origin = m,o
end
def keywords
[@origin]
end
end
class Filter
attr_accessor :text
def initialize(text)
@text = text
end
def parse
Wither::Engine.parse(self)
end
def to_sql
parse.to_sql
end
end
def self.filter(text)
parse(text).to_sql
end
def self.parse(text)
Wither::Filter.new(text).parse
end
end
require 'spec_helper'
describe Wither do
describe Wither::Engine do
def attempt_to_parse(text)
lambda{
Wither::Engine.parse_text(text)
}.should_not raise_error(Citrus::ParseError)
end
context "parsing" do
context "binary phrases" do
it "should parse > operators" do
attempt_to_parse("green_eggs > 1")
end
it "should parse < operators" do
attempt_to_parse("green_eggs < 1")
end
it "should parse >= operators" do
attempt_to_parse("green_eggs >= 1")
end
it "should parse <= operators" do
attempt_to_parse("green_eggs <= 1")
end
it "should parse = operators on numbers" do
attempt_to_parse("green_eggs = 2")
end
it "should parse = operators on alphas" do
attempt_to_parse("email_type = \"work\"")
end
it "should parse ~ operators" do
attempt_to_parse("note_text ~ \"taken\"")
end
end
context "specialty phrases" do
it "should parse proximity" do
attempt_to_parse("within 2 of user")
end
it "should parse single-term 'in' phrases" do
attempt_to_parse('state in ["KY"]')
end
it "should parse multi-term 'in' phrases" do
attempt_to_parse('state in ["KY","TN"]')
end
end
context "grouped phrases" do
it "should parse & operators" do
attempt_to_parse("corn > 500 & within 2 of origin")
end
it "should parse | operators" do
attempt_to_parse("corn > 500 | wheat > 500")
end
it "should parse - operators" do
attempt_to_parse('corn > 500 - status in ["purchased","pending"]')
end
end
context "precedent hints" do
it "should allow simple containing" do
attempt_to_parse("(corn > 500)")
end
it "should allow them first" do
attempt_to_parse("(corn > 500 & wheat > 500) | status = \"considering\"")
end
it "should allow them last" do
attempt_to_parse("status = \"considering\" | (corn > 500 & wheat > 500)")
end
it "should allow them grouped" do
attempt_to_parse("(corn > 500 -(phone_number_count > 2 - do_not_call = 1))")
end
end
pending "should consider starts|ends_with, exists, is_true, is_false"
end
context "keyword analysis" do
def keywords(text)
Wither::Engine.parse_text(text).keywords
end
context "for binary phrases" do
subject{ keywords("green_eggs > 1") }
it{ should have(1).thing }
it{ should include("green_eggs") }
end
context "for in phrases" do
subject{ keywords('state in ["KY"]') }
it{ should have(1).thing }
it{ should include("state") }
end
context "for proximity phrases" do
subject{ keywords("within 2 of origin") }
it{ should have(1).thing }
it{ should include("origin") }
end
context "for compound predicates" do
subject{ keywords("corn > 500 | wheat > 500") }
it{ should have(2).things }
it{ should include("corn") }
it{ should include("wheat") }
end
context "for groupings of compound predicates" do
subject{ keywords("status = \"considering\" | (corn > 500 & wheat > 500)") }
it{ should have(3).things }
it{ should include("status") }
it{ should include("corn") }
it{ should include("wheat") }
end
end
end
describe Wither::Config do
before(:each) do
@config = Wither::Config.new(WITHER_CONFIG)
end
end
describe Wither::Filter do
pending "should engage engine" do
target = "WITH corn as (select * from ag.entity_crops) select entity_id from corn where acres > 499"
Wither.filter("corn > 499").should == target
end
end
end
@bigfleet
Copy link
Author

I'd be open to https://gist.github.com/4625050#file-wither_spec-rb-L82 being anything, basically, but can't get a handle on anything lower than Statement no matter what's returned.

No matter what I put in the rule statement phrase, I never seem to get anything except statements, and can never end up invoking methods on anything more primitive in the language than that. All help appreciated.

@bigfleet
Copy link
Author

You can safely ignore the curly brace parts within the current grammar, I can eliminate them, or change them to raise "hell", and can never get them to invoke.

@bigfleet
Copy link
Author

The revision you are currently looking at works!

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