querybuilder 0.5.9 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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