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