querybuilder 0.5.9 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +29 -25
- data/Manifest.txt +20 -9
- data/README.rdoc +73 -10
- data/Rakefile +62 -30
- data/lib/extconf.rb +3 -0
- data/lib/query_builder.rb +39 -898
- data/lib/query_builder/error.rb +7 -0
- data/lib/query_builder/info.rb +3 -0
- data/lib/query_builder/parser.rb +80 -0
- data/lib/query_builder/processor.rb +714 -0
- data/lib/query_builder/query.rb +273 -0
- data/lib/querybuilder_ext.c +1870 -0
- data/lib/querybuilder_ext.rl +418 -0
- data/lib/querybuilder_rb.rb +1686 -0
- data/lib/querybuilder_rb.rl +214 -0
- data/lib/querybuilder_syntax.rl +47 -0
- data/old_QueryBuilder.rb +946 -0
- data/querybuilder.gemspec +42 -15
- data/tasks/build.rake +20 -0
- data/test/dummy_test.rb +21 -0
- data/test/mock/custom_queries/test.yml +5 -4
- data/test/mock/dummy.rb +9 -0
- data/test/mock/dummy_processor.rb +160 -0
- data/test/mock/queries/bar.yml +1 -1
- data/test/mock/queries/foo.yml +2 -2
- data/test/mock/user_processor.rb +34 -0
- data/test/query_test.rb +38 -0
- data/test/querybuilder/basic.yml +91 -0
- data/test/{query_builder → querybuilder}/custom.yml +11 -11
- data/test/querybuilder/errors.yml +32 -0
- data/test/querybuilder/filters.yml +115 -0
- data/test/querybuilder/group.yml +7 -0
- data/test/querybuilder/joins.yml +37 -0
- data/test/querybuilder/mixed.yml +18 -0
- data/test/querybuilder/rubyless.yml +15 -0
- data/test/querybuilder_test.rb +111 -0
- data/test/test_helper.rb +8 -3
- metadata +66 -19
- data/test/mock/dummy_query.rb +0 -114
- data/test/mock/user_query.rb +0 -55
- data/test/query_builder/basic.yml +0 -60
- data/test/query_builder/errors.yml +0 -50
- data/test/query_builder/filters.yml +0 -43
- data/test/query_builder/joins.yml +0 -25
- data/test/query_builder/mixed.yml +0 -12
- data/test/query_builder_test.rb +0 -36
@@ -0,0 +1,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
|
+
}%%
|
data/old_QueryBuilder.rb
ADDED
@@ -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
|