directiverecord 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +9 -0
  3. data/CHANGELOG.rdoc +5 -0
  4. data/Gemfile +3 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.md +49 -0
  7. data/Rakefile +10 -0
  8. data/VERSION +1 -0
  9. data/directiverecord.gemspec +27 -0
  10. data/lib/directive_record.rb +6 -0
  11. data/lib/directive_record/gem_ext.rb +1 -0
  12. data/lib/directive_record/gem_ext/active_record.rb +2 -0
  13. data/lib/directive_record/gem_ext/active_record/base.rb +13 -0
  14. data/lib/directive_record/gem_ext/active_record/relation.rb +17 -0
  15. data/lib/directive_record/query.rb +25 -0
  16. data/lib/directive_record/query/monetdb.rb +54 -0
  17. data/lib/directive_record/query/mysql.rb +36 -0
  18. data/lib/directive_record/query/sql.rb +291 -0
  19. data/lib/directive_record/relation.rb +70 -0
  20. data/lib/directive_record/version.rb +7 -0
  21. data/lib/directiverecord.rb +1 -0
  22. data/test/application/app/models/customer.rb +6 -0
  23. data/test/application/app/models/employee.rb +5 -0
  24. data/test/application/app/models/office.rb +14 -0
  25. data/test/application/app/models/order.rb +4 -0
  26. data/test/application/app/models/order_detail.rb +4 -0
  27. data/test/application/app/models/payment.rb +3 -0
  28. data/test/application/app/models/product.rb +3 -0
  29. data/test/application/app/models/product_line.rb +3 -0
  30. data/test/application/app/models/tag.rb +3 -0
  31. data/test/application/boot.rb +27 -0
  32. data/test/application/config/database.yml +8 -0
  33. data/test/application/db/database.sql +327 -0
  34. data/test/test_helper.rb +17 -0
  35. data/test/test_helper/coverage.rb +13 -0
  36. data/test/unit/gem_ext/active_record/test_base.rb +30 -0
  37. data/test/unit/gem_ext/active_record/test_relation.rb +42 -0
  38. data/test/unit/query/test_monetdb.rb +27 -0
  39. data/test/unit/query/test_mysql.rb +238 -0
  40. data/test/unit/query/test_sql.rb +46 -0
  41. data/test/unit/test_directive_record.rb +15 -0
  42. data/test/unit/test_query.rb +40 -0
  43. data/test/unit/test_relation.rb +42 -0
  44. metadata +221 -0
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ YjYwOWJkMDk4YmUyZWZkMjcwMThiZDk4NzQ1ZTUzMzJiM2YwYzg1NQ==
5
+ data.tar.gz: !binary |-
6
+ YjA3MDE0YmRjYjA4NTc3NjMyYmZkOGEzMTc4NGUzMjk2NGMwYzgwOQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ YmUxZGI1YmYxNWIzMjMyOGM5MWY2NmFmYWMxZGRlOTEyZjFmYjI3ZWE1Mjdk
10
+ M2MwNGIxYzIzMmYwZWU5ZTNiMGJjZmQ5MmRhNjNjNjAyNzdiNmVmZjk2NjYy
11
+ NmU4YTc1ODg4OWFkMmRkZmVhMzQ3MjAxMTc1ZmFiNjZmNTI4YzA=
12
+ data.tar.gz: !binary |-
13
+ YzdiM2Q0ZDI2ODFmMGJiNWNiY2M3MWU1ODQzMjUwMWViZDMxNWU2NzRkYmEy
14
+ MTBhZTMyN2YyMTVhZWZjOTU4NGYxMjBiYmRjZDg1YTExYTYyNDRkNTVhNTRh
15
+ YjJjZDhjNDFmZDAyMjgxOWZmYzdjY2Y1NDMyN2RhZDQ4NTA4MjM=
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ .DS_Store
2
+ .bundle
3
+ .yardoc
4
+ .rvmrc
5
+ Gemfile.lock
6
+ doc
7
+ pkg
8
+ *.log
9
+ test/coverage
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,5 @@
1
+ = DirectiveRecord CHANGELOG
2
+
3
+ == Version 0.1.0 (December 1, 2014)
4
+
5
+ * Initial release
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2014 Paul Engel
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ ## DirectiveRecord
2
+
3
+ A layer on top of ActiveRecord for using paths within queries without thinking about association joins
4
+
5
+ ### Installation
6
+
7
+ Add `DirectiveRecord` in your `Gemfile`:
8
+
9
+ gem "directiverecord"
10
+
11
+ Run the following in your console to install with Bundler:
12
+
13
+ $ bundle install
14
+
15
+ ### Demo
16
+
17
+ To try `DirectiveRecord` right out-of-the-box, please clone
18
+
19
+ https://github.com/archan937/directiverecord-console
20
+
21
+ and follow the README instructions. It is provided with a sample database and a Pry console in which you can play with `DirectiveRecord`.
22
+
23
+ The README is also provided with several [straightforward examples](https://github.com/archan937/directiverecord-console#using-the-console).
24
+
25
+ ### Testing
26
+
27
+ Run the following command for testing:
28
+
29
+ $ rake
30
+
31
+ You can also run a single test file:
32
+
33
+ $ ruby test/unit/query/test_mysql.rb
34
+
35
+ ### TODO
36
+
37
+ * Add more tests
38
+
39
+ ### License
40
+
41
+ Copyright (c) 2014 Paul Engel, released under the MIT License
42
+
43
+ http://github.com/archan937 – http://twitter.com/archan937 – http://gettopup.com – pm_engel@icloud.com
44
+
45
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
46
+
47
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
48
+
49
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ task :default => :test
7
+
8
+ Rake::TestTask.new do |test|
9
+ test.pattern = "test/**/test_*.rb"
10
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path("../lib/directive_record/version", __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.author = "Paul Engel"
6
+ gem.email = "pm_engel@icloud.com"
7
+ gem.summary = %q{A layer on top of ActiveRecord for using paths within queries without thinking about association joins}
8
+ gem.description = %q{A layer on top of ActiveRecord for using paths within queries without thinking about association joins}
9
+ gem.homepage = "https://github.com/archan937/directiverecord"
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "directiverecord"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = DirectiveRecord::VERSION
17
+
18
+ gem.add_dependency "activerecord", ">= 4.0"
19
+
20
+ gem.add_development_dependency "rake"
21
+ gem.add_development_dependency "yard"
22
+ gem.add_development_dependency "pry"
23
+ gem.add_development_dependency "mysql2"
24
+ gem.add_development_dependency "simplecov"
25
+ gem.add_development_dependency "minitest"
26
+ gem.add_development_dependency "mocha"
27
+ end
@@ -0,0 +1,6 @@
1
+ require "active_record"
2
+
3
+ require "directive_record/gem_ext"
4
+ require "directive_record/relation"
5
+ require "directive_record/query"
6
+ require "directive_record/version"
@@ -0,0 +1 @@
1
+ require "directive_record/gem_ext/active_record"
@@ -0,0 +1,2 @@
1
+ require "directive_record/gem_ext/active_record/base"
2
+ require "directive_record/gem_ext/active_record/relation"
@@ -0,0 +1,13 @@
1
+ module ActiveRecord
2
+ class Base
3
+
4
+ def self.to_qry(*args)
5
+ DirectiveRecord::Query.new(self).to_sql(*args)
6
+ end
7
+
8
+ def self.qry(*args)
9
+ connection.select_rows to_qry(*args)
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ module ActiveRecord
2
+ class Relation
3
+
4
+ def qry_options(*args)
5
+ DirectiveRecord::Relation.new(self).qry_options(*args)
6
+ end
7
+
8
+ def to_qry(*args)
9
+ klass.to_qry qry_options(*args)
10
+ end
11
+
12
+ def qry(*args)
13
+ klass.qry qry_options(*args)
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ require "directive_record/query/sql"
2
+ require "directive_record/query/mysql"
3
+ require "directive_record/query/monetdb"
4
+
5
+ module DirectiveRecord
6
+ module Query
7
+
8
+ def self.new(klass)
9
+ class_for(klass.connection.class.name.downcase).new(klass)
10
+ end
11
+
12
+ private
13
+
14
+ def self.class_for(connection_class)
15
+ if connection_class.include?("mysql")
16
+ MySQL
17
+ elsif connection_class.include?("monetdb")
18
+ MonetDB
19
+ else
20
+ raise NotImplementedError
21
+ end
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,54 @@
1
+ module DirectiveRecord
2
+ module Query
3
+ class MonetDB < SQL
4
+
5
+ private
6
+
7
+ def path_delimiter
8
+ "__"
9
+ end
10
+
11
+ def aggregate_delimiter
12
+ "__"
13
+ end
14
+
15
+ def group_by_all_sql
16
+ "all_rows"
17
+ end
18
+
19
+ def prepare_options!(options)
20
+ normalize_group_by! options
21
+ [:select, :where, :having, :group_by, :order_by].each do |key|
22
+ if options[key]
23
+ base.reflections.keys.each do |association|
24
+ options[key] = [options[key]].flatten.collect{|x| x.gsub(/^#{association}\.([a-z_\.]+)/) { "#{association}_#{$1.gsub(".", "_")}" }}
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ def finalize_options!(options)
31
+ if options[:having]
32
+ if options[:numerize_aliases]
33
+ map = Hash[options[:select].scan(/,?\s?(.*?) AS ([^,]+)/).collect(&:reverse)]
34
+ options[:aliases].each{|pattern, replacement| options[:having].gsub! pattern, map[replacement]}
35
+ else
36
+ (options[:aggregates] || {}).each do |path, aggregate|
37
+ options[:having].gsub! /\b#{aggregate}__#{path}\b/, "#{aggregate.to_s.upcase}(#{path})"
38
+ end
39
+ end
40
+ end
41
+
42
+ options[:select] = options[:select].split(", ").collect do |string|
43
+ expression, query_alias = string.match(/^(.*) AS (.*)$/).try(:captures)
44
+ if query_alias
45
+ options[:group_by].to_s.include?(expression) || !expression.match(/^\w+(\.\w+)*$/) ? string : "MAX(#{expression}) AS #{query_alias}"
46
+ else
47
+ string.match(/^\w+(\.\w+)*$/) ? "MAX(#{string})" : string
48
+ end
49
+ end.join(", ") if options[:group_by]
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,36 @@
1
+ module DirectiveRecord
2
+ module Query
3
+ class MySQL < SQL
4
+
5
+ private
6
+
7
+ def path_delimiter
8
+ "."
9
+ end
10
+
11
+ def aggregate_delimiter
12
+ ":"
13
+ end
14
+
15
+ def group_by_all_sql
16
+ "NULL"
17
+ end
18
+
19
+ def quote_alias(sql_alias)
20
+ "`#{sql_alias}`"
21
+ end
22
+
23
+ def finalize_options!(options)
24
+ return unless options[:numerize_aliases]
25
+ [:group_by, :having, :order_by].each do |key|
26
+ if sql = options[key]
27
+ options[:aliases].each do |pattern, replacement|
28
+ sql.gsub! pattern, replacement
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,291 @@
1
+ module DirectiveRecord
2
+ module Query
3
+ class SQL
4
+
5
+ def initialize(base)
6
+ @base = base
7
+ end
8
+
9
+ def to_sql(*args)
10
+ options = extract_options(args)
11
+ validate_options! options
12
+
13
+ optimize_query! options
14
+
15
+ prepare_options! options
16
+ normalize_options! options
17
+
18
+ parse_joins! options
19
+
20
+ prepend_base_alias! options
21
+ finalize_options! options
22
+
23
+ compose_sql options
24
+ end
25
+
26
+ private
27
+
28
+ def path_delimiter
29
+ raise NotImplementedError
30
+ end
31
+
32
+ def aggregate_delimiter
33
+ raise NotImplementedError
34
+ end
35
+
36
+ def group_by_all_sql
37
+ raise NotImplementedError
38
+ end
39
+
40
+ def select_aggregate_sql(method, path)
41
+ "#{method.to_s.upcase}(#{path})"
42
+ end
43
+
44
+ def select_aggregate_sql_alias(method, path)
45
+ quote_alias("#{method}#{aggregate_delimiter}#{path}")
46
+ end
47
+
48
+ def base
49
+ @base
50
+ end
51
+
52
+ def base_alias
53
+ @base_alias ||= quote_alias(base.table_name.split("_").collect{|x| x[0]}.join(""))
54
+ end
55
+
56
+ def quote_alias(sql_alias)
57
+ sql_alias
58
+ end
59
+
60
+ def extract_options(args)
61
+ options = args.extract_options!.deep_dup
62
+ options.reverse_merge! :select => (args.empty? ? "*" : args)
63
+ options
64
+ end
65
+
66
+ def validate_options!(options)
67
+ options.assert_valid_keys :select, :where, :group_by, :order_by, :limit, :offset, :aggregates, :numerize_aliases
68
+ end
69
+
70
+ def optimize_query!(options)
71
+ select = [options[:select]].flatten
72
+ if options[:where] && (select != %w(id)) && select.any?{|x| x.match(/^\w+(\.\w+)+$/)}
73
+ ids = base.connection.select_values(to_sql(options.merge(:select => "id"))).uniq + [0]
74
+ options[:where] = ["id IN (#{ids.join(", ")})"]
75
+ options.delete :limit
76
+ options.delete :offset
77
+ end
78
+ end
79
+
80
+ def prepare_options!(options); end
81
+
82
+ def normalize_options!(options)
83
+ normalize_select!(options)
84
+ normalize_where!(options)
85
+ normalize_group_by!(options)
86
+ normalize_order_by!(options)
87
+ options.reject!{|k, v| v.blank?}
88
+ end
89
+
90
+ def normalize_select!(options)
91
+ select = to_array! options, :select
92
+ select.uniq!
93
+
94
+ options[:scales] = select.inject({}) do |hash, sql|
95
+ if scale = column_for(sql).try(:scale)
96
+ hash[sql] = scale
97
+ end
98
+ hash
99
+ end
100
+
101
+ options[:aggregated] = {}
102
+ options[:aliases] = {}
103
+
104
+ options[:select] = options[:select].inject([]) do |array, path|
105
+ sql, sql_alias = ((path == ".*") ? "#{base_alias}.*" : path), nil
106
+
107
+ if aggregate_method = (options[:aggregates] || {})[path]
108
+ sql = select_aggregate_sql(aggregate_method, path)
109
+ sql_alias = options[:aggregated][path] = select_aggregate_sql_alias(aggregate_method, path)
110
+ end
111
+ if scale = options[:scales][path]
112
+ sql = "ROUND(#{sql}, #{scale})"
113
+ sql_alias ||= quote_alias(path)
114
+ end
115
+ if options[:numerize_aliases]
116
+ sql = sql.gsub(/ AS .*$/, "")
117
+ sql_alias = options[:aliases][prepend_base_alias(sql_alias || sql)] = "c#{array.size + 1}"
118
+ end
119
+
120
+ array << [sql, sql_alias].compact.join(" AS ")
121
+ array
122
+ end
123
+ end
124
+
125
+ def normalize_where!(options)
126
+ regexp = /^\S+/
127
+
128
+ where, having = (to_array!(options, :where) || []).partition do |statement|
129
+ !options[:aggregated].keys.include?(statement.strip.match(regexp).to_s) &&
130
+ statement.downcase.gsub(/((?<![\\])['"])((?:.(?!(?<![\\])\1))*.?)\1/, " ")
131
+ .scan(/([a-zA-Z_\.]+)?\s*(=|<=>|>=|>|<=|<|<>|!=|is|like|rlike|regexp|in|between|not|sounds|soundex)(\b|\s)/)
132
+ .all? do |(path, operator)|
133
+ path && column_for(path)
134
+ end
135
+ end
136
+
137
+ unless (attrs = base.scope_attributes).blank?
138
+ sql = base.send(:sanitize_sql_for_conditions, attrs, "").gsub(/``.`(\w+)`/) { $1 }
139
+ where << sql
140
+ end
141
+
142
+ options[:where], options[:having] = where, having.collect do |statement|
143
+ statement.strip.gsub(regexp){|path| options[:aggregated][path] || path}
144
+ end
145
+
146
+ [:where, :having].each do |key|
147
+ value = options[key]
148
+ options[key] = (value.collect{|x| "(#{x})"}.join(" AND ") unless value.empty?)
149
+ end
150
+ end
151
+
152
+ def normalize_group_by!(options)
153
+ group_by = to_array! options, :group_by
154
+ group_by.clear.push(group_by_all_sql) if group_by == [:all]
155
+ end
156
+
157
+ def normalize_order_by!(options)
158
+ options[:order_by] ||= (options[:group_by] || []).collect do |path|
159
+ direction = "DESC" if path.to_s == "date"
160
+ "#{path} #{direction}".strip
161
+ end
162
+
163
+ to_array!(options, :order_by).collect! do |x|
164
+ segments = x.split " "
165
+ direction = segments.pop if %w(asc desc).include?(segments[-1].downcase)
166
+ path = segments.join " "
167
+
168
+ if path != group_by_all_sql
169
+ scale = options[:scales][path]
170
+ select = begin
171
+ if aggregate_method = (options[:aggregates] || {})[path]
172
+ select_aggregate_sql(aggregate_method, path)
173
+ else
174
+ path
175
+ end
176
+ end
177
+ "#{scale ? "ROUND(#{select}, #{scale})" : select} #{direction.upcase if direction}".strip
178
+ end
179
+ end
180
+
181
+ options[:order_by].compact!
182
+ end
183
+
184
+ def to_array!(options, key)
185
+ if value = options[key]
186
+ options[key] = [value].flatten
187
+ end
188
+ end
189
+
190
+ def column_for(path)
191
+ segments = path.split(".")
192
+ column = segments.pop
193
+ model = segments.inject(base) do |klass, association|
194
+ klass.reflect_on_association(association.to_sym).klass
195
+ end
196
+ model.columns_hash[column]
197
+ rescue
198
+ nil
199
+ end
200
+
201
+ def parse_joins!(options)
202
+ return if (paths = extract_paths(options)).empty?
203
+
204
+ regexp = /INNER JOIN `([^`]+)`( `[^`]+`)? ON `[^`]+`.`([^`]+)` = `[^`]+`.`([^`]+)`/
205
+
206
+ options[:joins] = paths.collect do |path|
207
+ joins, associations = [], []
208
+ path.split(".").inject(base) do |klass, association|
209
+ association = association.to_sym
210
+
211
+ table_joins = klass.joins(association).to_sql.scan regexp
212
+ concerns_bridge_table = table_joins.size == 2
213
+ bridge_table_as = nil
214
+
215
+ table_joins.each_with_index do |table_join, index|
216
+ concerns_bridge_table_join = concerns_bridge_table && index == 0
217
+ join_table, possible_alias, join_table_column, table_column = table_join
218
+
219
+ table_as = (klass == base) ? base_alias : quote_alias(associations.join(path_delimiter))
220
+ join_table_as = quote_alias((associations + [association]).join(path_delimiter))
221
+
222
+ if concerns_bridge_table
223
+ if concerns_bridge_table_join
224
+ join_table_as = bridge_table_as = quote_alias("#{(associations + [association]).join(path_delimiter)}_bridge_table")
225
+ else
226
+ table_as = bridge_table_as
227
+ end
228
+ end
229
+
230
+ joins.push "LEFT JOIN #{join_table} #{join_table_as} ON #{join_table_as}.#{join_table_column} = #{table_as}.#{table_column}"
231
+ end
232
+
233
+ associations << association
234
+ klass.reflect_on_association(association).klass
235
+ end
236
+ joins
237
+ end.flatten.uniq.join("\n")
238
+ end
239
+
240
+ def extract_paths(options)
241
+ [:select, :where, :group_by, :having, :order_by].inject([]) do |paths, key|
242
+ if value = options[key]
243
+ value = value.join " " if value.is_a?(Array)
244
+ paths.concat value.gsub(/((?<![\\])['"])((?:.(?!(?<![\\])\1))*.?)\1/, " ").scan(/[a-zA-Z_]+\.[a-zA-Z_\.]+/).collect{|x| x.split(".")[0..-2].join "."}
245
+ else
246
+ paths
247
+ end
248
+ end.uniq
249
+ end
250
+
251
+ def prepend_base_alias!(options)
252
+ [:select, :where, :group_by, :having, :order_by].each do |key|
253
+ if value = options[key]
254
+ options[key] = prepend_base_alias value, options[:aliases]
255
+ end
256
+ end
257
+ end
258
+
259
+ def prepend_base_alias(sql, aliases = {})
260
+ columns = base.columns_hash.keys
261
+ sql = sql.join ", " if sql.is_a?(Array)
262
+ sql.gsub(/("[^"]*"|'[^']*'|`[^`]*`|[a-zA-Z_#{aggregate_delimiter}]+(\.[a-zA-Z_\*]+)*)/) do
263
+ columns.include?($1) ? "#{base_alias}.#{$1}" : begin
264
+ if (string = $1).match /^([a-zA-Z_\.]+)\.([a-zA-Z_\*]+)$/
265
+ path, column = $1, $2
266
+ "#{quote_alias path.gsub(".", path_delimiter)}.#{column}"
267
+ else
268
+ string
269
+ end
270
+ end
271
+ end
272
+ end
273
+
274
+ def finalize_options!(options); end
275
+
276
+ def compose_sql(options)
277
+ sql = ["SELECT #{options[:select]}", "FROM #{base.table_name} #{base_alias}", options[:joins]].compact
278
+
279
+ [:where, :group_by, :having, :order_by, :limit, :offset].each do |key|
280
+ unless (value = options[key]).blank?
281
+ keyword = key.to_s.upcase.gsub("_", " ")
282
+ sql << "#{keyword} #{value}"
283
+ end
284
+ end
285
+
286
+ sql.join "\n"
287
+ end
288
+
289
+ end
290
+ end
291
+ end