composite_primary_keys 1.0.8 → 1.0.10

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.
@@ -5,7 +5,7 @@ module CompositePrimaryKeys
5
5
  super
6
6
  base.send(:extend, ClassMethods)
7
7
  end
8
-
8
+
9
9
  module ClassMethods
10
10
  # Define an attribute reader method. Cope with nil column.
11
11
  def define_read_method(symbol, attr_name, column)
@@ -16,7 +16,7 @@ module CompositePrimaryKeys
16
16
  unless self.primary_keys.include?(attr_name.to_sym)
17
17
  access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
18
18
  end
19
-
19
+
20
20
  if cache_attribute?(attr_name)
21
21
  access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
22
22
  end
@@ -81,4 +81,4 @@ module CompositePrimaryKeys
81
81
  end
82
82
  end
83
83
  end
84
- end
84
+ end
@@ -2,72 +2,70 @@ module CompositePrimaryKeys
2
2
  module ActiveRecord #:nodoc:
3
3
  class CompositeKeyError < StandardError #:nodoc:
4
4
  end
5
-
5
+
6
6
  module Base #:nodoc:
7
-
7
+
8
8
  INVALID_FOR_COMPOSITE_KEYS = 'Not appropriate for composite primary keys'
9
- NOT_IMPLEMENTED_YET = 'Not implemented for composite primary keys yet'
10
-
9
+ NOT_IMPLEMENTED_YET = 'Not implemented for composite primary keys yet'
10
+
11
11
  def self.append_features(base)
12
12
  super
13
13
  base.send(:include, InstanceMethods)
14
14
  base.extend(ClassMethods)
15
15
  end
16
-
16
+
17
17
  module ClassMethods
18
18
  def set_primary_keys(*keys)
19
19
  keys = keys.first if keys.first.is_a?(Array)
20
- keys = keys.map {|k| k.to_sym }
21
- cattr_accessor :primary_keys
20
+ keys = keys.map { |k| k.to_sym }
21
+ cattr_accessor :primary_keys
22
22
  self.primary_keys = keys.to_composite_keys
23
-
23
+
24
24
  class_eval <<-EOV
25
25
  extend CompositeClassMethods
26
26
  include CompositeInstanceMethods
27
-
27
+
28
28
  include CompositePrimaryKeys::ActiveRecord::Associations
29
29
  include CompositePrimaryKeys::ActiveRecord::AssociationPreload
30
30
  include CompositePrimaryKeys::ActiveRecord::Calculations
31
31
  include CompositePrimaryKeys::ActiveRecord::AttributeMethods
32
32
  EOV
33
33
  end
34
-
34
+
35
35
  def composite?
36
36
  false
37
37
  end
38
38
  end
39
-
39
+
40
40
  module InstanceMethods
41
41
  def composite?; self.class.composite?; end
42
42
  end
43
-
44
- module CompositeInstanceMethods
45
-
43
+
44
+ module CompositeInstanceMethods
45
+
46
46
  # A model instance's primary keys is always available as model.ids
47
47
  # whether you name it the default 'id' or set it to something else.
48
48
  def id
49
49
  attr_names = self.class.primary_keys
50
- CompositeIds.new(
51
- attr_names.map {|attr_name| read_attribute(attr_name)}
52
- )
50
+ CompositeIds.new(attr_names.map { |attr_name| read_attribute(attr_name) })
53
51
  end
54
52
  alias_method :ids, :id
55
-
53
+
56
54
  def to_param
57
55
  id.to_s
58
56
  end
59
-
57
+
60
58
  def id_before_type_cast #:nodoc:
61
59
  raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::NOT_IMPLEMENTED_YET
62
60
  end
63
-
61
+
64
62
  def quoted_id #:nodoc:
65
63
  [self.class.primary_keys, ids].
66
64
  transpose.
67
65
  map {|attr_name,id| quote_value(id, column_for_attribute(attr_name))}.
68
66
  to_composite_ids
69
67
  end
70
-
68
+
71
69
  # Sets the primary ID.
72
70
  def id=(ids)
73
71
  ids = ids.split(ID_SEP) if ids.is_a?(String)
@@ -78,7 +76,7 @@ module CompositePrimaryKeys
78
76
  [primary_keys, ids].transpose.each {|key, an_id| write_attribute(key , an_id)}
79
77
  id
80
78
  end
81
-
79
+
82
80
  # Returns a clone of the record that hasn't been assigned an id yet and
83
81
  # is treated as a new record. Note that this is a "shallow" clone:
84
82
  # it copies the object's attributes only, not its associations.
@@ -91,21 +89,22 @@ module CompositePrimaryKeys
91
89
  record.send :instance_variable_set, '@attributes', attrs
92
90
  end
93
91
  end
94
-
95
-
92
+
93
+
96
94
  private
97
95
  # The xx_without_callbacks methods are overwritten as that is the end of the alias chain
98
-
96
+
99
97
  # Creates a new record with values matching those of the instance attributes.
100
98
  def create_without_callbacks
101
99
  unless self.id
102
100
  raise CompositeKeyError, "Composite keys do not generated ids from sequences, you must provide id values"
103
101
  end
104
102
  attributes_minus_pks = attributes_with_quotes(false)
105
- cols = quoted_column_names(attributes_minus_pks) << self.class.primary_key
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
106
105
  vals = attributes_minus_pks.values << quoted_id
107
106
  connection.insert(
108
- "INSERT INTO #{self.class.table_name} " +
107
+ "INSERT INTO #{self.class.quoted_table_name} " +
109
108
  "(#{cols.join(', ')}) " +
110
109
  "VALUES (#{vals.join(', ')})",
111
110
  "#{self.class.name} Create",
@@ -115,74 +114,93 @@ module CompositePrimaryKeys
115
114
  @new_record = false
116
115
  return true
117
116
  end
118
-
119
-
117
+
120
118
  # Updates the associated record with values matching those of the instance attributes.
121
119
  def update_without_callbacks
122
- where_class = [self.class.primary_key, quoted_id].transpose.map {|pair| "(#{pair[0]} = #{pair[1]})"}.join(" AND ")
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 ")
123
124
  connection.update(
124
- "UPDATE #{self.class.table_name} " +
125
+ "UPDATE #{self.class.quoted_table_name} " +
125
126
  "SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " +
126
- "WHERE #{where_class}",
127
+ "WHERE #{where_clause}",
127
128
  "#{self.class.name} Update"
128
129
  )
129
130
  return true
130
131
  end
131
-
132
+
132
133
  # Deletes the record in the database and freezes this instance to reflect that no changes should
133
134
  # be made (since they can't be persisted).
134
135
  def destroy_without_callbacks
135
- where_class = [self.class.primary_key, quoted_id].transpose.map do |pair|
136
- "(#{pair[0]} = #{pair[1]})"
137
- end.join(" AND ")
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 ")
138
140
  unless new_record?
139
141
  connection.delete(
140
- "DELETE FROM #{self.class.table_name} " +
141
- "WHERE #{where_class}",
142
+ "DELETE FROM #{self.class.quoted_table_name} " +
143
+ "WHERE #{where_clause}",
142
144
  "#{self.class.name} Destroy"
143
145
  )
144
146
  end
145
147
  freeze
146
148
  end
147
-
148
149
  end
149
-
150
+
150
151
  module CompositeClassMethods
151
152
  def primary_key; primary_keys; end
152
153
  def primary_key=(keys); primary_keys = keys; end
153
-
154
+
154
155
  def composite?
155
156
  true
156
157
  end
157
-
158
+
158
159
  #ids_to_s([[1,2],[7,3]]) -> "(1,2),(7,3)"
159
160
  #ids_to_s([[1,2],[7,3]], ',', ';') -> "1,2;7,3"
160
161
  def ids_to_s(many_ids, id_sep = CompositePrimaryKeys::ID_SEP, list_sep = ',', left_bracket = '(', right_bracket = ')')
161
162
  many_ids.map {|ids| "#{left_bracket}#{ids}#{right_bracket}"}.join(list_sep)
162
163
  end
163
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
+
164
182
  # Returns true if the given +ids+ represents the primary keys of a record in the database, false otherwise.
165
183
  # Example:
166
184
  # Person.exists?(5,7)
167
185
  def exists?(ids)
168
186
  obj = find(ids) rescue false
169
- !obj.nil? and obj.is_a?(self)
187
+ !obj.nil? and obj.is_a?(self)
170
188
  end
171
-
189
+
172
190
  # Deletes the record with the given +ids+ without instantiating an object first, e.g. delete(1,2)
173
191
  # If an array of ids is provided (e.g. delete([1,2], [3,4]), all of them
174
192
  # are deleted.
175
193
  def delete(*ids)
176
194
  unless ids.is_a?(Array); raise "*ids must be an Array"; end
177
195
  ids = [ids.to_composite_ids] if not ids.first.is_a?(Array)
178
- where_class = ids.map do |id_set|
196
+ where_clause = ids.map do |id_set|
179
197
  [primary_keys, id_set].transpose.map do |key, id|
180
- "#{table_name}.#{key.to_s}=#{sanitize(id)}"
198
+ "#{quoted_table_name}.#{connection.quote_column_name(key.to_s)}=#{sanitize(id)}"
181
199
  end.join(" AND ")
182
200
  end.join(") OR (")
183
- delete_all([ "(#{where_class})" ])
201
+ delete_all([ "(#{where_clause})" ])
184
202
  end
185
-
203
+
186
204
  # Destroys the record with the given +ids+ by instantiating the object and calling #destroy (all the callbacks are the triggered).
187
205
  # If an array of ids is provided, all of them are destroyed.
188
206
  def destroy(*ids)
@@ -194,7 +212,7 @@ module CompositePrimaryKeys
194
212
  end
195
213
  ids.first.is_a?(CompositeIds) ? ids.each { |id_set| find(id_set).destroy } : find(ids).destroy
196
214
  end
197
-
215
+
198
216
  # Returns an array of column objects for the table associated with this class.
199
217
  # Each column that matches to one of the primary keys has its
200
218
  # primary attribute set to true
@@ -205,7 +223,7 @@ module CompositePrimaryKeys
205
223
  end
206
224
  @columns
207
225
  end
208
-
226
+
209
227
  ## DEACTIVATED METHODS ##
210
228
  public
211
229
  # Lazy-set the sequence name to the connection's default. This method
@@ -213,24 +231,24 @@ module CompositePrimaryKeys
213
231
  def sequence_name #:nodoc:
214
232
  raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
215
233
  end
216
-
234
+
217
235
  def reset_sequence_name #:nodoc:
218
236
  raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
219
237
  end
220
-
238
+
221
239
  def set_primary_key(value = nil, &block)
222
240
  raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
223
241
  end
224
-
242
+
225
243
  private
226
244
  def find_one(id, options)
227
245
  raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
228
246
  end
229
-
247
+
230
248
  def find_some(ids, options)
231
249
  raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
232
250
  end
233
-
251
+
234
252
  def find_from_ids(ids, options)
235
253
  ids = ids.first if ids.last == nil
236
254
  conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
@@ -243,7 +261,7 @@ module CompositePrimaryKeys
243
261
  # find '2,1;7,3' -> ids = [['2','1'],['7','3']], inner [] are CompositeIds
244
262
  end
245
263
  ids = [ids.to_composite_ids] if not ids.first.kind_of?(Array)
246
- ids.each do |id_set|
264
+ ids.each do |id_set|
247
265
  unless id_set.is_a?(Array)
248
266
  raise "Ids must be in an Array, instead received: #{id_set.inspect}"
249
267
  end
@@ -251,69 +269,69 @@ module CompositePrimaryKeys
251
269
  raise "#{id_set.inspect}: Incorrect number of primary keys for #{class_name}: #{primary_keys.inspect}"
252
270
  end
253
271
  end
254
-
272
+
255
273
  # Let keys = [:a, :b]
256
274
  # If ids = [[10, 50], [11, 51]], then :conditions =>
257
- # "(#{table_name}.a, #{table_name}.b) IN ((10, 50), (11, 51))"
258
-
275
+ # "(#{quoted_table_name}.a, #{quoted_table_name}.b) IN ((10, 50), (11, 51))"
276
+
259
277
  conditions = ids.map do |id_set|
260
278
  [primary_keys, id_set].transpose.map do |key, id|
261
- col = columns_hash[key.to_s]
262
- val = quote_value(id, col)
263
-
264
- "#{table_name}.#{key.to_s}=#{val}"
265
- end.join(" AND ")
266
- end.join(") OR (")
267
- options.update :conditions => "(#{conditions})"
268
-
269
- result = find_every(options)
279
+ col = columns_hash[key.to_s]
280
+ val = quote_value(id, col)
281
+ "#{quoted_table_name}.#{connection.quote_column_name(key.to_s)}=#{val}"
282
+ end.join(" AND ")
283
+ end.join(") OR (")
270
284
 
271
- if result.size == ids.size
272
- ids.size == 1 ? result[0] : result
273
- else
274
- raise ::ActiveRecord::RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids.inspect})#{conditions}"
275
- end
276
- end
277
-
285
+ options.update :conditions => "(#{conditions})"
286
+
287
+ result = find_every(options)
288
+
289
+ if result.size == ids.size
290
+ ids.size == 1 ? result[0] : result
291
+ else
292
+ raise ::ActiveRecord::RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids.inspect})#{conditions}"
278
293
  end
279
294
  end
280
295
  end
281
296
  end
282
-
283
- module ActiveRecord
284
- ID_SEP = ','
285
- ID_SET_SEP = ';'
286
-
287
- class Base
288
- # Allows +attr_name+ to be the list of primary_keys, and returns the id
289
- # of the object
290
- # e.g. @object[@object.class.primary_key] => [1,1]
291
- def [](attr_name)
292
- if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first
293
- attr_name = attr_name.split(ID_SEP)
294
- end
295
- attr_name.is_a?(Array) ?
296
- attr_name.map {|name| read_attribute(name)} :
297
- read_attribute(attr_name)
298
- end
299
-
300
- # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
301
- # (Alias for the protected write_attribute method).
302
- def []=(attr_name, value)
303
- if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first
304
- attr_name = attr_name.split(ID_SEP)
305
- end
306
- if attr_name.is_a? Array
307
- value = value.split(ID_SEP) if value.is_a? String
308
- unless value.length == attr_name.length
309
- raise "Number of attr_names and values do not match"
310
- end
311
- #breakpoint
312
- [attr_name, value].transpose.map {|name,val| write_attribute(name.to_s, val)}
313
- else
314
- write_attribute(attr_name, value)
315
- end
297
+ end
298
+ end
299
+
300
+
301
+ module ActiveRecord
302
+ ID_SEP = ','
303
+ ID_SET_SEP = ';'
304
+
305
+ class Base
306
+ # Allows +attr_name+ to be the list of primary_keys, and returns the id
307
+ # of the object
308
+ # e.g. @object[@object.class.primary_key] => [1,1]
309
+ def [](attr_name)
310
+ if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first
311
+ attr_name = attr_name.split(ID_SEP)
312
+ end
313
+ attr_name.is_a?(Array) ?
314
+ attr_name.map {|name| read_attribute(name)} :
315
+ read_attribute(attr_name)
316
+ end
317
+
318
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
319
+ # (Alias for the protected write_attribute method).
320
+ def []=(attr_name, value)
321
+ if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first
322
+ attr_name = attr_name.split(ID_SEP)
323
+ end
324
+
325
+ if attr_name.is_a? Array
326
+ value = value.split(ID_SEP) if value.is_a? String
327
+ unless value.length == attr_name.length
328
+ raise "Number of attr_names and values do not match"
316
329
  end
317
-
330
+ #breakpoint
331
+ [attr_name, value].transpose.map {|name,val| write_attribute(name.to_s, val)}
332
+ else
333
+ write_attribute(attr_name, value)
318
334
  end
319
335
  end
336
+ end
337
+ end
@@ -5,7 +5,7 @@ module CompositePrimaryKeys
5
5
  super
6
6
  base.send(:extend, ClassMethods)
7
7
  end
8
-
8
+
9
9
  module ClassMethods
10
10
  def construct_calculation_sql(operation, column_name, options) #:nodoc:
11
11
  operation = operation.to_s.downcase
@@ -20,7 +20,7 @@ module CompositePrimaryKeys
20
20
  if merged_includes.any? && operation.to_s.downcase == 'count'
21
21
  options[:distinct] = true
22
22
  use_workaround = !connection.supports_count_distinct?
23
- column_name = options[:select] || primary_key.map{ |part| "#{table_name}.#{part}"}.join(',')
23
+ column_name = options[:select] || primary_key.map{ |part| "#{quoted_table_name}.#{connection.quote_column_name(part)}"}.join(',')
24
24
  end
25
25
 
26
26
  sql = "SELECT #{operation}(#{'DISTINCT ' if options[:distinct]}#{column_name}) AS #{aggregate_alias}"
@@ -28,9 +28,9 @@ module CompositePrimaryKeys
28
28
  # A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
29
29
  sql = "SELECT COUNT(*) AS #{aggregate_alias}" if use_workaround
30
30
 
31
- sql << ", #{options[:group_field]} AS #{options[:group_alias]}" if options[:group]
31
+ sql << ", #{connection.quote_column_name(options[:group_field])} AS #{options[:group_alias]}" if options[:group]
32
32
  sql << " FROM (SELECT DISTINCT #{column_name}" if use_workaround
33
- sql << " FROM #{table_name} "
33
+ sql << " FROM #{quoted_table_name} "
34
34
  if merged_includes.any?
35
35
  join_dependency = ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins])
36
36
  sql << join_dependency.join_associations.collect{|join| join.association_join }.join
@@ -38,13 +38,13 @@ module CompositePrimaryKeys
38
38
  add_joins!(sql, options, scope)
39
39
  add_conditions!(sql, options[:conditions], scope)
40
40
  add_limited_ids_condition!(sql, options, join_dependency) if \
41
- join_dependency &&
42
- !using_limitable_reflections?(join_dependency.reflections) &&
41
+ join_dependency &&
42
+ !using_limitable_reflections?(join_dependency.reflections) &&
43
43
  ((scope && scope[:limit]) || options[:limit])
44
44
 
45
45
  if options[:group]
46
46
  group_key = connection.adapter_name == 'FrontBase' ? :group_alias : :group_field
47
- sql << " GROUP BY #{options[group_key]} "
47
+ sql << " GROUP BY #{connection.quote_column_name(options[group_key])} "
48
48
  end
49
49
 
50
50
  if options[:group] && options[:having]
@@ -57,7 +57,7 @@ module CompositePrimaryKeys
57
57
  sql << " HAVING #{options[:having]} "
58
58
  end
59
59
 
60
- sql << " ORDER BY #{options[:order]} " if options[:order]
60
+ sql << " ORDER BY #{options[:order]} " if options[:order]
61
61
  add_limit!(sql, options, scope)
62
62
  sql << ') w1' if use_workaround # assign a dummy table name as required for postgresql
63
63
  sql