1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383
|
class Sequel::Dataset
# The Sequelizer module includes methods for translating Ruby expressions
# into SQL expressions, making it possible to specify dataset filters using
# blocks, e.g.:
#
# DB[:items].filter {:price < 100}
# DB[:items].filter {:category == 'ruby' && :date < Date.today - 7}
#
# Block filters can refer to literals, variables, constants, arguments,
# instance variables or anything else in order to create parameterized
# queries. Block filters can also refer to other dataset objects as
# sub-queries. Block filters are pretty much limitless!
#
# Block filters are based on ParseTree. If you do not have the ParseTree
# gem installed, block filters will raise an error.
#
# To enable full block filter support make sure you have both ParseTree and
# Ruby2Ruby installed:
#
# sudo gem install parsetree
# sudo gem install ruby2ruby
module Sequelizer
private
# Formats an comparison expression involving a left value and a right
# value. Comparison expressions differ according to the class of the right
# value. The stock implementation supports Range (inclusive and exclusive),
# Array (as a list of values to compare against), Dataset (as a subquery to
# compare against), or a regular value.
#
# dataset.compare_expr('id', 1..20) #=>
# "(id >= 1 AND id <= 20)"
# dataset.compare_expr('id', [3,6,10]) #=>
# "(id IN (3, 6, 10))"
# dataset.compare_expr('id', DB[:items].select(:id)) #=>
# "(id IN (SELECT id FROM items))"
# dataset.compare_expr('id', nil) #=>
# "(id IS NULL)"
# dataset.compare_expr('id', 3) #=>
# "(id = 3)"
def compare_expr(l, r)
case r
when Range
r.exclude_end? ? \
"(#{literal(l)} >= #{literal(r.begin)} AND #{literal(l)} < #{literal(r.end)})" : \
"(#{literal(l)} >= #{literal(r.begin)} AND #{literal(l)} <= #{literal(r.end)})"
when Array
"(#{literal(l)} IN (#{literal(r)}))"
when Sequel::Dataset
"(#{literal(l)} IN (#{r.sql}))"
when NilClass
"(#{literal(l)} IS NULL)"
when Regexp
collate_match_expr(l, r)
else
"(#{literal(l)} = #{literal(r)})"
end
end
# Formats a string matching expression with support for multiple choices.
# For more information see #match_expr.
def collate_match_expr(l, r)
if r.is_a?(Array)
"(#{r.map {|i| match_expr(l, i)}.join(' OR ')})"
else
match_expr(l, r)
end
end
# Formats a string matching expression. The stock implementation supports
# matching against strings only using the LIKE operator. Specific adapters
# can override this method to provide support for regular expressions.
def match_expr(l, r)
case r
when String
"(#{literal(l)} LIKE #{literal(r)})"
else
raise Sequel::Error, "Unsupported match pattern class (#{r.class})."
end
end
# Evaluates a method call. This method is used to evaluate Ruby expressions
# referring to indirect values, e.g.:
#
# dataset.filter {:category => category.to_s}
# dataset.filter {:x > y[0..3]}
#
# This method depends on the Ruby2Ruby gem. If you do not have Ruby2Ruby
# installed, this method will raise an error.
def ext_expr(e, b, opts)
eval(RubyToRuby.new.process(e), b)
end
# Translates a method call parse-tree to SQL expression. The following
# operators are recognized and translated to SQL expressions: >, <, >=, <=,
# ==, =~, +, -, *, /, %:
#
# :x == 1 #=> "(x = 1)"
# (:x + 100) < 200 #=> "((x + 100) < 200)"
#
# The in, in?, nil and nil? method calls are intercepted and passed to
# #compare_expr.
#
# :x.in [1, 2, 3] #=> "(x IN (1, 2, 3))"
# :x.in?(DB[:y].select(:z)) #=> "(x IN (SELECT z FROM y))"
# :x.nil? #=> "(x IS NULL)"
#
# The like and like? method calls are intercepted and passed to #match_expr.
#
# :x.like? 'ABC%' #=> "(x LIKE 'ABC%')"
#
# The method also supports SQL functions by invoking Symbol#[]:
#
# :avg[:x] #=> "avg(x)"
# :substring[:x, 5] #=> "substring(x, 5)"
#
# All other method calls are evaulated as normal Ruby code.
def call_expr(e, b, opts)
case op = e[2]
when :>, :<, :>=, :<=
l = eval_expr(e[1], b, opts)
r = eval_expr(e[3][1], b, opts)
if l.is_one_of?(Symbol, Sequel::LiteralString, Sequel::SQL::Expression) || \
r.is_one_of?(Symbol, Sequel::LiteralString, Sequel::SQL::Expression)
"(#{literal(l)} #{op} #{literal(r)})"
else
ext_expr(e, b, opts)
end
when :==
l = eval_expr(e[1], b, opts)
r = eval_expr(e[3][1], b, opts)
compare_expr(l, r)
when :=~
l = eval_expr(e[1], b, opts)
r = eval_expr(e[3][1], b, opts)
collate_match_expr(l, r)
when :+, :-, :*, :%, :/
l = eval_expr(e[1], b, opts)
r = eval_expr(e[3][1], b, opts)
if l.is_one_of?(Symbol, Sequel::LiteralString, Sequel::SQL::Expression) || \
r.is_one_of?(Symbol, Sequel::LiteralString, Sequel::SQL::Expression)
"(#{literal(l)} #{op} #{literal(r)})".lit
else
ext_expr(e, b, opts)
end
when :<<
l = eval_expr(e[1], b, opts)
r = eval_expr(e[3][1], b, opts)
"#{literal(l)} = #{literal(r)}".lit
when :|
l = eval_expr(e[1], b, opts)
r = eval_expr(e[3][1], b, opts)
if l.is_one_of?(Symbol, Sequel::SQL::Subscript)
l|r
else
ext_expr(e, b, opts)
end
when :in, :in?
# in/in? operators are supported using two forms:
# :x.in([1, 2, 3])
# :x.in(1, 2, 3) # variable arity
l = eval_expr(e[1], b, opts)
r = eval_expr((e[3].size == 2) ? e[3][1] : e[3], b, opts)
compare_expr(l, r)
when :nil, :nil?
l = eval_expr(e[1], b, opts)
compare_expr(l, nil)
when :like, :like?
l = eval_expr(e[1], b, opts)
r = eval_expr(e[3][1], b, opts)
collate_match_expr(l, r)
else
if (op == :[]) && (e[1][0] == :lit) && (Symbol === e[1][1])
# SQL Functions, e.g.: :sum[:x]
if e[3]
e[1][1][*eval_expr(e[3], b, opts)]
else
e[1][1][]
end
else
# external code
ext_expr(e, b, opts)
end
end
end
def fcall_expr(e, b, opts) #:nodoc:
ext_expr(e, b, opts)
end
def vcall_expr(e, b, opts) #:nodoc:
eval(e[1].to_s, b)
end
def iter_expr(e, b, opts) #:nodoc:
if e[1][0] == :call && e[1][2] == :each
unfold_each_expr(e, b, opts)
elsif e[1] == [:fcall, :proc]
eval_expr(e[3], b, opts) # inline proc
else
ext_expr(e, b, opts) # method call with inline proc
end
end
def replace_dvars(a, values)
a.map do |i|
if i.is_a?(Array) && (i[0] == :dvar)
if v = values[i[1]]
value_to_parse_tree(v)
else
i
end
elsif Array === i
replace_dvars(i, values)
else
i
end
end
end
def value_to_parse_tree(value)
c = Class.new
c.class_eval("def m; #{value.inspect}; end")
ParseTree.translate(c, :m)[2][1][2]
end
def unfold_each_expr(e, b, opts) #:nodoc:
source = eval_expr(e[1][1], b, opts)
block_dvars = []
if e[2][0] == :dasgn_curr
block_dvars << e[2][1]
elsif e[2][0] == :masgn
e[2][1].each do |i|
if i.is_a?(Array) && i[0] == :dasgn_curr
block_dvars << i[1]
end
end
end
new_block = [:block]
source.each do |*dvars|
iter_values = (Array === dvars[0]) ? dvars[0] : dvars
values = block_dvars.inject({}) {|m, i| m[i] = iter_values.shift; m}
iter = replace_dvars(e[3], values)
new_block << iter
end
pt_expr(new_block, b, opts)
end
# Evaluates a parse-tree into an SQL expression.
def eval_expr(e, b, opts)
case e[0]
when :call # method call
call_expr(e, b, opts)
when :fcall
fcall_expr(e, b, opts)
when :vcall
vcall_expr(e, b, opts)
when :ivar, :cvar, :dvar, :const, :gvar # local ref
eval(e[1].to_s, b)
when :nth_ref
eval("$#{e[1]}", b)
when :lvar # local context
if e[1] == :block
sub_proc = eval(e[1].to_s, b)
sub_proc.to_sql(self)
else
eval(e[1].to_s, b)
end
when :lit, :str # literal
e[1]
when :dot2 # inclusive range
eval_expr(e[1], b, opts)..eval_expr(e[2], b, opts)
when :dot3 # exclusive range
eval_expr(e[1], b, opts)...eval_expr(e[2], b, opts)
when :colon2 # qualified constant ref
eval_expr(e[1], b, opts).const_get(e[2])
when :false
false
when :true
true
when :nil
nil
when :array
# array
e[1..-1].map {|i| eval_expr(i, b, opts)}
when :match3
# =~/!~ operator
l = eval_expr(e[2], b, opts)
r = eval_expr(e[1], b, opts)
compare_expr(l, r)
when :iter
iter_expr(e, b, opts)
when :dasgn, :dasgn_curr
# assignment
l = e[1]
r = eval_expr(e[2], b, opts)
raise Sequel::Error::InvalidExpression, "#{l} = #{r}. Did you mean :#{l} == #{r}?"
when :if
op, c, br1, br2 = *e
if ext_expr(c, b, opts)
eval_expr(br1, b, opts)
elsif br2
eval_expr(br2, b, opts)
end
when :dstr
ext_expr(e, b, opts)
else
raise Sequel::Error::InvalidExpression, "Invalid expression tree: #{e.inspect}"
end
end
JOIN_AND = " AND ".freeze
JOIN_COMMA = ", ".freeze
def pt_expr(e, b, opts = {}) #:nodoc:
case e[0]
when :not # negation: !x, (x != y), (x !~ y)
if (e[1][0] == :lit) && (Symbol === e[1][1])
# translate (!:x) into (x = 'f')
compare_expr(e[1][1], false)
else
"(NOT #{pt_expr(e[1], b, opts)})"
end
when :and # x && y
"(#{e[1..-1].map {|i| pt_expr(i, b, opts)}.join(JOIN_AND)})"
when :or # x || y
"(#{pt_expr(e[1], b, opts)} OR #{pt_expr(e[2], b, opts)})"
when :call, :vcall, :iter, :match3, :if # method calls, blocks
eval_expr(e, b, opts)
when :block # block of statements
if opts[:comma_separated]
"#{e[1..-1].map {|i| pt_expr(i, b, opts)}.join(JOIN_COMMA)}"
else
"(#{e[1..-1].map {|i| pt_expr(i, b, opts)}.join(JOIN_AND)})"
end
else # literals
if e == [:lvar, :block]
eval_expr(e, b, opts)
else
literal(eval_expr(e, b, opts))
end
end
end
end
end
class Proc
def to_sql(dataset, opts = {})
dataset.send(:pt_expr, to_sexp[2], self.binding, opts)
end
end
begin
require 'parse_tree'
rescue Exception
class Proc
def to_sql(*args)
raise Sequel::Error, "You must have the ParseTree gem installed in order to use block filters."
end
end
end
begin
require 'ruby2ruby'
rescue Exception
module Sequel::Dataset::Sequelizer
def ext_expr(*args)
raise Sequel::Error, "You must have the Ruby2Ruby gem installed in order to use this block filter."
end
end
end
class Proc
# replacement for Proc#to_sexp as defined in ruby2ruby.
# see also: http://rubyforge.org/tracker/index.php?func=detail&aid=18095&group_id=1513&atid=5921
# The ruby2ruby implementation leaks memory, so we fix it.
def to_sexp
block = self
c = Class.new {define_method(:m, &block)}
ParseTree.translate(c, :m)[2]
end
end
|