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