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