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.
- 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
|