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,214 @@
1
+ module QueryBuilder
2
+ class Parser
3
+ %%{
4
+ machine querybuilder;
5
+
6
+ action str_a {
7
+ str_buf += fc.chr
8
+ }
9
+
10
+ action string {
11
+ last << [:string, str_buf]
12
+ str_buf = ""
13
+ }
14
+
15
+ action dstring {
16
+ last << [:dstring, str_buf]
17
+ str_buf = ""
18
+ }
19
+
20
+ action rubyless {
21
+ last << [:rubyless, str_buf]
22
+ str_buf = ""
23
+ }
24
+
25
+ action integer {
26
+ last << [:integer, str_buf]
27
+ str_buf = ""
28
+ }
29
+
30
+ action real {
31
+ last << [:real, str_buf]
32
+ str_buf = ""
33
+ }
34
+
35
+ action field {
36
+ last << [:field, str_buf]
37
+ str_buf = ""
38
+ }
39
+
40
+ action method {
41
+ last << [:method, str_buf]
42
+ str_buf = ""
43
+ }
44
+
45
+ action raw {
46
+ last << [:raw, str_buf]
47
+ str_buf = ""
48
+ }
49
+
50
+ action function {
51
+ last = apply_op(stack, :function)
52
+ str_buf = ""
53
+ }
54
+
55
+ action direction {
56
+ last = apply_op(stack, str_buf.downcase.to_sym, false)
57
+ str_buf = ""
58
+ }
59
+
60
+ action relation {
61
+ if clause_state == :relation || clause_state == :parenthesis
62
+ last = insert(stack, [:relation, str_buf])
63
+ str_buf = ""
64
+ end
65
+ }
66
+
67
+ action operator {
68
+ last = apply_op(stack, str_buf.downcase.to_sym)
69
+ str_buf = ""
70
+ }
71
+
72
+ action is {
73
+ # We need the 'is' operator to avoid confusion with 'in site'.
74
+ last = apply_op(stack, :is)
75
+ }
76
+
77
+ action interval {
78
+ last = apply_op(stack, :interval)
79
+ last << str_buf
80
+ str_buf = ""
81
+ }
82
+
83
+ action filter {
84
+ last = apply_op(stack, :filter)
85
+ clause_state = :filter
86
+ }
87
+
88
+ action goto_expr_p {
89
+ # remember current machine state 'cs'
90
+ last << [:par, cs]
91
+ stack.push last.last
92
+ last = last.last
93
+ fgoto expr_p;
94
+ }
95
+
96
+ action expr_close {
97
+ pop_stack(stack, :par_close)
98
+ # reset machine state 'cs'
99
+ cs = stack.last.delete_at(1)
100
+ # one more time to remove [:par...] line
101
+ stack.pop
102
+ last = stack.last
103
+ # closing ')' must be parsed twice
104
+ fhold;
105
+ }
106
+
107
+ action goto_clause_p {
108
+ # remember current machine state 'cs'
109
+ clause_state = :parenthesis
110
+ last << [:clause_par, cs]
111
+ stack.push last.last
112
+ last = last.last
113
+ fgoto clause_p;
114
+ }
115
+
116
+ action clause_close {
117
+ pop_stack(stack, :clause_par_close)
118
+ clause_state = :relation
119
+ # reset machine state 'cs'
120
+ cs = stack.last.delete_at(1)
121
+ # one more time to remove [:clause_par...] line
122
+ stack.pop
123
+ last = stack.last
124
+ # closing ')' must be parsed twice
125
+ fhold;
126
+ }
127
+
128
+ action scope {
129
+ last = apply_op(stack, :scope)
130
+ last << str_buf
131
+ str_buf = ""
132
+ }
133
+
134
+ action offset {
135
+ last = apply_op(stack, :offset)
136
+ }
137
+
138
+ action param {
139
+ last << [:param, str_buf]
140
+ str_buf = ""
141
+ }
142
+
143
+ action paginate {
144
+ last = apply_op(stack, :paginate)
145
+ }
146
+
147
+ action limit {
148
+ last = apply_op(stack, :limit)
149
+ str_buf = ""
150
+ }
151
+
152
+ action order {
153
+ last = apply_op(stack, :order)
154
+ str_buf = ""
155
+ }
156
+
157
+ action group {
158
+ last = apply_op(stack, :group)
159
+ }
160
+
161
+ action from_ {
162
+ last = apply_op(stack, :from)
163
+ clause_state = :relation
164
+ }
165
+
166
+ action join_clause {
167
+ if clause_state == :relation
168
+ last = apply_op(stack, "clause_#{str_buf}".to_sym)
169
+ str_buf = ""
170
+ end
171
+ }
172
+
173
+ action clause {
174
+ last = insert(stack, [:clause])
175
+ }
176
+
177
+ action error {
178
+ p = p - 3
179
+ p = 0 if p < 0
180
+ raise QueryBuilder::SyntaxError.new("Syntax error near #{data[p..-1].chomp.inspect}.")
181
+ }
182
+
183
+ action debug {
184
+ print("_#{data[p..p]}")
185
+ }
186
+
187
+ include querybuilder "querybuilder_syntax.rl";
188
+ }%%
189
+
190
+ %% write data;
191
+
192
+ def self.parse(arg)
193
+ if arg.kind_of?(Array)
194
+ data = "(#{arg.join(') or (')})\n"
195
+ else
196
+ data = "#{arg}\n"
197
+ end
198
+ stack = [[:query]]
199
+ last = stack.last
200
+ str_buf = ""
201
+ clause_state = :relation
202
+ eof = 0;
203
+ %% write init;
204
+ %% write exec;
205
+
206
+ if p < pe
207
+ p = p - 3
208
+ p = 0 if p < 0
209
+ raise QueryBuilder::SyntaxError.new("Syntax error near #{data[p..-1].chomp.inspect}.")
210
+ end
211
+ stack.first
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,47 @@
1
+ %%{
2
+ machine querybuilder;
3
+ # Pseudo sql syntax:
4
+ #
5
+ # 'RELATION [where CLAUSE] [in SCOPE]
6
+ # [from SUB_QUERY] [limit num(,num)] [offset num] [paginate key] [order by ORDER_CLAUSE] [group by GROUP_CLAUSE]'
7
+ #
8
+ # The where CLAUSE can contain the following operators
9
+
10
+ ws = ' ' | '\t' | '\n';
11
+ var = ws* ([a-zA-Z_]+) $str_a;
12
+ dquote = ([^"\\] | '\n') $str_a | ('\\' (any | '\n') $str_a);
13
+ squote = ([^'\\] | '\n') $str_a | ('\\' (any | '\n') $str_a);
14
+ string = ws* ("'" squote* "'" >string | '"' dquote* '"' >dstring);
15
+ field = var %field ('.' %function var %method)*;
16
+ rcontent = ('"' dquote* '"') $str_a | ([^\}"\\] | '\n') $str_a | ('\\' (any | '\n') $str_a) ;
17
+ rubyless = ws* "#{" rcontent+ "}" >rubyless ('.' %function var %method)*;
18
+ real = ws* ('-'? ('0'..'9' digit* '.' digit+) ) $str_a %real;
19
+ integer = ws* ('-'? digit+ ) $str_a %integer;
20
+ number = (real | integer);
21
+ op = ws* ('+' | '-' | '<' | '<=' | '=' | '>=' | '>') $str_a;
22
+ text_op = ws+ (('or' | 'and' | 'lt' | 'le' | 'eq' | 'ne' | 'ge' | 'gt' | 'match') $str_a | ('not' $str_a %operator ws+)? 'like' $str_a);
23
+ operator = (op %operator | text_op %operator ws+ );
24
+ interval = ws+ ('second'|'minute'|'hour'|'day'|'week'|'month'|'year') $str_a %interval 's'?;
25
+ value = ((field | string | number | rubyless) interval? | ws* '(' >goto_expr_p ws* ')');
26
+ is_null = ws+ 'is' %is ws+ (('not' ws+)* ('null' | 'NULL')) $str_a %raw;
27
+ expr = value (operator value | is_null)*;
28
+ expr_p := expr ws* ')' $expr_close;
29
+
30
+ relation = ws* var %relation;
31
+ filter = expr ;
32
+ filters = ws+ 'where' %filter ws filter;
33
+ scope = ws+ 'in' ws var %scope;
34
+
35
+ offset = ws+ 'offset' %offset integer;
36
+ paginate = ws+ 'paginate' %paginate var %param;
37
+ limit = ws+ 'limit' %limit integer (ws* ',' integer)?;
38
+ direction= ws+ ('asc' | 'desc' | 'ASC' | 'DESC') $str_a %direction;
39
+ order = ws+ 'order' ws+ 'by' %order field (direction)? (ws* ',' field (direction)?)*;
40
+ group = ws+ 'group' ws+ 'by' %group field (ws* ',' field)*;
41
+
42
+ part = (relation filters? scope? | ws* '(' >goto_clause_p ws* ')');
43
+ clause = (part (ws+ 'from' %from_ part)* | '(' >goto_clause_p ws* ')' );
44
+ clause_p:= clause ws* ')' $clause_close;
45
+ main := clause (ws+ ('or' | 'and') $str_a %join_clause ws clause)* group? order? limit? offset? paginate? ('\n' | ws)+ $err(error);
46
+
47
+ }%%
@@ -0,0 +1,946 @@
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.5'
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 set of directories. If the file does not contain "host" or "hosts" keys,
32
+ # the filename is used as host.
33
+ #
34
+ # ==== Parameters
35
+ # query<String>:: Path to list of custom queries yaml files.
36
+ #
37
+ # ==== Examples
38
+ # DummyQuery.load_custom_queries("/path/to/some/*/directory")
39
+ #
40
+ # The format of a custom query definition is:
41
+ #
42
+ # hosts:
43
+ # - test.host
44
+ # DummyQuery: # QueryBuilder class
45
+ # abc: # query's relation name
46
+ # select: # selected fields
47
+ # - 'a'
48
+ # - '34 AS number'
49
+ # - 'c'
50
+ # tables: # tables used
51
+ # - 'test'
52
+ # join_tables: # joins
53
+ # test:
54
+ # - LEFT JOIN other ON other.test_id = test.id
55
+ # where: # filters
56
+ # - '1'
57
+ # - '2'
58
+ # - '3'
59
+ # order: 'a DESC' # order clause
60
+ #
61
+ # Once loaded, this 'custom query' can be used in a query like:
62
+ # "images from abc where a > 54"
63
+ def load_custom_queries(directories)
64
+ klass = nil
65
+ Dir.glob(directories).each do |dir|
66
+ if File.directory?(dir)
67
+ Dir.foreach(dir) do |file|
68
+ next unless file =~ /(.+).yml$/
69
+ custom_query_groups = $1
70
+ definitions = YAML::load(File.read(File.join(dir,file)))
71
+ custom_query_groups = [definitions.delete('groups') || definitions.delete('group') || custom_query_groups].flatten
72
+ definitions.each do |klass,v|
73
+ klass = Module.const_get(klass)
74
+ raise ArgumentError.new("invalid class for CustomQueries (#{klass})") unless klass.ancestors.include?(QueryBuilder)
75
+ @@custom_queries[klass] ||= {}
76
+ custom_query_groups.each do |custom_query_group|
77
+ @@custom_queries[klass][custom_query_group] ||= {}
78
+ klass_queries = @@custom_queries[klass][custom_query_group]
79
+ v.each do |k,v|
80
+ klass_queries[k] = v
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ rescue NameError => err
88
+ raise ArgumentError.new("invalid class for CustomQueries (#{klass})")
89
+ end
90
+
91
+ # Return the parser built from the query. The class of the returned object can be different
92
+ # from the class used to call "new". For example: NodeQuery.new("comments from nodes in project") would
93
+ # return a CommentQuery since that is the final fetched objects (final_parser).
94
+ #
95
+ # ==== Parameters
96
+ # query<String>:: Pseudo sql query string.
97
+ # opts<Hash>:: List of options.
98
+ # * custom_query_group<String>:: Name of 'yaml' custom query to use (eg. 'test' for 'test.yml')
99
+ # * skip_after_parse<Boolean>:: If true, skip 'after_parse' method.
100
+ # * ignore_warnings<Boolean>:: If true, the query will always succeed (returns a dummy query instead of nil).
101
+ #
102
+ # ==== Returns
103
+ # QueryBuilder:: A query builder subclass object.
104
+ # The object can be invalid if there were errors found during compilation.
105
+ #
106
+ # ==== Examples
107
+ # DummyQuery.new("objects in project order by name ASC, id DESC", :custom_query_group => 'test')
108
+ #
109
+ def new(query, opts = {})
110
+ obj = super(query, opts)
111
+ obj.final_parser
112
+ end
113
+ end
114
+
115
+ # Build a new query from a pseudo sql string. See QueryBuilder::new for details.
116
+ def initialize(query, opts = {})
117
+ if opts[:pre_query]
118
+ init_with_pre_query(opts[:pre_query], opts[:elements])
119
+ else
120
+ init_with_query(query, opts)
121
+ end
122
+
123
+ parse_elements(@elements)
124
+ end
125
+
126
+ # Convert query object to a string. This string should then be evaluated.
127
+ #
128
+ # ==== Parameters
129
+ # type<Symbol>:: Type of query to build (:find or :count).
130
+ #
131
+ # ==== Returns
132
+ # NilClass:: If the query is not valid and "ignore_warnings" was not set to true during initialize.
133
+ # String:: A string representing the query with its bind parameters.
134
+ #
135
+ # ==== Examples
136
+ # query.to_s
137
+ # => "[%Q{SELECT objects.* FROM objects WHERE objects.project_id = ?}, project_id]"
138
+ #
139
+ # DummyQuery.new("nodes in site").to_s
140
+ # => "%Q{SELECT objects.* FROM objects}"
141
+ #
142
+ # query.to_s(:count)
143
+ # => "[%Q{SELECT COUNT(*) FROM objects WHERE objects.project_id = ?}, project_id]"
144
+ def to_s(type = :find)
145
+ return nil if !valid?
146
+ return "%Q{SELECT #{main_table}.* FROM #{main_table} WHERE 0}" if @tables.empty? # all alternate queries invalid and 'ignore_warnings' set.
147
+ statement, bind_values = build_statement(type)
148
+ bind_values.empty? ? "\"#{statement}\"" : "[#{[["\"#{statement}\""] + bind_values].join(', ')}]"
149
+ end
150
+
151
+ # Convert the query object into an SQL query.
152
+ #
153
+ # ==== Parameters
154
+ # bindings<Binding>:: Binding context in which to evaluate bind clauses (query arguments).
155
+ # type<Symbol>:: Type of SQL query (:find or :count)
156
+ #
157
+ # ==== Returns
158
+ # NilClass:: If the query is not valid and "ignore_warnings" was not set to true during initialize.
159
+ # String:: An SQL query, ready for execution (no more bind variables).
160
+ #
161
+ # ==== Examples
162
+ # query.sql(binding)
163
+ # => "SELECT objects.* FROM objects WHERE objects.project_id = 12489"
164
+ #
165
+ # query.sql(bindings, :count)
166
+ # => "SELECT COUNT(*) FROM objects WHERE objects.project_id = 12489"
167
+ def sql(bindings, type = :find)
168
+ return nil if !valid?
169
+ return "SELECT #{main_table}.* FROM #{main_table} WHERE 0" if @tables.empty? # all alternate queries invalid and 'ignore_warnings' set.
170
+ statement, bind_values = build_statement(type)
171
+ connection = get_connection(bindings)
172
+ statement.gsub('?') { eval_bound_value(bind_values.shift, connection, bindings) }
173
+ end
174
+
175
+
176
+ # Test query validity
177
+ #
178
+ # ==== Returns
179
+ # TrueClass:: True if object is valid.
180
+ def valid?
181
+ @errors == []
182
+ end
183
+
184
+ # Name of the pagination key when 'paginate' is used.
185
+ #
186
+ # ==== Parameters
187
+ # parameters
188
+ #
189
+ # ==== Returns
190
+ # String:: Pagination key name.
191
+ #
192
+ # ==== Examples
193
+ # DummyQuery.new("objects in site limit 5 paginate pak").pagination_key
194
+ # => "pak"
195
+ def pagination_key
196
+ @offset_limit_order_group[:paginate]
197
+ end
198
+
199
+ # Main class for the query (useful when queries move from class to class)
200
+ #
201
+ # ==== Returns
202
+ # Class:: Class of element
203
+ #
204
+ # ==== Examples
205
+ # DummyQuery.new("comments from nodes in project").main_class
206
+ # => Comment
207
+ def main_class
208
+ Module.const_get(@@main_class[self.class])
209
+ end
210
+
211
+ protected
212
+
213
+ def current_table
214
+ @current_table || main_table
215
+ end
216
+
217
+ def main_table
218
+ @main_table || @@main_table[self.class]
219
+ end
220
+
221
+ def parse_part(part, is_last)
222
+
223
+ rest, context = part.split(' in ')
224
+ clause, filters = rest.split(/\s+where\s+/)
225
+
226
+ if @just_changed_class
227
+ # just changed class: parse filters && context
228
+ parse_filters(filters) if filters
229
+ @just_changed_class = false
230
+ return nil
231
+ elsif new_class = parse_change_class(clause, is_last)
232
+ if context
233
+ last_filter = @where.pop # pop/push is to keep queries in correct order (helps reading sql)
234
+ parse_context(context, true)
235
+ @where << last_filter
236
+ end
237
+ return new_class
238
+ else
239
+ add_table(main_table)
240
+ parse_filters(filters) if filters
241
+ parse_context(context, is_last) if context # .. in project
242
+ parse_relation(clause, context)
243
+ return nil
244
+ end
245
+ end
246
+
247
+ def parse_filters(clause)
248
+ # TODO: add 'match' parameter (#105)
249
+ rest = clause.strip
250
+ types = [:par_open, :value, :bool_op, :op, :par_close]
251
+ allowed = [:par_open, :value]
252
+ after_value = [:op, :bool_op, :par_close]
253
+ par_count = 0
254
+ last_bool_op = ''
255
+ has_or = false
256
+ operation = []
257
+ res = ""
258
+ while rest != ''
259
+ # puts rest.inspect
260
+ if rest =~ /\A\s+/
261
+ rest = rest[$&.size..-1]
262
+ res << " "
263
+ elsif rest[0..0] == '('
264
+ unless allowed.include?(:par_open)
265
+ @errors << clause_error(clause, rest, res)
266
+ return
267
+ end
268
+ res << '('
269
+ rest = rest[1..-1]
270
+ par_count += 1
271
+ elsif rest[0..0] == ')'
272
+ unless allowed.include?(:par_close)
273
+ @errors << clause_error(clause, rest, res)
274
+ return
275
+ end
276
+ res << ')'
277
+ rest = rest[1..-1]
278
+ par_count -= 1
279
+ if par_count < 0
280
+ @errors << clause_error(clause, rest, res)
281
+ return
282
+ end
283
+ allowed = [:op, :bool_op]
284
+ elsif rest =~ /\A((>=|<=|<>|\!=|<|=|>)|((not\s+like|like|lt|le|eq|ne|ge|gt)\s+))/
285
+ unless allowed.include?(:op)
286
+ @errors << clause_error(clause, rest, res)
287
+ return
288
+ end
289
+ op = $1.strip
290
+ rest = rest[op.size..-1]
291
+ op = {'lt' => '<', 'le' => '<=', 'eq' => '=', 'ne' => '<>', '!=' => '<>', 'ge' => '>=', 'gt' => '>', 'like' => 'LIKE', 'not like' => 'NOT LIKE'}[op] || $1
292
+ unless append_to_op(res, operation, op)
293
+ return
294
+ end
295
+ allowed = [:value, :par_open]
296
+ elsif rest =~ /\A("|')([^\1]*?)\1/
297
+ unless allowed.include?(:value)
298
+ @errors << clause_error(clause, rest, res)
299
+ return
300
+ end
301
+ rest = rest[$&.size..-1]
302
+ unless append_to_op(res, operation, map_literal($2))
303
+ return
304
+ end
305
+ allowed = after_value
306
+ elsif rest =~ /\A(\d+|[\w:]+)\s+(second|minute|hour|day|week|month|year)s?/
307
+ unless allowed.include?(:value)
308
+ @errors << clause_error(clause, rest, res)
309
+ return
310
+ end
311
+ rest = rest[$&.size..-1]
312
+ fld, type = $1, $2
313
+ unless append_to_op(res, operation, :field => fld, :dstring => "INTERVAL $1 #{type.upcase}")
314
+ return
315
+ end
316
+ allowed = after_value
317
+ elsif rest =~ /\A(-?\d+)/
318
+ unless allowed.include?(:value)
319
+ @errors << clause_error(clause, rest, res)
320
+ return
321
+ end
322
+ rest = rest[$&.size..-1]
323
+ unless append_to_op(res, operation, $1)
324
+ return
325
+ end
326
+ allowed = after_value
327
+ elsif rest =~ /\A(is\s+not\s+null|is\s+null)/
328
+ unless allowed.include?(:bool_op)
329
+ @errors << clause_error(clause, rest, res)
330
+ return
331
+ end
332
+ rest = rest[$&.size..-1]
333
+ res << $1.upcase
334
+ allowed = [:par_close, :bool_op]
335
+ elsif rest[0..7] == 'REF_DATE'
336
+ unless allowed.include?(:value)
337
+ @errors << clause_error(clause, rest, res)
338
+ return
339
+ end
340
+ rest = rest[8..-1]
341
+ unless append_to_op(res, operation, @ref_date)
342
+ return
343
+ end
344
+ allowed = after_value
345
+ elsif rest =~ /\A(\+|\-)/
346
+ unless allowed.include?(:op)
347
+ @errors << clause_error(clause, rest, res)
348
+ return
349
+ end
350
+ rest = rest[$&.size..-1]
351
+ unless append_to_op(res, operation, $1)
352
+ return
353
+ end
354
+ allowed = [:value, :par_open]
355
+ elsif rest =~ /\A(and|or)/
356
+ unless allowed.include?(:bool_op)
357
+ @errors << clause_error(clause, rest, res)
358
+ return
359
+ end
360
+ rest = rest[$&.size..-1]
361
+ res << $1.upcase
362
+ has_or ||= $1 == 'or'
363
+ allowed = [:par_open, :value]
364
+ elsif rest =~ /\A[\w:]+/
365
+ unless allowed.include?(:value)
366
+ @errors << clause_error(clause, rest, res)
367
+ return
368
+ end
369
+ rest = rest[$&.size..-1]
370
+ fld = $&
371
+ unless append_to_op(res, operation, fld)
372
+ return
373
+ end
374
+ allowed = after_value
375
+ else
376
+ @errors << clause_error(clause, rest, res)
377
+ return
378
+ end
379
+ end
380
+
381
+ if par_count > 0
382
+ @errors << "invalid clause #{clause.inspect}: missing closing ')'"
383
+ elsif allowed.include?(:value)
384
+ @errors << "invalid clause #{clause.inspect}"
385
+ else
386
+ @where << (has_or ? "(#{res})" : res)
387
+ end
388
+ end
389
+
390
+ def append_to_op(res, operation, opts)
391
+ if opts.kind_of?(String)
392
+ operation << opts
393
+ elsif field = field_or_attr(opts[:field], table, :filter)
394
+ operation << (opts[:dstring] ? opts[:dstring].sub('$1', field) : field)
395
+ else
396
+ # late evaluation
397
+ operation << opts
398
+ end
399
+
400
+ if operation.size == 3
401
+ if op = parse_operation(*operation)
402
+ res << op
403
+ operation.replace([])
404
+ return true
405
+ else
406
+ return false
407
+ end
408
+ else
409
+ return true
410
+ end
411
+ end
412
+
413
+ def parse_operation(value1, operator, value2)
414
+ if value1.kind_of?(String) && value2.kind_of?(String)
415
+ "#{value1} #{operator} #{value2}"
416
+ elsif value1.kind_of?(Hash)
417
+ @errors << "invalid field or value #{value1[:field].inspect}"
418
+ return false
419
+ else
420
+ @errors << "invalid field or value #{value2[:field].inspect}"
421
+ return false
422
+ end
423
+ end
424
+
425
+ def parse_order_clause(order)
426
+ return @order unless order
427
+ res = []
428
+
429
+ order.split(',').each do |clause|
430
+ if clause == 'random'
431
+ res << "RAND()"
432
+ else
433
+ if clause =~ /^\s*([^\s]+) (ASC|asc|DESC|desc)/
434
+ fld_name, direction = $1, $2
435
+ else
436
+ fld_name = clause
437
+ direction = 'ASC'
438
+ end
439
+ if fld = field_or_attr(fld_name, table, :order)
440
+ res << "#{fld} #{direction.upcase}"
441
+ elsif fld.nil?
442
+ @errors << "invalid field '#{fld_name}'"
443
+ end
444
+ end
445
+ end
446
+ res == [] ? nil : " ORDER BY #{res.join(', ')}"
447
+ end
448
+
449
+ def parse_group_clause(group)
450
+ return @group unless group
451
+ res = []
452
+
453
+ group.split(',').each do |field|
454
+ if fld = map_field(field, table, :group)
455
+ res << fld
456
+ else
457
+ @errors << "invalid field '#{field}'"
458
+ end
459
+ end
460
+ res == [] ? nil : " GROUP BY #{res.join(', ')}"
461
+ end
462
+
463
+ def parse_limit_clause(limit)
464
+ return @limit unless limit
465
+ if limit.kind_of?(Fixnum)
466
+ " LIMIT #{limit}"
467
+ elsif limit =~ /^\s*(\d+)\s*,\s*(\d+)/
468
+ @offset = " OFFSET #{$1}"
469
+ " LIMIT #{$2}"
470
+ elsif limit =~ /(\d+)/
471
+ " LIMIT #{$1}"
472
+ else
473
+ @errors << "invalid limit clause '#{limit}'"
474
+ nil
475
+ end
476
+ end
477
+
478
+ def parse_paginate_clause(paginate)
479
+ return @offset unless paginate
480
+ if !@limit
481
+ # TODO: raise error ?
482
+ @errors << "invalid paginate clause '#{paginate}' (used without limit)"
483
+ nil
484
+ elsif (fld = map_literal(paginate, :ruby)) && (page_size = @limit[/ LIMIT (\d+)/,1])
485
+ @page_size = [2,page_size.to_i].max
486
+ " OFFSET #{insert_bind("((#{fld}.to_i > 0 ? #{fld}.to_i : 1)-1)*#{@page_size}")}"
487
+ else
488
+ @errors << "invalid paginate clause '#{paginate}'"
489
+ nil
490
+ end
491
+ end
492
+
493
+ def parse_offset_clause(offset)
494
+ return @offset unless offset
495
+ if !@limit
496
+ # TODO: raise error ?
497
+ @errors << "invalid offset clause '#{offset}' (used without limit)"
498
+ nil
499
+ elsif offset.strip =~ /^\d+$/
500
+ " OFFSET #{offset}"
501
+ else
502
+ @errors << "invalid offset clause '#{offset}'"
503
+ nil
504
+ end
505
+ end
506
+
507
+ def add_table(use_name, table_name = nil)
508
+ table_name ||= use_name
509
+ if !@table_counter[use_name]
510
+ @table_counter[use_name] = 0
511
+ if use_name != table_name
512
+ @tables << "#{table_name} as #{use_name}"
513
+ else
514
+ @tables << table_name
515
+ end
516
+ else
517
+ @table_counter[use_name] += 1
518
+ @tables << "#{table_name} AS #{table(use_name)}"
519
+ end
520
+ end
521
+
522
+ # return a unique table name for the current sub-query context, adding the table when necessary
523
+ def needs_table(table1, table2, filter)
524
+ @needed_tables[table2] ||= {}
525
+ @needed_tables[table2][table] ||= begin
526
+ add_table(table2)
527
+ @where << filter.gsub('TABLE1', table).gsub('TABLE2', table(table2))
528
+ table(table2)
529
+ end
530
+ end
531
+
532
+ # versions LEFT JOIN dyn_attributes ON ...
533
+ def needs_join_table(table1, type, table2, clause, join_name = nil)
534
+ join_name ||= "#{table1}=#{type}=#{table2}"
535
+ @needed_join_tables[join_name] ||= {}
536
+ @needed_join_tables[join_name][table] ||= begin
537
+ # define join for this part ('table' = unique for each part)
538
+
539
+ first_table = table(table1)
540
+
541
+ if !@table_counter[table2]
542
+ @table_counter[table2] = 0
543
+ second_table = table2
544
+ else
545
+ @table_counter[table2] += 1
546
+ second_table = "#{table2} AS #{table(table2)}"
547
+ end
548
+ @join_tables[first_table] ||= []
549
+ @join_tables[first_table] << "#{type} JOIN #{second_table} ON #{clause.gsub('TABLE1',table(table1)).gsub('TABLE2',table(table2))}"
550
+ table(table2)
551
+ end
552
+ end
553
+
554
+ def table_counter(table_name)
555
+ @table_counter[table_name] || 0
556
+ end
557
+
558
+ def table_at(table_name, index)
559
+ if index < 0
560
+ return nil # no table at this address
561
+ end
562
+ index == 0 ? table_name : "#{table_name[0..1]}#{index}"
563
+ end
564
+
565
+ def table(table_name=main_table, index=0)
566
+ table_at(table_name, table_counter(table_name) + index)
567
+ end
568
+
569
+ def merge_alternate_queries(alt_queries)
570
+ counter = 1
571
+ if valid?
572
+ counter = 1
573
+ else
574
+ if @ignore_warnings
575
+ # reset current query
576
+ @tables = []
577
+ @join_tables = {}
578
+ @where = []
579
+ @errors = []
580
+ @distinct = nil
581
+ end
582
+ counter = 0
583
+ end
584
+
585
+ if @where.compact == []
586
+ where_list = []
587
+ else
588
+ where_list = [@where.compact.reverse.join(' AND ')]
589
+ end
590
+
591
+ alt_queries.each do |query|
592
+ next unless query.main_class == self.main_class # no mixed class target !
593
+ @errors += query.errors unless @ignore_warnings
594
+ next unless query.valid?
595
+ query.where.compact!
596
+ next if query.where.empty?
597
+ counter += 1
598
+ merge_tables(query)
599
+ @distinct ||= query.distinct
600
+ where_list << query.where.reverse.join(' AND ')
601
+ end
602
+
603
+ @where_list = where_list
604
+
605
+ @tables.uniq!
606
+
607
+ fix_where_list(where_list)
608
+
609
+ if counter > 1
610
+ @distinct = @tables.size > 1
611
+ @where = ["((#{where_list.join(') OR (')}))"]
612
+ else
613
+ @where = where_list
614
+ end
615
+ end
616
+
617
+ def merge_tables(sub_query)
618
+ @tables += sub_query.tables
619
+ sub_query.join_tables.each do |k,v|
620
+ @join_tables[k] ||= []
621
+ @join_tables[k] << v
622
+ end
623
+ end
624
+
625
+ def prepare_custom_query_arguments(key, value)
626
+ if value.kind_of?(Array)
627
+ value.map {|e| parse_custom_query_argument(key, e)}
628
+ elsif value.kind_of?(Hash)
629
+ value.each do |k,v|
630
+ if v.kind_of?(Array)
631
+ value[k] = v.map {|e| parse_custom_query_argument(key, e)}
632
+ else
633
+ value[k] = parse_custom_query_argument(key, v)
634
+ end
635
+ end
636
+ else
637
+ parse_custom_query_argument(key, value)
638
+ end
639
+ end
640
+
641
+ # Map a field to be used inside a query. An attr is a field from table at index 0 = @node attribute.
642
+ def field_or_attr(fld, table_alias = table, context = nil)
643
+ if fld =~ /^\d+$/
644
+ return fld
645
+ elsif !(list = @select.select {|e| e =~ /\A#{fld}\Z|AS #{fld}|\.#{fld}\Z/}).empty?
646
+ res = list.first
647
+ if res =~ /\A(.*) AS #{fld}\Z/
648
+ res = $1
649
+ end
650
+ return context == :filter ? "(#{res})" : res
651
+ elsif table_alias
652
+ map_field(fld, table_alias, context)
653
+ else
654
+ map_attr(fld)
655
+ end
656
+ end
657
+
658
+ def build_statement(type = :find)
659
+ statement = type == :find ? find_statement : count_statement
660
+
661
+ # get bind variables
662
+ bind_values = []
663
+ statement.gsub!(/\[\[(.*?)\]\]/) do
664
+ bind_values << $1
665
+ '?'
666
+ end
667
+ [statement, bind_values]
668
+ end
669
+
670
+ def find_statement
671
+ table_list = []
672
+ @tables.each do |t|
673
+ table_name = t.split(/\s+/).last # objects AS ob1
674
+ if joins = @join_tables[table_name]
675
+ table_list << "#{t} #{joins.join(' ')}"
676
+ else
677
+ table_list << t
678
+ end
679
+ end
680
+
681
+ group = @group
682
+ if !group && @distinct
683
+ group = @tables.size > 1 ? " GROUP BY #{table}.id" : " GROUP BY id"
684
+ end
685
+
686
+
687
+ "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
688
+ end
689
+
690
+ def count_statement
691
+ table_list = []
692
+ @tables.each do |t|
693
+ table_name = t.split(/\s+/).last # objects AS ob1
694
+ if joins = @join_tables[table_name]
695
+ table_list << "#{t} #{joins.join(' ')}"
696
+ else
697
+ table_list << t
698
+ end
699
+ end
700
+
701
+ if @group =~ /GROUP\s+BY\s+(.+)/
702
+ # we need to COALESCE in order to count groups where $1 is NULL.
703
+ fields = $1.split(",").map{|f| "COALESCE(#{f.strip},0)"}.join(",")
704
+ count_on = "COUNT(DISTINCT #{fields})"
705
+ elsif @distinct
706
+ count_on = "COUNT(DISTINCT #{table}.id)"
707
+ else
708
+ count_on = "COUNT(*)"
709
+ end
710
+
711
+ "SELECT #{count_on} FROM #{table_list.flatten.join(',')}" + (@where == [] ? '' : " WHERE #{@where.reverse.join(' AND ')}")
712
+ end
713
+
714
+ # Adapted from Rail's ActiveRecord code. We need "eval" because
715
+ # QueryBuilder is a compiler and it has absolutely no knowledge
716
+ # of the running context.
717
+ def eval_bound_value(value_as_string, connection, bindings)
718
+ value = eval(value_as_string, bindings)
719
+ if value.respond_to?(:map) && !value.kind_of?(String) #!value.acts_like?(:string)
720
+ if value.respond_to?(:empty?) && value.empty?
721
+ connection.quote(nil)
722
+ else
723
+ value.map { |v| connection.quote(v) }.join(',')
724
+ end
725
+ else
726
+ connection.quote(value)
727
+ end
728
+ end
729
+
730
+ def get_connection(bindings)
731
+ eval "#{main_class}.connection", bindings
732
+ end
733
+
734
+ # ******** Overwrite these **********
735
+ def class_from_table(table_name)
736
+ Object
737
+ end
738
+
739
+ def default_context_filter
740
+ raise NameError.new("default_context_filter not defined for class #{self.class}")
741
+ end
742
+
743
+ # Default sort order
744
+ def default_order_clause
745
+ nil
746
+ end
747
+
748
+ def after_parse
749
+ # do nothing
750
+ end
751
+
752
+ def parse_change_class(rel, is_last)
753
+ nil
754
+ end
755
+
756
+ def parse_relation(clause, context)
757
+ return nil
758
+ end
759
+
760
+ def context_filter_fields(clause, is_last = false)
761
+ nil
762
+ end
763
+
764
+ def parse_context(clause, is_last = false)
765
+
766
+ if fields = context_filter_fields(clause, is_last)
767
+ @where << "#{field_or_attr(fields[0])} = #{field_or_attr(fields[1], table(main_table,-1))}" if fields != :void
768
+ else
769
+ @errors << "invalid context '#{clause}'"
770
+ end
771
+ end
772
+
773
+ # Map a litteral value to be used inside a query
774
+ def map_literal(value, env = :sql)
775
+ env == :sql ? insert_bind(value.inspect) : value
776
+ end
777
+
778
+
779
+ # Overwrite this and take car to check for valid fields.
780
+ def map_field(fld, table_alias, context = nil)
781
+ if fld == 'id'
782
+ "#{table_alias}.#{fld}"
783
+ else
784
+ # TODO: error, raise / ignore ?
785
+ end
786
+ end
787
+
788
+ def map_attr(fld)
789
+ insert_bind(fld.to_s)
790
+ end
791
+
792
+ # ******** And maybe overwrite these **********
793
+ def parse_custom_query_argument(key, value)
794
+ return nil unless value
795
+ value = value.gsub('REF_DATE', @ref_date)
796
+ case key
797
+ when :order
798
+ " ORDER BY #{value}"
799
+ when :group
800
+ " GROUP BY #{value}"
801
+ else
802
+ value
803
+ end
804
+ end
805
+
806
+ def extract_custom_query(list)
807
+ list[-1].split(' ').first
808
+ end
809
+
810
+ private
811
+
812
+ def parse_elements(elements)
813
+ # "final_parser" is the parser who will respond to 'to_sql'. It might be a sub-parser for another class.
814
+ @final_parser = self
815
+
816
+ if @@custom_queries[self.class] &&
817
+ @@custom_queries[self.class][@opts[:custom_query_group]] &&
818
+ custom_query = @@custom_queries[self.class][@opts[:custom_query_group]][extract_custom_query(elements)]
819
+ custom_query.each do |k,v|
820
+ instance_variable_set("@#{k}", prepare_custom_query_arguments(k.to_sym, v))
821
+ end
822
+ # set table counters
823
+ @tables.each do |t|
824
+ base, as, tbl = t.split(' ')
825
+ @table_counter[base] ||= 0
826
+ @table_counter[base] += 1 if tbl
827
+ end
828
+ # parse filters
829
+ clause, filters = elements[-1].split(/\s+where\s+/)
830
+
831
+ parse_filters(filters) if filters
832
+
833
+ @limit = parse_limit_clause(@offset_limit_order_group[:limit])
834
+ if @offset_limit_order_group[:paginate]
835
+ @offset = parse_paginate_clause(@offset_limit_order_group[:paginate])
836
+ else
837
+ @offset = parse_offset_clause(@offset_limit_order_group[:offset])
838
+ end
839
+
840
+ @order = parse_order_clause(@offset_limit_order_group[:order])
841
+ else
842
+ i, new_class = 0, nil
843
+ elements.each_index do |i|
844
+ break if new_class = parse_part(elements[i], i == 0) # yes, is_last is first (parsing reverse)
845
+ end
846
+
847
+ if new_class
848
+ # move to another parser class
849
+ @final_parser = new_class.new(nil, :pre_query => self, :elements => elements[i..-1])
850
+ else
851
+ @distinct ||= elements.size > 1
852
+ @select << "#{table}.*"
853
+
854
+ merge_alternate_queries(@alt_queries) if @alt_queries
855
+
856
+ @limit = parse_limit_clause(@offset_limit_order_group[:limit])
857
+ if @offset_limit_order_group[:paginate]
858
+ @offset = parse_paginate_clause(@offset_limit_order_group[:paginate])
859
+ else
860
+ @offset = parse_offset_clause(@offset_limit_order_group[:offset])
861
+ end
862
+
863
+
864
+ @group = parse_group_clause(@offset_limit_order_group[:group])
865
+ @order = parse_order_clause(@offset_limit_order_group[:order] || default[:order]_clause)
866
+ end
867
+ end
868
+
869
+ if @final_parser == self
870
+ after_parse unless @opts[:skip_after_parse]
871
+ @where.compact!
872
+ end
873
+ end
874
+
875
+ def init_with_query(query, opts)
876
+ @opts = opts
877
+
878
+ if query.kind_of?(Array)
879
+ @query = query[0]
880
+ if query.size > 1
881
+ @alt_queries = query[1..-1].map {|q| self.class.new(q, opts.merge(:skip_after_parse => true))}
882
+ end
883
+ else
884
+ @query = query
885
+ end
886
+
887
+
888
+ @offset_limit_order_group = {}
889
+ if @query == nil || @query == ''
890
+ elements = [main_table]
891
+ else
892
+ elements = @query.split(' from ')
893
+ last_element = elements.last
894
+ last_element, @offset_limit_order_group[:offset] = last_element.split(' offset ')
895
+ last_element, @offset_limit_order_group[:paginate] = last_element.split(' paginate ')
896
+ last_element, @offset_limit_order_group[:limit] = last_element.split(' limit ')
897
+ last_element, @offset_limit_order_group[:order] = last_element.split(' order by ')
898
+ elements[-1], @offset_limit_order_group[:group] = last_element.split(' group by ')
899
+ end
900
+
901
+ @offset_limit_order_group[:limit] = opts[:limit] || @offset_limit_order_group[:limit]
902
+ # In order to know the table names of the dependencies, we need to parse it backwards.
903
+ # We first find the closest elements, then the final ones. For example, "pages from project" we need
904
+ # project information before getting 'pages'.
905
+ @elements = elements.reverse
906
+
907
+ @tables = []
908
+ @join_tables = {}
909
+ @table_counter = {}
910
+ @where = []
911
+ # list of tables that need to be added for filter clauses (should be added only once per part)
912
+ @needed_tables = {}
913
+ # list of tables that need to be added through a join (should be added only once per part)
914
+ @needed_join_tables = {}
915
+
916
+ @errors = []
917
+
918
+ @select = []
919
+
920
+ @ignore_warnings = opts[:ignore_warnings]
921
+
922
+ @ref_date = opts[:ref_date] ? "'#{opts[:ref_date]}'" : 'now()'
923
+ end
924
+
925
+ def init_with_pre_query(pre_query, elements)
926
+ pre_query.instance_variables.each do |iv|
927
+ next if iv == '@query' || iv == '@final_parser'
928
+ instance_variable_set(iv, pre_query.instance_variable_get(iv))
929
+ end
930
+ @just_changed_class = true
931
+ @elements = elements
932
+ end
933
+
934
+ def clause_error(clause, rest, res)
935
+ "invalid clause #{clause.inspect} near \"#{res[-2..-1]}#{rest[0..1]}\""
936
+ end
937
+
938
+ def insert_bind(str)
939
+ "[[#{str}]]"
940
+ end
941
+
942
+ # Make sure all clauses are compatible (where_list is a list of strings, not arrays)
943
+ def fix_where_list(where_list)
944
+ # do nothing
945
+ end
946
+ end