mingusbabcock-composite_primary_keys 2.2.2 → 2.2.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. data/History.txt +156 -0
  2. data/Manifest.txt +122 -0
  3. data/README.txt +41 -0
  4. data/README_DB2.txt +33 -0
  5. data/Rakefile +65 -0
  6. data/init.rb +2 -0
  7. data/install.rb +30 -0
  8. data/lib/adapter_helper/base.rb +63 -0
  9. data/lib/adapter_helper/mysql.rb +13 -0
  10. data/lib/adapter_helper/oracle.rb +12 -0
  11. data/lib/adapter_helper/postgresql.rb +13 -0
  12. data/lib/adapter_helper/sqlite3.rb +13 -0
  13. data/lib/composite_primary_keys.rb +56 -0
  14. data/lib/composite_primary_keys/association_preload.rb +253 -0
  15. data/lib/composite_primary_keys/associations.rb +428 -0
  16. data/lib/composite_primary_keys/attribute_methods.rb +84 -0
  17. data/lib/composite_primary_keys/base.rb +341 -0
  18. data/lib/composite_primary_keys/calculations.rb +69 -0
  19. data/lib/composite_primary_keys/composite_arrays.rb +30 -0
  20. data/lib/composite_primary_keys/connection_adapters/ibm_db_adapter.rb +21 -0
  21. data/lib/composite_primary_keys/connection_adapters/oracle_adapter.rb +15 -0
  22. data/lib/composite_primary_keys/connection_adapters/postgresql_adapter.rb +53 -0
  23. data/lib/composite_primary_keys/connection_adapters/sqlite3_adapter.rb +15 -0
  24. data/lib/composite_primary_keys/fixtures.rb +8 -0
  25. data/lib/composite_primary_keys/migration.rb +20 -0
  26. data/lib/composite_primary_keys/reflection.rb +19 -0
  27. data/lib/composite_primary_keys/version.rb +8 -0
  28. data/loader.rb +24 -0
  29. data/local/database_connections.rb.sample +10 -0
  30. data/local/paths.rb.sample +2 -0
  31. data/local/tasks.rb.sample +2 -0
  32. data/scripts/console.rb +48 -0
  33. data/scripts/txt2html +67 -0
  34. data/scripts/txt2js +59 -0
  35. data/tasks/activerecord_selection.rake +43 -0
  36. data/tasks/databases.rake +12 -0
  37. data/tasks/databases/mysql.rake +30 -0
  38. data/tasks/databases/oracle.rake +25 -0
  39. data/tasks/databases/postgresql.rake +26 -0
  40. data/tasks/databases/sqlite3.rake +28 -0
  41. data/tasks/deployment.rake +22 -0
  42. data/tasks/local_setup.rake +13 -0
  43. data/tasks/website.rake +18 -0
  44. data/test/README_tests.txt +67 -0
  45. data/test/abstract_unit.rb +94 -0
  46. data/test/connections/native_ibm_db/connection.rb +23 -0
  47. data/test/connections/native_mysql/connection.rb +13 -0
  48. data/test/connections/native_oracle/connection.rb +14 -0
  49. data/test/connections/native_postgresql/connection.rb +9 -0
  50. data/test/connections/native_sqlite/connection.rb +9 -0
  51. data/test/fixtures/article.rb +5 -0
  52. data/test/fixtures/articles.yml +6 -0
  53. data/test/fixtures/comment.rb +6 -0
  54. data/test/fixtures/comments.yml +16 -0
  55. data/test/fixtures/db_definitions/db2-create-tables.sql +113 -0
  56. data/test/fixtures/db_definitions/db2-drop-tables.sql +16 -0
  57. data/test/fixtures/db_definitions/mysql.sql +174 -0
  58. data/test/fixtures/db_definitions/oracle.drop.sql +39 -0
  59. data/test/fixtures/db_definitions/oracle.sql +188 -0
  60. data/test/fixtures/db_definitions/postgresql.sql +199 -0
  61. data/test/fixtures/db_definitions/sqlite.sql +160 -0
  62. data/test/fixtures/department.rb +5 -0
  63. data/test/fixtures/departments.yml +3 -0
  64. data/test/fixtures/employee.rb +4 -0
  65. data/test/fixtures/employees.yml +9 -0
  66. data/test/fixtures/group.rb +3 -0
  67. data/test/fixtures/groups.yml +3 -0
  68. data/test/fixtures/hack.rb +6 -0
  69. data/test/fixtures/hacks.yml +2 -0
  70. data/test/fixtures/membership.rb +7 -0
  71. data/test/fixtures/membership_status.rb +3 -0
  72. data/test/fixtures/membership_statuses.yml +10 -0
  73. data/test/fixtures/memberships.yml +6 -0
  74. data/test/fixtures/product.rb +7 -0
  75. data/test/fixtures/product_tariff.rb +5 -0
  76. data/test/fixtures/product_tariffs.yml +12 -0
  77. data/test/fixtures/products.yml +6 -0
  78. data/test/fixtures/reading.rb +4 -0
  79. data/test/fixtures/readings.yml +10 -0
  80. data/test/fixtures/reference_code.rb +7 -0
  81. data/test/fixtures/reference_codes.yml +28 -0
  82. data/test/fixtures/reference_type.rb +7 -0
  83. data/test/fixtures/reference_types.yml +9 -0
  84. data/test/fixtures/street.rb +3 -0
  85. data/test/fixtures/streets.yml +15 -0
  86. data/test/fixtures/suburb.rb +6 -0
  87. data/test/fixtures/suburbs.yml +9 -0
  88. data/test/fixtures/tariff.rb +6 -0
  89. data/test/fixtures/tariffs.yml +13 -0
  90. data/test/fixtures/user.rb +10 -0
  91. data/test/fixtures/users.yml +6 -0
  92. data/test/hash_tricks.rb +34 -0
  93. data/test/plugins/pagination.rb +405 -0
  94. data/test/plugins/pagination_helper.rb +135 -0
  95. data/test/test_associations.rb +160 -0
  96. data/test/test_attribute_methods.rb +22 -0
  97. data/test/test_attributes.rb +84 -0
  98. data/test/test_clone.rb +34 -0
  99. data/test/test_composite_arrays.rb +51 -0
  100. data/test/test_create.rb +68 -0
  101. data/test/test_delete.rb +96 -0
  102. data/test/test_dummy.rb +28 -0
  103. data/test/test_exists.rb +29 -0
  104. data/test/test_find.rb +73 -0
  105. data/test/test_ids.rb +97 -0
  106. data/test/test_miscellaneous.rb +39 -0
  107. data/test/test_pagination.rb +38 -0
  108. data/test/test_polymorphic.rb +31 -0
  109. data/test/test_santiago.rb +27 -0
  110. data/test/test_tutorial_examle.rb +26 -0
  111. data/test/test_update.rb +40 -0
  112. data/website/index.html +199 -0
  113. data/website/index.txt +159 -0
  114. data/website/javascripts/rounded_corners_lite.inc.js +285 -0
  115. data/website/stylesheets/screen.css +126 -0
  116. data/website/template.js +3 -0
  117. data/website/template.rhtml +53 -0
  118. data/website/version-raw.js +3 -0
  119. data/website/version-raw.txt +2 -0
  120. data/website/version.js +4 -0
  121. data/website/version.txt +3 -0
  122. metadata +180 -18
@@ -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,341 @@
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
+ # Creates WHERE condition from list of composited ids
166
+ # User.update_all({:role => 'admin'}, :conditions => composite_where_clause([[1, 2], [2, 2]])) #=> UPDATE admins SET admin.role='admin' WHERE (admin.type=1 AND admin.type2=2) OR (admin.type=2 AND admin.type2=2)
167
+ # User.find(:all, :conditions => composite_where_clause([[1, 2], [2, 2]])) #=> SELECT * FROM admins WHERE (admin.type=1 AND admin.type2=2) OR (admin.type=2 AND admin.type2=2)
168
+ def composite_where_clause(ids)
169
+ if ids.is_a?(String)
170
+ ids = [[ids]]
171
+ elsif not ids.first.is_a?(Array) # if single comp key passed, turn into an array of 1
172
+ ids = [ids.to_composite_ids]
173
+ end
174
+
175
+ ids.map do |id_set|
176
+ [primary_keys, id_set].transpose.map do |key, id|
177
+ "#{table_name}.#{key.to_s}=#{sanitize(id)}"
178
+ end.join(" AND ")
179
+ end.join(") OR (")
180
+ end
181
+
182
+ # Returns true if the given +ids+ represents the primary keys of a record in the database, false otherwise.
183
+ # Example:
184
+ # Person.exists?(5,7)
185
+ def exists?(ids)
186
+ if ids.is_a?(Array) && ids.first.is_a?(String)
187
+ count(:conditions => ids) > 0
188
+ else
189
+ obj = find(ids) rescue false
190
+ !obj.nil? and obj.is_a?(self)
191
+ end
192
+ end
193
+
194
+ # Deletes the record with the given +ids+ without instantiating an object first, e.g. delete(1,2)
195
+ # If an array of ids is provided (e.g. delete([1,2], [3,4]), all of them
196
+ # are deleted.
197
+ def delete(*ids)
198
+ unless ids.is_a?(Array); raise "*ids must be an Array"; end
199
+ ids = [ids.to_composite_ids] if not ids.first.is_a?(Array)
200
+ where_clause = ids.map do |id_set|
201
+ [primary_keys, id_set].transpose.map do |key, id|
202
+ "#{quoted_table_name}.#{connection.quote_column_name(key.to_s)}=#{sanitize(id)}"
203
+ end.join(" AND ")
204
+ end.join(") OR (")
205
+ delete_all([ "(#{where_clause})" ])
206
+ end
207
+
208
+ # Destroys the record with the given +ids+ by instantiating the object and calling #destroy (all the callbacks are the triggered).
209
+ # If an array of ids is provided, all of them are destroyed.
210
+ def destroy(*ids)
211
+ unless ids.is_a?(Array); raise "*ids must be an Array"; end
212
+ if ids.first.is_a?(Array)
213
+ ids = ids.map{|compids| compids.to_composite_ids}
214
+ else
215
+ ids = ids.to_composite_ids
216
+ end
217
+ ids.first.is_a?(CompositeIds) ? ids.each { |id_set| find(id_set).destroy } : find(ids).destroy
218
+ end
219
+
220
+ # Returns an array of column objects for the table associated with this class.
221
+ # Each column that matches to one of the primary keys has its
222
+ # primary attribute set to true
223
+ def columns
224
+ unless @columns
225
+ @columns = connection.columns(table_name, "#{name} Columns")
226
+ @columns.each {|column| column.primary = primary_keys.include?(column.name.to_sym)}
227
+ end
228
+ @columns
229
+ end
230
+
231
+ ## DEACTIVATED METHODS ##
232
+ public
233
+ # Lazy-set the sequence name to the connection's default. This method
234
+ # is only ever called once since set_sequence_name overrides it.
235
+ def sequence_name #:nodoc:
236
+ raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
237
+ end
238
+
239
+ def reset_sequence_name #:nodoc:
240
+ raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
241
+ end
242
+
243
+ def set_primary_key(value = nil, &block)
244
+ raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
245
+ end
246
+
247
+ private
248
+ def find_one(id, options)
249
+ raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
250
+ end
251
+
252
+ def find_some(ids, options)
253
+ raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
254
+ end
255
+
256
+ def find_from_ids(ids, options)
257
+ ids = ids.first if ids.last == nil
258
+ conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
259
+ # if ids is just a flat list, then its size must = primary_key.length (one id per primary key, in order)
260
+ # if ids is list of lists, then each inner list must follow rule above
261
+ if ids.first.is_a? String
262
+ # find '2,1' -> ids = ['2,1']
263
+ # find '2,1;7,3' -> ids = ['2,1;7,3']
264
+ ids = ids.first.split(ID_SET_SEP).map {|id_set| id_set.split(ID_SEP).to_composite_ids}
265
+ # find '2,1;7,3' -> ids = [['2','1'],['7','3']], inner [] are CompositeIds
266
+ end
267
+ ids = [ids.to_composite_ids] if not ids.first.kind_of?(Array)
268
+ ids.each do |id_set|
269
+ unless id_set.is_a?(Array)
270
+ raise "Ids must be in an Array, instead received: #{id_set.inspect}"
271
+ end
272
+ unless id_set.length == primary_keys.length
273
+ raise "#{id_set.inspect}: Incorrect number of primary keys for #{class_name}: #{primary_keys.inspect}"
274
+ end
275
+ end
276
+
277
+ # Let keys = [:a, :b]
278
+ # If ids = [[10, 50], [11, 51]], then :conditions =>
279
+ # "(#{quoted_table_name}.a, #{quoted_table_name}.b) IN ((10, 50), (11, 51))"
280
+
281
+ conditions = ids.map do |id_set|
282
+ [primary_keys, id_set].transpose.map do |key, id|
283
+ col = columns_hash[key.to_s]
284
+ val = quote_value(id, col)
285
+ "#{quoted_table_name}.#{connection.quote_column_name(key.to_s)}=#{val}"
286
+ end.join(" AND ")
287
+ end.join(") OR (")
288
+
289
+ options.update :conditions => "(#{conditions})"
290
+
291
+ result = find_every(options)
292
+
293
+ if result.size == ids.size
294
+ ids.size == 1 ? result[0] : result
295
+ else
296
+ raise ::ActiveRecord::RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids.inspect})#{conditions}"
297
+ end
298
+ end
299
+ end
300
+ end
301
+ end
302
+ end
303
+
304
+
305
+ module ActiveRecord
306
+ ID_SEP = ','
307
+ ID_SET_SEP = ';'
308
+
309
+ class Base
310
+ # Allows +attr_name+ to be the list of primary_keys, and returns the id
311
+ # of the object
312
+ # e.g. @object[@object.class.primary_key] => [1,1]
313
+ def [](attr_name)
314
+ if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first
315
+ attr_name = attr_name.split(ID_SEP)
316
+ end
317
+ attr_name.is_a?(Array) ?
318
+ attr_name.map {|name| read_attribute(name)} :
319
+ read_attribute(attr_name)
320
+ end
321
+
322
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
323
+ # (Alias for the protected write_attribute method).
324
+ def []=(attr_name, value)
325
+ if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first
326
+ attr_name = attr_name.split(ID_SEP)
327
+ end
328
+
329
+ if attr_name.is_a? Array
330
+ value = value.split(ID_SEP) if value.is_a? String
331
+ unless value.length == attr_name.length
332
+ raise "Number of attr_names and values do not match"
333
+ end
334
+ #breakpoint
335
+ [attr_name, value].transpose.map {|name,val| write_attribute(name.to_s, val)}
336
+ else
337
+ write_attribute(attr_name, value)
338
+ end
339
+ end
340
+ end
341
+ end
@@ -0,0 +1,69 @@
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
+
39
+ add_joins!(sql, options[:joins], scope)
40
+ add_conditions!(sql, options[:conditions], scope)
41
+ add_limited_ids_condition!(sql, options, join_dependency) if \
42
+ join_dependency &&
43
+ !using_limitable_reflections?(join_dependency.reflections) &&
44
+ ((scope && scope[:limit]) || options[:limit])
45
+
46
+ if options[:group]
47
+ group_key = connection.adapter_name == 'FrontBase' ? :group_alias : :group_field
48
+ sql << " GROUP BY #{connection.quote_column_name(options[group_key])} "
49
+ end
50
+
51
+ if options[:group] && options[:having]
52
+ # FrontBase requires identifiers in the HAVING clause and chokes on function calls
53
+ if connection.adapter_name == 'FrontBase'
54
+ options[:having].downcase!
55
+ options[:having].gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias)
56
+ end
57
+
58
+ sql << " HAVING #{options[:having]} "
59
+ end
60
+
61
+ sql << " ORDER BY #{options[:order]} " if options[:order]
62
+ add_limit!(sql, options, scope)
63
+ sql << ') w1' if use_workaround # assign a dummy table name as required for postgresql
64
+ sql
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end