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
data/History.txt
CHANGED
@@ -1,45 +1,49 @@
|
|
1
|
-
== 0.
|
2
|
-
|
3
|
-
*
|
4
|
-
*
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
*
|
9
|
-
*
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
*
|
14
|
-
*
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
1
|
+
== 0.7.0 2010-05-27
|
2
|
+
|
3
|
+
* Major enhancements:
|
4
|
+
* Fixed change class bugs.
|
5
|
+
* A model can now include 'QueryBuilder' (better interface between class and compiler).
|
6
|
+
* Better defaults declaration.
|
7
|
+
* Added before_process and after_process hooks.
|
8
|
+
* Using %Q{} to render dynamic string.
|
9
|
+
* Added 'match' operator.
|
10
|
+
* Added functions with dot syntax.
|
11
|
+
* Added hook to deal with noop join scopes.
|
12
|
+
* Better processing of clause_or (merging queries).
|
13
|
+
* Fixed a bug preventing parse of empty literals (compiled extension).
|
14
|
+
* Enabled functions on query parameters (RubyLess).
|
15
|
+
* Added support for 'is null' and 'is not null'.
|
16
|
+
|
17
|
+
== 0.6.0 2010-03-19
|
18
|
+
|
19
|
+
* 4 major enhancements:
|
20
|
+
* Complete rewrite of parser and processor.
|
21
|
+
* Non compatible with previous parser.
|
22
|
+
* Change in pseudo sql syntax: only support for "asc" or "desc" (downcase).
|
23
|
+
* Using jeweler instead of hoe to package gem.
|
20
24
|
|
21
25
|
== 0.5.4 2009-04-09
|
22
26
|
|
23
27
|
* 1 minor enhancement:
|
24
|
-
* Fixed a bug counting records wrong with multiple group fields
|
28
|
+
* Fixed a bug counting records wrong with multiple group fields.
|
25
29
|
|
26
30
|
== 0.5.3 2009-04-08
|
27
31
|
|
28
32
|
* 1 minor enhancement:
|
29
|
-
* Added support for limit and paginate in custom queries
|
33
|
+
* Added support for limit and paginate in custom queries.
|
30
34
|
|
31
35
|
== 0.5.2 2009-04-03
|
32
36
|
|
33
37
|
* 1 minor enhancement:
|
34
|
-
* Added support for main_table option in custom queries
|
35
|
-
* More tests for custom queries
|
38
|
+
* Added support for main_table option in custom queries.
|
39
|
+
* More tests for custom queries.
|
36
40
|
|
37
41
|
== 0.5.1 2009-03-03
|
38
42
|
|
39
43
|
* 1 minor enhancement:
|
40
|
-
* Added support for glob directories in load_custom_queries
|
44
|
+
* Added support for glob directories in load_custom_queries.
|
41
45
|
|
42
46
|
== 0.5.0 2009-01-23
|
43
47
|
|
44
48
|
* 1 major enhancement:
|
45
|
-
* Initial release (extraction from zena: http://zenadmin.org)
|
49
|
+
* Initial release (extraction from zena: http://zenadmin.org).
|
data/Manifest.txt
CHANGED
@@ -2,19 +2,30 @@ History.txt
|
|
2
2
|
Manifest.txt
|
3
3
|
README.rdoc
|
4
4
|
Rakefile
|
5
|
-
lib/
|
5
|
+
lib/extconf.rb
|
6
|
+
lib/parser.rb
|
7
|
+
lib/processor.rb
|
8
|
+
lib/query.rb
|
6
9
|
lib/querybuilder.rb
|
10
|
+
lib/querybuilder_ext.c
|
11
|
+
lib/querybuilder_rb.rb
|
12
|
+
lib/querybuilder_rb.rl
|
13
|
+
lib/querybuilder_ext.rl
|
14
|
+
lib/querybuilder_syntax.rl
|
15
|
+
lib/version.rb
|
7
16
|
script/console
|
8
17
|
script/destroy
|
9
18
|
script/generate
|
10
19
|
test/mock/custom_queries
|
11
20
|
test/mock/custom_queries/test.yml
|
12
|
-
test/mock/
|
13
|
-
test/mock/
|
14
|
-
test/
|
15
|
-
test/
|
16
|
-
test/
|
17
|
-
test/
|
18
|
-
test/
|
21
|
+
test/mock/dummy_processor.rb
|
22
|
+
test/mock/user_processor.rb
|
23
|
+
test/querybuilder/basic.yml
|
24
|
+
test/querybuilder/custom.yml
|
25
|
+
test/querybuilder/errors.yml
|
26
|
+
test/querybuilder/filters.yml
|
27
|
+
test/querybuilder/joins.yml
|
28
|
+
test/querybuilder/mixed.yml
|
19
29
|
test/test_helper.rb
|
20
|
-
test/
|
30
|
+
test/querybuilder_test.rb
|
31
|
+
test/query_test.rb
|
data/README.rdoc
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
= QueryBuilder
|
2
2
|
|
3
|
-
* http://
|
3
|
+
* http://zenadmin.org/524
|
4
4
|
|
5
5
|
== DESCRIPTION:
|
6
6
|
|
@@ -10,29 +10,84 @@ can be used for two purposes:
|
|
10
10
|
1. protect your database from illegal SQL by securing queries
|
11
11
|
2. ease writing complex relational queries by abstracting table internals
|
12
12
|
|
13
|
+
QueryBuilder is just a parser that produces as AST tree and a default Processor to help you apply
|
14
|
+
scopes, change classes, insert joins, etc. Compared to things like arel, QueryBuilder lets you build
|
15
|
+
your own expressive language and let your end users safely play with it.
|
16
|
+
|
17
|
+
Small comparison between native Rails, Arel and Pseudo SQL to display the portraits of the current
|
18
|
+
user's friends:
|
19
|
+
|
20
|
+
Rails:
|
21
|
+
|
22
|
+
Images.find(:all, :joins => "INNER JOIN people ON images.id = people.portrait_id INNER JOIN friends ON friends.target_id = people.id", :conditions => ["friends.source_id = ?", visitor.id])
|
23
|
+
|
24
|
+
Arel:
|
25
|
+
|
26
|
+
Table(:images).join(people).on(images[:id].eq(people[:portrait_id])).join(friends).on(friends[:target_id].eq(people[:id])).where(friends[:source_id].eq(visitor.id))
|
27
|
+
|
28
|
+
Pseudo SQL:
|
29
|
+
|
30
|
+
portraits from friends
|
31
|
+
|
32
|
+
I am sure that I made not mistake in the 3 words of my pseudo-sql query (which just says what I think). The other two completely leak the underlying implementation and I could have made tons of syntax errors or security breaches...
|
33
|
+
|
34
|
+
== PSEUDO SQL Syntax
|
35
|
+
|
36
|
+
Everything in brackets is optional:
|
37
|
+
|
38
|
+
'RELATION [where CLAUSE] [in SCOPE]
|
39
|
+
[from SUB_QUERY] [group by GROUP_CLAUSE] [order by ORDER_CLAUSE] [limit num(,num)] [offset num] [paginate key]'
|
40
|
+
|
41
|
+
The where CLAUSE can contain the following operators (lt, gt, le, etc are the same as '<' and company but avoid escaping in
|
42
|
+
xml/html environments):
|
43
|
+
|
44
|
+
'+' | '-' | '<' | '<=' | '=' | '>=' | '>'
|
45
|
+
'or' | 'and' | 'lt' | 'le' | 'eq' | 'ne' | 'ge' | 'gt'
|
46
|
+
'like' | 'not like' | 'match'
|
47
|
+
|
48
|
+
This lets you build complex queries like:
|
49
|
+
|
50
|
+
images where tag = 'nature' and event_at.year = #{now.year - 1} in project from favorite_projects paginate p
|
51
|
+
|
52
|
+
In the compiler, 'images' could be resolved as a filter on class type (filter_relation), "tag = 'nature'" as a
|
53
|
+
special case in process_equal, 'year' will be resolved depending on the SQL connection to something like
|
54
|
+
strftime('%Y', event_at), 'project' as a scope and 'favorite_projects' as a join_relation.
|
55
|
+
|
56
|
+
This might seem very complex, but usually, you start with a basic compiler and augment it when needs arise to
|
57
|
+
build more powerful queries.
|
58
|
+
|
59
|
+
A last note: if you insert <tt>#{something}</tt> (ruby dynamic string), it will be used as a bound variable evaluated
|
60
|
+
using RubyLess. This is perfectly safe:
|
61
|
+
|
62
|
+
images where name like '#{params[:img]}%' in site limit 5
|
63
|
+
|
64
|
+
And will be resolved as something like this:
|
65
|
+
|
66
|
+
Image.find_by_sql([%Q{SELECT images.* WHERE name LIKE ? LIMIT 5}%, "#{params[:img]}%"])
|
67
|
+
|
13
68
|
== SYNOPSIS:
|
14
|
-
|
15
|
-
# Create your own query class (
|
16
|
-
|
69
|
+
|
70
|
+
# Create your own query class (QueryDummy) to parse your specific models (see test/mock).
|
71
|
+
|
17
72
|
# Compile a query:
|
18
73
|
query = DummyQuery.new("images where name like '%flower%' from favorites")
|
19
|
-
|
74
|
+
|
20
75
|
# Get compilation result:
|
21
76
|
query.to_s
|
22
77
|
=> "['SELECT ... FROM ... WHERE links.source_id = ?', @node.id]"
|
23
|
-
|
78
|
+
|
24
79
|
# Evaluate bind variables (produces executable SQL):
|
25
80
|
query.sql(binding)
|
26
81
|
=> "SELECT ... FROM ... WHERE links.source_id = 1234"
|
27
|
-
|
82
|
+
|
28
83
|
# Compile to get count instead of records:
|
29
84
|
query.to_s(:count)
|
30
85
|
=> "['SELECT COUNT(*) ... WHERE links.source_id = ?', @node.id]"
|
31
|
-
|
86
|
+
|
32
87
|
query.sql(binding, :count)
|
33
88
|
=> "SELECT COUNT(*) ... WHERE links.source_id = 1234"
|
34
|
-
|
35
|
-
|
89
|
+
|
90
|
+
|
36
91
|
== REQUIREMENTS:
|
37
92
|
|
38
93
|
* yamltest
|
@@ -41,6 +96,14 @@ can be used for two purposes:
|
|
41
96
|
|
42
97
|
sudo gem install querybuilder
|
43
98
|
|
99
|
+
== Creating your own builder
|
100
|
+
|
101
|
+
To process queries for your own classes, you need to create a sub class of QueryBuilder::Processor and
|
102
|
+
overwrite processing methods (See QueryNode or QueryComment in Zena CMS project for an example).
|
103
|
+
|
104
|
+
Warning: when you write filters, if you join multiple clauses by hand with " AND " or " OR ", you have
|
105
|
+
to enclose the group in parenthesis to avoid problems.
|
106
|
+
|
44
107
|
== LICENSE:
|
45
108
|
|
46
109
|
(The MIT License)
|
data/Rakefile
CHANGED
@@ -1,52 +1,84 @@
|
|
1
|
-
require '
|
2
|
-
$LOAD_PATH.unshift((Pathname(__FILE__).dirname + 'lib').expand_path)
|
3
|
-
|
4
|
-
require 'querybuilder'
|
1
|
+
require 'rubygems'
|
5
2
|
require 'rake'
|
6
|
-
require '
|
3
|
+
require(File.join(File.dirname(__FILE__), 'lib/query_builder/info'))
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'jeweler'
|
7
|
+
Jeweler::Tasks.new do |gem|
|
8
|
+
gem.version = QueryBuilder::VERSION
|
9
|
+
gem.name = 'querybuilder'
|
10
|
+
gem.summary = %Q{QueryBuilder is an interpreter for the "pseudo sql" language.}
|
11
|
+
gem.description = %Q{QueryBuilder is an interpreter for the "pseudo sql" language. This language
|
12
|
+
can be used for two purposes:
|
13
|
+
|
14
|
+
1. protect your database from illegal SQL by securing queries
|
15
|
+
2. ease writing complex relational queries by abstracting table internals}
|
16
|
+
gem.email = "gaspard@teti.ch"
|
17
|
+
gem.homepage = "http://zenadmin.org/524"
|
18
|
+
gem.authors = ["Gaspard Bucher"]
|
19
|
+
gem.add_dependency "rubyless", ">= 0.5.0"
|
20
|
+
gem.add_development_dependency "shoulda", ">= 0"
|
21
|
+
gem.add_development_dependency "yamltest", ">= 0.5.0"
|
22
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
23
|
+
end
|
24
|
+
Jeweler::GemcutterTasks.new
|
25
|
+
rescue LoadError
|
26
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
27
|
+
end
|
7
28
|
|
29
|
+
require 'rake/testtask'
|
8
30
|
Rake::TestTask.new(:test) do |test|
|
9
|
-
test.libs
|
10
|
-
test.pattern
|
11
|
-
test.verbose
|
31
|
+
test.libs << 'lib' << 'test'
|
32
|
+
test.pattern = 'test/**/*_test.rb'
|
33
|
+
test.verbose = true
|
12
34
|
end
|
13
35
|
|
14
36
|
begin
|
15
37
|
require 'rcov/rcovtask'
|
16
38
|
Rcov::RcovTask.new do |test|
|
17
|
-
test.libs << 'test'
|
18
|
-
test.pattern = 'test
|
39
|
+
test.libs << 'test'
|
40
|
+
test.pattern = 'test/**/*_test.rb'
|
19
41
|
test.verbose = true
|
20
|
-
test.rcov_opts = ['-T', '--exclude-only', '"test\/,^\/"']
|
21
42
|
end
|
22
43
|
rescue LoadError
|
23
44
|
task :rcov do
|
24
|
-
abort "RCov is not available. In order to run rcov, you must: sudo gem install rcov"
|
45
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
25
46
|
end
|
26
47
|
end
|
27
48
|
|
49
|
+
task :test => :check_dependencies
|
50
|
+
|
28
51
|
task :default => :test
|
29
52
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
Jeweler::Tasks.new do |gemspec|
|
34
|
-
gemspec.version = QueryBuilder::VERSION
|
35
|
-
gemspec.name = "querybuilder"
|
36
|
-
gemspec.summary = %Q{QueryBuilder is an interpreter for the "pseudo sql" language}
|
37
|
-
gemspec.description = %Q{QueryBuilder is an interpreter for the "pseudo sql" language. This language
|
38
|
-
can be used for two purposes:
|
53
|
+
require 'rake/rdoctask'
|
54
|
+
Rake::RDocTask.new do |rdoc|
|
55
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
39
56
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
57
|
+
rdoc.rdoc_dir = 'rdoc'
|
58
|
+
rdoc.title = "QueryBuilder #{version}"
|
59
|
+
rdoc.rdoc_files.include('README*')
|
60
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
61
|
+
end
|
45
62
|
|
46
|
-
|
63
|
+
desc "Rebuild sources files from ragel parser definitions"
|
64
|
+
task :ragel do
|
65
|
+
[
|
66
|
+
"cd lib && ragel querybuilder_ext.rl -o querybuilder_ext.c",
|
67
|
+
"cd lib && ragel querybuilder_rb.rl -R -o querybuilder_rb.rb",
|
68
|
+
].each do |cmd|
|
69
|
+
puts cmd
|
70
|
+
system cmd
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
desc "Build native extensions"
|
75
|
+
task :build => :ragel do
|
76
|
+
[
|
77
|
+
"ruby lib/extconf.rb",
|
78
|
+
"cd lib && make",
|
79
|
+
].each do |cmd|
|
80
|
+
puts cmd
|
81
|
+
system cmd
|
47
82
|
end
|
48
|
-
Jeweler::GemcutterTasks.new
|
49
|
-
rescue LoadError
|
50
|
-
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
51
83
|
end
|
52
84
|
|
data/lib/extconf.rb
ADDED
data/lib/query_builder.rb
CHANGED
@@ -1,912 +1,53 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
=
|
7
|
-
|
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.9'
|
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, query_list|
|
73
|
-
constant = nil
|
74
|
-
klass.split('::').each do |m|
|
75
|
-
constant = constant ? constant.const_get(m) : Module.const_get(m)
|
76
|
-
end
|
77
|
-
klass = constant
|
78
|
-
raise ArgumentError.new("invalid class for CustomQueries (#{klass})") unless klass.ancestors.include?(QueryBuilder)
|
79
|
-
@@custom_queries[klass] ||= {}
|
80
|
-
custom_query_groups.each do |custom_query_group|
|
81
|
-
@@custom_queries[klass][custom_query_group] ||= {}
|
82
|
-
klass_queries = @@custom_queries[klass][custom_query_group]
|
83
|
-
query_list.each do |k, query|
|
84
|
-
klass_queries[k] = query
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
rescue NameError => err
|
92
|
-
raise ArgumentError.new("invalid class for CustomQueries (#{klass})")
|
93
|
-
end
|
94
|
-
|
95
|
-
# Return the parser built from the query. The class of the returned object can be different
|
96
|
-
# from the class used to call "new". For example: NodeQuery.new("comments from nodes in project") would
|
97
|
-
# return a CommentQuery since that is the final fetched objects (final_parser).
|
98
|
-
#
|
99
|
-
# ==== Parameters
|
100
|
-
# query<String>:: Pseudo sql query string.
|
101
|
-
# opts<Hash>:: List of options.
|
102
|
-
# * custom_query_group<String>:: Name of 'yaml' custom query to use (eg. 'test' for 'test.yml')
|
103
|
-
# * skip_after_parse<Boolean>:: If true, skip 'after_parse' method.
|
104
|
-
# * ignore_warnings<Boolean>:: If true, the query will always succeed (returns a dummy query instead of nil).
|
105
|
-
#
|
106
|
-
# ==== Returns
|
107
|
-
# QueryBuilder:: A query builder subclass object.
|
108
|
-
# The object can be invalid if there were errors found during compilation.
|
109
|
-
#
|
110
|
-
# ==== Examples
|
111
|
-
# DummyQuery.new("objects in project order by name ASC, id DESC", :custom_query_group => 'test')
|
112
|
-
#
|
113
|
-
def new(query, opts = {})
|
114
|
-
obj = super(query, opts)
|
115
|
-
obj.final_parser
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
# Build a new query from a pseudo sql string. See QueryBuilder::new for details.
|
120
|
-
def initialize(query, opts = {})
|
121
|
-
if opts[:pre_query]
|
122
|
-
init_with_pre_query(opts[:pre_query], opts[:elements])
|
1
|
+
module QueryBuilder
|
2
|
+
def self.resolve_const(klass)
|
3
|
+
if klass.kind_of?(String)
|
4
|
+
constant = nil
|
5
|
+
klass.split('::').each do |m|
|
6
|
+
constant = constant ? constant.const_get(m) : Module.const_get(m)
|
7
|
+
end
|
8
|
+
constant
|
123
9
|
else
|
124
|
-
|
125
|
-
end
|
126
|
-
|
127
|
-
parse_elements(@elements)
|
128
|
-
end
|
129
|
-
|
130
|
-
# Convert query object to a string. This string should then be evaluated.
|
131
|
-
#
|
132
|
-
# ==== Parameters
|
133
|
-
# type<Symbol>:: Type of query to build (:find or :count).
|
134
|
-
#
|
135
|
-
# ==== Returns
|
136
|
-
# NilClass:: If the query is not valid and "ignore_warnings" was not set to true during initialize.
|
137
|
-
# String:: A string representing the query with its bind parameters.
|
138
|
-
#
|
139
|
-
# ==== Examples
|
140
|
-
# query.to_s
|
141
|
-
# => "[\"SELECT objects.* FROM objects WHERE objects.project_id = ?\", project_id]"
|
142
|
-
#
|
143
|
-
# DummyQuery.new("nodes in site").to_s
|
144
|
-
# => "\"SELECT objects.* FROM objects\""
|
145
|
-
#
|
146
|
-
# query.to_s(:count)
|
147
|
-
# => "[\"SELECT COUNT(*) FROM objects WHERE objects.project_id = ?\", project_id]"
|
148
|
-
def to_s(type = :find)
|
149
|
-
return nil if !valid?
|
150
|
-
return "\"SELECT #{main_table}.* FROM #{main_table} WHERE 0\"" if @tables.empty? # all alternate queries invalid and 'ignore_warnings' set.
|
151
|
-
statement, bind_values = build_statement(type)
|
152
|
-
bind_values.empty? ? "\"#{statement}\"" : "[#{[["\"#{statement}\""] + bind_values].join(', ')}]"
|
153
|
-
end
|
154
|
-
|
155
|
-
# Convert the query object into an SQL query.
|
156
|
-
#
|
157
|
-
# ==== Parameters
|
158
|
-
# bindings<Binding>:: Binding context in which to evaluate bind clauses (query arguments).
|
159
|
-
# type<Symbol>:: Type of SQL query (:find or :count)
|
160
|
-
#
|
161
|
-
# ==== Returns
|
162
|
-
# NilClass:: If the query is not valid and "ignore_warnings" was not set to true during initialize.
|
163
|
-
# String:: An SQL query, ready for execution (no more bind variables).
|
164
|
-
#
|
165
|
-
# ==== Examples
|
166
|
-
# query.sql(binding)
|
167
|
-
# => "SELECT objects.* FROM objects WHERE objects.project_id = 12489"
|
168
|
-
#
|
169
|
-
# query.sql(bindings, :count)
|
170
|
-
# => "SELECT COUNT(*) FROM objects WHERE objects.project_id = 12489"
|
171
|
-
def sql(bindings, type = :find)
|
172
|
-
return nil if !valid?
|
173
|
-
return "SELECT #{main_table}.* FROM #{main_table} WHERE 0" if @tables.empty? # all alternate queries invalid and 'ignore_warnings' set.
|
174
|
-
statement, bind_values = build_statement(type)
|
175
|
-
connection = get_connection(bindings)
|
176
|
-
statement.gsub('?') { eval_bound_value(bind_values.shift, connection, bindings) }
|
177
|
-
end
|
178
|
-
|
179
|
-
|
180
|
-
# Test query validity
|
181
|
-
#
|
182
|
-
# ==== Returns
|
183
|
-
# TrueClass:: True if object is valid.
|
184
|
-
def valid?
|
185
|
-
@errors == []
|
186
|
-
end
|
187
|
-
|
188
|
-
# Name of the pagination key when 'paginate' is used.
|
189
|
-
#
|
190
|
-
# ==== Parameters
|
191
|
-
# parameters
|
192
|
-
#
|
193
|
-
# ==== Returns
|
194
|
-
# String:: Pagination key name.
|
195
|
-
#
|
196
|
-
# ==== Examples
|
197
|
-
# DummyQuery.new("objects in site limit 5 paginate pak").pagination_key
|
198
|
-
# => "pak"
|
199
|
-
def pagination_key
|
200
|
-
@offset_limit_order_group[:paginate]
|
201
|
-
end
|
202
|
-
|
203
|
-
# Main class for the query (useful when queries move from class to class)
|
204
|
-
#
|
205
|
-
# ==== Returns
|
206
|
-
# Class:: Class of element
|
207
|
-
#
|
208
|
-
# ==== Examples
|
209
|
-
# DummyQuery.new("comments from nodes in project").main_class
|
210
|
-
# => Comment
|
211
|
-
def main_class
|
212
|
-
constant = nil
|
213
|
-
@@main_class[self.class].split('::').each do |m|
|
214
|
-
constant = constant ? constant.const_get(m) : Module.const_get(m)
|
10
|
+
klass
|
215
11
|
end
|
216
|
-
constant
|
217
12
|
end
|
218
13
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
end
|
224
|
-
|
225
|
-
def main_table
|
226
|
-
@main_table || @@main_table[self.class]
|
227
|
-
end
|
228
|
-
|
229
|
-
def parse_part(part, is_last)
|
230
|
-
|
231
|
-
rest, context = part.split(' in ')
|
232
|
-
clause, filters = rest.split(/\s+where\s+/)
|
233
|
-
|
234
|
-
if @just_changed_class
|
235
|
-
# just changed class: parse filters && context
|
236
|
-
parse_filters(filters) if filters
|
237
|
-
@just_changed_class = false
|
238
|
-
return nil
|
239
|
-
elsif new_class = parse_change_class(clause, is_last)
|
240
|
-
if context
|
241
|
-
last_filter = @where.pop # pop/push is to keep queries in correct order (helps reading sql)
|
242
|
-
parse_context(context, true)
|
243
|
-
@where << last_filter
|
244
|
-
end
|
245
|
-
return new_class
|
246
|
-
else
|
247
|
-
add_table(main_table)
|
248
|
-
parse_filters(filters) if filters
|
249
|
-
parse_context(context, is_last) if context # .. in project
|
250
|
-
parse_relation(clause, context)
|
251
|
-
return nil
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
|
-
def parse_filters(clause)
|
256
|
-
# TODO: add 'match' parameter (#105)
|
257
|
-
rest = clause.strip
|
258
|
-
types = [:par_open, :value, :bool_op, :op, :par_close]
|
259
|
-
allowed = [:par_open, :value]
|
260
|
-
after_value = [:op, :bool_op, :par_close]
|
261
|
-
par_count = 0
|
262
|
-
last_bool_op = ''
|
263
|
-
has_or = false
|
264
|
-
res = ""
|
265
|
-
while rest != ''
|
266
|
-
# puts rest.inspect
|
267
|
-
if rest =~ /\A\s+/
|
268
|
-
rest = rest[$&.size..-1]
|
269
|
-
res << " "
|
270
|
-
elsif rest[0..0] == '('
|
271
|
-
unless allowed.include?(:par_open)
|
272
|
-
@errors << clause_error(clause, rest, res)
|
273
|
-
return
|
274
|
-
end
|
275
|
-
res << '('
|
276
|
-
rest = rest[1..-1]
|
277
|
-
par_count += 1
|
278
|
-
elsif rest[0..0] == ')'
|
279
|
-
unless allowed.include?(:par_close)
|
280
|
-
@errors << clause_error(clause, rest, res)
|
281
|
-
return
|
282
|
-
end
|
283
|
-
res << ')'
|
284
|
-
rest = rest[1..-1]
|
285
|
-
par_count -= 1
|
286
|
-
if par_count < 0
|
287
|
-
@errors << clause_error(clause, rest, res)
|
288
|
-
return
|
289
|
-
end
|
290
|
-
allowed = [:op, :bool_op]
|
291
|
-
elsif rest =~ /\A((>=|<=|<>|\!=|<|=|>)|((not\s+like|like|lt|le|eq|ne|ge|gt)\s+))/
|
292
|
-
unless allowed.include?(:op)
|
293
|
-
@errors << clause_error(clause, rest, res)
|
294
|
-
return
|
295
|
-
end
|
296
|
-
op = $1.strip
|
297
|
-
rest = rest[op.size..-1]
|
298
|
-
op = {'lt' => '<', 'le' => '<=', 'eq' => '=', 'ne' => '<>', '!=' => '<>', 'ge' => '>=', 'gt' => '>', 'like' => 'LIKE', 'not like' => 'NOT LIKE'}[op] || $1
|
299
|
-
res << op
|
300
|
-
allowed = [:value, :par_open]
|
301
|
-
elsif rest =~ /\A("|')([^\1]*?)\1/
|
302
|
-
unless allowed.include?(:value)
|
303
|
-
@errors << clause_error(clause, rest, res)
|
304
|
-
return
|
305
|
-
end
|
306
|
-
rest = rest[$&.size..-1]
|
307
|
-
res << map_literal($2)
|
308
|
-
allowed = after_value
|
309
|
-
elsif rest =~ /\A(\d+|[\w:]+)\s+(second|minute|hour|day|week|month|year)s?/
|
310
|
-
unless allowed.include?(:value)
|
311
|
-
@errors << clause_error(clause, rest, res)
|
312
|
-
return
|
313
|
-
end
|
314
|
-
rest = rest[$&.size..-1]
|
315
|
-
fld, type = $1, $2
|
316
|
-
unless field = field_or_attr(fld, table, :filter)
|
317
|
-
@errors << "invalid field or value #{fld.inspect}"
|
318
|
-
return
|
319
|
-
end
|
320
|
-
res << "INTERVAL #{field} #{type.upcase}"
|
321
|
-
allowed = after_value
|
322
|
-
elsif rest =~ /\A(-?\d+)/
|
323
|
-
unless allowed.include?(:value)
|
324
|
-
@errors << clause_error(clause, rest, res)
|
325
|
-
return
|
326
|
-
end
|
327
|
-
rest = rest[$&.size..-1]
|
328
|
-
res << $1
|
329
|
-
allowed = after_value
|
330
|
-
elsif rest =~ /\A(is\s+not\s+null|is\s+null)/
|
331
|
-
unless allowed.include?(:bool_op)
|
332
|
-
@errors << clause_error(clause, rest, res)
|
333
|
-
return
|
334
|
-
end
|
335
|
-
rest = rest[$&.size..-1]
|
336
|
-
res << $1.upcase
|
337
|
-
allowed = [:par_close, :bool_op]
|
338
|
-
elsif rest[0..7] == 'REF_DATE'
|
339
|
-
unless allowed.include?(:value)
|
340
|
-
@errors << clause_error(clause, rest, res)
|
341
|
-
return
|
342
|
-
end
|
343
|
-
rest = rest[8..-1]
|
344
|
-
res << @ref_date
|
345
|
-
allowed = after_value
|
346
|
-
elsif rest =~ /\A(\+|\-)/
|
347
|
-
unless allowed.include?(:op)
|
348
|
-
@errors << clause_error(clause, rest, res)
|
349
|
-
return
|
350
|
-
end
|
351
|
-
rest = rest[$&.size..-1]
|
352
|
-
res << $1
|
353
|
-
allowed = [:value, :par_open]
|
354
|
-
elsif rest =~ /\A(and|or)/
|
355
|
-
unless allowed.include?(:bool_op)
|
356
|
-
@errors << clause_error(clause, rest, res)
|
357
|
-
return
|
358
|
-
end
|
359
|
-
rest = rest[$&.size..-1]
|
360
|
-
res << $1.upcase
|
361
|
-
has_or ||= $1 == 'or'
|
362
|
-
allowed = [:par_open, :value]
|
363
|
-
elsif rest =~ /\A[\w:]+/
|
364
|
-
unless allowed.include?(:value)
|
365
|
-
@errors << clause_error(clause, rest, res)
|
366
|
-
return
|
367
|
-
end
|
368
|
-
rest = rest[$&.size..-1]
|
369
|
-
fld = $&
|
370
|
-
unless field = field_or_attr(fld, table, :filter)
|
371
|
-
@errors << "invalid field or value #{fld.inspect}"
|
372
|
-
return
|
373
|
-
end
|
374
|
-
res << field
|
375
|
-
allowed = after_value
|
376
|
-
else
|
377
|
-
@errors << clause_error(clause, rest, res)
|
378
|
-
return
|
379
|
-
end
|
380
|
-
end
|
381
|
-
|
382
|
-
if par_count > 0
|
383
|
-
@errors << "invalid clause #{clause.inspect}: missing closing ')'"
|
384
|
-
elsif allowed.include?(:value)
|
385
|
-
@errors << "invalid clause #{clause.inspect}"
|
386
|
-
else
|
387
|
-
@where << (has_or ? "(#{res})" : res)
|
388
|
-
end
|
389
|
-
end
|
390
|
-
|
391
|
-
def parse_order_clause(order)
|
392
|
-
return @order unless order
|
393
|
-
res = []
|
394
|
-
|
395
|
-
order.split(',').each do |clause|
|
396
|
-
if clause == 'random'
|
397
|
-
res << "RAND()"
|
398
|
-
else
|
399
|
-
if clause =~ /^\s*([^\s]+) (ASC|asc|DESC|desc)/
|
400
|
-
fld_name, direction = $1, $2
|
401
|
-
else
|
402
|
-
fld_name = clause
|
403
|
-
direction = 'ASC'
|
404
|
-
end
|
405
|
-
if fld = field_or_attr(fld_name, table, :order)
|
406
|
-
res << "#{fld} #{direction.upcase}"
|
407
|
-
elsif fld.nil?
|
408
|
-
@errors << "invalid field '#{fld_name}'"
|
409
|
-
end
|
410
|
-
end
|
411
|
-
end
|
412
|
-
res == [] ? nil : " ORDER BY #{res.join(', ')}"
|
413
|
-
end
|
414
|
-
|
415
|
-
def parse_group_clause(group)
|
416
|
-
return @group unless group
|
417
|
-
res = []
|
418
|
-
|
419
|
-
group.split(',').each do |field|
|
420
|
-
if fld = map_field(field, table, :group)
|
421
|
-
res << fld
|
422
|
-
else
|
423
|
-
@errors << "invalid field '#{field}'"
|
424
|
-
end
|
425
|
-
end
|
426
|
-
res == [] ? nil : " GROUP BY #{res.join(', ')}"
|
427
|
-
end
|
428
|
-
|
429
|
-
def parse_limit_clause(limit)
|
430
|
-
return @limit unless limit
|
431
|
-
if limit.kind_of?(Fixnum)
|
432
|
-
" LIMIT #{limit}"
|
433
|
-
elsif limit =~ /^\s*(\d+)\s*,\s*(\d+)/
|
434
|
-
@offset = " OFFSET #{$1}"
|
435
|
-
" LIMIT #{$2}"
|
436
|
-
elsif limit =~ /(\d+)/
|
437
|
-
" LIMIT #{$1}"
|
438
|
-
else
|
439
|
-
@errors << "invalid limit clause '#{limit}'"
|
440
|
-
nil
|
441
|
-
end
|
442
|
-
end
|
443
|
-
|
444
|
-
def parse_paginate_clause(paginate)
|
445
|
-
return @offset unless paginate
|
446
|
-
if !@limit
|
447
|
-
# TODO: raise error ?
|
448
|
-
@errors << "invalid paginate clause '#{paginate}' (used without limit)"
|
449
|
-
nil
|
450
|
-
elsif (fld = map_literal(paginate, :ruby)) && (page_size = @limit[/ LIMIT (\d+)/,1])
|
451
|
-
@page_size = [2,page_size.to_i].max
|
452
|
-
" OFFSET #{insert_bind("((#{fld}.to_i > 0 ? #{fld}.to_i : 1)-1)*#{@page_size}")}"
|
453
|
-
else
|
454
|
-
@errors << "invalid paginate clause '#{paginate}'"
|
455
|
-
nil
|
456
|
-
end
|
457
|
-
end
|
458
|
-
|
459
|
-
def parse_offset_clause(offset)
|
460
|
-
return @offset unless offset
|
461
|
-
if !@limit
|
462
|
-
# TODO: raise error ?
|
463
|
-
@errors << "invalid offset clause '#{offset}' (used without limit)"
|
464
|
-
nil
|
465
|
-
elsif offset.strip =~ /^\d+$/
|
466
|
-
" OFFSET #{offset}"
|
467
|
-
else
|
468
|
-
@errors << "invalid offset clause '#{offset}'"
|
469
|
-
nil
|
470
|
-
end
|
471
|
-
end
|
472
|
-
|
473
|
-
def add_table(use_name, table_name = nil)
|
474
|
-
table_name ||= use_name
|
475
|
-
if !@table_counter[use_name]
|
476
|
-
@table_counter[use_name] = 0
|
477
|
-
if use_name != table_name
|
478
|
-
@tables << "#{table_name} as #{use_name}"
|
479
|
-
else
|
480
|
-
@tables << table_name
|
481
|
-
end
|
482
|
-
else
|
483
|
-
@table_counter[use_name] += 1
|
484
|
-
@tables << "#{table_name} AS #{table(use_name)}"
|
485
|
-
end
|
486
|
-
end
|
487
|
-
|
488
|
-
# return a unique table name for the current sub-query context, adding the table when necessary
|
489
|
-
def needs_table(table1, table2, filter)
|
490
|
-
@needed_tables[table2] ||= {}
|
491
|
-
@needed_tables[table2][table] ||= begin
|
492
|
-
add_table(table2)
|
493
|
-
@where << filter.gsub('TABLE1', table).gsub('TABLE2', table(table2))
|
494
|
-
table(table2)
|
495
|
-
end
|
496
|
-
end
|
497
|
-
|
498
|
-
# versions LEFT JOIN dyn_attributes ON ...
|
499
|
-
def needs_join_table(table1, type, table2, clause, join_name = nil)
|
500
|
-
join_name ||= "#{table1}=#{type}=#{table2}"
|
501
|
-
@needed_join_tables[join_name] ||= {}
|
502
|
-
@needed_join_tables[join_name][table] ||= begin
|
503
|
-
# define join for this part ('table' = unique for each part)
|
504
|
-
|
505
|
-
first_table = table(table1)
|
506
|
-
|
507
|
-
if !@table_counter[table2]
|
508
|
-
@table_counter[table2] = 0
|
509
|
-
second_table = table2
|
510
|
-
else
|
511
|
-
@table_counter[table2] += 1
|
512
|
-
second_table = "#{table2} AS #{table(table2)}"
|
513
|
-
end
|
514
|
-
@join_tables[first_table] ||= []
|
515
|
-
@join_tables[first_table] << "#{type} JOIN #{second_table} ON #{clause.gsub('TABLE1',table(table1)).gsub('TABLE2',table(table2))}"
|
516
|
-
table(table2)
|
517
|
-
end
|
518
|
-
end
|
519
|
-
|
520
|
-
def table_counter(table_name)
|
521
|
-
@table_counter[table_name] || 0
|
522
|
-
end
|
523
|
-
|
524
|
-
def table_at(table_name, index)
|
525
|
-
if index < 0
|
526
|
-
return nil # no table at this address
|
527
|
-
end
|
528
|
-
index == 0 ? table_name : "#{table_name[0..1]}#{index}"
|
529
|
-
end
|
530
|
-
|
531
|
-
def table(table_name=main_table, index=0)
|
532
|
-
table_at(table_name, table_counter(table_name) + index)
|
533
|
-
end
|
534
|
-
|
535
|
-
def merge_alternate_queries(alt_queries)
|
536
|
-
counter = 1
|
537
|
-
if valid?
|
538
|
-
counter = 1
|
539
|
-
else
|
540
|
-
if @ignore_warnings
|
541
|
-
# reset current query
|
542
|
-
@tables = []
|
543
|
-
@join_tables = {}
|
544
|
-
@where = []
|
545
|
-
@errors = []
|
546
|
-
@distinct = nil
|
547
|
-
end
|
548
|
-
counter = 0
|
549
|
-
end
|
550
|
-
|
551
|
-
if @where.compact == []
|
552
|
-
where_list = []
|
553
|
-
else
|
554
|
-
where_list = [@where.compact.reverse.join(' AND ')]
|
555
|
-
end
|
556
|
-
|
557
|
-
alt_queries.each do |query|
|
558
|
-
next unless query.main_class == self.main_class # no mixed class target !
|
559
|
-
@errors += query.errors unless @ignore_warnings
|
560
|
-
next unless query.valid?
|
561
|
-
query.where.compact!
|
562
|
-
next if query.where.empty?
|
563
|
-
counter += 1
|
564
|
-
merge_tables(query)
|
565
|
-
@distinct ||= query.distinct
|
566
|
-
where_list << query.where.reverse.join(' AND ')
|
567
|
-
end
|
568
|
-
|
569
|
-
@where_list = where_list
|
570
|
-
|
571
|
-
@tables.uniq!
|
572
|
-
|
573
|
-
fix_where_list(where_list)
|
574
|
-
|
575
|
-
if counter > 1
|
576
|
-
@distinct = @tables.size > 1
|
577
|
-
@where = ["((#{where_list.join(') OR (')}))"]
|
578
|
-
else
|
579
|
-
@where = where_list
|
580
|
-
end
|
581
|
-
end
|
582
|
-
|
583
|
-
def merge_tables(sub_query)
|
584
|
-
@tables += sub_query.tables
|
585
|
-
sub_query.join_tables.each do |k,v|
|
586
|
-
@join_tables[k] ||= []
|
587
|
-
@join_tables[k] << v
|
588
|
-
end
|
589
|
-
end
|
14
|
+
def self.included(base)
|
15
|
+
base.extend ClassMethods
|
16
|
+
class << base
|
17
|
+
attr_accessor :query_compiler
|
590
18
|
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
elsif value.kind_of?(Hash)
|
595
|
-
value.each do |k,v|
|
596
|
-
if v.kind_of?(Array)
|
597
|
-
value[k] = v.map {|e| parse_custom_query_argument(key, e)}
|
598
|
-
else
|
599
|
-
value[k] = parse_custom_query_argument(key, v)
|
600
|
-
end
|
601
|
-
end
|
602
|
-
else
|
603
|
-
parse_custom_query_argument(key, value)
|
19
|
+
# Inheritable accessor
|
20
|
+
def query_compiler
|
21
|
+
@query_compiler ||= (superclass.respond_to?(:query_compiler) ? superclass.query_compiler : nil)
|
604
22
|
end
|
605
23
|
end
|
24
|
+
end
|
606
25
|
|
607
|
-
|
608
|
-
def
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
res = list.first
|
613
|
-
if res =~ /\A(.*) AS #{fld}\Z/
|
614
|
-
res = $1
|
615
|
-
end
|
616
|
-
return context == :filter ? "(#{res})" : res
|
617
|
-
elsif table_name
|
618
|
-
map_field(fld, table_name, context)
|
619
|
-
else
|
620
|
-
map_attr(fld)
|
621
|
-
end
|
622
|
-
end
|
623
|
-
|
624
|
-
def build_statement(type = :find)
|
625
|
-
statement = type == :find ? find_statement : count_statement
|
626
|
-
|
627
|
-
# get bind variables
|
628
|
-
bind_values = []
|
629
|
-
statement.gsub!(/\[\[(.*?)\]\]/) do
|
630
|
-
bind_values << $1
|
631
|
-
'?'
|
632
|
-
end
|
633
|
-
[statement, bind_values]
|
634
|
-
end
|
635
|
-
|
636
|
-
def find_statement
|
637
|
-
table_list = []
|
638
|
-
@tables.each do |t|
|
639
|
-
table_name = t.split(/\s+/).last # objects AS ob1
|
640
|
-
if joins = @join_tables[table_name]
|
641
|
-
table_list << "#{t} #{joins.join(' ')}"
|
642
|
-
else
|
643
|
-
table_list << t
|
644
|
-
end
|
645
|
-
end
|
646
|
-
|
647
|
-
group = @group
|
648
|
-
if !group && @distinct
|
649
|
-
group = @tables.size > 1 ? " GROUP BY #{table}.id" : " GROUP BY id"
|
650
|
-
end
|
651
|
-
|
652
|
-
|
653
|
-
"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
|
654
|
-
end
|
655
|
-
|
656
|
-
def count_statement
|
657
|
-
table_list = []
|
658
|
-
@tables.each do |t|
|
659
|
-
table_name = t.split(/\s+/).last # objects AS ob1
|
660
|
-
if joins = @join_tables[table_name]
|
661
|
-
table_list << "#{t} #{joins.join(' ')}"
|
662
|
-
else
|
663
|
-
table_list << t
|
664
|
-
end
|
665
|
-
end
|
666
|
-
|
667
|
-
if @group =~ /GROUP\s+BY\s+(.+)/
|
668
|
-
# we need to COALESCE in order to count groups where $1 is NULL.
|
669
|
-
fields = $1.split(",").map{|f| "COALESCE(#{f.strip},0)"}.join(",")
|
670
|
-
count_on = "COUNT(DISTINCT #{fields})"
|
671
|
-
elsif @distinct
|
672
|
-
count_on = "COUNT(DISTINCT #{table}.id)"
|
673
|
-
else
|
674
|
-
count_on = "COUNT(*)"
|
675
|
-
end
|
676
|
-
|
677
|
-
"SELECT #{count_on} FROM #{table_list.flatten.join(',')}" + (@where == [] ? '' : " WHERE #{@where.reverse.join(' AND ')}")
|
678
|
-
end
|
679
|
-
|
680
|
-
# Adapted from Rail's ActiveRecord code. We need "eval" because
|
681
|
-
# QueryBuilder is a compiler and it has absolutely no knowledge
|
682
|
-
# of the running context.
|
683
|
-
def eval_bound_value(value_as_string, connection, bindings)
|
684
|
-
value = eval(value_as_string, bindings)
|
685
|
-
if value.respond_to?(:map) && !value.kind_of?(String) #!value.acts_like?(:string)
|
686
|
-
if value.respond_to?(:empty?) && value.empty?
|
687
|
-
connection.quote(nil)
|
688
|
-
else
|
689
|
-
value.map { |v| connection.quote(v) }.join(',')
|
690
|
-
end
|
691
|
-
else
|
692
|
-
connection.quote(value)
|
26
|
+
module ClassMethods
|
27
|
+
def build_query(count, pseudo_sql, opts = {})
|
28
|
+
raise Exception.new("No query_compiler for #{self}") unless query_compiler
|
29
|
+
if count == :first
|
30
|
+
opts[:limit] = 1
|
693
31
|
end
|
32
|
+
opts[:rubyless_helper] ||= self
|
33
|
+
query_compiler.new(pseudo_sql, opts.merge(:custom_query_group => query_group)).query
|
694
34
|
end
|
695
35
|
|
696
|
-
def
|
697
|
-
eval "#{main_class}.connection", bindings
|
698
|
-
end
|
699
|
-
|
700
|
-
# ******** Overwrite these **********
|
701
|
-
def class_from_table(table_name)
|
702
|
-
Object
|
703
|
-
end
|
704
|
-
|
705
|
-
def default_context_filter
|
706
|
-
raise NameError.new("default_context_filter not defined for class #{self.class}")
|
707
|
-
end
|
708
|
-
|
709
|
-
# Default sort order
|
710
|
-
def default_order_clause
|
711
|
-
nil
|
712
|
-
end
|
713
|
-
|
714
|
-
def after_parse
|
715
|
-
# do nothing
|
716
|
-
end
|
717
|
-
|
718
|
-
def parse_change_class(rel, is_last)
|
36
|
+
def query_group
|
719
37
|
nil
|
720
38
|
end
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
@where << "#{field_or_attr(fields[0])} = #{field_or_attr(fields[1], table(main_table,-1))}" if fields != :void
|
734
|
-
else
|
735
|
-
@errors << "invalid context '#{clause}'"
|
736
|
-
end
|
737
|
-
end
|
738
|
-
|
739
|
-
# Map a litteral value to be used inside a query
|
740
|
-
def map_literal(value, env = :sql)
|
741
|
-
env == :sql ? insert_bind(value.inspect) : value
|
742
|
-
end
|
743
|
-
|
744
|
-
|
745
|
-
# Overwrite this and take car to check for valid fields.
|
746
|
-
def map_field(fld, table_name, context = nil)
|
747
|
-
if fld == 'id'
|
748
|
-
"#{table_name}.#{fld}"
|
749
|
-
else
|
750
|
-
# TODO: error, raise / ignore ?
|
751
|
-
end
|
752
|
-
end
|
753
|
-
|
754
|
-
def map_attr(fld)
|
755
|
-
insert_bind(fld.to_s)
|
756
|
-
end
|
757
|
-
|
758
|
-
# ******** And maybe overwrite these **********
|
759
|
-
def parse_custom_query_argument(key, value)
|
760
|
-
return nil unless value
|
761
|
-
value = value.gsub('REF_DATE', @ref_date)
|
762
|
-
case key
|
763
|
-
when :order
|
764
|
-
" ORDER BY #{value}"
|
765
|
-
when :group
|
766
|
-
" GROUP BY #{value}"
|
767
|
-
else
|
768
|
-
value
|
769
|
-
end
|
770
|
-
end
|
771
|
-
|
772
|
-
def extract_custom_query(list)
|
773
|
-
list[-1].split(' ').first
|
774
|
-
end
|
775
|
-
|
776
|
-
private
|
777
|
-
|
778
|
-
def parse_elements(elements)
|
779
|
-
# "final_parser" is the parser who will respond to 'to_sql'. It might be a sub-parser for another class.
|
780
|
-
@final_parser = self
|
781
|
-
|
782
|
-
if @@custom_queries[self.class] &&
|
783
|
-
@@custom_queries[self.class][@opts[:custom_query_group]] &&
|
784
|
-
custom_query = @@custom_queries[self.class][@opts[:custom_query_group]][extract_custom_query(elements)]
|
785
|
-
custom_query.each do |k,v|
|
786
|
-
instance_variable_set("@#{k}", prepare_custom_query_arguments(k.to_sym, v))
|
787
|
-
end
|
788
|
-
# set table counters
|
789
|
-
@tables.each do |t|
|
790
|
-
base, as, tbl = t.split(' ')
|
791
|
-
@table_counter[base] ||= 0
|
792
|
-
@table_counter[base] += 1 if tbl
|
793
|
-
end
|
794
|
-
# parse filters
|
795
|
-
clause, filters = elements[-1].split(/\s+where\s+/)
|
796
|
-
|
797
|
-
parse_filters(filters) if filters
|
798
|
-
|
799
|
-
@limit = parse_limit_clause(@offset_limit_order_group[:limit])
|
800
|
-
if @offset_limit_order_group[:paginate]
|
801
|
-
@offset = parse_paginate_clause(@offset_limit_order_group[:paginate])
|
802
|
-
else
|
803
|
-
@offset = parse_offset_clause(@offset_limit_order_group[:offset])
|
804
|
-
end
|
805
|
-
|
806
|
-
@order = parse_order_clause(@offset_limit_order_group[:order])
|
807
|
-
else
|
808
|
-
i, new_class = 0, nil
|
809
|
-
elements.each_index do |i|
|
810
|
-
break if new_class = parse_part(elements[i], i == 0) # yes, is_last is first (parsing reverse)
|
811
|
-
end
|
812
|
-
|
813
|
-
if new_class
|
814
|
-
# move to another parser class
|
815
|
-
@final_parser = new_class.new(nil, :pre_query => self, :elements => elements[i..-1])
|
816
|
-
else
|
817
|
-
@distinct ||= elements.size > 1
|
818
|
-
@select << "#{table}.*"
|
819
|
-
|
820
|
-
merge_alternate_queries(@alt_queries) if @alt_queries
|
821
|
-
|
822
|
-
@limit = parse_limit_clause(@offset_limit_order_group[:limit])
|
823
|
-
if @offset_limit_order_group[:paginate]
|
824
|
-
@offset = parse_paginate_clause(@offset_limit_order_group[:paginate])
|
825
|
-
else
|
826
|
-
@offset = parse_offset_clause(@offset_limit_order_group[:offset])
|
827
|
-
end
|
828
|
-
|
829
|
-
|
830
|
-
@group = parse_group_clause(@offset_limit_order_group[:group])
|
831
|
-
@order = parse_order_clause(@offset_limit_order_group[:order] || default_order_clause)
|
832
|
-
end
|
833
|
-
end
|
834
|
-
|
835
|
-
if @final_parser == self
|
836
|
-
after_parse unless @opts[:skip_after_parse]
|
837
|
-
@where.compact!
|
838
|
-
end
|
839
|
-
end
|
840
|
-
|
841
|
-
def init_with_query(query, opts)
|
842
|
-
@opts = opts
|
843
|
-
|
844
|
-
if query.kind_of?(Array)
|
845
|
-
@query = query[0]
|
846
|
-
if query.size > 1
|
847
|
-
@alt_queries = query[1..-1].map {|q| self.class.new(q, opts.merge(:skip_after_parse => true))}
|
848
|
-
end
|
849
|
-
else
|
850
|
-
@query = query
|
851
|
-
end
|
852
|
-
|
853
|
-
|
854
|
-
@offset_limit_order_group = {}
|
855
|
-
if @query == nil || @query == ''
|
856
|
-
elements = [main_table]
|
857
|
-
else
|
858
|
-
elements = @query.split(' from ')
|
859
|
-
last_element = elements.last
|
860
|
-
last_element, @offset_limit_order_group[:offset] = last_element.split(' offset ')
|
861
|
-
last_element, @offset_limit_order_group[:paginate] = last_element.split(' paginate ')
|
862
|
-
last_element, @offset_limit_order_group[:limit] = last_element.split(' limit ')
|
863
|
-
last_element, @offset_limit_order_group[:order] = last_element.split(' order by ')
|
864
|
-
elements[-1], @offset_limit_order_group[:group] = last_element.split(' group by ')
|
865
|
-
end
|
866
|
-
|
867
|
-
@offset_limit_order_group[:limit] = opts[:limit] || @offset_limit_order_group[:limit]
|
868
|
-
# In order to know the table names of the dependencies, we need to parse it backwards.
|
869
|
-
# We first find the closest elements, then the final ones. For example, "pages from project" we need
|
870
|
-
# project information before getting 'pages'.
|
871
|
-
@elements = elements.reverse
|
872
|
-
|
873
|
-
@tables = []
|
874
|
-
@join_tables = {}
|
875
|
-
@table_counter = {}
|
876
|
-
@where = []
|
877
|
-
# list of tables that need to be added for filter clauses (should be added only once per part)
|
878
|
-
@needed_tables = {}
|
879
|
-
# list of tables that need to be added through a join (should be added only once per part)
|
880
|
-
@needed_join_tables = {}
|
881
|
-
|
882
|
-
@errors = []
|
883
|
-
|
884
|
-
@select = []
|
885
|
-
|
886
|
-
@ignore_warnings = opts[:ignore_warnings]
|
887
|
-
|
888
|
-
@ref_date = opts[:ref_date] ? "'#{opts[:ref_date]}'" : 'now()'
|
889
|
-
end
|
890
|
-
|
891
|
-
def init_with_pre_query(pre_query, elements)
|
892
|
-
pre_query.instance_variables.each do |iv|
|
893
|
-
next if iv == '@query' || iv == '@final_parser'
|
894
|
-
instance_variable_set(iv, pre_query.instance_variable_get(iv))
|
895
|
-
end
|
896
|
-
@just_changed_class = true
|
897
|
-
@elements = elements
|
898
|
-
end
|
899
|
-
|
900
|
-
def clause_error(clause, rest, res)
|
901
|
-
"invalid clause #{clause.inspect} near \"#{res[-2..-1]}#{rest[0..1]}\""
|
902
|
-
end
|
903
|
-
|
904
|
-
def insert_bind(str)
|
905
|
-
"[[#{str}]]"
|
906
|
-
end
|
907
|
-
|
908
|
-
# Make sure all clauses are compatible (where_list is a list of strings, not arrays)
|
909
|
-
def fix_where_list(where_list)
|
910
|
-
# do nothing
|
911
|
-
end
|
39
|
+
end # ClassMethods
|
40
|
+
end # QueryBuilder
|
41
|
+
|
42
|
+
require 'query_builder/info'
|
43
|
+
require 'query_builder/query'
|
44
|
+
require 'query_builder/error'
|
45
|
+
begin
|
46
|
+
require 'querybuilder_ext'
|
47
|
+
puts "using C parser"
|
48
|
+
rescue LoadError
|
49
|
+
require 'querybuilder_rb'
|
50
|
+
puts "using ruby parser"
|
912
51
|
end
|
52
|
+
require 'query_builder/parser'
|
53
|
+
require 'query_builder/processor'
|