]> git.openstreetmap.org Git - rails.git/blob - vendor/gems/composite_primary_keys-2.2.2/lib/composite_primary_keys/base.rb
Merge branch 'master' into openstreetbugs
[rails.git] / vendor / gems / composite_primary_keys-2.2.2 / lib / composite_primary_keys / base.rb
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