querybuilder 0.5.9 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. data/History.txt +29 -25
  2. data/Manifest.txt +20 -9
  3. data/README.rdoc +73 -10
  4. data/Rakefile +62 -30
  5. data/lib/extconf.rb +3 -0
  6. data/lib/query_builder.rb +39 -898
  7. data/lib/query_builder/error.rb +7 -0
  8. data/lib/query_builder/info.rb +3 -0
  9. data/lib/query_builder/parser.rb +80 -0
  10. data/lib/query_builder/processor.rb +714 -0
  11. data/lib/query_builder/query.rb +273 -0
  12. data/lib/querybuilder_ext.c +1870 -0
  13. data/lib/querybuilder_ext.rl +418 -0
  14. data/lib/querybuilder_rb.rb +1686 -0
  15. data/lib/querybuilder_rb.rl +214 -0
  16. data/lib/querybuilder_syntax.rl +47 -0
  17. data/old_QueryBuilder.rb +946 -0
  18. data/querybuilder.gemspec +42 -15
  19. data/tasks/build.rake +20 -0
  20. data/test/dummy_test.rb +21 -0
  21. data/test/mock/custom_queries/test.yml +5 -4
  22. data/test/mock/dummy.rb +9 -0
  23. data/test/mock/dummy_processor.rb +160 -0
  24. data/test/mock/queries/bar.yml +1 -1
  25. data/test/mock/queries/foo.yml +2 -2
  26. data/test/mock/user_processor.rb +34 -0
  27. data/test/query_test.rb +38 -0
  28. data/test/querybuilder/basic.yml +91 -0
  29. data/test/{query_builder → querybuilder}/custom.yml +11 -11
  30. data/test/querybuilder/errors.yml +32 -0
  31. data/test/querybuilder/filters.yml +115 -0
  32. data/test/querybuilder/group.yml +7 -0
  33. data/test/querybuilder/joins.yml +37 -0
  34. data/test/querybuilder/mixed.yml +18 -0
  35. data/test/querybuilder/rubyless.yml +15 -0
  36. data/test/querybuilder_test.rb +111 -0
  37. data/test/test_helper.rb +8 -3
  38. metadata +66 -19
  39. data/test/mock/dummy_query.rb +0 -114
  40. data/test/mock/user_query.rb +0 -55
  41. data/test/query_builder/basic.yml +0 -60
  42. data/test/query_builder/errors.yml +0 -50
  43. data/test/query_builder/filters.yml +0 -43
  44. data/test/query_builder/joins.yml +0 -25
  45. data/test/query_builder/mixed.yml +0 -12
  46. data/test/query_builder_test.rb +0 -36
data/History.txt CHANGED
@@ -1,45 +1,49 @@
1
- == 0.5.9 2010-08-30
2
-
3
- * 1 major enhancement
4
- * Bug fix when using more then one 'group' in custom query definition file.
5
-
6
- == 0.5.8 2010-02-15
7
-
8
- * 1 major enhancement
9
- * Const_get code was not included in gem
10
-
11
- == 0.5.7 2010-02-08
12
-
13
- * 1 minor enhancement
14
- * Fixed class const_get to enable custom queries on classes in modules
15
-
16
- == 0.5.6 2009-10-15
17
-
18
- * 1 minor enhancement
19
- * Fixed library name (was not loaded on case sensitive systems)
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/query_builder.rb
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/dummy_query.rb
13
- test/mock/user_query.rb
14
- test/QueryBuilder/basic.yml
15
- test/QueryBuilder/errors.yml
16
- test/QueryBuilder/filters.yml
17
- test/QueryBuilder/joins.yml
18
- test/QueryBuilder/mixed.yml
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/test_QueryBuilder.rb
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://github.com/zena/querybuilder/tree/master
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 (DummyQuery) to parse your specific models (see test/mock).
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 'pathname'
2
- $LOAD_PATH.unshift((Pathname(__FILE__).dirname + 'lib').expand_path)
3
-
4
- require 'querybuilder'
1
+ require 'rubygems'
5
2
  require 'rake'
6
- require 'rake/testtask'
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 << 'lib' << 'test'
10
- test.pattern = 'test/**/**_test.rb'
11
- test.verbose = true
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' << 'lib'
18
- test.pattern = 'test/**/**_test.rb'
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
- # GEM management
31
- begin
32
- require 'jeweler'
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
- 1. protect your database from illegal SQL by securing queries
41
- 2. ease writing complex relational queries by abstracting table internals}
42
- gemspec.email = "gaspard@teti.ch"
43
- gemspec.homepage = "http://zenadmin.org/524"
44
- gemspec.authors = ["Gaspard Bucher"]
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
- gemspec.add_development_dependency('yamltest', '>= 0.5.0')
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
@@ -0,0 +1,3 @@
1
+ # file: extconf.rb
2
+ require 'mkmf'
3
+ create_makefile('querybuilder_ext')
data/lib/query_builder.rb CHANGED
@@ -1,912 +1,53 @@
1
- $:.unshift(File.dirname(__FILE__)) unless
2
- $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
-
4
- require 'yaml'
5
-
6
- =begin rdoc
7
- QueryBuilder is a tool to secure and simplify the creation of SQL queries from untrusted users.
8
-
9
- Syntax of a query is "RELATION [where ...|] [in ...|from SUB_QUERY|]".
10
- =end
11
- class QueryBuilder
12
- attr_reader :tables, :where, :errors, :join_tables, :distinct, :final_parser, :page_size
13
- VERSION = '0.5.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
- init_with_query(query, opts)
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
- protected
220
-
221
- def current_table
222
- @current_table || main_table
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
- def prepare_custom_query_arguments(key, value)
592
- if value.kind_of?(Array)
593
- value.map {|e| parse_custom_query_argument(key, e)}
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
- # Map a field to be used inside a query. An attr is a field from table at index 0 = @node attribute.
608
- def field_or_attr(fld, table_name = table, context = nil)
609
- if fld =~ /^\d+$/
610
- return fld
611
- elsif !(list = @select.select {|e| e =~ /\A#{fld}\Z|AS #{fld}|\.#{fld}\Z/}).empty?
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 get_connection(bindings)
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
- def parse_relation(clause, context)
723
- return nil
724
- end
725
-
726
- def context_filter_fields(clause, is_last = false)
727
- nil
728
- end
729
-
730
- def parse_context(clause, is_last = false)
731
-
732
- if fields = context_filter_fields(clause, is_last)
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'