X-Git-Url: https://git.openstreetmap.org/rails.git/blobdiff_plain/9156448ad6f1601c1c49a75ce58b0a0e932a51ed..a69f380fa5641192b55738d54f2c26e1403f6975:/vendor/gems/composite_primary_keys-2.2.2/lib/composite_primary_keys/base.rb diff --git a/vendor/gems/composite_primary_keys-2.2.2/lib/composite_primary_keys/base.rb b/vendor/gems/composite_primary_keys-2.2.2/lib/composite_primary_keys/base.rb new file mode 100644 index 000000000..a4c7ff93a --- /dev/null +++ b/vendor/gems/composite_primary_keys-2.2.2/lib/composite_primary_keys/base.rb @@ -0,0 +1,341 @@ +module CompositePrimaryKeys + module ActiveRecord #:nodoc: + class CompositeKeyError < StandardError #:nodoc: + end + + module Base #:nodoc: + + INVALID_FOR_COMPOSITE_KEYS = 'Not appropriate for composite primary keys' + NOT_IMPLEMENTED_YET = 'Not implemented for composite primary keys yet' + + def self.append_features(base) + super + base.send(:include, InstanceMethods) + base.extend(ClassMethods) + end + + module ClassMethods + def set_primary_keys(*keys) + keys = keys.first if keys.first.is_a?(Array) + keys = keys.map { |k| k.to_sym } + cattr_accessor :primary_keys + self.primary_keys = keys.to_composite_keys + + class_eval <<-EOV + extend CompositeClassMethods + include CompositeInstanceMethods + + include CompositePrimaryKeys::ActiveRecord::Associations + include CompositePrimaryKeys::ActiveRecord::AssociationPreload + include CompositePrimaryKeys::ActiveRecord::Calculations + include CompositePrimaryKeys::ActiveRecord::AttributeMethods + EOV + end + + def composite? + false + end + end + + module InstanceMethods + def composite?; self.class.composite?; end + end + + module CompositeInstanceMethods + + # A model instance's primary keys is always available as model.ids + # whether you name it the default 'id' or set it to something else. + def id + attr_names = self.class.primary_keys + CompositeIds.new(attr_names.map { |attr_name| read_attribute(attr_name) }) + end + alias_method :ids, :id + + def to_param + id.to_s + end + + def id_before_type_cast #:nodoc: + raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::NOT_IMPLEMENTED_YET + end + + def quoted_id #:nodoc: + [self.class.primary_keys, ids]. + transpose. + map {|attr_name,id| quote_value(id, column_for_attribute(attr_name))}. + to_composite_ids + end + + # Sets the primary ID. + def id=(ids) + ids = ids.split(ID_SEP) if ids.is_a?(String) + ids.flatten! + unless ids.is_a?(Array) and ids.length == self.class.primary_keys.length + raise "#{self.class}.id= requires #{self.class.primary_keys.length} ids" + end + [primary_keys, ids].transpose.each {|key, an_id| write_attribute(key , an_id)} + id + end + + # Returns a clone of the record that hasn't been assigned an id yet and + # is treated as a new record. Note that this is a "shallow" clone: + # it copies the object's attributes only, not its associations. + # The extent of a "deep" clone is application-specific and is therefore + # left to the application to implement according to its need. + def clone + attrs = self.attributes_before_type_cast + self.class.primary_keys.each {|key| attrs.delete(key.to_s)} + self.class.new do |record| + record.send :instance_variable_set, '@attributes', attrs + end + end + + + private + # The xx_without_callbacks methods are overwritten as that is the end of the alias chain + + # Creates a new record with values matching those of the instance attributes. + def create_without_callbacks + unless self.id + raise CompositeKeyError, "Composite keys do not generated ids from sequences, you must provide id values" + end + attributes_minus_pks = attributes_with_quotes(false) + quoted_pk_columns = self.class.primary_key.map { |col| connection.quote_column_name(col) } + cols = quoted_column_names(attributes_minus_pks) << quoted_pk_columns + vals = attributes_minus_pks.values << quoted_id + connection.insert( + "INSERT INTO #{self.class.quoted_table_name} " + + "(#{cols.join(', ')}) " + + "VALUES (#{vals.join(', ')})", + "#{self.class.name} Create", + self.class.primary_key, + self.id + ) + @new_record = false + return true + end + + # Updates the associated record with values matching those of the instance attributes. + def update_without_callbacks + where_clause_terms = [self.class.primary_key, quoted_id].transpose.map do |pair| + "(#{connection.quote_column_name(pair[0])} = #{pair[1]})" + end + where_clause = where_clause_terms.join(" AND ") + connection.update( + "UPDATE #{self.class.quoted_table_name} " + + "SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " + + "WHERE #{where_clause}", + "#{self.class.name} Update" + ) + return true + end + + # Deletes the record in the database and freezes this instance to reflect that no changes should + # be made (since they can't be persisted). + def destroy_without_callbacks + where_clause_terms = [self.class.primary_key, quoted_id].transpose.map do |pair| + "(#{connection.quote_column_name(pair[0])} = #{pair[1]})" + end + where_clause = where_clause_terms.join(" AND ") + unless new_record? + connection.delete( + "DELETE FROM #{self.class.quoted_table_name} " + + "WHERE #{where_clause}", + "#{self.class.name} Destroy" + ) + end + freeze + end + end + + module CompositeClassMethods + def primary_key; primary_keys; end + def primary_key=(keys); primary_keys = keys; end + + def composite? + true + end + + #ids_to_s([[1,2],[7,3]]) -> "(1,2),(7,3)" + #ids_to_s([[1,2],[7,3]], ',', ';') -> "1,2;7,3" + def ids_to_s(many_ids, id_sep = CompositePrimaryKeys::ID_SEP, list_sep = ',', left_bracket = '(', right_bracket = ')') + many_ids.map {|ids| "#{left_bracket}#{ids}#{right_bracket}"}.join(list_sep) + end + + # Creates WHERE condition from list of composited ids + # 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) + # 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) + def composite_where_clause(ids) + if ids.is_a?(String) + ids = [[ids]] + elsif not ids.first.is_a?(Array) # if single comp key passed, turn into an array of 1 + ids = [ids.to_composite_ids] + end + + ids.map do |id_set| + [primary_keys, id_set].transpose.map do |key, id| + "#{table_name}.#{key.to_s}=#{sanitize(id)}" + end.join(" AND ") + end.join(") OR (") + end + + # Returns true if the given +ids+ represents the primary keys of a record in the database, false otherwise. + # Example: + # Person.exists?(5,7) + def exists?(ids) + if ids.is_a?(Array) && ids.first.is_a?(String) + count(:conditions => ids) > 0 + else + obj = find(ids) rescue false + !obj.nil? and obj.is_a?(self) + end + end + + # Deletes the record with the given +ids+ without instantiating an object first, e.g. delete(1,2) + # If an array of ids is provided (e.g. delete([1,2], [3,4]), all of them + # are deleted. + def delete(*ids) + unless ids.is_a?(Array); raise "*ids must be an Array"; end + ids = [ids.to_composite_ids] if not ids.first.is_a?(Array) + where_clause = ids.map do |id_set| + [primary_keys, id_set].transpose.map do |key, id| + "#{quoted_table_name}.#{connection.quote_column_name(key.to_s)}=#{sanitize(id)}" + end.join(" AND ") + end.join(") OR (") + delete_all([ "(#{where_clause})" ]) + end + + # Destroys the record with the given +ids+ by instantiating the object and calling #destroy (all the callbacks are the triggered). + # If an array of ids is provided, all of them are destroyed. + def destroy(*ids) + unless ids.is_a?(Array); raise "*ids must be an Array"; end + if ids.first.is_a?(Array) + ids = ids.map{|compids| compids.to_composite_ids} + else + ids = ids.to_composite_ids + end + ids.first.is_a?(CompositeIds) ? ids.each { |id_set| find(id_set).destroy } : find(ids).destroy + end + + # Returns an array of column objects for the table associated with this class. + # Each column that matches to one of the primary keys has its + # primary attribute set to true + def columns + unless @columns + @columns = connection.columns(table_name, "#{name} Columns") + @columns.each {|column| column.primary = primary_keys.include?(column.name.to_sym)} + end + @columns + end + + ## DEACTIVATED METHODS ## + public + # Lazy-set the sequence name to the connection's default. This method + # is only ever called once since set_sequence_name overrides it. + def sequence_name #:nodoc: + raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS + end + + def reset_sequence_name #:nodoc: + raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS + end + + def set_primary_key(value = nil, &block) + raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS + end + + private + def find_one(id, options) + raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS + end + + def find_some(ids, options) + raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS + end + + def find_from_ids(ids, options) + ids = ids.first if ids.last == nil + conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions] + # if ids is just a flat list, then its size must = primary_key.length (one id per primary key, in order) + # if ids is list of lists, then each inner list must follow rule above + if ids.first.is_a? String + # find '2,1' -> ids = ['2,1'] + # find '2,1;7,3' -> ids = ['2,1;7,3'] + ids = ids.first.split(ID_SET_SEP).map {|id_set| id_set.split(ID_SEP).to_composite_ids} + # find '2,1;7,3' -> ids = [['2','1'],['7','3']], inner [] are CompositeIds + end + ids = [ids.to_composite_ids] if not ids.first.kind_of?(Array) + ids.each do |id_set| + unless id_set.is_a?(Array) + raise "Ids must be in an Array, instead received: #{id_set.inspect}" + end + unless id_set.length == primary_keys.length + raise "#{id_set.inspect}: Incorrect number of primary keys for #{class_name}: #{primary_keys.inspect}" + end + end + + # Let keys = [:a, :b] + # If ids = [[10, 50], [11, 51]], then :conditions => + # "(#{quoted_table_name}.a, #{quoted_table_name}.b) IN ((10, 50), (11, 51))" + + conditions = ids.map do |id_set| + [primary_keys, id_set].transpose.map do |key, id| + col = columns_hash[key.to_s] + val = quote_value(id, col) + "#{quoted_table_name}.#{connection.quote_column_name(key.to_s)}=#{val}" + end.join(" AND ") + end.join(") OR (") + + options.update :conditions => "(#{conditions})" + + result = find_every(options) + + if result.size == ids.size + ids.size == 1 ? result[0] : result + else + raise ::ActiveRecord::RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids.inspect})#{conditions}" + end + end + end + end + end +end + + +module ActiveRecord + ID_SEP = ',' + ID_SET_SEP = ';' + + class Base + # Allows +attr_name+ to be the list of primary_keys, and returns the id + # of the object + # e.g. @object[@object.class.primary_key] => [1,1] + def [](attr_name) + if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first + attr_name = attr_name.split(ID_SEP) + end + attr_name.is_a?(Array) ? + attr_name.map {|name| read_attribute(name)} : + read_attribute(attr_name) + end + + # Updates the attribute identified by attr_name with the specified +value+. + # (Alias for the protected write_attribute method). + def []=(attr_name, value) + if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first + attr_name = attr_name.split(ID_SEP) + end + + if attr_name.is_a? Array + value = value.split(ID_SEP) if value.is_a? String + unless value.length == attr_name.length + raise "Number of attr_names and values do not match" + end + #breakpoint + [attr_name, value].transpose.map {|name,val| write_attribute(name.to_s, val)} + else + write_attribute(attr_name, value) + end + end + end +end