querybuilder 0.5.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 ADDED
@@ -0,0 +1,4 @@
1
+ == 0.5.0 2009-01-23
2
+
3
+ * 1 major enhancement:
4
+ * Initial release (extraction from zena: http://zenadmin.org)
data/Manifest.txt ADDED
@@ -0,0 +1,19 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.rdoc
4
+ Rakefile
5
+ lib/QueryBuilder.rb
6
+ script/console
7
+ script/destroy
8
+ script/generate
9
+ test/mock/custom_queries
10
+ test/mock/custom_queries/test.yml
11
+ test/mock/dummy_query.rb
12
+ test/mock/user_query.rb
13
+ test/QueryBuilder/basic.yml
14
+ test/QueryBuilder/errors.yml
15
+ test/QueryBuilder/filters.yml
16
+ test/QueryBuilder/joins.yml
17
+ test/QueryBuilder/mixed.yml
18
+ test/test_helper.rb
19
+ test/test_QueryBuilder.rb
data/README.rdoc ADDED
@@ -0,0 +1,67 @@
1
+ = querybuilder
2
+
3
+ * http://github.com/zena/querybuilder/tree/master
4
+
5
+ == DESCRIPTION:
6
+
7
+ QueryBuilder is an interpreter for the "pseudo sql" language. This language
8
+ can be used for two purposes:
9
+
10
+ 1. protect your database from illegal SQL by securing queries
11
+ 2. ease writing complex relational queries by abstracting table internals
12
+
13
+ == SYNOPSIS:
14
+
15
+ # Create your own query class (DummyQuery) to parse your specific models (see test/mock).
16
+
17
+ # Compile a query:
18
+ query = DummyQuery.new("images where name like '%flower%' from favorites")
19
+
20
+ # Get compilation result:
21
+ query.to_s
22
+ => "['SELECT ... FROM ... WHERE links.source_id = ?', @node.id]"
23
+
24
+ # Evaluate bind variables (produces executable SQL):
25
+ query.sql(binding)
26
+ => "SELECT ... FROM ... WHERE links.source_id = 1234"
27
+
28
+ # Compile to get count instead of records:
29
+ query.to_s(:count)
30
+ => "['SELECT COUNT(*) ... WHERE links.source_id = ?', @node.id]"
31
+
32
+ query.sql(binding, :count)
33
+ => "SELECT COUNT(*) ... WHERE links.source_id = 1234"
34
+
35
+
36
+ == REQUIREMENTS:
37
+
38
+ * yamltest
39
+
40
+ == INSTALL:
41
+
42
+ sudo gem install querybuilder
43
+
44
+ == LICENSE:
45
+
46
+ (The MIT License)
47
+
48
+ Copyright (c) 2008-2009 Gaspard Bucher
49
+
50
+ Permission is hereby granted, free of charge, to any person obtaining
51
+ a copy of this software and associated documentation files (the
52
+ 'Software'), to deal in the Software without restriction, including
53
+ without limitation the rights to use, copy, modify, merge, publish,
54
+ distribute, sublicense, and/or sell copies of the Software, and to
55
+ permit persons to whom the Software is furnished to do so, subject to
56
+ the following conditions:
57
+
58
+ The above copyright notice and this permission notice shall be
59
+ included in all copies or substantial portions of the Software.
60
+
61
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
62
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
63
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
64
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
65
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
66
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
67
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ %w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
2
+ require File.dirname(__FILE__) + '/lib/QueryBuilder'
3
+
4
+ # Generate all the Rake tasks
5
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
6
+ $hoe = Hoe.new('querybuilder', QueryBuilder::VERSION) do |p|
7
+ p.developer('Gaspard Bucher', 'gaspard@teti.ch')
8
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
9
+ p.rubyforge_name = 'querybuilder'
10
+ p.extra_deps = [
11
+ ['yamltest','>= 0.5.0'],
12
+ ]
13
+ p.extra_dev_deps = [
14
+ ['newgem', ">= #{::Newgem::VERSION}"]
15
+ ]
16
+
17
+ p.clean_globs |= %w[**/.DS_Store tmp *.log]
18
+ path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
19
+ p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
20
+ p.rsync_args = '-av --delete --ignore-errors'
21
+ end
22
+
23
+ require 'newgem/tasks' # load /tasks/*.rake
24
+ Dir['tasks/**/*.rake'].each { |t| load t }
25
+
26
+ # TODO - want other tests/tasks run by default? Add them to the list
27
+ # task :default => [:spec, :features]
@@ -0,0 +1,881 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'yaml'
5
+
6
+ =begin rdoc
7
+ QueryBuilder is a tool to secure and simplify the creation of SQL queries from untrusted users.
8
+
9
+ Syntax of a query is "RELATION [where ...|] [in ...|from SUB_QUERY|]".
10
+ =end
11
+ class QueryBuilder
12
+ attr_reader :tables, :where, :errors, :join_tables, :distinct, :final_parser, :page_size
13
+ VERSION = '0.5.0'
14
+
15
+ @@main_table = {}
16
+ @@main_class = {}
17
+ @@custom_queries = {}
18
+
19
+ class << self
20
+ # This is the table name of the main class.
21
+ def set_main_table(table_name)
22
+ @@main_table[self] = table_name.to_s
23
+ end
24
+
25
+ # This is the class of the returned elements if there is no class change in the query. This
26
+ # should correspond to the class used to build call "Foo.find_by_sql(...)" (Foo).
27
+ def set_main_class(main_class)
28
+ @@main_class[self] = main_class.to_s
29
+ end
30
+
31
+ # Load prepared SQL definitions from a directory.
32
+ #
33
+ # ==== Parameters
34
+ # query<String>:: Path to list of custom queries yaml files.
35
+ #
36
+ # ==== Examples
37
+ # DummyQuery.load_custom_queries("/path/to/directory")
38
+ #
39
+ # The format of a custom query definition is:
40
+ #
41
+ # DummyQuery: # QueryBuilder class
42
+ # abc: # query's relation name
43
+ # select: # selected fields
44
+ # - 'a'
45
+ # - '34 AS number'
46
+ # - 'c'
47
+ # tables: # tables used
48
+ # - 'test'
49
+ # where: # filters
50
+ # - '1'
51
+ # - '2'
52
+ # - '3'
53
+ # order: 'a DESC' # order clause
54
+ #
55
+ # Once loaded, this 'custom query' can be used in a query like:
56
+ # "images from abc where a > 54"
57
+ def load_custom_queries(dir)
58
+ klass = nil
59
+ if File.directory?(dir)
60
+ Dir.foreach(dir) do |file|
61
+ next unless file =~ /(.+).yml$/
62
+ custom_query_group = $1
63
+ definitions = YAML::load(File.read(File.join(dir,file)))
64
+ definitions.each do |klass,v|
65
+ klass = Module.const_get(klass)
66
+ raise ArgumentError.new("invalid class for CustomQueries (#{klass})") unless klass.ancestors.include?(QueryBuilder)
67
+ @@custom_queries[klass] ||= {}
68
+ @@custom_queries[klass][custom_query_group] ||= {}
69
+ klass_queries = @@custom_queries[klass][custom_query_group]
70
+ v.each do |k,v|
71
+ klass_queries[k] = v
72
+ end
73
+ end
74
+ end
75
+ end
76
+ rescue NameError => err
77
+ raise ArgumentError.new("invalid class for CustomQueries (#{klass})")
78
+ end
79
+
80
+ # Return the parser built from the query. The class of the returned object can be different
81
+ # from the class used to call "new". For example: NodeQuery.new("comments from nodes in project") would
82
+ # return a CommentQuery since that is the final fetched objects (final_parser).
83
+ #
84
+ # ==== Parameters
85
+ # query<String>:: Pseudo sql query string.
86
+ # opts<Hash>:: List of options.
87
+ # * custom_query_group<String>:: Name of 'yaml' custom query to use (eg. 'test' for 'test.yml')
88
+ # * skip_after_parse<Boolean>:: If true, skip 'after_parse' method.
89
+ # * ignore_warnings<Boolean>:: If true, the query will always succeed (returns a dummy query instead of nil).
90
+ #
91
+ # ==== Returns
92
+ # QueryBuilder:: A query builder subclass object.
93
+ # The object can be invalid if there were errors found during compilation.
94
+ #
95
+ # ==== Examples
96
+ # DummyQuery.new("objects in project order by name ASC, id DESC", :custom_query_group => 'test')
97
+ #
98
+ def new(query, opts = {})
99
+ obj = super(query, opts)
100
+ obj.final_parser
101
+ end
102
+ end
103
+
104
+ # Build a new query from a pseudo sql string. See QueryBuilder::new for details.
105
+ def initialize(query, opts = {})
106
+ if opts[:pre_query]
107
+ init_with_pre_query(opts[:pre_query], opts[:elements])
108
+ else
109
+ init_with_query(query, opts)
110
+ end
111
+
112
+ parse_elements(@elements)
113
+ end
114
+
115
+ # Convert query object to a string. This string should then be evaluated.
116
+ #
117
+ # ==== Parameters
118
+ # type<Symbol>:: Type of query to build (:find or :count).
119
+ #
120
+ # ==== Returns
121
+ # NilClass:: If the query is not valid and "ignore_warnings" was not set to true during initialize.
122
+ # String:: A string representing the query with its bind parameters.
123
+ #
124
+ # ==== Examples
125
+ # query.to_s
126
+ # => "[\"SELECT objects.* FROM objects WHERE objects.project_id = ?\", project_id]"
127
+ #
128
+ # DummyQuery.new("nodes in site").to_s
129
+ # => "\"SELECT objects.* FROM objects\""
130
+ #
131
+ # query.to_s(:count)
132
+ # => "[\"SELECT COUNT(*) FROM objects WHERE objects.project_id = ?\", project_id]"
133
+ def to_s(type = :find)
134
+ return nil if !valid?
135
+ return "\"SELECT #{@main_table}.* FROM #{@main_table} WHERE 0\"" if @tables.empty? # all alternate queries invalid and 'ignore_warnings' set.
136
+ statement, bind_values = build_statement(type)
137
+ bind_values.empty? ? "\"#{statement}\"" : "[#{[["\"#{statement}\""] + bind_values].join(', ')}]"
138
+ end
139
+
140
+ # Convert the query object into an SQL query.
141
+ #
142
+ # ==== Parameters
143
+ # bindings<Binding>:: Binding context in which to evaluate bind clauses (query arguments).
144
+ # type<Symbol>:: Type of SQL query (:find or :count)
145
+ #
146
+ # ==== Returns
147
+ # NilClass:: If the query is not valid and "ignore_warnings" was not set to true during initialize.
148
+ # String:: An SQL query, ready for execution (no more bind variables).
149
+ #
150
+ # ==== Examples
151
+ # query.sql(binding)
152
+ # => "SELECT objects.* FROM objects WHERE objects.project_id = 12489"
153
+ #
154
+ # query.sql(bindings, :count)
155
+ # => "SELECT COUNT(*) FROM objects WHERE objects.project_id = 12489"
156
+ def sql(bindings, type = :find)
157
+ return nil if !valid?
158
+ return "SELECT #{@main_table}.* FROM #{@main_table} WHERE 0" if @tables.empty? # all alternate queries invalid and 'ignore_warnings' set.
159
+ statement, bind_values = build_statement(type)
160
+ connection = get_connection(bindings)
161
+ statement.gsub('?') { eval_bound_value(bind_values.shift, connection, bindings) }
162
+ end
163
+
164
+
165
+ # Test query validity
166
+ #
167
+ # ==== Returns
168
+ # TrueClass:: True if object is valid.
169
+ def valid?
170
+ @errors == []
171
+ end
172
+
173
+ # Name of the pagination key when 'paginate' is used.
174
+ #
175
+ # ==== Parameters
176
+ # parameters
177
+ #
178
+ # ==== Returns
179
+ # String:: Pagination key name.
180
+ #
181
+ # ==== Examples
182
+ # DummyQuery.new("objects in site limit 5 paginate pak").pagination_key
183
+ # => "pak"
184
+ def pagination_key
185
+ @offset_limit_order_group[:paginate]
186
+ end
187
+
188
+ # Main class for the query (useful when queries move from class to class)
189
+ #
190
+ # ==== Returns
191
+ # Class:: Class of element
192
+ #
193
+ # ==== Examples
194
+ # DummyQuery.new("comments from nodes in project").main_class
195
+ # => Comment
196
+ def main_class
197
+ Module.const_get(@@main_class[self.class])
198
+ end
199
+
200
+ protected
201
+
202
+ def current_table
203
+ @current_table || main_table
204
+ end
205
+
206
+ def main_table
207
+ @@main_table[self.class]
208
+ end
209
+
210
+ def parse_part(part, is_last)
211
+
212
+ rest, context = part.split(' in ')
213
+ clause, filters = rest.split(/\s+where\s+/)
214
+
215
+ if @just_changed_class
216
+ # just changed class: parse filters && context
217
+ parse_filters(filters) if filters
218
+ @just_changed_class = false
219
+ return nil
220
+ elsif new_class = parse_change_class(clause, is_last)
221
+ if context
222
+ last_filter = @where.pop # pop/push is to keep queries in correct order (helps reading sql)
223
+ parse_context(context, true)
224
+ @where << last_filter
225
+ end
226
+ return new_class
227
+ else
228
+ add_table(main_table)
229
+ parse_filters(filters) if filters
230
+ parse_context(context, is_last) if context # .. in project
231
+ parse_relation(clause, context)
232
+ return nil
233
+ end
234
+ end
235
+
236
+ def parse_filters(clause)
237
+ # TODO: add 'match' parameter (#105)
238
+ rest = clause.strip
239
+ types = [:par_open, :value, :bool_op, :op, :par_close]
240
+ allowed = [:par_open, :value]
241
+ after_value = [:op, :bool_op, :par_close]
242
+ par_count = 0
243
+ last_bool_op = ''
244
+ has_or = false
245
+ res = ""
246
+ while rest != ''
247
+ # puts rest.inspect
248
+ if rest =~ /\A\s+/
249
+ rest = rest[$&.size..-1]
250
+ res << " "
251
+ elsif rest[0..0] == '('
252
+ unless allowed.include?(:par_open)
253
+ @errors << clause_error(clause, rest, res)
254
+ return
255
+ end
256
+ res << '('
257
+ rest = rest[1..-1]
258
+ par_count += 1
259
+ elsif rest[0..0] == ')'
260
+ unless allowed.include?(:par_close)
261
+ @errors << clause_error(clause, rest, res)
262
+ return
263
+ end
264
+ res << ')'
265
+ rest = rest[1..-1]
266
+ par_count -= 1
267
+ if par_count < 0
268
+ @errors << clause_error(clause, rest, res)
269
+ return
270
+ end
271
+ allowed = [:op, :bool_op]
272
+ elsif rest =~ /\A((>=|<=|<>|<|=|>)|((not\s+like|like|lt|le|eq|ne|ge|gt)\s+))/
273
+ unless allowed.include?(:op)
274
+ @errors << clause_error(clause, rest, res)
275
+ return
276
+ end
277
+ op = $1.strip
278
+ rest = rest[op.size..-1]
279
+ op = {'lt' => '<','le' => '<=','eq' => '=','ne' => '<>','ge' => '>=','gt' => '>','like' => 'LIKE', 'not like' => 'NOT LIKE'}[op] || $1
280
+ res << op
281
+ allowed = [:value, :par_open]
282
+ elsif rest =~ /\A("|')([^\1]*?)\1/
283
+ unless allowed.include?(:value)
284
+ @errors << clause_error(clause, rest, res)
285
+ return
286
+ end
287
+ rest = rest[$&.size..-1]
288
+ res << map_literal($2)
289
+ allowed = after_value
290
+ elsif rest =~ /\A(\d+|[\w:]+)\s+(second|minute|hour|day|week|month|year)s?/
291
+ unless allowed.include?(:value)
292
+ @errors << clause_error(clause, rest, res)
293
+ return
294
+ end
295
+ rest = rest[$&.size..-1]
296
+ fld, type = $1, $2
297
+ unless field = field_or_attr(fld, table, :filter)
298
+ @errors << "invalid field or value #{fld.inspect}"
299
+ return
300
+ end
301
+ res << "INTERVAL #{field} #{type.upcase}"
302
+ allowed = after_value
303
+ elsif rest =~ /\A(-?\d+)/
304
+ unless allowed.include?(:value)
305
+ @errors << clause_error(clause, rest, res)
306
+ return
307
+ end
308
+ rest = rest[$&.size..-1]
309
+ res << $1
310
+ allowed = after_value
311
+ elsif rest =~ /\A(is\s+not\s+null|is\s+null)/
312
+ unless allowed.include?(:bool_op)
313
+ @errors << clause_error(clause, rest, res)
314
+ return
315
+ end
316
+ rest = rest[$&.size..-1]
317
+ res << $1.upcase
318
+ allowed = [:par_close, :bool_op]
319
+ elsif rest[0..7] == 'REF_DATE'
320
+ unless allowed.include?(:value)
321
+ @errors << clause_error(clause, rest, res)
322
+ return
323
+ end
324
+ rest = rest[8..-1]
325
+ res << @ref_date
326
+ allowed = after_value
327
+ elsif rest =~ /\A(\+|\-)/
328
+ unless allowed.include?(:op)
329
+ @errors << clause_error(clause, rest, res)
330
+ return
331
+ end
332
+ rest = rest[$&.size..-1]
333
+ res << $1
334
+ allowed = [:value, :par_open]
335
+ elsif rest =~ /\A(and|or)/
336
+ unless allowed.include?(:bool_op)
337
+ @errors << clause_error(clause, rest, res)
338
+ return
339
+ end
340
+ rest = rest[$&.size..-1]
341
+ res << $1.upcase
342
+ has_or ||= $1 == 'or'
343
+ allowed = [:par_open, :value]
344
+ elsif rest =~ /\A[\w:]+/
345
+ unless allowed.include?(:value)
346
+ @errors << clause_error(clause, rest, res)
347
+ return
348
+ end
349
+ rest = rest[$&.size..-1]
350
+ fld = $&
351
+ unless field = field_or_attr(fld, table, :filter)
352
+ @errors << "invalid field or value #{fld.inspect}"
353
+ return
354
+ end
355
+ res << field
356
+ allowed = after_value
357
+ else
358
+ @errors << clause_error(clause, rest, res)
359
+ return
360
+ end
361
+ end
362
+
363
+ if par_count > 0
364
+ @errors << "invalid clause #{clause.inspect}: missing closing ')'"
365
+ elsif allowed.include?(:value)
366
+ @errors << "include clause #{clause.inspect}"
367
+ else
368
+ @where << (has_or ? "(#{res})" : res)
369
+ end
370
+ end
371
+
372
+ def parse_order_clause(order)
373
+ return @order unless order
374
+ res = []
375
+
376
+ order.split(',').each do |clause|
377
+ if clause == 'random'
378
+ res << "RAND()"
379
+ else
380
+ if clause =~ /^\s*([^\s]+) (ASC|asc|DESC|desc)/
381
+ fld_name, direction = $1, $2
382
+ else
383
+ fld_name = clause
384
+ direction = 'ASC'
385
+ end
386
+ if fld = field_or_attr(fld_name, table, :order)
387
+ res << "#{fld} #{direction.upcase}"
388
+ elsif fld.nil?
389
+ @errors << "invalid field '#{fld_name}'"
390
+ end
391
+ end
392
+ end
393
+ res == [] ? nil : " ORDER BY #{res.join(', ')}"
394
+ end
395
+
396
+ def parse_group_clause(group)
397
+ return @group unless group
398
+ res = []
399
+
400
+ group.split(',').each do |field|
401
+ if fld = map_field(field, table, :group)
402
+ res << fld
403
+ else
404
+ @errors << "invalid field '#{field}'"
405
+ end
406
+ end
407
+ res == [] ? nil : " GROUP BY #{res.join(', ')}"
408
+ end
409
+
410
+ def parse_limit_clause(limit)
411
+ return @limit unless limit
412
+ if limit.kind_of?(Fixnum)
413
+ " LIMIT #{limit}"
414
+ elsif limit =~ /^\s*(\d+)\s*,\s*(\d+)/
415
+ @offset = " OFFSET #{$1}"
416
+ " LIMIT #{$2}"
417
+ elsif limit =~ /(\d+)/
418
+ " LIMIT #{$1}"
419
+ else
420
+ @errors << "invalid limit clause '#{limit}'"
421
+ nil
422
+ end
423
+ end
424
+
425
+ def parse_paginate_clause(paginate)
426
+ return @offset unless paginate
427
+ if !@limit
428
+ # TODO: raise error ?
429
+ @errors << "invalid paginate clause '#{paginate}' (used without limit)"
430
+ nil
431
+ elsif (fld = map_literal(paginate, :ruby)) && (page_size = @limit[/ LIMIT (\d+)/,1])
432
+ @page_size = [2,page_size.to_i].max
433
+ " OFFSET #{insert_bind("((#{fld}.to_i > 0 ? #{fld}.to_i : 1)-1)*#{@page_size}")}"
434
+ else
435
+ @errors << "invalid paginate clause '#{paginate}'"
436
+ nil
437
+ end
438
+ end
439
+
440
+ def parse_offset_clause(offset)
441
+ return @offset unless offset
442
+ if !@limit
443
+ # TODO: raise error ?
444
+ @errors << "invalid offset clause '#{offset}' (used without limit)"
445
+ nil
446
+ elsif offset.strip =~ /^\d+$/
447
+ " OFFSET #{offset}"
448
+ else
449
+ @errors << "invalid offset clause '#{offset}'"
450
+ nil
451
+ end
452
+ end
453
+
454
+ def add_table(table_name)
455
+ if !@table_counter[table_name]
456
+ @table_counter[table_name] = 0
457
+ @tables << table_name
458
+ else
459
+ @table_counter[table_name] += 1
460
+ @tables << "#{table_name} AS #{table(table_name)}"
461
+ end
462
+ end
463
+
464
+ # return a unique table name for the current sub-query context, adding the table when necessary
465
+ def needs_table(table1, table2, filter)
466
+ @needed_tables[table2] ||= {}
467
+ @needed_tables[table2][table] ||= begin
468
+ add_table(table2)
469
+ @where << filter.gsub('TABLE1', table).gsub('TABLE2', table(table2))
470
+ table(table2)
471
+ end
472
+ end
473
+
474
+ # versions LEFT JOIN dyn_attributes ON ...
475
+ def needs_join_table(table1, type, table2, clause, join_name = nil)
476
+ join_name ||= "#{table1}=#{type}=#{table2}"
477
+ @needed_join_tables[join_name] ||= {}
478
+ @needed_join_tables[join_name][table] ||= begin
479
+ # define join for this part ('table' = unique for each part)
480
+
481
+ first_table = table(table1)
482
+
483
+ if !@table_counter[table2]
484
+ @table_counter[table2] = 0
485
+ second_table = table2
486
+ else
487
+ @table_counter[table2] += 1
488
+ second_table = "#{table2} AS #{table(table2)}"
489
+ end
490
+ @join_tables[first_table] ||= []
491
+ @join_tables[first_table] << "#{type} JOIN #{second_table} ON #{clause.gsub('TABLE1',table(table1)).gsub('TABLE2',table(table2))}"
492
+ table(table2)
493
+ end
494
+ end
495
+
496
+ def table_counter(table_name)
497
+ @table_counter[table_name] || 0
498
+ end
499
+
500
+ def table_at(table_name, index)
501
+ if index < 0
502
+ return nil # no table at this address
503
+ end
504
+ index == 0 ? table_name : "#{table_name[0..1]}#{index}"
505
+ end
506
+
507
+ def table(table_name=main_table, index=0)
508
+ table_at(table_name, table_counter(table_name) + index)
509
+ end
510
+
511
+ def merge_alternate_queries(alt_queries)
512
+ counter = 1
513
+ if valid?
514
+ counter = 1
515
+ else
516
+ if @ignore_warnings
517
+ # reset current query
518
+ @tables = []
519
+ @join_tables = {}
520
+ @where = []
521
+ @errors = []
522
+ @distinct = nil
523
+ end
524
+ counter = 0
525
+ end
526
+
527
+ if @where.compact == []
528
+ where_list = []
529
+ else
530
+ where_list = [@where.compact.reverse.join(' AND ')]
531
+ end
532
+
533
+ alt_queries.each do |query|
534
+ next unless query.main_class == self.main_class # no mixed class target !
535
+ @errors += query.errors unless @ignore_warnings
536
+ next unless query.valid?
537
+ query.where.compact!
538
+ next if query.where.empty?
539
+ counter += 1
540
+ merge_tables(query)
541
+ @distinct ||= query.distinct
542
+ where_list << query.where.reverse.join(' AND ')
543
+ end
544
+
545
+ @where_list = where_list
546
+
547
+ @tables.uniq!
548
+
549
+ fix_where_list(where_list)
550
+
551
+ if counter > 1
552
+ @distinct = @tables.size > 1
553
+ @where = ["((#{where_list.join(') OR (')}))"]
554
+ else
555
+ @where = where_list
556
+ end
557
+ end
558
+
559
+ def merge_tables(sub_query)
560
+ @tables += sub_query.tables
561
+ sub_query.join_tables.each do |k,v|
562
+ @join_tables[k] ||= []
563
+ @join_tables[k] << v
564
+ end
565
+ end
566
+
567
+ def prepare_custom_query_arguments(key, value)
568
+ if value.kind_of?(Array)
569
+ value.map {|e| parse_custom_query_argument(key, e)}
570
+ elsif value.kind_of?(Hash)
571
+ value.each do |k,v|
572
+ if v.kind_of?(Array)
573
+ value[k] = v.map {|e| parse_custom_query_argument(key, e)}
574
+ else
575
+ value[k] = parse_custom_query_argument(key, v)
576
+ end
577
+ end
578
+ else
579
+ parse_custom_query_argument(key, value)
580
+ end
581
+ end
582
+
583
+ # Map a field to be used inside a query. An attr is a field from table at index 0 = @node attribute.
584
+ def field_or_attr(fld, table_name = table, context = nil)
585
+ if fld =~ /^\d+$/
586
+ return fld
587
+ elsif @select.join =~ / AS #{fld}/
588
+ @select.each do |s|
589
+ if s =~ /\A(.*) AS #{fld}\Z/
590
+ return context == :filter ? "(#{$1})" : fld
591
+ end
592
+ end
593
+ elsif table_name
594
+ map_field(fld, table_name, context)
595
+ else
596
+ map_attr(fld)
597
+ end
598
+ end
599
+
600
+ def build_statement(type = :find)
601
+ statement = type == :find ? find_statement : count_statement
602
+
603
+ # get bind variables
604
+ bind_values = []
605
+ statement.gsub!(/\[\[(.*?)\]\]/) do
606
+ bind_values << $1
607
+ '?'
608
+ end
609
+ [statement, bind_values]
610
+ end
611
+
612
+ def find_statement
613
+ table_list = []
614
+ @tables.each do |t|
615
+ table_name = t.split(/\s+/).last # objects AS ob1
616
+ if joins = @join_tables[table_name]
617
+ table_list << "#{t} #{joins.join(' ')}"
618
+ else
619
+ table_list << t
620
+ end
621
+ end
622
+
623
+ group = @group
624
+ if !group && @distinct
625
+ group = @tables.size > 1 ? " GROUP BY #{table}.id" : " GROUP BY id"
626
+ end
627
+
628
+
629
+ "SELECT #{@select.join(',')} FROM #{table_list.flatten.join(',')}" + (@where == [] ? '' : " WHERE #{@where.reverse.join(' AND ')}") + group.to_s + @order.to_s + @limit.to_s + @offset.to_s
630
+ end
631
+
632
+ def count_statement
633
+ table_list = []
634
+ @tables.each do |t|
635
+ table_name = t.split(/\s+/).last # objects AS ob1
636
+ if joins = @join_tables[table_name]
637
+ table_list << "#{t} #{joins.join(' ')}"
638
+ else
639
+ table_list << t
640
+ end
641
+ end
642
+
643
+ if @group =~ /GROUP\s+BY\s+(.+)/
644
+ # we need to COALESCE in order to count groups where $1 is NULL.
645
+ count_on = "COUNT(DISTINCT COALESCE(#{$1},0))"
646
+ elsif @distinct
647
+ count_on = "COUNT(DISTINCT #{table}.id)"
648
+ else
649
+ count_on = "COUNT(*)"
650
+ end
651
+
652
+ "SELECT #{count_on} FROM #{table_list.flatten.join(',')}" + (@where == [] ? '' : " WHERE #{@where.reverse.join(' AND ')}")
653
+ end
654
+
655
+ # Adapted from Rail's ActiveRecord code. We need "eval" because
656
+ # QueryBuilder is a compiler and it has absolutely no knowledge
657
+ # of the running context.
658
+ def eval_bound_value(value_as_string, connection, bindings)
659
+ value = eval(value_as_string, bindings)
660
+ if value.respond_to?(:map) && !value.kind_of?(String) #!value.acts_like?(:string)
661
+ if value.respond_to?(:empty?) && value.empty?
662
+ connection.quote(nil)
663
+ else
664
+ value.map { |v| connection.quote(v) }.join(',')
665
+ end
666
+ else
667
+ connection.quote(value)
668
+ end
669
+ end
670
+
671
+ def get_connection(bindings)
672
+ eval "#{main_class}.connection", bindings
673
+ end
674
+
675
+ # ******** Overwrite these **********
676
+ def class_from_table(table_name)
677
+ Object
678
+ end
679
+
680
+ def default_context_filter
681
+ raise NameError.new("default_context_filter not defined for class #{self.class}")
682
+ end
683
+
684
+ # Default sort order
685
+ def default_order_clause
686
+ nil
687
+ end
688
+
689
+ def after_parse
690
+ # do nothing
691
+ end
692
+
693
+ def parse_change_class(rel, is_last)
694
+ nil
695
+ end
696
+
697
+ def parse_relation(clause, context)
698
+ return nil
699
+ end
700
+
701
+ def context_filter_fields(clause, is_last = false)
702
+ nil
703
+ end
704
+
705
+ def parse_context(clause, is_last = false)
706
+
707
+ if fields = context_filter_fields(clause, is_last)
708
+ @where << "#{field_or_attr(fields[0])} = #{field_or_attr(fields[1], table(main_table,-1))}" if fields != :void
709
+ else
710
+ @errors << "invalid context '#{clause}'"
711
+ end
712
+ end
713
+
714
+ # Map a litteral value to be used inside a query
715
+ def map_literal(value, env = :sql)
716
+ env == :sql ? insert_bind(value.inspect) : value
717
+ end
718
+
719
+
720
+ # Overwrite this and take car to check for valid fields.
721
+ def map_field(fld, table_name, context = nil)
722
+ if fld == 'id'
723
+ "#{table_name}.#{fld}"
724
+ else
725
+ # TODO: error, raise / ignore ?
726
+ end
727
+ end
728
+
729
+ def map_attr(fld)
730
+ insert_bind(fld.to_s)
731
+ end
732
+
733
+ # ******** And maybe overwrite these **********
734
+ def parse_custom_query_argument(key, value)
735
+ return nil unless value
736
+ value = value.gsub('REF_DATE', @ref_date)
737
+ case key
738
+ when :order
739
+ " ORDER BY #{value}"
740
+ when :group
741
+ " GROUP BY #{value}"
742
+ else
743
+ value
744
+ end
745
+ end
746
+
747
+ def extract_custom_query(list)
748
+ list[-1].split(' ').first
749
+ end
750
+
751
+ private
752
+
753
+ def parse_elements(elements)
754
+ # "final_parser" is the parser who will respond to 'to_sql'. It might be a sub-parser for another class.
755
+ @final_parser = self
756
+
757
+ if @@custom_queries[self.class] &&
758
+ @@custom_queries[self.class][@opts[:custom_query_group]] &&
759
+ custom_query = @@custom_queries[self.class][@opts[:custom_query_group]][extract_custom_query(elements)]
760
+ custom_query.each do |k,v|
761
+ instance_variable_set("@#{k}", prepare_custom_query_arguments(k.to_sym, v))
762
+ end
763
+ # set table counters
764
+ @tables.each do |t|
765
+ base, as, tbl = t.split(' ')
766
+ @table_counter[base] ||= 0
767
+ @table_counter[base] += 1 if tbl
768
+ end
769
+ # parse filters
770
+ clause, filters = elements[-1].split(/\s+where\s+/)
771
+
772
+ parse_filters(filters) if filters
773
+ @order = parse_order_clause(@offset_limit_order_group[:order])
774
+ else
775
+ i, new_class = 0, nil
776
+ elements.each_index do |i|
777
+ break if new_class = parse_part(elements[i], i == 0) # yes, is_last is first (parsing reverse)
778
+ end
779
+
780
+ if new_class
781
+ # move to another parser class
782
+ @final_parser = new_class.new(nil, :pre_query => self, :elements => elements[i..-1])
783
+ else
784
+ @distinct ||= elements.size > 1
785
+ @select << "#{table}.*"
786
+
787
+ merge_alternate_queries(@alt_queries) if @alt_queries
788
+
789
+ @limit = parse_limit_clause(@offset_limit_order_group[:limit])
790
+ if @offset_limit_order_group[:paginate]
791
+ @offset = parse_paginate_clause(@offset_limit_order_group[:paginate])
792
+ else
793
+ @offset = parse_offset_clause(@offset_limit_order_group[:offset])
794
+ end
795
+
796
+
797
+ @group = parse_group_clause(@offset_limit_order_group[:group])
798
+ @order = parse_order_clause(@offset_limit_order_group[:order] || default_order_clause)
799
+ end
800
+ end
801
+
802
+ if @final_parser == self
803
+ after_parse unless @opts[:skip_after_parse]
804
+ @where.compact!
805
+ end
806
+ end
807
+
808
+ def init_with_query(query, opts)
809
+ @opts = opts
810
+
811
+ if query.kind_of?(Array)
812
+ @query = query[0]
813
+ if query.size > 1
814
+ @alt_queries = query[1..-1].map {|q| self.class.new(q, opts.merge(:skip_after_parse => true))}
815
+ end
816
+ else
817
+ @query = query
818
+ end
819
+
820
+
821
+ @offset_limit_order_group = {}
822
+ if @query == nil || @query == ''
823
+ elements = [main_table]
824
+ else
825
+ elements = @query.split(' from ')
826
+ last_element = elements.last
827
+ last_element, @offset_limit_order_group[:offset] = last_element.split(' offset ')
828
+ last_element, @offset_limit_order_group[:paginate] = last_element.split(' paginate ')
829
+ last_element, @offset_limit_order_group[:limit] = last_element.split(' limit ')
830
+ last_element, @offset_limit_order_group[:order] = last_element.split(' order by ')
831
+ elements[-1], @offset_limit_order_group[:group] = last_element.split(' group by ')
832
+ end
833
+
834
+ @offset_limit_order_group[:limit] = opts[:limit] || @offset_limit_order_group[:limit]
835
+ # In order to know the table names of the dependencies, we need to parse it backwards.
836
+ # We first find the closest elements, then the final ones. For example, "pages from project" we need
837
+ # project information before getting 'pages'.
838
+ @elements = elements.reverse
839
+
840
+ @tables = []
841
+ @join_tables = {}
842
+ @table_counter = {}
843
+ @where = []
844
+ # list of tables that need to be added for filter clauses (should be added only once per part)
845
+ @needed_tables = {}
846
+ # list of tables that need to be added through a join (should be added only once per part)
847
+ @needed_join_tables = {}
848
+
849
+ @errors = []
850
+
851
+ @main_table ||= 'objects'
852
+
853
+ @select = []
854
+
855
+ @ignore_warnings = opts[:ignore_warnings]
856
+
857
+ @ref_date = opts[:ref_date] ? "'#{opts[:ref_date]}'" : 'now()'
858
+ end
859
+
860
+ def init_with_pre_query(pre_query, elements)
861
+ pre_query.instance_variables.each do |iv|
862
+ next if iv == '@query' || iv == '@final_parser'
863
+ instance_variable_set(iv, pre_query.instance_variable_get(iv))
864
+ end
865
+ @just_changed_class = true
866
+ @elements = elements
867
+ end
868
+
869
+ def clause_error(clause, rest, res)
870
+ "invalid clause #{clause.inspect} near \"#{res[-2..-1]}#{rest[0..1]}\""
871
+ end
872
+
873
+ def insert_bind(str)
874
+ "[[#{str}]]"
875
+ end
876
+
877
+ # Make sure all clauses are compatible (where_list is a list of strings, not arrays)
878
+ def fix_where_list(where_list)
879
+ # do nothing
880
+ end
881
+ end