querybuilder 0.5.9 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +29 -25
- data/Manifest.txt +20 -9
- data/README.rdoc +73 -10
- data/Rakefile +62 -30
- data/lib/extconf.rb +3 -0
- data/lib/query_builder.rb +39 -898
- data/lib/query_builder/error.rb +7 -0
- data/lib/query_builder/info.rb +3 -0
- data/lib/query_builder/parser.rb +80 -0
- data/lib/query_builder/processor.rb +714 -0
- data/lib/query_builder/query.rb +273 -0
- data/lib/querybuilder_ext.c +1870 -0
- data/lib/querybuilder_ext.rl +418 -0
- data/lib/querybuilder_rb.rb +1686 -0
- data/lib/querybuilder_rb.rl +214 -0
- data/lib/querybuilder_syntax.rl +47 -0
- data/old_QueryBuilder.rb +946 -0
- data/querybuilder.gemspec +42 -15
- data/tasks/build.rake +20 -0
- data/test/dummy_test.rb +21 -0
- data/test/mock/custom_queries/test.yml +5 -4
- data/test/mock/dummy.rb +9 -0
- data/test/mock/dummy_processor.rb +160 -0
- data/test/mock/queries/bar.yml +1 -1
- data/test/mock/queries/foo.yml +2 -2
- data/test/mock/user_processor.rb +34 -0
- data/test/query_test.rb +38 -0
- data/test/querybuilder/basic.yml +91 -0
- data/test/{query_builder → querybuilder}/custom.yml +11 -11
- data/test/querybuilder/errors.yml +32 -0
- data/test/querybuilder/filters.yml +115 -0
- data/test/querybuilder/group.yml +7 -0
- data/test/querybuilder/joins.yml +37 -0
- data/test/querybuilder/mixed.yml +18 -0
- data/test/querybuilder/rubyless.yml +15 -0
- data/test/querybuilder_test.rb +111 -0
- data/test/test_helper.rb +8 -3
- metadata +66 -19
- data/test/mock/dummy_query.rb +0 -114
- data/test/mock/user_query.rb +0 -55
- data/test/query_builder/basic.yml +0 -60
- data/test/query_builder/errors.yml +0 -50
- data/test/query_builder/filters.yml +0 -43
- data/test/query_builder/joins.yml +0 -25
- data/test/query_builder/mixed.yml +0 -12
- data/test/query_builder_test.rb +0 -36
@@ -0,0 +1,80 @@
|
|
1
|
+
module QueryBuilder
|
2
|
+
class Parser
|
3
|
+
class << self
|
4
|
+
# http://dev.mysql.com/doc/refman/5.1/en/operator-precedence.html
|
5
|
+
OP_PRECEDENCE = {
|
6
|
+
:function => 50,
|
7
|
+
:interval => 40,
|
8
|
+
:binary => 39, :collate => 39,
|
9
|
+
:"!" => 38,
|
10
|
+
:"@-" => 37, :"@~" => 37,
|
11
|
+
:"^" => 36,
|
12
|
+
:"*" => 35, :"/" => 35, :div => 35, :"%" => 35, :mod => 35,
|
13
|
+
:"-" => 34, :"+" => 34,
|
14
|
+
:"<<" => 33, :">>" => 33,
|
15
|
+
:"&" => 32,
|
16
|
+
:"|" => 31,
|
17
|
+
:"=" => 30, :"<=>" => 30, :">=" => 30, :">" => 30, :"<=" => 30, :"<" => 30, :"<>" => 30, :"!=" => 30, :is => 30, :like => 30, :regexp => 30, :in => 30,
|
18
|
+
:lt => 30, :le => 30, :eq => 30, :ne => 30, :ge => 30, :gt => 30, :match => 30,
|
19
|
+
:between => 29, :case => 29, :when => 29, :then => 29, :else => 29,
|
20
|
+
:not => 28,
|
21
|
+
:"&&" => 27, :and => 27,
|
22
|
+
:xor => 26,
|
23
|
+
:"||" => 25, :or => 25,
|
24
|
+
:":=" => 24,
|
25
|
+
:relation => 13, :filter => 13,
|
26
|
+
:scope => 12,
|
27
|
+
:from => 11, # this is not the same as SQL 'FROM', it's "icons from friends"
|
28
|
+
:asc => 10, :desc => 10,
|
29
|
+
:clause => 5,
|
30
|
+
:clause_and => 4,
|
31
|
+
:clause_or => 3,
|
32
|
+
:offset => 2, :paginate => 2, :limit => 2, :order => 2, :group => 2,
|
33
|
+
:query => 1,
|
34
|
+
:par_close => 0, :clause_par_close => 0,
|
35
|
+
:par => -1, :clause_par => -1
|
36
|
+
}
|
37
|
+
# group < from < filter < relation < scope
|
38
|
+
|
39
|
+
# Transform the stack to wrap the last element with an operator:
|
40
|
+
# [a, b, c] ==> [a, b, [op, c, d]]
|
41
|
+
def apply_op(stack, op, change_last = true)
|
42
|
+
pop_stack(stack, op)
|
43
|
+
last = stack.last
|
44
|
+
change_elem = last.last
|
45
|
+
last[-1] = [op.to_sym, change_elem]
|
46
|
+
if change_last
|
47
|
+
stack.push last[-1]
|
48
|
+
end
|
49
|
+
stack.last
|
50
|
+
end
|
51
|
+
|
52
|
+
def insert(stack, arg)
|
53
|
+
# insert [:relation, "..."]
|
54
|
+
# stack: [[:query]] --> [[:query, [:relation, "..."]], [:relation, "..."]]
|
55
|
+
pop_stack(stack, arg.first)
|
56
|
+
last = stack.last
|
57
|
+
last << arg
|
58
|
+
stack.push last.last
|
59
|
+
stack.last
|
60
|
+
end
|
61
|
+
|
62
|
+
def pop_stack(stack, op)
|
63
|
+
# debug_stack(stack, op)
|
64
|
+
stack_op = stack.last.first
|
65
|
+
while OP_PRECEDENCE[op] <= OP_PRECEDENCE[stack_op]
|
66
|
+
stack.pop
|
67
|
+
stack_op = stack.last.first
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def debug_stack(stack, msg = '')
|
72
|
+
puts "======= #{msg} ======="
|
73
|
+
stack.reverse_each do |s|
|
74
|
+
puts s.inspect
|
75
|
+
end
|
76
|
+
puts "======================"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,714 @@
|
|
1
|
+
require 'rubyless'
|
2
|
+
|
3
|
+
module QueryBuilder
|
4
|
+
class Processor
|
5
|
+
attr_reader :context, :query, :sxp, :ancestor
|
6
|
+
|
7
|
+
class << self
|
8
|
+
# class variable
|
9
|
+
attr_accessor :main_table, :main_class, :custom_queries
|
10
|
+
attr_accessor :defaults
|
11
|
+
attr_accessor :before_process_callbacks, :after_process_callbacks
|
12
|
+
|
13
|
+
def set_main_table(table_name)
|
14
|
+
self.main_table = table_name
|
15
|
+
end
|
16
|
+
|
17
|
+
def set_main_class(klass)
|
18
|
+
self.main_class = klass
|
19
|
+
end
|
20
|
+
|
21
|
+
def set_default(key, value)
|
22
|
+
self.defaults ||= {}
|
23
|
+
self.defaults[key] = value
|
24
|
+
end
|
25
|
+
|
26
|
+
def before_process(callback)
|
27
|
+
(self.before_process_callbacks ||= []) << callback
|
28
|
+
end
|
29
|
+
|
30
|
+
def after_process(callback)
|
31
|
+
(self.after_process_callbacks ||= []) << callback
|
32
|
+
end
|
33
|
+
|
34
|
+
def insert_bind(str)
|
35
|
+
"[[#{str}]]"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Load prepared SQL definitions from a set of directories. If the file does not contain "group" or "groups" keys,
|
39
|
+
# the filename is used as group.
|
40
|
+
#
|
41
|
+
# ==== Parameters
|
42
|
+
# query<String>:: Path to list of custom queries yaml files.
|
43
|
+
#
|
44
|
+
# ==== Examples
|
45
|
+
# DummyQuery.load_custom_queries("/path/to/some/*/directory")
|
46
|
+
#
|
47
|
+
# The format of a custom query definition is:
|
48
|
+
#
|
49
|
+
# groups:
|
50
|
+
# - test.host
|
51
|
+
# DummyQuery: # QueryBuilder class
|
52
|
+
# abc: # query's relation name
|
53
|
+
# select: # selected fields
|
54
|
+
# - 'a'
|
55
|
+
# - '34 AS number'
|
56
|
+
# - 'c'
|
57
|
+
# tables: # tables used
|
58
|
+
# - 'test'
|
59
|
+
# join_tables: # joins
|
60
|
+
# test:
|
61
|
+
# - LEFT JOIN other ON other.test_id = test.id
|
62
|
+
# where: # filters
|
63
|
+
# - '1'
|
64
|
+
# - '2'
|
65
|
+
# - '3'
|
66
|
+
# order: 'a DESC' # order clause
|
67
|
+
#
|
68
|
+
# Once loaded, this 'custom query' can be used in a query like:
|
69
|
+
# "images from abc where a > 54"
|
70
|
+
def load_custom_queries(directories)
|
71
|
+
klass = nil
|
72
|
+
self.custom_queries ||= {}
|
73
|
+
Dir.glob(directories).each do |dir|
|
74
|
+
if File.directory?(dir)
|
75
|
+
Dir.foreach(dir) do |file|
|
76
|
+
next unless file =~ /(.+).yml$/
|
77
|
+
custom_query_groups = $1
|
78
|
+
definitions = YAML::load(File.read(File.join(dir,file)))
|
79
|
+
custom_query_groups = [definitions.delete('groups') || definitions.delete('group') || custom_query_groups].flatten
|
80
|
+
definitions.each do |klass,v|
|
81
|
+
begin
|
82
|
+
klass = QueryBuilder.resolve_const(klass)
|
83
|
+
klass = klass.query_compiler if klass.respond_to?(:query_compiler)
|
84
|
+
raise ArgumentError.new("Invalid Processor class '#{klass}' in '#{file}' custom query. Should be a descendant of QueryBuilder::Processor.") unless klass.ancestors.include?(Processor)
|
85
|
+
rescue NameError
|
86
|
+
raise ArgumentError.new("Unknown Processor class '#{klass}' in '#{file}' custom query.")
|
87
|
+
end
|
88
|
+
custom_queries[klass] ||= {}
|
89
|
+
custom_query_groups.each do |custom_query_group|
|
90
|
+
custom_queries[klass][custom_query_group] ||= {}
|
91
|
+
klass_queries = custom_queries[klass][custom_query_group]
|
92
|
+
v.each do |k,v|
|
93
|
+
klass_queries[k] = v
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
rescue NameError => err
|
101
|
+
raise ArgumentError.new("Invalid Processor class (#{klass})")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
OPERATOR_TO_METHOD = {
|
106
|
+
:"!" => :not,
|
107
|
+
:"@-" => :change_sign, :"@~" => :invert_bits,
|
108
|
+
:"^" => :bitwise_xor,
|
109
|
+
:* => :times, :/ => :division, :DIV => :integer_division, :% => :modulo, :mod => :modulo,
|
110
|
+
:- => :minus, :+ => :addition,
|
111
|
+
:<< => :left_shift, :>> => :right_shift,
|
112
|
+
:& => :bitwise_and,
|
113
|
+
:| => :bitwise_or,
|
114
|
+
:eq => :equal, :ge => :greater_or_equal,
|
115
|
+
:gt => :greater, :le => :less_or_equal, :lt => :less, :ne => :not_equal,
|
116
|
+
:"=" => :equal, :"<=>" => :null_safe_equal, :>= => :greater_or_equal,
|
117
|
+
:> => :greater, :<= => :less_or_equal, :< => :less, :"<>" => :not_equal, :"!=" => :not_equal,
|
118
|
+
:"&&" => :and,
|
119
|
+
:"||" => :or,
|
120
|
+
:":=" => :assign
|
121
|
+
}
|
122
|
+
|
123
|
+
METHOD_TO_OPERATOR = {
|
124
|
+
:greater => :>, :greater_or_equal => :>=,
|
125
|
+
:equal => :'=',
|
126
|
+
:less_or_equal => :<=, :less => :<,
|
127
|
+
:not_equal => :'<>'
|
128
|
+
}
|
129
|
+
|
130
|
+
def initialize(source, opts = {})
|
131
|
+
@opts = opts
|
132
|
+
@rubyless_helper = @opts[:rubyless_helper]
|
133
|
+
if source.kind_of?(Processor)
|
134
|
+
# experimental: class change
|
135
|
+
@context = source.context
|
136
|
+
@query = source.query
|
137
|
+
@sxp = source.sxp
|
138
|
+
@ancestor = source # used during class change to return back to previous 'this'
|
139
|
+
elsif source.kind_of?(String)
|
140
|
+
@sxp = Parser.parse(source)
|
141
|
+
|
142
|
+
@context = opts.merge(:first => true, :last => true)
|
143
|
+
@query = Query.new(self.class)
|
144
|
+
before_process
|
145
|
+
|
146
|
+
process_all
|
147
|
+
|
148
|
+
after_process
|
149
|
+
|
150
|
+
if limit = @opts[:limit]
|
151
|
+
@query.limit = " LIMIT #{limit}"
|
152
|
+
end
|
153
|
+
else
|
154
|
+
raise "Cannot use #{source.class} as source: expected a String or QueryBuilder::Processor"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
protected
|
159
|
+
def before_process
|
160
|
+
(self.class.before_process_callbacks || []).each do |callback|
|
161
|
+
send(callback)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def process_all
|
166
|
+
if @sxp == [:query]
|
167
|
+
# empty query
|
168
|
+
process([:query, [:relation, main_table]])
|
169
|
+
else
|
170
|
+
process(@sxp)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def after_process
|
175
|
+
(self.class.after_process_callbacks || []).each do |callback|
|
176
|
+
send(callback)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Returns the currently running processor (can be different if the class changed).
|
181
|
+
def this
|
182
|
+
@this || self
|
183
|
+
end
|
184
|
+
|
185
|
+
def this=(processor)
|
186
|
+
@this = processor
|
187
|
+
end
|
188
|
+
|
189
|
+
def default
|
190
|
+
self.class.defaults
|
191
|
+
end
|
192
|
+
|
193
|
+
def process(sxp)
|
194
|
+
return sxp if sxp.kind_of?(String)
|
195
|
+
op = OPERATOR_TO_METHOD[sxp.first] || sxp.first
|
196
|
+
method = "process_#{op}"
|
197
|
+
if this.respond_to?(method)
|
198
|
+
this.send(method, *sxp[1..-1])
|
199
|
+
elsif sxp.size == 3
|
200
|
+
op = METHOD_TO_OPERATOR[op] || sxp.first
|
201
|
+
this.process_op(*([op] + sxp[1..-1]))
|
202
|
+
else
|
203
|
+
raise QueryBuilder::SyntaxError.new("Method '#{method}' to handle #{sxp.first.inspect} not implemented.")
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def process_clause_or(clause1, clause2)
|
208
|
+
clauses = [clause2]
|
209
|
+
|
210
|
+
while true
|
211
|
+
if clause1.first == :clause_or
|
212
|
+
clauses << clause1[2]
|
213
|
+
clause1 = clause1[1]
|
214
|
+
else
|
215
|
+
clauses << clause1
|
216
|
+
break
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
clauses.map! do |clause|
|
221
|
+
process(clause)
|
222
|
+
query = @query
|
223
|
+
@query = Query.new(this.class)
|
224
|
+
query
|
225
|
+
end
|
226
|
+
|
227
|
+
merge_queries(clauses.reverse)
|
228
|
+
end
|
229
|
+
|
230
|
+
def merge_queries(queries)
|
231
|
+
@query = queries.first
|
232
|
+
@query.tables = queries.inject([]) {|list, query| list + query.tables}.uniq
|
233
|
+
filters = queries.map do |query|
|
234
|
+
query.where.size > 1 ? "(#{query.filter})" : query.where.first
|
235
|
+
end
|
236
|
+
|
237
|
+
@query.where = ["(#{filters.join(' OR ')})"]
|
238
|
+
distinct!
|
239
|
+
end
|
240
|
+
|
241
|
+
def process_clause_par(content)
|
242
|
+
process(content)
|
243
|
+
end
|
244
|
+
|
245
|
+
# A query can be made of many clauses:
|
246
|
+
# [letters from friends] or [images in project]
|
247
|
+
def process_query(args)
|
248
|
+
this.process(args)
|
249
|
+
if @query.order.nil? && order = this.default[:order]
|
250
|
+
sxp = Parser.parse("foo order by #{order}")
|
251
|
+
order = sxp[1]
|
252
|
+
order[1] = [:void] # replace [:relation, "foo"] by [:void]
|
253
|
+
this.process(order)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
# Parse sub-query from right to left
|
258
|
+
def process_from(query, sub_query)
|
259
|
+
distinct!
|
260
|
+
this.with(:first => false) do
|
261
|
+
this.process(sub_query)
|
262
|
+
end
|
263
|
+
this.with(:last => false) do
|
264
|
+
this.process(query)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# Returns true if the query is the one producing the final result.
|
269
|
+
def first?
|
270
|
+
this.context[:first]
|
271
|
+
end
|
272
|
+
|
273
|
+
# Returns true if the query is the closest to Ruby objects.
|
274
|
+
def last?
|
275
|
+
this.context[:last]
|
276
|
+
end
|
277
|
+
=begin
|
278
|
+
(3) (2) (1)
|
279
|
+
letters from friends in project from foo
|
280
|
+
|
281
|
+
[:from,
|
282
|
+
[:from,
|
283
|
+
[:relation, "letters"], (3)
|
284
|
+
[:scope,
|
285
|
+
[:relation, "friends"], (2)
|
286
|
+
"project"
|
287
|
+
]
|
288
|
+
],
|
289
|
+
[:relation, "foo"] (1)
|
290
|
+
]
|
291
|
+
|
292
|
+
1. relation "foo"
|
293
|
+
scope: nil ---> nothing to do
|
294
|
+
where: obj1.id = lk1.src_id AND lk1.rel_id = FOO AND lk1.trg_id = [[@node.id]]
|
295
|
+
2. relation "friends"
|
296
|
+
scope: "project"
|
297
|
+
where: obj2.project_id = obj1.project_id
|
298
|
+
where: obj3.id = lk2.src_id AND lk2.rel_id = FRIENDS AND lk2.trg_id = obj2.id
|
299
|
+
3. relation "letters"
|
300
|
+
scope: nil ---> parent
|
301
|
+
where: objects.parent_id = obj3.id
|
302
|
+
where: objects.kpath like 'NNL%'
|
303
|
+
|
304
|
+
In case (1) or (2), scope should be processed before the relation. In case (3),
|
305
|
+
it should be processed after.
|
306
|
+
=end
|
307
|
+
def process_relation(relation)
|
308
|
+
if custom_query(relation)
|
309
|
+
# load custom query
|
310
|
+
elsif class_relation(relation)
|
311
|
+
# changed class
|
312
|
+
elsif (context[:scope_type] = :join) && join_relation(relation)
|
313
|
+
elsif (context[:scope_type] = :context) && context_relation(relation)
|
314
|
+
elsif (context[:scope_type] = :filter) && filter_relation(relation)
|
315
|
+
else
|
316
|
+
raise QueryBuilder::SyntaxError.new("Unknown relation '#{relation}'.")
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
# See if the relation name means that we need to change processor class.
|
321
|
+
def class_relation(relation)
|
322
|
+
nil
|
323
|
+
end
|
324
|
+
|
325
|
+
# Try to use the relation name as a join relation (relation involving another table).
|
326
|
+
def join_relation(relation)
|
327
|
+
nil
|
328
|
+
end
|
329
|
+
|
330
|
+
# Try to use the relation name as a contextual relation (context filtering like "parent", "project", etc).
|
331
|
+
def context_relation(relation)
|
332
|
+
nil
|
333
|
+
end
|
334
|
+
|
335
|
+
# Try to use the relation name as a filtering relation and use default scope for scoping (ex: "images", "notes").
|
336
|
+
def filter_relation(relation)
|
337
|
+
nil
|
338
|
+
end
|
339
|
+
|
340
|
+
def process_scope(relation, scope)
|
341
|
+
this.with(:scope => scope) do
|
342
|
+
this.process(relation)
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
def apply_scope(scope)
|
347
|
+
context[:processing] = :scope
|
348
|
+
if fields = scope_fields(scope)
|
349
|
+
add_filter("#{field_or_attr(fields[0])} = #{field_or_attr(fields[1], table(main_table, -1))}") if fields != []
|
350
|
+
else
|
351
|
+
raise QueryBuilder::SyntaxError.new("Invalid scope '#{scope}'.")
|
352
|
+
end
|
353
|
+
true
|
354
|
+
end
|
355
|
+
|
356
|
+
def field_or_attr(fld_name, table_alias = table)
|
357
|
+
if table_alias
|
358
|
+
this.with(:table_alias => table_alias) do
|
359
|
+
this.process_field(fld_name)
|
360
|
+
end
|
361
|
+
else
|
362
|
+
this.process_attr(fld_name)
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
def process_field(fld_name)
|
367
|
+
if fld = @query.attributes_alias[fld_name]
|
368
|
+
# use custom query alias value defined in select clause: 'custom_a AS validation'
|
369
|
+
processing_filter? ? "(#{fld})" : fld
|
370
|
+
else
|
371
|
+
raise QueryBuilder::SyntaxError.new("Unknown field '#{fld_name}'.")
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def process_integer(value)
|
376
|
+
value
|
377
|
+
end
|
378
|
+
|
379
|
+
# Used by SQL functions
|
380
|
+
def process_method(fld_name)
|
381
|
+
fld_name
|
382
|
+
end
|
383
|
+
|
384
|
+
def process_attr(fld_name)
|
385
|
+
if @rubyless_helper
|
386
|
+
insert_bind(RubyLess.translate(fld_name, @rubyless_helper))
|
387
|
+
else
|
388
|
+
insert_bind(fld_name)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
def process_filter(relation, filter)
|
393
|
+
process(relation)
|
394
|
+
context[:processing] = :filter
|
395
|
+
add_filter process(filter)
|
396
|
+
end
|
397
|
+
|
398
|
+
def process_par(content)
|
399
|
+
content.first == :or ? process(content) : "(#{process(content)})"
|
400
|
+
end
|
401
|
+
|
402
|
+
def process_string(string)
|
403
|
+
quote(string)
|
404
|
+
end
|
405
|
+
|
406
|
+
def process_dstring(string)
|
407
|
+
raise QueryBuilder::SyntaxError.new("Cannot parse rubyless (missing binding context).") unless helper = @rubyless_helper
|
408
|
+
res = RubyLess.translate_string(string, helper)
|
409
|
+
res.literal ? quote(res.literal) : insert_bind(res)
|
410
|
+
end
|
411
|
+
|
412
|
+
def process_rubyless(string)
|
413
|
+
# compile RubyLess...
|
414
|
+
raise QueryBuilder::SyntaxError.new("Cannot parse rubyless (missing binding context).") unless helper = @rubyless_helper
|
415
|
+
res = RubyLess.translate(string, helper)
|
416
|
+
res.literal ? quote(res.literal) : insert_bind(res)
|
417
|
+
end
|
418
|
+
|
419
|
+
def process_interval(value, interval)
|
420
|
+
"INTERVAL #{this.process(value)} #{interval.upcase}"
|
421
|
+
end
|
422
|
+
|
423
|
+
def process_or(left, right)
|
424
|
+
"(#{this.process(left)} OR #{this.process(right)})"
|
425
|
+
end
|
426
|
+
|
427
|
+
def process_op(op, left, right)
|
428
|
+
"#{process(left)} #{op.to_s.upcase} #{process(right)}"
|
429
|
+
end
|
430
|
+
|
431
|
+
def process_raw(expr)
|
432
|
+
expr.upcase
|
433
|
+
end
|
434
|
+
|
435
|
+
def process_not(expr)
|
436
|
+
if expr.first == :like
|
437
|
+
"#{this.process(expr[1])} NOT LIKE #{this.process(expr[2])}"
|
438
|
+
else
|
439
|
+
"NOT #{this.process(expr)}"
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
def process_order(*args)
|
444
|
+
variables = args
|
445
|
+
process(variables.shift) # parse query
|
446
|
+
context[:processing] = :order
|
447
|
+
@query.order = " ORDER BY #{variables.map {|var| process(var)}.join(', ')}"
|
448
|
+
end
|
449
|
+
|
450
|
+
def process_group(*args)
|
451
|
+
variables = args
|
452
|
+
process(variables.shift) # parse query
|
453
|
+
context[:processing] = :group
|
454
|
+
@query.group = " GROUP BY #{variables.map {|var| process(var)}.join(', ')}"
|
455
|
+
end
|
456
|
+
|
457
|
+
def process_void(*args)
|
458
|
+
# do nothing
|
459
|
+
end
|
460
|
+
|
461
|
+
def process_limit(*args)
|
462
|
+
variables = args
|
463
|
+
process(variables.shift) # parse query
|
464
|
+
context[:processing] = :limit
|
465
|
+
if variables.size == 1
|
466
|
+
@query.limit = " LIMIT #{process(variables.first)}"
|
467
|
+
else
|
468
|
+
@query.limit = " LIMIT #{process(variables.last)}"
|
469
|
+
@query.offset = " OFFSET #{process(variables.first)}"
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
def process_offset(query, offset)
|
474
|
+
process(query)
|
475
|
+
raise QueryBuilder::SyntaxError.new("Invalid offset (used without limit).") unless @query.limit
|
476
|
+
context[:processing] = :limit
|
477
|
+
@query.offset = " OFFSET #{process(offset)}"
|
478
|
+
end
|
479
|
+
|
480
|
+
def process_paginate(query, paginate_fld)
|
481
|
+
process(query)
|
482
|
+
raise QueryBuilder::SyntaxError.new("Invalid paginate clause '#{paginate}' (used without limit).") unless @query.limit
|
483
|
+
context[:processing] = :paginate
|
484
|
+
fld = process(paginate_fld)
|
485
|
+
if fld && (page_size = @query.limit[/ LIMIT (\d+)/,1])
|
486
|
+
@query.page_size = [2, page_size.to_i].max
|
487
|
+
@query.offset = " OFFSET #{insert_bind("((#{fld}.to_i > 0 ? #{fld}.to_i : 1)-1)*#{@query.page_size}")}"
|
488
|
+
@query.pagination_key = fld
|
489
|
+
else
|
490
|
+
raise QueryBuilder::SyntaxError.new("Invalid paginate clause '#{paginate}'.")
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
def process_equal(left, right)
|
495
|
+
process_op(:"=", left, right)
|
496
|
+
end
|
497
|
+
|
498
|
+
# Used by paginate
|
499
|
+
def process_param(param)
|
500
|
+
param
|
501
|
+
end
|
502
|
+
|
503
|
+
def process_asc(field)
|
504
|
+
"#{process(field)} ASC"
|
505
|
+
end
|
506
|
+
|
507
|
+
def process_desc(field)
|
508
|
+
"#{process(field)} DESC"
|
509
|
+
end
|
510
|
+
|
511
|
+
def process_function(arg, method)
|
512
|
+
raise QueryBuilder::SyntaxError.new("SQL function '#{process(method)}' not allowed.")
|
513
|
+
end
|
514
|
+
|
515
|
+
# ******** And maybe overwrite these **********
|
516
|
+
def parse_custom_query_argument(key, value)
|
517
|
+
return nil unless value
|
518
|
+
case key
|
519
|
+
when :order
|
520
|
+
" ORDER BY #{value}"
|
521
|
+
when :group
|
522
|
+
" GROUP BY #{value}"
|
523
|
+
else
|
524
|
+
value
|
525
|
+
end
|
526
|
+
end
|
527
|
+
|
528
|
+
def insert_bind(str)
|
529
|
+
self.class.insert_bind(str)
|
530
|
+
end
|
531
|
+
|
532
|
+
def with(hash)
|
533
|
+
context_bak = @context
|
534
|
+
res = ''
|
535
|
+
@context = @context.merge(hash)
|
536
|
+
res = yield
|
537
|
+
@context = context_bak
|
538
|
+
res
|
539
|
+
end
|
540
|
+
|
541
|
+
def restoring_this
|
542
|
+
processor = self.this
|
543
|
+
yield
|
544
|
+
if processor != self.this
|
545
|
+
# changed class, we need to change back
|
546
|
+
change_processor(processor)
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
def change_processor(processor, opts = {})
|
551
|
+
if @this
|
552
|
+
@this.change_processor(processor)
|
553
|
+
else
|
554
|
+
if processor.kind_of?(String)
|
555
|
+
processor = QueryBuilder.resolve_const(processor)
|
556
|
+
end
|
557
|
+
|
558
|
+
if processor.kind_of?(Processor)
|
559
|
+
# instance of processor
|
560
|
+
elsif processor <= Processor
|
561
|
+
processor = processor.new(this, opts)
|
562
|
+
else
|
563
|
+
raise QueryBuilder::SyntaxError.new("Cannot use #{processor} as Query compiler (not a QueryBuilder::Processor).")
|
564
|
+
end
|
565
|
+
|
566
|
+
@query.processor_class = processor.class
|
567
|
+
update_processor(processor)
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
571
|
+
def update_processor(processor)
|
572
|
+
@this = processor
|
573
|
+
if @ancestor
|
574
|
+
@ancestor.update_processor(processor)
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
def custom_query(relation)
|
579
|
+
return false unless first? && last? # current safety net until "from" is correctly implemented and tested
|
580
|
+
custom_queries = self.class.custom_queries[self.class]
|
581
|
+
if custom_queries &&
|
582
|
+
custom_queries[@opts[:custom_query_group]] &&
|
583
|
+
custom_query = custom_queries[@opts[:custom_query_group]][relation]
|
584
|
+
|
585
|
+
custom_query.each do |k,v|
|
586
|
+
@query.send(:instance_variable_set, "@#{k}", prepare_custom_query_arguments(k.to_sym, v))
|
587
|
+
end
|
588
|
+
# rebuild table alias
|
589
|
+
@query.rebuild_tables!
|
590
|
+
# rebuild 'select' aliases
|
591
|
+
@query.rebuild_attributes_hash!
|
592
|
+
true
|
593
|
+
end
|
594
|
+
end
|
595
|
+
|
596
|
+
private
|
597
|
+
|
598
|
+
%W{filter scope limit offset paginate group order}.each do |context|
|
599
|
+
# Return true if we are expanding a filter / scope / limit / etc
|
600
|
+
class_eval(%Q{
|
601
|
+
def processing_#{context}?
|
602
|
+
context[:processing] == :#{context}
|
603
|
+
end
|
604
|
+
}, __FILE__, __LINE__ - 2)
|
605
|
+
end
|
606
|
+
|
607
|
+
# Return true if we are expanding a filter
|
608
|
+
def processing_scope?
|
609
|
+
context[:processing] == :scope
|
610
|
+
end
|
611
|
+
|
612
|
+
# Parse custom query arguments for special keywords (RELATION, NODE_ATTR, ETC)
|
613
|
+
# There might be a better way to use custom queries that avoids this parsing
|
614
|
+
def prepare_custom_query_arguments(key, value)
|
615
|
+
if value.kind_of?(Array)
|
616
|
+
value.map {|e| parse_custom_query_argument(key, e)}
|
617
|
+
elsif value.kind_of?(Hash)
|
618
|
+
value.each do |k,v|
|
619
|
+
if v.kind_of?(Array)
|
620
|
+
value[k] = v.map {|e| parse_custom_query_argument(key, e)}
|
621
|
+
else
|
622
|
+
value[k] = parse_custom_query_argument(key, v)
|
623
|
+
end
|
624
|
+
end
|
625
|
+
else
|
626
|
+
parse_custom_query_argument(key, value)
|
627
|
+
end
|
628
|
+
end
|
629
|
+
|
630
|
+
def table(table_name = nil, index = 0)
|
631
|
+
if table_name.nil?
|
632
|
+
# Current context's table_alias
|
633
|
+
context[:table_alias] ||
|
634
|
+
# Search in the used tables:
|
635
|
+
@query.table(@query.main_table, index) ||
|
636
|
+
# Tables have not been introduced (custom_query):
|
637
|
+
@query.main_table
|
638
|
+
else
|
639
|
+
@query.table(table_name, index)
|
640
|
+
end
|
641
|
+
end
|
642
|
+
|
643
|
+
def add_table(use_name, table_name = nil)
|
644
|
+
if use_name == main_table && first?
|
645
|
+
# we are now using final table
|
646
|
+
context[:table_alias] = use_name
|
647
|
+
avoid_alias = true
|
648
|
+
else
|
649
|
+
avoid_alias = false
|
650
|
+
end
|
651
|
+
|
652
|
+
if use_name == main_table
|
653
|
+
if context[:scope_type] == :join
|
654
|
+
context[:scope_type] = nil
|
655
|
+
# pre scope
|
656
|
+
if context[:scope] && need_join_scope?(context[:scope])
|
657
|
+
@query.add_table(main_table, main_table, avoid_alias)
|
658
|
+
apply_scope(context[:scope])
|
659
|
+
end
|
660
|
+
@query.add_table(use_name, table_name, avoid_alias)
|
661
|
+
elsif context[:scope_type] == :filter
|
662
|
+
context[:scope_type] = nil
|
663
|
+
# post scope
|
664
|
+
@query.add_table(use_name, table_name, avoid_alias)
|
665
|
+
apply_scope(context[:scope] || default[:scope])
|
666
|
+
else
|
667
|
+
# scope already applied / skip
|
668
|
+
@query.add_table(use_name, table_name, avoid_alias)
|
669
|
+
end
|
670
|
+
else
|
671
|
+
# no scope
|
672
|
+
# can only scope main_table
|
673
|
+
@query.add_table(use_name, table_name)
|
674
|
+
end
|
675
|
+
end
|
676
|
+
|
677
|
+
# Hook to use dummy scopes (such as 'in site')
|
678
|
+
def need_join_scope?(scope_name)
|
679
|
+
true
|
680
|
+
end
|
681
|
+
|
682
|
+
def main_table
|
683
|
+
@query.main_table
|
684
|
+
end
|
685
|
+
|
686
|
+
def set_main_class(klass)
|
687
|
+
@query.set_main_class(klass)
|
688
|
+
end
|
689
|
+
|
690
|
+
def needs_join_table(*args)
|
691
|
+
@query.needs_join_table(*args)
|
692
|
+
end
|
693
|
+
|
694
|
+
def add_filter(*args)
|
695
|
+
@query.add_filter(*args)
|
696
|
+
end
|
697
|
+
|
698
|
+
def add_select(*args)
|
699
|
+
@query.add_select(*args)
|
700
|
+
end
|
701
|
+
|
702
|
+
def quote(arg)
|
703
|
+
connection.quote(arg)
|
704
|
+
end
|
705
|
+
|
706
|
+
def connection
|
707
|
+
@connection ||= @query.default_class.connection
|
708
|
+
end
|
709
|
+
|
710
|
+
def distinct!
|
711
|
+
@query.distinct = true
|
712
|
+
end
|
713
|
+
end
|
714
|
+
end
|