]> git.openstreetmap.org Git - rails.git/blobdiff - vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/associations.rb
Freezing composite primary key so that we can apply TomH's patch to it, and that...
[rails.git] / vendor / gems / composite_primary_keys-1.1.0 / lib / composite_primary_keys / associations.rb
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/associations.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/associations.rb
new file mode 100644 (file)
index 0000000..4ea4a7b
--- /dev/null
@@ -0,0 +1,428 @@
+module CompositePrimaryKeys
+  module ActiveRecord
+    module Associations
+      def self.append_features(base)
+        super
+        base.send(:extend, ClassMethods)
+      end
+
+      # Composite key versions of Association functions
+      module ClassMethods
+
+        def construct_counter_sql_with_included_associations(options, join_dependency)
+          scope = scope(:find)
+          sql = "SELECT COUNT(DISTINCT #{quoted_table_columns(primary_key)})"
+
+          # A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
+          if !self.connection.supports_count_distinct?
+            sql = "SELECT COUNT(*) FROM (SELECT DISTINCT #{quoted_table_columns(primary_key)}"
+          end
+          
+          sql << " FROM #{quoted_table_name} "
+          sql << join_dependency.join_associations.collect{|join| join.association_join }.join
+
+          add_joins!(sql, options, scope)
+          add_conditions!(sql, options[:conditions], scope)
+          add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
+
+          add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
+
+          if !self.connection.supports_count_distinct?
+            sql << ")"
+          end
+
+          return sanitize_sql(sql)
+        end
+
+        def construct_finder_sql_with_included_associations(options, join_dependency)
+          scope = scope(:find)
+          sql = "SELECT #{column_aliases(join_dependency)} FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
+          sql << join_dependency.join_associations.collect{|join| join.association_join }.join
+
+          add_joins!(sql, options, scope)
+          add_conditions!(sql, options[:conditions], scope)
+          add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && options[:limit]
+
+          sql << "ORDER BY #{options[:order]} " if options[:order]
+
+          add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
+
+          return sanitize_sql(sql)
+        end
+
+        def table_columns(columns)
+          columns.collect {|column| "#{self.quoted_table_name}.#{connection.quote_column_name(column)}"}
+        end
+
+        def quoted_table_columns(columns)
+          table_columns(columns).join(ID_SEP)
+        end
+
+      end
+
+    end
+  end
+end
+
+module ActiveRecord::Associations::ClassMethods
+  class JoinDependency
+    def construct_association(record, join, row)
+      case join.reflection.macro
+        when :has_many, :has_and_belongs_to_many
+          collection = record.send(join.reflection.name)
+          collection.loaded
+
+          join_aliased_primary_keys = join.active_record.composite? ?
+            join.aliased_primary_key : [join.aliased_primary_key]
+          return nil if
+            record.id.to_s != join.parent.record_id(row).to_s or not
+            join_aliased_primary_keys.select {|key| row[key].nil?}.blank?
+          association = join.instantiate(row)
+          collection.target.push(association) unless collection.target.include?(association)
+        when :has_one, :belongs_to
+          return if record.id.to_s != join.parent.record_id(row).to_s or
+                    [*join.aliased_primary_key].any? { |key| row[key].nil? }
+          association = join.instantiate(row)
+          record.send("set_#{join.reflection.name}_target", association)
+        else
+          raise ConfigurationError, "unknown macro: #{join.reflection.macro}"
+      end
+      return association
+    end
+
+    class JoinBase
+      def aliased_primary_key
+        active_record.composite? ?
+          primary_key.inject([]) {|aliased_keys, key| aliased_keys << "#{ aliased_prefix }_r#{aliased_keys.length}"} :
+          "#{ aliased_prefix }_r0"
+      end
+
+      def record_id(row)
+        active_record.composite? ?
+          aliased_primary_key.map {|key| row[key]}.to_composite_ids :
+          row[aliased_primary_key]
+      end
+
+      def column_names_with_alias
+        unless @column_names_with_alias
+          @column_names_with_alias = []
+          keys = active_record.composite? ? primary_key.map(&:to_s) : [primary_key]
+          (keys + (column_names - keys)).each_with_index do |column_name, i|
+            @column_names_with_alias << [column_name, "#{ aliased_prefix }_r#{ i }"]
+          end
+        end
+        return @column_names_with_alias
+      end
+    end
+
+    class JoinAssociation < JoinBase
+      alias single_association_join association_join
+      def association_join
+        reflection.active_record.composite? ? composite_association_join : single_association_join
+      end
+
+      def composite_association_join
+        join = case reflection.macro
+          when :has_and_belongs_to_many
+            " LEFT OUTER JOIN %s ON %s " % [
+              table_alias_for(options[:join_table], aliased_join_table_name),
+                composite_join_clause(
+                  full_keys(aliased_join_table_name, options[:foreign_key] || reflection.active_record.to_s.classify.foreign_key),
+                  full_keys(reflection.active_record.table_name, reflection.active_record.primary_key)
+                )
+              ] +
+             " LEFT OUTER JOIN %s ON %s " % [
+                table_name_and_alias,
+                composite_join_clause(
+                  full_keys(aliased_table_name, klass.primary_key),
+                  full_keys(aliased_join_table_name, options[:association_foreign_key] || klass.table_name.classify.foreign_key)
+                )
+              ]
+          when :has_many, :has_one
+            case
+              when reflection.macro == :has_many && reflection.options[:through]
+                through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : ''
+                if through_reflection.options[:as] # has_many :through against a polymorphic join
+                  raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
+                else
+                  if source_reflection.macro == :has_many && source_reflection.options[:as]
+                    raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
+                  else
+                    case source_reflection.macro
+                      when :belongs_to
+                        first_key  = primary_key
+                        second_key = options[:foreign_key] || klass.to_s.classify.foreign_key
+                      when :has_many
+                        first_key  = through_reflection.klass.to_s.classify.foreign_key
+                        second_key = options[:foreign_key] || primary_key
+                    end
+
+                    " LEFT OUTER JOIN %s ON %s "  % [
+                      table_alias_for(through_reflection.klass.table_name, aliased_join_table_name),
+                       composite_join_clause(
+                         full_keys(aliased_join_table_name, through_reflection.primary_key_name),
+                         full_keys(parent.aliased_table_name, parent.primary_key)
+                       )
+                     ] +
+                     " LEFT OUTER JOIN %s ON %s " % [
+                       table_name_and_alias,
+                       composite_join_clause(
+                         full_keys(aliased_table_name, first_key),
+                         full_keys(aliased_join_table_name, second_key)
+                       )
+                    ]
+                  end
+                end
+
+              when reflection.macro == :has_many && reflection.options[:as]
+                raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
+              when reflection.macro == :has_one && reflection.options[:as]
+                raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
+              else
+                foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
+                " LEFT OUTER JOIN %s ON %s " % [
+                  table_name_and_alias,
+                  composite_join_clause(
+                    full_keys(aliased_table_name, foreign_key),
+                    full_keys(parent.aliased_table_name, parent.primary_key)),
+                ]
+            end
+          when :belongs_to
+            " LEFT OUTER JOIN %s ON %s " % [
+               table_name_and_alias,
+               composite_join_clause(
+                 full_keys(aliased_table_name, reflection.klass.primary_key),
+                 full_keys(parent.aliased_table_name, options[:foreign_key] || klass.to_s.foreign_key)),
+              ]
+          else
+            ""
+        end || ''
+        join << %(AND %s.%s = %s ) % [
+          aliased_table_name,
+          reflection.active_record.connection.quote_column_name(reflection.active_record.inheritance_column),
+          klass.connection.quote(klass.name)] unless klass.descends_from_active_record?
+        join << "AND #{interpolate_sql(sanitize_sql(reflection.options[:conditions]))} " if reflection.options[:conditions]
+        join
+      end
+
+      def full_keys(table_name, keys)
+        connection = reflection.active_record.connection
+        quoted_table_name = connection.quote_table_name(table_name)
+        if keys.is_a?(Array) 
+          keys.collect {|key| "#{quoted_table_name}.#{connection.quote_column_name(key)}"}.join(CompositePrimaryKeys::ID_SEP) 
+        else
+          "#{quoted_table_name}.#{connection.quote_column_name(keys)}"
+        end
+      end
+
+      def composite_join_clause(full_keys1, full_keys2)
+        full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
+        full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
+        where_clause = [full_keys1, full_keys2].transpose.map do |key1, key2|
+          "#{key1}=#{key2}"
+        end.join(" AND ")
+        "(#{where_clause})"
+      end
+    end
+  end
+end
+
+module ActiveRecord::Associations
+  class AssociationProxy #:nodoc:
+
+    def composite_where_clause(full_keys, ids)
+      full_keys = full_keys.split(CompositePrimaryKeys::ID_SEP) if full_keys.is_a?(String)
+
+      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
+
+      where_clause = ids.map do |id_set|
+        transposed = id_set.size == 1 ? [[full_keys, id_set.first]] : [full_keys, id_set].transpose
+        transposed.map do |full_key, id|
+          "#{full_key.to_s}=#{@reflection.klass.sanitize(id)}"
+        end.join(" AND ")
+      end.join(") OR (")
+
+      "(#{where_clause})"
+    end
+
+    def composite_join_clause(full_keys1, full_keys2)
+      full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
+      full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
+
+      where_clause = [full_keys1, full_keys2].transpose.map do |key1, key2|
+        "#{key1}=#{key2}"
+      end.join(" AND ")
+
+      "(#{where_clause})"
+    end
+
+    def full_composite_join_clause(table1, full_keys1, table2, full_keys2)
+      connection = @reflection.active_record.connection
+      full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
+      full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
+
+      quoted1 = connection.quote_table_name(table1)
+      quoted2 = connection.quote_table_name(table2)
+      
+      where_clause = [full_keys1, full_keys2].transpose.map do |key_pair|
+        "#{quoted1}.#{connection.quote_column_name(key_pair.first)}=#{quoted2}.#{connection.quote_column_name(key_pair.last)}"
+      end.join(" AND ")
+
+      "(#{where_clause})"
+    end
+
+    def full_keys(table_name, keys)
+      connection = @reflection.active_record.connection
+      quoted_table_name = connection.quote_table_name(table_name)
+      keys = keys.split(CompositePrimaryKeys::ID_SEP) if keys.is_a?(String)
+      if keys.is_a?(Array) 
+        keys.collect {|key| "#{quoted_table_name}.#{connection.quote_column_name(key)}"}.join(CompositePrimaryKeys::ID_SEP) 
+      else
+        "#{quoted_table_name}.#{connection.quote_column_name(keys)}"
+      end
+    end
+
+    def full_columns_equals(table_name, keys, quoted_ids)
+      connection = @reflection.active_record.connection
+      quoted_table_name = connection.quote_table_name(table_name)
+      if keys.is_a?(Symbol) or (keys.is_a?(String) and keys == keys.to_s.split(CompositePrimaryKeys::ID_SEP))
+        return "#{quoted_table_name}.#{connection.quote_column_name(keys)} = #{quoted_ids}"
+      end
+      keys = keys.split(CompositePrimaryKeys::ID_SEP) if keys.is_a?(String)
+      quoted_ids = quoted_ids.split(CompositePrimaryKeys::ID_SEP) if quoted_ids.is_a?(String)
+      keys_ids = [keys, quoted_ids].transpose
+      keys_ids.collect {|key, id| "(#{quoted_table_name}.#{connection.quote_column_name(key)} = #{id})"}.join(' AND ')
+    end 
+
+    def set_belongs_to_association_for(record)
+      if @reflection.options[:as]
+        record["#{@reflection.options[:as]}_id"]   = @owner.id unless @owner.new_record?
+        record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
+      else
+        key_values = @reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP).zip([@owner.id].flatten)
+        key_values.each{|key, value| record[key] = value} unless @owner.new_record?
+      end
+    end
+  end
+
+  class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
+    def construct_sql
+      @reflection.options[:finder_sql] &&= interpolate_sql(@reflection.options[:finder_sql])
+
+      if @reflection.options[:finder_sql]
+        @finder_sql = @reflection.options[:finder_sql]
+      else
+        @finder_sql = full_columns_equals(@reflection.options[:join_table], @reflection.primary_key_name, @owner.quoted_id)
+        @finder_sql << " AND (#{conditions})" if conditions
+      end
+
+      @join_sql = "INNER JOIN #{@reflection.active_record.connection.quote_table_name(@reflection.options[:join_table])} ON " +
+      full_composite_join_clause(@reflection.klass.table_name, @reflection.klass.primary_key, @reflection.options[:join_table], @reflection.association_foreign_key)
+    end
+  end
+
+  class HasManyAssociation < AssociationCollection #:nodoc:
+    def construct_sql
+      case
+        when @reflection.options[:finder_sql]
+          @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
+
+        when @reflection.options[:as]
+          @finder_sql = 
+            "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " + 
+            "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
+          @finder_sql << " AND (#{conditions})" if conditions
+
+        else
+          @finder_sql = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, @owner.quoted_id)
+          @finder_sql << " AND (#{conditions})" if conditions
+      end
+
+      if @reflection.options[:counter_sql]
+        @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
+      elsif @reflection.options[:finder_sql]
+        # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
+        @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
+        @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
+      else
+        @counter_sql = @finder_sql
+      end
+    end
+
+    def delete_records(records)
+      if @reflection.options[:dependent]
+        records.each { |r| r.destroy }
+      else
+        connection = @reflection.active_record.connection
+        field_names = @reflection.primary_key_name.split(',')
+        field_names.collect! {|n| connection.quote_column_name(n) + " = NULL"}
+        records.each do |r|
+          where_clause = nil
+          
+          if r.quoted_id.to_s.include?(CompositePrimaryKeys::ID_SEP)
+            where_clause_terms = [@reflection.klass.primary_key, r.quoted_id].transpose.map do |pair|
+              "(#{connection.quote_column_name(pair[0])} = #{pair[1]})"
+            end
+            where_clause = where_clause_terms.join(" AND ")
+          else
+            where_clause = connection.quote_column_name(@reflection.klass.primary_key) + ' = ' +  r.quoted_id
+          end
+          
+          @reflection.klass.update_all(  field_names.join(',') , where_clause)
+        end
+      end
+    end
+  end
+
+  class HasOneAssociation < BelongsToAssociation #:nodoc:
+    def construct_sql
+      case
+        when @reflection.options[:as]
+          @finder_sql = 
+            "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " + 
+            "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
+        else
+          @finder_sql = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, @owner.quoted_id)
+      end
+
+      @finder_sql << " AND (#{conditions})" if conditions
+    end
+  end
+
+  class HasManyThroughAssociation < HasManyAssociation #:nodoc:
+    def construct_conditions_with_composite_keys
+      if @reflection.through_reflection.options[:as]
+        construct_conditions_without_composite_keys
+      else
+        conditions = full_columns_equals(@reflection.through_reflection.table_name, @reflection.through_reflection.primary_key_name, @owner.quoted_id)
+        conditions << " AND (#{sql_conditions})" if sql_conditions
+        conditions
+      end
+    end
+    alias_method_chain :construct_conditions, :composite_keys
+
+    def construct_joins_with_composite_keys(custom_joins = nil)
+      if @reflection.through_reflection.options[:as] || @reflection.source_reflection.options[:as]
+        construct_joins_without_composite_keys(custom_joins)
+      else
+        if @reflection.source_reflection.macro == :belongs_to
+          reflection_primary_key = @reflection.klass.primary_key
+          source_primary_key     = @reflection.source_reflection.primary_key_name
+        else
+          reflection_primary_key = @reflection.source_reflection.primary_key_name
+          source_primary_key     = @reflection.klass.primary_key
+        end
+
+        "INNER JOIN %s ON %s #{@reflection.options[:joins]} #{custom_joins}" % [
+          @reflection.through_reflection.quoted_table_name,
+          composite_join_clause(full_keys(@reflection.table_name, reflection_primary_key), full_keys(@reflection.through_reflection.table_name, source_primary_key))
+        ]
+      end
+    end
+    alias_method_chain :construct_joins, :composite_keys
+  end
+end