composite_primary_keys 0.7.5 → 0.8.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/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