jwulff-composite_primary_keys 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/VERSION.yml +4 -0
- data/lib/adapter_helper/base.rb +63 -0
- data/lib/adapter_helper/mysql.rb +13 -0
- data/lib/adapter_helper/oracle.rb +12 -0
- data/lib/adapter_helper/postgresql.rb +13 -0
- data/lib/adapter_helper/sqlite3.rb +13 -0
- data/lib/composite_primary_keys.rb +55 -0
- data/lib/composite_primary_keys/association_preload.rb +236 -0
- data/lib/composite_primary_keys/associations.rb +428 -0
- data/lib/composite_primary_keys/attribute_methods.rb +84 -0
- data/lib/composite_primary_keys/base.rb +320 -0
- data/lib/composite_primary_keys/calculations.rb +68 -0
- data/lib/composite_primary_keys/composite_arrays.rb +30 -0
- data/lib/composite_primary_keys/connection_adapters/ibm_db_adapter.rb +21 -0
- data/lib/composite_primary_keys/connection_adapters/oracle_adapter.rb +15 -0
- data/lib/composite_primary_keys/connection_adapters/postgresql_adapter.rb +53 -0
- data/lib/composite_primary_keys/connection_adapters/sqlite3_adapter.rb +15 -0
- data/lib/composite_primary_keys/fixtures.rb +8 -0
- data/lib/composite_primary_keys/migration.rb +20 -0
- data/lib/composite_primary_keys/reflection.rb +19 -0
- data/lib/composite_primary_keys/version.rb +8 -0
- metadata +77 -0
@@ -0,0 +1,84 @@
|
|
1
|
+
module CompositePrimaryKeys
|
2
|
+
module ActiveRecord
|
3
|
+
module AttributeMethods #:nodoc:
|
4
|
+
def self.append_features(base)
|
5
|
+
super
|
6
|
+
base.send(:extend, ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
# Define an attribute reader method. Cope with nil column.
|
11
|
+
def define_read_method(symbol, attr_name, column)
|
12
|
+
cast_code = column.type_cast_code('v') if column
|
13
|
+
cast_code = "::#{cast_code}" if cast_code && cast_code.match('ActiveRecord::.*')
|
14
|
+
access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
|
15
|
+
|
16
|
+
unless self.primary_keys.include?(attr_name.to_sym)
|
17
|
+
access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
|
18
|
+
end
|
19
|
+
|
20
|
+
if cache_attribute?(attr_name)
|
21
|
+
access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
|
22
|
+
end
|
23
|
+
|
24
|
+
evaluate_attribute_method attr_name, "def #{symbol}; #{access_code}; end"
|
25
|
+
end
|
26
|
+
|
27
|
+
# Evaluate the definition for an attribute related method
|
28
|
+
def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name)
|
29
|
+
unless primary_keys.include?(method_name.to_sym)
|
30
|
+
generated_methods << method_name
|
31
|
+
end
|
32
|
+
|
33
|
+
begin
|
34
|
+
class_eval(method_definition, __FILE__, __LINE__)
|
35
|
+
rescue SyntaxError => err
|
36
|
+
generated_methods.delete(attr_name)
|
37
|
+
if logger
|
38
|
+
logger.warn "Exception occurred during reader method compilation."
|
39
|
+
logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
|
40
|
+
logger.warn "#{err.message}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Allows access to the object attributes, which are held in the @attributes hash, as though they
|
47
|
+
# were first-class methods. So a Person class with a name attribute can use Person#name and
|
48
|
+
# Person#name= and never directly use the attributes hash -- except for multiple assigns with
|
49
|
+
# ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
|
50
|
+
# the completed attribute is not nil or 0.
|
51
|
+
#
|
52
|
+
# It's also possible to instantiate related objects, so a Client class belonging to the clients
|
53
|
+
# table with a master_id foreign key can instantiate master through Client#master.
|
54
|
+
def method_missing(method_id, *args, &block)
|
55
|
+
method_name = method_id.to_s
|
56
|
+
|
57
|
+
# If we haven't generated any methods yet, generate them, then
|
58
|
+
# see if we've created the method we're looking for.
|
59
|
+
if !self.class.generated_methods?
|
60
|
+
self.class.define_attribute_methods
|
61
|
+
|
62
|
+
if self.class.generated_methods.include?(method_name)
|
63
|
+
return self.send(method_id, *args, &block)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
if self.class.primary_keys.include?(method_name.to_sym)
|
68
|
+
ids[self.class.primary_keys.index(method_name.to_sym)]
|
69
|
+
elsif md = self.class.match_attribute_method?(method_name)
|
70
|
+
attribute_name, method_type = md.pre_match, md.to_s
|
71
|
+
if @attributes.include?(attribute_name)
|
72
|
+
__send__("attribute#{method_type}", attribute_name, *args, &block)
|
73
|
+
else
|
74
|
+
super
|
75
|
+
end
|
76
|
+
elsif @attributes.include?(method_name)
|
77
|
+
read_attribute(method_name)
|
78
|
+
else
|
79
|
+
super
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,320 @@
|
|
1
|
+
module CompositePrimaryKeys
|
2
|
+
module ActiveRecord #:nodoc:
|
3
|
+
class CompositeKeyError < StandardError #:nodoc:
|
4
|
+
end
|
5
|
+
|
6
|
+
module Base #:nodoc:
|
7
|
+
|
8
|
+
INVALID_FOR_COMPOSITE_KEYS = 'Not appropriate for composite primary keys'
|
9
|
+
NOT_IMPLEMENTED_YET = 'Not implemented for composite primary keys yet'
|
10
|
+
|
11
|
+
def self.append_features(base)
|
12
|
+
super
|
13
|
+
base.send(:include, InstanceMethods)
|
14
|
+
base.extend(ClassMethods)
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def set_primary_keys(*keys)
|
19
|
+
keys = keys.first if keys.first.is_a?(Array)
|
20
|
+
keys = keys.map { |k| k.to_sym }
|
21
|
+
cattr_accessor :primary_keys
|
22
|
+
self.primary_keys = keys.to_composite_keys
|
23
|
+
|
24
|
+
class_eval <<-EOV
|
25
|
+
extend CompositeClassMethods
|
26
|
+
include CompositeInstanceMethods
|
27
|
+
|
28
|
+
include CompositePrimaryKeys::ActiveRecord::Associations
|
29
|
+
include CompositePrimaryKeys::ActiveRecord::AssociationPreload
|
30
|
+
include CompositePrimaryKeys::ActiveRecord::Calculations
|
31
|
+
include CompositePrimaryKeys::ActiveRecord::AttributeMethods
|
32
|
+
EOV
|
33
|
+
end
|
34
|
+
|
35
|
+
def composite?
|
36
|
+
false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module InstanceMethods
|
41
|
+
def composite?; self.class.composite?; end
|
42
|
+
end
|
43
|
+
|
44
|
+
module CompositeInstanceMethods
|
45
|
+
|
46
|
+
# A model instance's primary keys is always available as model.ids
|
47
|
+
# whether you name it the default 'id' or set it to something else.
|
48
|
+
def id
|
49
|
+
attr_names = self.class.primary_keys
|
50
|
+
CompositeIds.new(attr_names.map { |attr_name| read_attribute(attr_name) })
|
51
|
+
end
|
52
|
+
alias_method :ids, :id
|
53
|
+
|
54
|
+
def to_param
|
55
|
+
id.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
def id_before_type_cast #:nodoc:
|
59
|
+
raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::NOT_IMPLEMENTED_YET
|
60
|
+
end
|
61
|
+
|
62
|
+
def quoted_id #:nodoc:
|
63
|
+
[self.class.primary_keys, ids].
|
64
|
+
transpose.
|
65
|
+
map {|attr_name,id| quote_value(id, column_for_attribute(attr_name))}.
|
66
|
+
to_composite_ids
|
67
|
+
end
|
68
|
+
|
69
|
+
# Sets the primary ID.
|
70
|
+
def id=(ids)
|
71
|
+
ids = ids.split(ID_SEP) if ids.is_a?(String)
|
72
|
+
ids.flatten!
|
73
|
+
unless ids.is_a?(Array) and ids.length == self.class.primary_keys.length
|
74
|
+
raise "#{self.class}.id= requires #{self.class.primary_keys.length} ids"
|
75
|
+
end
|
76
|
+
[primary_keys, ids].transpose.each {|key, an_id| write_attribute(key , an_id)}
|
77
|
+
id
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns a clone of the record that hasn't been assigned an id yet and
|
81
|
+
# is treated as a new record. Note that this is a "shallow" clone:
|
82
|
+
# it copies the object's attributes only, not its associations.
|
83
|
+
# The extent of a "deep" clone is application-specific and is therefore
|
84
|
+
# left to the application to implement according to its need.
|
85
|
+
def clone
|
86
|
+
attrs = self.attributes_before_type_cast
|
87
|
+
self.class.primary_keys.each {|key| attrs.delete(key.to_s)}
|
88
|
+
self.class.new do |record|
|
89
|
+
record.send :instance_variable_set, '@attributes', attrs
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
private
|
95
|
+
# The xx_without_callbacks methods are overwritten as that is the end of the alias chain
|
96
|
+
|
97
|
+
# Creates a new record with values matching those of the instance attributes.
|
98
|
+
def create_without_callbacks
|
99
|
+
unless self.id
|
100
|
+
raise CompositeKeyError, "Composite keys do not generated ids from sequences, you must provide id values"
|
101
|
+
end
|
102
|
+
attributes_minus_pks = attributes_with_quotes(false)
|
103
|
+
quoted_pk_columns = self.class.primary_key.map { |col| connection.quote_column_name(col) }
|
104
|
+
cols = quoted_column_names(attributes_minus_pks) << quoted_pk_columns
|
105
|
+
vals = attributes_minus_pks.values << quoted_id
|
106
|
+
connection.insert(
|
107
|
+
"INSERT INTO #{self.class.quoted_table_name} " +
|
108
|
+
"(#{cols.join(', ')}) " +
|
109
|
+
"VALUES (#{vals.join(', ')})",
|
110
|
+
"#{self.class.name} Create",
|
111
|
+
self.class.primary_key,
|
112
|
+
self.id
|
113
|
+
)
|
114
|
+
@new_record = false
|
115
|
+
return true
|
116
|
+
end
|
117
|
+
|
118
|
+
# Updates the associated record with values matching those of the instance attributes.
|
119
|
+
def update_without_callbacks
|
120
|
+
where_clause_terms = [self.class.primary_key, quoted_id].transpose.map do |pair|
|
121
|
+
"(#{connection.quote_column_name(pair[0])} = #{pair[1]})"
|
122
|
+
end
|
123
|
+
where_clause = where_clause_terms.join(" AND ")
|
124
|
+
connection.update(
|
125
|
+
"UPDATE #{self.class.quoted_table_name} " +
|
126
|
+
"SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " +
|
127
|
+
"WHERE #{where_clause}",
|
128
|
+
"#{self.class.name} Update"
|
129
|
+
)
|
130
|
+
return true
|
131
|
+
end
|
132
|
+
|
133
|
+
# Deletes the record in the database and freezes this instance to reflect that no changes should
|
134
|
+
# be made (since they can't be persisted).
|
135
|
+
def destroy_without_callbacks
|
136
|
+
where_clause_terms = [self.class.primary_key, quoted_id].transpose.map do |pair|
|
137
|
+
"(#{connection.quote_column_name(pair[0])} = #{pair[1]})"
|
138
|
+
end
|
139
|
+
where_clause = where_clause_terms.join(" AND ")
|
140
|
+
unless new_record?
|
141
|
+
connection.delete(
|
142
|
+
"DELETE FROM #{self.class.quoted_table_name} " +
|
143
|
+
"WHERE #{where_clause}",
|
144
|
+
"#{self.class.name} Destroy"
|
145
|
+
)
|
146
|
+
end
|
147
|
+
freeze
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
module CompositeClassMethods
|
152
|
+
def primary_key; primary_keys; end
|
153
|
+
def primary_key=(keys); primary_keys = keys; end
|
154
|
+
|
155
|
+
def composite?
|
156
|
+
true
|
157
|
+
end
|
158
|
+
|
159
|
+
#ids_to_s([[1,2],[7,3]]) -> "(1,2),(7,3)"
|
160
|
+
#ids_to_s([[1,2],[7,3]], ',', ';') -> "1,2;7,3"
|
161
|
+
def ids_to_s(many_ids, id_sep = CompositePrimaryKeys::ID_SEP, list_sep = ',', left_bracket = '(', right_bracket = ')')
|
162
|
+
many_ids.map {|ids| "#{left_bracket}#{ids}#{right_bracket}"}.join(list_sep)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Returns true if the given +ids+ represents the primary keys of a record in the database, false otherwise.
|
166
|
+
# Example:
|
167
|
+
# Person.exists?(5,7)
|
168
|
+
def exists?(ids)
|
169
|
+
obj = find(ids) rescue false
|
170
|
+
!obj.nil? and obj.is_a?(self)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Deletes the record with the given +ids+ without instantiating an object first, e.g. delete(1,2)
|
174
|
+
# If an array of ids is provided (e.g. delete([1,2], [3,4]), all of them
|
175
|
+
# are deleted.
|
176
|
+
def delete(*ids)
|
177
|
+
unless ids.is_a?(Array); raise "*ids must be an Array"; end
|
178
|
+
ids = [ids.to_composite_ids] if not ids.first.is_a?(Array)
|
179
|
+
where_clause = ids.map do |id_set|
|
180
|
+
[primary_keys, id_set].transpose.map do |key, id|
|
181
|
+
"#{quoted_table_name}.#{connection.quote_column_name(key.to_s)}=#{sanitize(id)}"
|
182
|
+
end.join(" AND ")
|
183
|
+
end.join(") OR (")
|
184
|
+
delete_all([ "(#{where_clause})" ])
|
185
|
+
end
|
186
|
+
|
187
|
+
# Destroys the record with the given +ids+ by instantiating the object and calling #destroy (all the callbacks are the triggered).
|
188
|
+
# If an array of ids is provided, all of them are destroyed.
|
189
|
+
def destroy(*ids)
|
190
|
+
unless ids.is_a?(Array); raise "*ids must be an Array"; end
|
191
|
+
if ids.first.is_a?(Array)
|
192
|
+
ids = ids.map{|compids| compids.to_composite_ids}
|
193
|
+
else
|
194
|
+
ids = ids.to_composite_ids
|
195
|
+
end
|
196
|
+
ids.first.is_a?(CompositeIds) ? ids.each { |id_set| find(id_set).destroy } : find(ids).destroy
|
197
|
+
end
|
198
|
+
|
199
|
+
# Returns an array of column objects for the table associated with this class.
|
200
|
+
# Each column that matches to one of the primary keys has its
|
201
|
+
# primary attribute set to true
|
202
|
+
def columns
|
203
|
+
unless @columns
|
204
|
+
@columns = connection.columns(table_name, "#{name} Columns")
|
205
|
+
@columns.each {|column| column.primary = primary_keys.include?(column.name.to_sym)}
|
206
|
+
end
|
207
|
+
@columns
|
208
|
+
end
|
209
|
+
|
210
|
+
## DEACTIVATED METHODS ##
|
211
|
+
public
|
212
|
+
# Lazy-set the sequence name to the connection's default. This method
|
213
|
+
# is only ever called once since set_sequence_name overrides it.
|
214
|
+
def sequence_name #:nodoc:
|
215
|
+
raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
|
216
|
+
end
|
217
|
+
|
218
|
+
def reset_sequence_name #:nodoc:
|
219
|
+
raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
|
220
|
+
end
|
221
|
+
|
222
|
+
def set_primary_key(value = nil, &block)
|
223
|
+
raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
|
224
|
+
end
|
225
|
+
|
226
|
+
private
|
227
|
+
def find_one(id, options)
|
228
|
+
raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
|
229
|
+
end
|
230
|
+
|
231
|
+
def find_some(ids, options)
|
232
|
+
raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
|
233
|
+
end
|
234
|
+
|
235
|
+
def find_from_ids(ids, options)
|
236
|
+
ids = ids.first if ids.last == nil
|
237
|
+
conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
|
238
|
+
# if ids is just a flat list, then its size must = primary_key.length (one id per primary key, in order)
|
239
|
+
# if ids is list of lists, then each inner list must follow rule above
|
240
|
+
if ids.first.is_a? String
|
241
|
+
# find '2,1' -> ids = ['2,1']
|
242
|
+
# find '2,1;7,3' -> ids = ['2,1;7,3']
|
243
|
+
ids = ids.first.split(ID_SET_SEP).map {|id_set| id_set.split(ID_SEP).to_composite_ids}
|
244
|
+
# find '2,1;7,3' -> ids = [['2','1'],['7','3']], inner [] are CompositeIds
|
245
|
+
end
|
246
|
+
ids = [ids.to_composite_ids] if not ids.first.kind_of?(Array)
|
247
|
+
ids.each do |id_set|
|
248
|
+
unless id_set.is_a?(Array)
|
249
|
+
raise "Ids must be in an Array, instead received: #{id_set.inspect}"
|
250
|
+
end
|
251
|
+
unless id_set.length == primary_keys.length
|
252
|
+
raise "#{id_set.inspect}: Incorrect number of primary keys for #{class_name}: #{primary_keys.inspect}"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# Let keys = [:a, :b]
|
257
|
+
# If ids = [[10, 50], [11, 51]], then :conditions =>
|
258
|
+
# "(#{quoted_table_name}.a, #{quoted_table_name}.b) IN ((10, 50), (11, 51))"
|
259
|
+
|
260
|
+
conditions = ids.map do |id_set|
|
261
|
+
[primary_keys, id_set].transpose.map do |key, id|
|
262
|
+
col = columns_hash[key.to_s]
|
263
|
+
val = quote_value(id, col)
|
264
|
+
"#{quoted_table_name}.#{connection.quote_column_name(key.to_s)}=#{val}"
|
265
|
+
end.join(" AND ")
|
266
|
+
end.join(") OR (")
|
267
|
+
|
268
|
+
options.update :conditions => "(#{conditions})"
|
269
|
+
|
270
|
+
result = find_every(options)
|
271
|
+
|
272
|
+
if result.size == ids.size
|
273
|
+
ids.size == 1 ? result[0] : result
|
274
|
+
else
|
275
|
+
raise ::ActiveRecord::RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids.inspect})#{conditions}"
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
|
284
|
+
module ActiveRecord
|
285
|
+
ID_SEP = ','
|
286
|
+
ID_SET_SEP = ';'
|
287
|
+
|
288
|
+
class Base
|
289
|
+
# Allows +attr_name+ to be the list of primary_keys, and returns the id
|
290
|
+
# of the object
|
291
|
+
# e.g. @object[@object.class.primary_key] => [1,1]
|
292
|
+
def [](attr_name)
|
293
|
+
if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first
|
294
|
+
attr_name = attr_name.split(ID_SEP)
|
295
|
+
end
|
296
|
+
attr_name.is_a?(Array) ?
|
297
|
+
attr_name.map {|name| read_attribute(name)} :
|
298
|
+
read_attribute(attr_name)
|
299
|
+
end
|
300
|
+
|
301
|
+
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
|
302
|
+
# (Alias for the protected write_attribute method).
|
303
|
+
def []=(attr_name, value)
|
304
|
+
if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first
|
305
|
+
attr_name = attr_name.split(ID_SEP)
|
306
|
+
end
|
307
|
+
|
308
|
+
if attr_name.is_a? Array
|
309
|
+
value = value.split(ID_SEP) if value.is_a? String
|
310
|
+
unless value.length == attr_name.length
|
311
|
+
raise "Number of attr_names and values do not match"
|
312
|
+
end
|
313
|
+
#breakpoint
|
314
|
+
[attr_name, value].transpose.map {|name,val| write_attribute(name.to_s, val)}
|
315
|
+
else
|
316
|
+
write_attribute(attr_name, value)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module CompositePrimaryKeys
|
2
|
+
module ActiveRecord
|
3
|
+
module Calculations
|
4
|
+
def self.append_features(base)
|
5
|
+
super
|
6
|
+
base.send(:extend, ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def construct_calculation_sql(operation, column_name, options) #:nodoc:
|
11
|
+
operation = operation.to_s.downcase
|
12
|
+
options = options.symbolize_keys
|
13
|
+
|
14
|
+
scope = scope(:find)
|
15
|
+
merged_includes = merge_includes(scope ? scope[:include] : [], options[:include])
|
16
|
+
aggregate_alias = column_alias_for(operation, column_name)
|
17
|
+
use_workaround = !connection.supports_count_distinct? && options[:distinct] && operation.to_s.downcase == 'count'
|
18
|
+
join_dependency = nil
|
19
|
+
|
20
|
+
if merged_includes.any? && operation.to_s.downcase == 'count'
|
21
|
+
options[:distinct] = true
|
22
|
+
use_workaround = !connection.supports_count_distinct?
|
23
|
+
column_name = options[:select] || primary_key.map{ |part| "#{quoted_table_name}.#{connection.quote_column_name(part)}"}.join(',')
|
24
|
+
end
|
25
|
+
|
26
|
+
sql = "SELECT #{operation}(#{'DISTINCT ' if options[:distinct]}#{column_name}) AS #{aggregate_alias}"
|
27
|
+
|
28
|
+
# A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
|
29
|
+
sql = "SELECT COUNT(*) AS #{aggregate_alias}" if use_workaround
|
30
|
+
|
31
|
+
sql << ", #{connection.quote_column_name(options[:group_field])} AS #{options[:group_alias]}" if options[:group]
|
32
|
+
sql << " FROM (SELECT DISTINCT #{column_name}" if use_workaround
|
33
|
+
sql << " FROM #{quoted_table_name} "
|
34
|
+
if merged_includes.any?
|
35
|
+
join_dependency = ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins])
|
36
|
+
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
|
37
|
+
end
|
38
|
+
add_joins!(sql, options, scope)
|
39
|
+
add_conditions!(sql, options[:conditions], scope)
|
40
|
+
add_limited_ids_condition!(sql, options, join_dependency) if \
|
41
|
+
join_dependency &&
|
42
|
+
!using_limitable_reflections?(join_dependency.reflections) &&
|
43
|
+
((scope && scope[:limit]) || options[:limit])
|
44
|
+
|
45
|
+
if options[:group]
|
46
|
+
group_key = connection.adapter_name == 'FrontBase' ? :group_alias : :group_field
|
47
|
+
sql << " GROUP BY #{connection.quote_column_name(options[group_key])} "
|
48
|
+
end
|
49
|
+
|
50
|
+
if options[:group] && options[:having]
|
51
|
+
# FrontBase requires identifiers in the HAVING clause and chokes on function calls
|
52
|
+
if connection.adapter_name == 'FrontBase'
|
53
|
+
options[:having].downcase!
|
54
|
+
options[:having].gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias)
|
55
|
+
end
|
56
|
+
|
57
|
+
sql << " HAVING #{options[:having]} "
|
58
|
+
end
|
59
|
+
|
60
|
+
sql << " ORDER BY #{options[:order]} " if options[:order]
|
61
|
+
add_limit!(sql, options, scope)
|
62
|
+
sql << ') w1' if use_workaround # assign a dummy table name as required for postgresql
|
63
|
+
sql
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|