querybuilder 0.5.9 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. data/History.txt +29 -25
  2. data/Manifest.txt +20 -9
  3. data/README.rdoc +73 -10
  4. data/Rakefile +62 -30
  5. data/lib/extconf.rb +3 -0
  6. data/lib/query_builder.rb +39 -898
  7. data/lib/query_builder/error.rb +7 -0
  8. data/lib/query_builder/info.rb +3 -0
  9. data/lib/query_builder/parser.rb +80 -0
  10. data/lib/query_builder/processor.rb +714 -0
  11. data/lib/query_builder/query.rb +273 -0
  12. data/lib/querybuilder_ext.c +1870 -0
  13. data/lib/querybuilder_ext.rl +418 -0
  14. data/lib/querybuilder_rb.rb +1686 -0
  15. data/lib/querybuilder_rb.rl +214 -0
  16. data/lib/querybuilder_syntax.rl +47 -0
  17. data/old_QueryBuilder.rb +946 -0
  18. data/querybuilder.gemspec +42 -15
  19. data/tasks/build.rake +20 -0
  20. data/test/dummy_test.rb +21 -0
  21. data/test/mock/custom_queries/test.yml +5 -4
  22. data/test/mock/dummy.rb +9 -0
  23. data/test/mock/dummy_processor.rb +160 -0
  24. data/test/mock/queries/bar.yml +1 -1
  25. data/test/mock/queries/foo.yml +2 -2
  26. data/test/mock/user_processor.rb +34 -0
  27. data/test/query_test.rb +38 -0
  28. data/test/querybuilder/basic.yml +91 -0
  29. data/test/{query_builder → querybuilder}/custom.yml +11 -11
  30. data/test/querybuilder/errors.yml +32 -0
  31. data/test/querybuilder/filters.yml +115 -0
  32. data/test/querybuilder/group.yml +7 -0
  33. data/test/querybuilder/joins.yml +37 -0
  34. data/test/querybuilder/mixed.yml +18 -0
  35. data/test/querybuilder/rubyless.yml +15 -0
  36. data/test/querybuilder_test.rb +111 -0
  37. data/test/test_helper.rb +8 -3
  38. metadata +66 -19
  39. data/test/mock/dummy_query.rb +0 -114
  40. data/test/mock/user_query.rb +0 -55
  41. data/test/query_builder/basic.yml +0 -60
  42. data/test/query_builder/errors.yml +0 -50
  43. data/test/query_builder/filters.yml +0 -43
  44. data/test/query_builder/joins.yml +0 -25
  45. data/test/query_builder/mixed.yml +0 -12
  46. data/test/query_builder_test.rb +0 -36
@@ -0,0 +1,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