composite_primary_keys 0.7.5 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. data/Manifest.txt +75 -0
  2. data/Rakefile +83 -101
  3. data/lib/composite_primary_keys.rb +8 -0
  4. data/lib/composite_primary_keys/associations.rb +66 -18
  5. data/lib/composite_primary_keys/base.rb +169 -159
  6. data/lib/composite_primary_keys/calculations.rb +63 -0
  7. data/lib/composite_primary_keys/connection_adapters/postgresql_adapter.rb +11 -0
  8. data/lib/composite_primary_keys/version.rb +2 -2
  9. data/scripts/txt2html +4 -2
  10. data/scripts/txt2js +3 -2
  11. data/test/abstract_unit.rb +9 -2
  12. data/test/connections/native_mysql/connection.rb +5 -2
  13. data/test/connections/native_postgresql/connection.rb +15 -0
  14. data/test/connections/native_sqlite/connection.rb +10 -0
  15. data/test/fixtures/db_definitions/mysql.sql +20 -0
  16. data/test/fixtures/db_definitions/postgresql.sql +100 -0
  17. data/test/fixtures/db_definitions/sqlite.sql +84 -0
  18. data/test/fixtures/group.rb +3 -0
  19. data/test/fixtures/groups.yml +3 -0
  20. data/test/fixtures/membership.rb +7 -0
  21. data/test/fixtures/membership_status.rb +3 -0
  22. data/test/fixtures/membership_statuses.yml +8 -0
  23. data/test/fixtures/memberships.yml +6 -0
  24. data/test/{associations_test.rb → test_associations.rb} +22 -12
  25. data/test/{attributes_test.rb → test_attributes.rb} +4 -5
  26. data/test/{clone_test.rb → test_clone.rb} +2 -3
  27. data/test/{create_test.rb → test_create.rb} +2 -3
  28. data/test/{delete_test.rb → test_delete.rb} +2 -3
  29. data/test/{dummy_test.rb → test_dummy.rb} +4 -5
  30. data/test/{find_test.rb → test_find.rb} +3 -4
  31. data/test/{ids_test.rb → test_ids.rb} +4 -4
  32. data/test/{miscellaneous_test.rb → test_miscellaneous.rb} +2 -3
  33. data/test/{pagination_test.rb → test_pagination.rb} +4 -3
  34. data/test/{santiago_test.rb → test_santiago.rb} +5 -3
  35. data/test/test_tutorial_examle.rb +29 -0
  36. data/test/{update_test.rb → test_update.rb} +2 -3
  37. data/website/index.html +267 -201
  38. data/website/index.txt +74 -70
  39. data/website/stylesheets/screen.css +33 -3
  40. data/website/version-raw.js +1 -1
  41. data/website/version.js +1 -1
  42. metadata +80 -66
  43. data/scripts/http-access2-2.0.6.gem +0 -0
  44. data/scripts/rubyforge +0 -217
  45. data/scripts/rubyforge-orig +0 -217
  46. data/test/fixtures/db_definitions/mysql.drop.sql +0 -10
@@ -25,6 +25,7 @@ module CompositePrimaryKeys
25
25
  extend CompositePrimaryKeys::ActiveRecord::Base::CompositeClassMethods
26
26
  include CompositePrimaryKeys::ActiveRecord::Base::CompositeInstanceMethods
27
27
  include CompositePrimaryKeys::ActiveRecord::Associations
28
+ extend CompositePrimaryKeys::ActiveRecord::Calculations::ClassMethods
28
29
  EOV
29
30
  end
30
31
 
@@ -44,7 +45,7 @@ module CompositePrimaryKeys
44
45
  def id
45
46
  attr_names = self.class.primary_keys
46
47
  CompositeIds.new(
47
- attr_names.map {|attr_name| read_attribute(attr_name)}
48
+ attr_names.map {|attr_name| read_attribute(attr_name)}
48
49
  )
49
50
  end
50
51
  alias_method :ids, :id
@@ -59,9 +60,9 @@ module CompositePrimaryKeys
59
60
 
60
61
  def quoted_id #:nodoc:
61
62
  [self.class.primary_keys, ids].
62
- transpose.
63
- map {|attr_name,id| quote(id, column_for_attribute(attr_name))}.
64
- to_composite_ids
63
+ transpose.
64
+ map {|attr_name,id| quote_value(id, column_for_attribute(attr_name))}.
65
+ to_composite_ids
65
66
  end
66
67
 
67
68
  # Sets the primary ID.
@@ -132,15 +133,19 @@ module CompositePrimaryKeys
132
133
 
133
134
  # Creates a new record with values matching those of the instance attributes.
134
135
  def create_without_callbacks
135
- unless self.id; raise CompositeKeyError, "Composite keys do not generated ids from sequences, you must provide id values"; end
136
+ unless self.id
137
+ raise CompositeKeyError, "Composite keys do not generated ids from sequences, you must provide id values"
138
+ end
136
139
  attributes_minus_pks = attributes_with_quotes(false)
137
140
  cols = quoted_column_names(attributes_minus_pks) << self.class.primary_key
138
141
  vals = attributes_minus_pks.values << quoted_id
139
142
  connection.insert(
140
- "INSERT INTO #{self.class.table_name} " +
141
- "(#{cols.join(', ')}) " +
142
- "VALUES(#{vals.join(', ')})",
143
- "#{self.class.name} Create"
143
+ "INSERT INTO #{self.class.table_name} " +
144
+ "(#{cols.join(', ')}) " +
145
+ "VALUES (#{vals.join(', ')})",
146
+ "#{self.class.name} Create",
147
+ self.class.primary_key,
148
+ self.id
144
149
  )
145
150
  @new_record = false
146
151
  return true
@@ -164,178 +169,183 @@ module CompositePrimaryKeys
164
169
  def destroy_without_callbacks
165
170
  where_class = [self.class.primary_key, quoted_id].transpose.map do |pair|
166
171
  "(#{pair[0]} = #{pair[1]})"
167
- end.join(" AND ")
168
- unless new_record?
169
- connection.delete(
170
- "DELETE FROM #{self.class.table_name} " +
171
- "WHERE #{where_class}",
172
- "#{self.class.name} Destroy"
173
- )
174
- end
175
- freeze
172
+ end.join(" AND ")
173
+ unless new_record?
174
+ connection.delete(
175
+ "DELETE FROM #{self.class.table_name} " +
176
+ "WHERE #{where_class}",
177
+ "#{self.class.name} Destroy"
178
+ )
176
179
  end
180
+ freeze
181
+ end
177
182
 
183
+ end
184
+
185
+ module CompositeClassMethods
186
+ def primary_key; primary_keys; end
187
+ def primary_key=(keys); primary_keys = keys; end
188
+
189
+ def composite?
190
+ true
178
191
  end
179
192
 
180
- module CompositeClassMethods
181
- def primary_key; primary_keys; end
182
- def primary_key=(keys); primary_keys = keys; end
183
-
184
- def composite?
185
- true
186
- end
187
-
188
- #ids_to_s([[1,2],[7,3]]) -> "(1,2),(7,3)"
189
- #ids_to_s([[1,2],[7,3]], ',', ';') -> "1,2;7,3"
190
- def ids_to_s(many_ids, id_sep = CompositePrimaryKeys::ID_SEP, list_sep = ',', left_bracket = '(', right_bracket = ')')
191
- many_ids.map {|ids| "#{left_bracket}#{ids}#{right_bracket}"}.join(list_sep)
193
+ #ids_to_s([[1,2],[7,3]]) -> "(1,2),(7,3)"
194
+ #ids_to_s([[1,2],[7,3]], ',', ';') -> "1,2;7,3"
195
+ def ids_to_s(many_ids, id_sep = CompositePrimaryKeys::ID_SEP, list_sep = ',', left_bracket = '(', right_bracket = ')')
196
+ many_ids.map {|ids| "#{left_bracket}#{ids}#{right_bracket}"}.join(list_sep)
197
+ end
198
+
199
+ # Returns true if the given +ids+ represents the primary keys of a record in the database, false otherwise.
200
+ # Example:
201
+ # Person.exists?(5,7)
202
+ def exists?(ids)
203
+ obj = find(ids) rescue false
204
+ !obj.nil? and obj.is_a?(self)
205
+ end
206
+
207
+ # Deletes the record with the given +ids+ without instantiating an object first, e.g. delete(1,2)
208
+ # If an array of ids is provided (e.g. delete([1,2], [3,4]), all of them
209
+ # are deleted.
210
+ def delete(*ids)
211
+ unless ids.is_a?(Array); raise "*ids must be an Array"; end
212
+ ids = [ids.to_composite_ids] if not ids.first.is_a?(Array)
213
+ where_class = ids.map do |id_set|
214
+ [primary_keys, id_set].transpose.map do |key, id|
215
+ "#{table_name}.#{key.to_s}=#{sanitize(id)}"
216
+ end.join(" AND ")
217
+ end.join(") OR (")
218
+ delete_all([ "(#{where_class})" ])
219
+ end
220
+
221
+ # Destroys the record with the given +ids+ by instantiating the object and calling #destroy (all the callbacks are the triggered).
222
+ # If an array of ids is provided, all of them are destroyed.
223
+ def destroy(*ids)
224
+ unless ids.is_a?(Array); raise "*ids must be an Array"; end
225
+ if ids.first.is_a?(Array)
226
+ ids = ids.map{|compids| compids.to_composite_ids}
227
+ else
228
+ ids = ids.to_composite_ids
192
229
  end
193
-
194
- # Returns true if the given +ids+ represents the primary keys of a record in the database, false otherwise.
195
- # Example:
196
- # Person.exists?(5,7)
197
- def exists?(ids)
198
- obj = find(ids) rescue false
199
- !obj.nil? and obj.is_a?(self)
230
+ ids.first.is_a?(CompositeIds) ? ids.each { |id_set| find(id_set).destroy } : find(ids).destroy
231
+ end
232
+
233
+ # Returns an array of column objects for the table associated with this class.
234
+ # Each column that matches to one of the primary keys has its
235
+ # primary attribute set to true
236
+ def columns
237
+ unless @columns
238
+ @columns = connection.columns(table_name, "#{name} Columns")
239
+ @columns.each {|column| column.primary = primary_keys.include?(column.name.to_sym)}
200
240
  end
201
-
202
- # Deletes the record with the given +ids+ without instantiating an object first, e.g. delete(1,2)
203
- # If an array of ids is provided (e.g. delete([1,2], [3,4]), all of them
204
- # are deleted.
205
- def delete(*ids)
206
- unless ids.is_a?(Array); raise "*ids must be an Array"; end
207
- ids = [ids.to_composite_ids] if not ids.first.is_a?(Array)
208
- delete_all([ "(#{primary_keys}) IN (#{ids_to_s(ids)})" ])
241
+ @columns
242
+ end
243
+
244
+ ## DEACTIVATED METHODS ##
245
+ public
246
+ # Lazy-set the sequence name to the connection's default. This method
247
+ # is only ever called once since set_sequence_name overrides it.
248
+ def sequence_name #:nodoc:
249
+ raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
250
+ end
251
+
252
+ def reset_sequence_name #:nodoc:
253
+ raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
254
+ end
255
+
256
+ def set_primary_key(value = nil, &block)
257
+ raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
258
+ end
259
+
260
+ private
261
+ def find_one(id, options)
262
+ raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
263
+ end
264
+
265
+ def find_some(ids, options)
266
+ raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
267
+ end
268
+
269
+ def find_from_ids(ids, options)
270
+ ids = ids.first if ids.last == nil
271
+ conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
272
+ # if ids is just a flat list, then its size must = primary_key.length (one id per primary key, in order)
273
+ # if ids is list of lists, then each inner list must follow rule above
274
+ if ids.first.is_a? String
275
+ # find '2,1' -> ids = ['2,1']
276
+ # find '2,1;7,3' -> ids = ['2,1;7,3']
277
+ ids = ids.first.split(ID_SET_SEP).map {|id_set| id_set.split(ID_SEP).to_composite_ids}
278
+ # find '2,1;7,3' -> ids = [['2','1'],['7','3']], inner [] are CompositeIds
209
279
  end
210
-
211
- # Destroys the record with the given +ids+ by instantiating the object and calling #destroy (all the callbacks are the triggered).
212
- # If an array of ids is provided, all of them are destroyed.
213
- def destroy(*ids)
214
- unless ids.is_a?(Array); raise "*ids must be an Array"; end
215
- if ids.first.is_a?(Array)
216
- ids = ids.map{|compids| compids.to_composite_ids}
217
- else
218
- ids = ids.to_composite_ids
280
+ ids = [ids.to_composite_ids] if not ids.first.kind_of?(Array)
281
+ ids.each do |id_set|
282
+ unless id_set.is_a?(Array)
283
+ raise "Ids must be in an Array, instead received: #{id_set.inspect}"
219
284
  end
220
- ids.first.is_a?(CompositeIds) ? ids.each { |id_set| find(id_set).destroy } : find(ids).destroy
221
- end
222
-
223
- # Returns an array of column objects for the table associated with this class.
224
- # Each column that matches to one of the primary keys has its
225
- # primary attribute set to true
226
- def columns
227
- unless @columns
228
- @columns = connection.columns(table_name, "#{name} Columns")
229
- @columns.each {|column| column.primary = primary_keys.include?(column.name.to_sym)}
285
+ unless id_set.length == primary_keys.length
286
+ raise "#{id_set.inspect}: Incorrect number of primary keys for #{class_name}: #{primary_keys.inspect}"
230
287
  end
231
- @columns
232
- end
233
-
234
- ## DEACTIVATED METHODS ##
235
- public
236
- # Lazy-set the sequence name to the connection's default. This method
237
- # is only ever called once since set_sequence_name overrides it.
238
- def sequence_name #:nodoc:
239
- raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
240
- end
241
-
242
- def reset_sequence_name #:nodoc:
243
- raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
244
- end
245
-
246
- def set_primary_key(value = nil, &block)
247
- raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
248
288
  end
249
289
 
250
- private
251
- def find_one(id, options)
252
- raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
253
- end
290
+ # Let keys = [:a, :b]
291
+ # If ids = [[10, 50], [11, 51]], then :conditions =>
292
+ # "(#{table_name}.a, #{table_name}.b) IN ((10, 50), (11, 51))"
254
293
 
255
- def find_some(ids, options)
256
- raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
257
- end
258
-
259
- def find_from_ids(ids, options)
260
- conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
261
- # if ids is just a flat list, then its size must = primary_key.length (one id per primary key, in order)
262
- # if ids is list of lists, then each inner list must follow rule above
263
-
264
- if ids.first.is_a? String
265
- # find '2,1' -> ids = ['2,1']
266
- # find '2,1;7,3' -> ids = ['2,1;7,3']
267
- ids = ids.first.split(ID_SET_SEP).map {|id_set| id_set.split(ID_SEP).to_composite_ids}
268
- # find '2,1;7,3' -> ids = [['2','1'],['7','3']], inner [] are CompositeIds
269
- end
270
- ids = [ids.to_composite_ids] if not ids.first.kind_of?(Array)
271
- ids.each do |id_set|
272
- unless id_set.is_a?(Array)
273
- raise "Ids must be in an Array, instead received: #{id_set.inspect}"
274
- end
275
- unless id_set.length == primary_keys.length
276
- raise "#{id_set.inspect}: Incorrect number of primary keys for #{class_name}: #{primary_keys.inspect}"
294
+ conditions = ids.map do |id_set|
295
+ [primary_keys, id_set].transpose.map do |key, id|
296
+ "#{table_name}.#{key.to_s}=#{sanitize(id)}"
297
+ end.join(" AND ")
298
+ end.join(") OR (")
299
+ options.update :conditions => "(#{conditions})"
300
+
301
+ result = find_every(options)
302
+
303
+ if result.size == ids.size
304
+ ids.size == 1 ? result[0] : result
305
+ else
306
+ raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions}"
277
307
  end
278
308
  end
279
309
 
280
- # Let keys = [:a, :b]
281
- # If ids = [[10, 50], [11, 51]], then :conditions =>
282
- # "(#{table_name}.a, #{table_name}.b) IN ((10, 50), (11, 51))"
283
-
284
- conditions = ids.map do |id_set|
285
- [primary_keys, id_set].transpose.map do |key, id|
286
- "#{table_name}.#{key.to_s}=#{sanitize(id)}"
287
- end.join(" AND ")
288
- end.join(") OR (")
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 RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions}"
297
- end
298
- end
299
-
300
- end
301
310
  end
302
311
  end
303
312
  end
313
+ end
314
+
315
+ module ActiveRecord
316
+ ID_SEP = ','
317
+ ID_SET_SEP = ';'
304
318
 
305
- module ActiveRecord
306
- ID_SEP = ','
307
- ID_SET_SEP = ';'
319
+ class Base
320
+ # Allows +attr_name+ to be the list of primary_keys, and returns the id
321
+ # of the object
322
+ # e.g. @object[@object.class.primary_key] => [1,1]
323
+ def [](attr_name)
324
+ if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first
325
+ attr_name = attr_name.split(ID_SEP)
326
+ end
327
+ attr_name.is_a?(Array) ?
328
+ attr_name.map {|name| read_attribute(name)} :
329
+ read_attribute(attr_name)
330
+ end
308
331
 
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)
332
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
333
+ # (Alias for the protected write_attribute method).
334
+ def []=(attr_name, value)
335
+ if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first
336
+ attr_name = attr_name.split(ID_SEP)
320
337
  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
- if attr_name.is_a? Array
329
- value = value.split(ID_SEP) if value.is_a? String
330
- unless value.length == attr_name.length
331
- raise "Number of attr_names and values do not match"
332
- end
333
- #breakpoint
334
- [attr_name, value].transpose.map {|name,val| write_attribute(name.to_s, val)}
335
- else
336
- write_attribute(attr_name, value)
338
+ if attr_name.is_a? Array
339
+ value = value.split(ID_SEP) if value.is_a? String
340
+ unless value.length == attr_name.length
341
+ raise "Number of attr_names and values do not match"
337
342
  end
343
+ #breakpoint
344
+ [attr_name, value].transpose.map {|name,val| write_attribute(name.to_s, val)}
345
+ else
346
+ write_attribute(attr_name, value)
338
347
  end
339
-
340
348
  end
349
+
341
350
  end
351
+ end
@@ -0,0 +1,63 @@
1
+ module CompositePrimaryKeys
2
+ module ActiveRecord
3
+ module Calculations
4
+ module ClassMethods
5
+ def construct_calculation_sql(operation, column_name, options) #:nodoc:
6
+ operation = operation.to_s.downcase
7
+ options = options.symbolize_keys
8
+
9
+ scope = scope(:find)
10
+ merged_includes = merge_includes(scope ? scope[:include] : [], options[:include])
11
+ aggregate_alias = column_alias_for(operation, column_name)
12
+ use_workaround = !connection.supports_count_distinct? && options[:distinct] && operation.to_s.downcase == 'count'
13
+ join_dependency = nil
14
+
15
+ if merged_includes.any? && operation.to_s.downcase == 'count'
16
+ options[:distinct] = true
17
+ use_workaround = !connection.supports_count_distinct?
18
+ column_name = options[:select] || primary_key.map{ |part| "#{table_name}.#{part}"}.join(',')
19
+ end
20
+
21
+ sql = "SELECT #{operation}(#{'DISTINCT ' if options[:distinct]}#{column_name}) AS #{aggregate_alias}"
22
+
23
+ # A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
24
+ sql = "SELECT COUNT(*) AS #{aggregate_alias}" if use_workaround
25
+
26
+ sql << ", #{options[:group_field]} AS #{options[:group_alias]}" if options[:group]
27
+ sql << " FROM (SELECT DISTINCT #{column_name}" if use_workaround
28
+ sql << " FROM #{table_name} "
29
+ if merged_includes.any?
30
+ join_dependency = ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins])
31
+ sql << join_dependency.join_associations.collect{|join| join.association_join }.join
32
+ end
33
+ add_joins!(sql, options, scope)
34
+ add_conditions!(sql, options[:conditions], scope)
35
+ add_limited_ids_condition!(sql, options, join_dependency) if \
36
+ join_dependency &&
37
+ !using_limitable_reflections?(join_dependency.reflections) &&
38
+ ((scope && scope[:limit]) || options[:limit])
39
+
40
+ if options[:group]
41
+ group_key = Base.connection.adapter_name == 'FrontBase' ? :group_alias : :group_field
42
+ sql << " GROUP BY #{options[group_key]} "
43
+ end
44
+
45
+ if options[:group] && options[:having]
46
+ # FrontBase requires identifiers in the HAVING clause and chokes on function calls
47
+ if Base.connection.adapter_name == 'FrontBase'
48
+ options[:having].downcase!
49
+ options[:having].gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias)
50
+ end
51
+
52
+ sql << " HAVING #{options[:having]} "
53
+ end
54
+
55
+ sql << " ORDER BY #{options[:order]} " if options[:order]
56
+ add_limit!(sql, options, scope)
57
+ sql << ') as w1' if use_workaround
58
+ sql
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,11 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ class PostgreSQLAdapter < AbstractAdapter
4
+
5
+ # This mightn't be in Core, but count(distinct x,y) doesn't work for me
6
+ def supports_count_distinct? #:nodoc:
7
+ false
8
+ end
9
+ end
10
+ end
11
+ end