querybuilder 0.5.9 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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'