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.
Files changed (46) hide show
  1. data/History.txt +29 -25
  2. data/Manifest.txt +20 -9
  3. data/README.rdoc +73 -10
  4. data/Rakefile +62 -30
  5. data/lib/extconf.rb +3 -0
  6. data/lib/query_builder.rb +39 -898
  7. data/lib/query_builder/error.rb +7 -0
  8. data/lib/query_builder/info.rb +3 -0
  9. data/lib/query_builder/parser.rb +80 -0
  10. data/lib/query_builder/processor.rb +714 -0
  11. data/lib/query_builder/query.rb +273 -0
  12. data/lib/querybuilder_ext.c +1870 -0
  13. data/lib/querybuilder_ext.rl +418 -0
  14. data/lib/querybuilder_rb.rb +1686 -0
  15. data/lib/querybuilder_rb.rl +214 -0
  16. data/lib/querybuilder_syntax.rl +47 -0
  17. data/old_QueryBuilder.rb +946 -0
  18. data/querybuilder.gemspec +42 -15
  19. data/tasks/build.rake +20 -0
  20. data/test/dummy_test.rb +21 -0
  21. data/test/mock/custom_queries/test.yml +5 -4
  22. data/test/mock/dummy.rb +9 -0
  23. data/test/mock/dummy_processor.rb +160 -0
  24. data/test/mock/queries/bar.yml +1 -1
  25. data/test/mock/queries/foo.yml +2 -2
  26. data/test/mock/user_processor.rb +34 -0
  27. data/test/query_test.rb +38 -0
  28. data/test/querybuilder/basic.yml +91 -0
  29. data/test/{query_builder → querybuilder}/custom.yml +11 -11
  30. data/test/querybuilder/errors.yml +32 -0
  31. data/test/querybuilder/filters.yml +115 -0
  32. data/test/querybuilder/group.yml +7 -0
  33. data/test/querybuilder/joins.yml +37 -0
  34. data/test/querybuilder/mixed.yml +18 -0
  35. data/test/querybuilder/rubyless.yml +15 -0
  36. data/test/querybuilder_test.rb +111 -0
  37. data/test/test_helper.rb +8 -3
  38. metadata +66 -19
  39. data/test/mock/dummy_query.rb +0 -114
  40. data/test/mock/user_query.rb +0 -55
  41. data/test/query_builder/basic.yml +0 -60
  42. data/test/query_builder/errors.yml +0 -50
  43. data/test/query_builder/filters.yml +0 -43
  44. data/test/query_builder/joins.yml +0 -25
  45. data/test/query_builder/mixed.yml +0 -12
  46. data/test/query_builder_test.rb +0 -36
@@ -0,0 +1,7 @@
1
+ module QueryBuilder
2
+ class Error < Exception
3
+ end
4
+
5
+ class SyntaxError < Error
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module QueryBuilder
2
+ VERSION = '0.7.0'
3
+ end
@@ -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