Add version 2.2.2 of composite_primary_keys.

This commit is contained in:
Tom Hughes 2009-05-08 17:54:49 +00:00
parent 9156448ad6
commit a69f380fa5
147 changed files with 6140 additions and 0 deletions

View file

@ -0,0 +1,63 @@
module AdapterHelper
class Base
class << self
attr_accessor :adapter
def load_connection_from_env(adapter)
self.adapter = adapter
unless ENV['cpk_adapters']
puts error_msg_setup_helper
exit
end
ActiveRecord::Base.configurations = YAML.load(ENV['cpk_adapters'])
unless spec = ActiveRecord::Base.configurations[adapter]
puts error_msg_adapter_helper
exit
end
spec[:adapter] = adapter
spec
end
def error_msg_setup_helper
<<-EOS
Setup Helper:
CPK now has a place for your individual testing configuration.
That is, instead of hardcoding it in the Rakefile and test/connections files,
there is now a local/database_connections.rb file that is NOT in the
repository. Your personal DB information (username, password etc) can
be stored here without making it difficult to submit patches etc.
Installation:
i) cp locals/database_connections.rb.sample locals/database_connections.rb
ii) For #{adapter} connection details see "Adapter Setup Helper" below.
iii) Rerun this task
#{error_msg_adapter_helper}
Current ENV:
#{ENV.inspect}
EOS
end
def error_msg_adapter_helper
<<-EOS
Adapter Setup Helper:
To run #{adapter} tests, you need to setup your #{adapter} connections.
In your local/database_connections.rb file, within the ENV['cpk_adapter'] hash, add:
"#{adapter}" => { adapter settings }
That is, it will look like:
ENV['cpk_adapters'] = {
"#{adapter}" => {
:adapter => "#{adapter}",
:username => "root",
:password => "root",
# ...
}
}.to_yaml
EOS
end
end
end
end

View file

@ -0,0 +1,13 @@
require File.join(File.dirname(__FILE__), 'base')
module AdapterHelper
class MySQL < Base
class << self
def load_connection_from_env
spec = super('mysql')
spec[:database] ||= 'composite_primary_keys_unittest'
spec
end
end
end
end

View file

@ -0,0 +1,12 @@
require File.join(File.dirname(__FILE__), 'base')
module AdapterHelper
class Oracle < Base
class << self
def load_connection_from_env
spec = super('oracle')
spec
end
end
end
end

View file

@ -0,0 +1,13 @@
require File.join(File.dirname(__FILE__), 'base')
module AdapterHelper
class Postgresql < Base
class << self
def load_connection_from_env
spec = super('postgresql')
spec[:database] ||= 'composite_primary_keys_unittest'
spec
end
end
end
end

View file

@ -0,0 +1,13 @@
require File.join(File.dirname(__FILE__), 'base')
module AdapterHelper
class Sqlite3 < Base
class << self
def load_connection_from_env
spec = super('sqlite3')
spec[:dbfile] ||= "tmp/test.db"
spec
end
end
end
end

View file

@ -0,0 +1,55 @@
#--
# Copyright (c) 2006 Nic Williams
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
$:.unshift(File.dirname(__FILE__)) unless
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
unless defined?(ActiveRecord)
begin
require 'active_record'
rescue LoadError
require 'rubygems'
require_gem 'activerecord'
end
end
require 'composite_primary_keys/fixtures'
require 'composite_primary_keys/composite_arrays'
require 'composite_primary_keys/associations'
require 'composite_primary_keys/association_preload'
require 'composite_primary_keys/reflection'
require 'composite_primary_keys/base'
require 'composite_primary_keys/calculations'
require 'composite_primary_keys/migration'
require 'composite_primary_keys/attribute_methods'
ActiveRecord::Base.class_eval do
include CompositePrimaryKeys::ActiveRecord::Base
end
Dir[File.dirname(__FILE__) + '/composite_primary_keys/connection_adapters/*.rb'].each do |adapter|
begin
require adapter.gsub('.rb','')
rescue MissingSourceFile
end
end

View file

@ -0,0 +1,253 @@
module CompositePrimaryKeys
module ActiveRecord
module AssociationPreload
def self.append_features(base)
super
base.send(:extend, ClassMethods)
end
# Composite key versions of Association functions
module ClassMethods
def preload_has_and_belongs_to_many_association(records, reflection, preload_options={})
table_name = reflection.klass.quoted_table_name
id_to_record_map, ids = construct_id_map_for_composite(records)
records.each {|record| record.send(reflection.name).loaded}
options = reflection.options
if composite?
primary_key = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP)
where = (primary_key * ids.size).in_groups_of(primary_key.size).map do |keys|
"(" + keys.map{|key| "t0.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")"
end.join(" OR ")
conditions = [where, ids].flatten
joins = "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{full_composite_join_clause(reflection, reflection.klass.table_name, reflection.klass.primary_key, 't0', reflection.association_foreign_key)}"
parent_primary_keys = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP).map{|k| "t0.#{connection.quote_column_name(k)}"}
parent_record_id = connection.concat(*parent_primary_keys.zip(["','"] * (parent_primary_keys.size - 1)).flatten.compact)
else
conditions = ["t0.#{connection.quote_column_name(reflection.primary_key_name)} IN (?)", ids]
joins = "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{connection.quote_column_name(reflection.klass.primary_key)} = t0.#{connection.quote_column_name(reflection.association_foreign_key)})"
parent_record_id = reflection.primary_key_name
end
conditions.first << append_conditions(reflection, preload_options)
associated_records = reflection.klass.find(:all,
:conditions => conditions,
:include => options[:include],
:joins => joins,
:select => "#{options[:select] || table_name+'.*'}, #{parent_record_id} as parent_record_id_",
:order => options[:order])
set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'parent_record_id_')
end
def preload_has_many_association(records, reflection, preload_options={})
id_to_record_map, ids = construct_id_map_for_composite(records)
records.each {|record| record.send(reflection.name).loaded}
options = reflection.options
if options[:through]
through_records = preload_through_records(records, reflection, options[:through])
through_reflection = reflections[options[:through]]
through_primary_key = through_reflection.primary_key_name
unless through_records.empty?
source = reflection.source_reflection.name
#add conditions from reflection!
through_records.first.class.preload_associations(through_records, source, reflection.options)
through_records.each do |through_record|
key = through_primary_key.to_s.split(CompositePrimaryKeys::ID_SEP).map{|k| through_record.send(k)}.join(CompositePrimaryKeys::ID_SEP)
add_preloaded_records_to_collection(id_to_record_map[key], reflection.name, through_record.send(source))
end
end
else
associated_records = find_associated_records(ids, reflection, preload_options)
set_association_collection_records(id_to_record_map, reflection.name, associated_records, reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP))
end
end
def preload_through_records(records, reflection, through_association)
through_reflection = reflections[through_association]
through_primary_key = through_reflection.primary_key_name
if reflection.options[:source_type]
interface = reflection.source_reflection.options[:foreign_type]
preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]}
records.compact!
records.first.class.preload_associations(records, through_association, preload_options)
# Dont cache the association - we would only be caching a subset
through_records = []
records.each do |record|
proxy = record.send(through_association)
if proxy.respond_to?(:target)
through_records << proxy.target
proxy.reset
else # this is a has_one :through reflection
through_records << proxy if proxy
end
end
through_records.flatten!
else
records.first.class.preload_associations(records, through_association)
through_records = records.map {|record| record.send(through_association)}.flatten
end
through_records.compact!
through_records
end
def preload_belongs_to_association(records, reflection, preload_options={})
options = reflection.options
primary_key_name = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP)
if options[:polymorphic]
raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
else
# I need to keep the original ids for each record (as opposed to the stringified) so
# that they get properly converted for each db so the id_map ends up looking like:
#
# { '1,2' => {:id => [1,2], :records => [...records...]}}
id_map = {}
records.each do |record|
key = primary_key_name.map{|k| record.send(k)}
key_as_string = key.join(CompositePrimaryKeys::ID_SEP)
if key_as_string
mapped_records = (id_map[key_as_string] ||= {:id => key, :records => []})
mapped_records[:records] << record
end
end
klasses_and_ids = [[reflection.klass.name, id_map]]
end
klasses_and_ids.each do |klass_and_id|
klass_name, id_map = *klass_and_id
klass = klass_name.constantize
table_name = klass.quoted_table_name
connection = reflection.active_record.connection
if composite?
primary_key = klass.primary_key.to_s.split(CompositePrimaryKeys::ID_SEP)
ids = id_map.keys.uniq.map {|id| id_map[id][:id]}
where = (primary_key * ids.size).in_groups_of(primary_key.size).map do |keys|
"(" + keys.map{|key| "#{table_name}.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")"
end.join(" OR ")
conditions = [where, ids].flatten
else
conditions = ["#{table_name}.#{connection.quote_column_name(primary_key)} IN (?)", id_map.keys.uniq]
end
conditions.first << append_conditions(reflection, preload_options)
associated_records = klass.find(:all,
:conditions => conditions,
:include => options[:include],
:select => options[:select],
:joins => options[:joins],
:order => options[:order])
set_association_single_records(id_map, reflection.name, associated_records, primary_key)
end
end
def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key)
associated_records.each do |associated_record|
associated_record_key = associated_record[key]
associated_record_key = associated_record_key.is_a?(Array) ? associated_record_key.join(CompositePrimaryKeys::ID_SEP) : associated_record_key.to_s
mapped_records = id_to_record_map[associated_record_key]
add_preloaded_records_to_collection(mapped_records, reflection_name, associated_record)
end
end
def set_association_single_records(id_to_record_map, reflection_name, associated_records, key)
seen_keys = {}
associated_records.each do |associated_record|
associated_record_key = associated_record[key]
associated_record_key = associated_record_key.is_a?(Array) ? associated_record_key.join(CompositePrimaryKeys::ID_SEP) : associated_record_key.to_s
#this is a has_one or belongs_to: there should only be one record.
#Unfortunately we can't (in portable way) ask the database for 'all records where foo_id in (x,y,z), but please
# only one row per distinct foo_id' so this where we enforce that
next if seen_keys[associated_record_key]
seen_keys[associated_record_key] = true
mapped_records = id_to_record_map[associated_record_key][:records]
mapped_records.each do |mapped_record|
mapped_record.send("set_#{reflection_name}_target", associated_record)
end
end
end
def find_associated_records(ids, reflection, preload_options)
options = reflection.options
table_name = reflection.klass.quoted_table_name
if interface = reflection.options[:as]
raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
else
connection = reflection.active_record.connection
foreign_key = reflection.primary_key_name
conditions = ["#{table_name}.#{connection.quote_column_name(foreign_key)} IN (?)", ids]
if composite?
foreign_keys = foreign_key.to_s.split(CompositePrimaryKeys::ID_SEP)
where = (foreign_keys * ids.size).in_groups_of(foreign_keys.size).map do |keys|
"(" + keys.map{|key| "#{table_name}.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")"
end.join(" OR ")
conditions = [where, ids].flatten
end
end
conditions.first << append_conditions(reflection, preload_options)
reflection.klass.find(:all,
:select => (preload_options[:select] || options[:select] || "#{table_name}.*"),
:include => preload_options[:include] || options[:include],
:conditions => conditions,
:joins => options[:joins],
:group => preload_options[:group] || options[:group],
:order => preload_options[:order] || options[:order])
end
# Given a collection of ActiveRecord objects, constructs a Hash which maps
# the objects' IDs to the relevant objects. Returns a 2-tuple
# <tt>(id_to_record_map, ids)</tt> where +id_to_record_map+ is the Hash,
# and +ids+ is an Array of record IDs.
def construct_id_map_for_composite(records)
id_to_record_map = {}
ids = []
records.each do |record|
primary_key ||= record.class.primary_key
ids << record.id
mapped_records = (id_to_record_map[record.id.to_s] ||= [])
mapped_records << record
end
ids.uniq!
return id_to_record_map, ids
end
def full_composite_join_clause(reflection, 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)
where_clause = [full_keys1, full_keys2].transpose.map do |key_pair|
quoted1 = connection.quote_table_name(table1)
quoted2 = connection.quote_table_name(table2)
"#{quoted1}.#{connection.quote_column_name(key_pair.first)}=#{quoted2}.#{connection.quote_column_name(key_pair.last)}"
end.join(" AND ")
"(#{where_clause})"
end
end
end
end
end

View file

@ -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[:joins], 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[:joins], 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

View file

@ -0,0 +1,84 @@
module CompositePrimaryKeys
module ActiveRecord
module AttributeMethods #:nodoc:
def self.append_features(base)
super
base.send(:extend, ClassMethods)
end
module ClassMethods
# Define an attribute reader method. Cope with nil column.
def define_read_method(symbol, attr_name, column)
cast_code = column.type_cast_code('v') if column
cast_code = "::#{cast_code}" if cast_code && cast_code.match('ActiveRecord::.*')
access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
unless self.primary_keys.include?(attr_name.to_sym)
access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
end
if cache_attribute?(attr_name)
access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
end
evaluate_attribute_method attr_name, "def #{symbol}; #{access_code}; end"
end
# Evaluate the definition for an attribute related method
def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name)
unless primary_keys.include?(method_name.to_sym)
generated_methods << method_name
end
begin
class_eval(method_definition, __FILE__, __LINE__)
rescue SyntaxError => err
generated_methods.delete(attr_name)
if logger
logger.warn "Exception occurred during reader method compilation."
logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
logger.warn "#{err.message}"
end
end
end
end
# Allows access to the object attributes, which are held in the @attributes hash, as though they
# were first-class methods. So a Person class with a name attribute can use Person#name and
# Person#name= and never directly use the attributes hash -- except for multiple assigns with
# ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
# the completed attribute is not nil or 0.
#
# It's also possible to instantiate related objects, so a Client class belonging to the clients
# table with a master_id foreign key can instantiate master through Client#master.
def method_missing(method_id, *args, &block)
method_name = method_id.to_s
# If we haven't generated any methods yet, generate them, then
# see if we've created the method we're looking for.
if !self.class.generated_methods?
self.class.define_attribute_methods
if self.class.generated_methods.include?(method_name)
return self.send(method_id, *args, &block)
end
end
if self.class.primary_keys.include?(method_name.to_sym)
ids[self.class.primary_keys.index(method_name.to_sym)]
elsif md = self.class.match_attribute_method?(method_name)
attribute_name, method_type = md.pre_match, md.to_s
if @attributes.include?(attribute_name)
__send__("attribute#{method_type}", attribute_name, *args, &block)
else
super
end
elsif @attributes.include?(method_name)
read_attribute(method_name)
else
super
end
end
end
end
end

View file

@ -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 <tt>attr_name</tt> 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

View file

@ -0,0 +1,69 @@
module CompositePrimaryKeys
module ActiveRecord
module Calculations
def self.append_features(base)
super
base.send(:extend, ClassMethods)
end
module ClassMethods
def construct_calculation_sql(operation, column_name, options) #:nodoc:
operation = operation.to_s.downcase
options = options.symbolize_keys
scope = scope(:find)
merged_includes = merge_includes(scope ? scope[:include] : [], options[:include])
aggregate_alias = column_alias_for(operation, column_name)
use_workaround = !connection.supports_count_distinct? && options[:distinct] && operation.to_s.downcase == 'count'
join_dependency = nil
if merged_includes.any? && operation.to_s.downcase == 'count'
options[:distinct] = true
use_workaround = !connection.supports_count_distinct?
column_name = options[:select] || primary_key.map{ |part| "#{quoted_table_name}.#{connection.quote_column_name(part)}"}.join(',')
end
sql = "SELECT #{operation}(#{'DISTINCT ' if options[:distinct]}#{column_name}) AS #{aggregate_alias}"
# A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
sql = "SELECT COUNT(*) AS #{aggregate_alias}" if use_workaround
sql << ", #{connection.quote_column_name(options[:group_field])} AS #{options[:group_alias]}" if options[:group]
sql << " FROM (SELECT DISTINCT #{column_name}" if use_workaround
sql << " FROM #{quoted_table_name} "
if merged_includes.any?
join_dependency = ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins])
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
end
add_joins!(sql, options[:joins], scope)
add_conditions!(sql, options[:conditions], scope)
add_limited_ids_condition!(sql, options, join_dependency) if \
join_dependency &&
!using_limitable_reflections?(join_dependency.reflections) &&
((scope && scope[:limit]) || options[:limit])
if options[:group]
group_key = connection.adapter_name == 'FrontBase' ? :group_alias : :group_field
sql << " GROUP BY #{connection.quote_column_name(options[group_key])} "
end
if options[:group] && options[:having]
# FrontBase requires identifiers in the HAVING clause and chokes on function calls
if connection.adapter_name == 'FrontBase'
options[:having].downcase!
options[:having].gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias)
end
sql << " HAVING #{options[:having]} "
end
sql << " ORDER BY #{options[:order]} " if options[:order]
add_limit!(sql, options, scope)
sql << ') w1' if use_workaround # assign a dummy table name as required for postgresql
sql
end
end
end
end
end

View file

@ -0,0 +1,30 @@
module CompositePrimaryKeys
ID_SEP = ','
ID_SET_SEP = ';'
module ArrayExtension
def to_composite_keys
CompositeKeys.new(self)
end
def to_composite_ids
CompositeIds.new(self)
end
end
class CompositeArray < Array
def to_s
join(ID_SEP)
end
end
class CompositeKeys < CompositeArray
end
class CompositeIds < CompositeArray
end
end
Array.send(:include, CompositePrimaryKeys::ArrayExtension)

View file

@ -0,0 +1,21 @@
module ActiveRecord
module ConnectionAdapters
class IBM_DBAdapter < AbstractAdapter
# This mightn't be in Core, but count(distinct x,y) doesn't work for me
def supports_count_distinct? #:nodoc:
false
end
alias_method :quote_original, :quote
def quote(value, column = nil)
if value.kind_of?(String) && column && [:integer, :float].include?(column.type)
value = column.type == :integer ? value.to_i : value.to_f
value.to_s
else
quote_original(value, column)
end
end
end
end
end

View file

@ -0,0 +1,15 @@
module ActiveRecord
module ConnectionAdapters
class OracleAdapter < AbstractAdapter
# This mightn't be in Core, but count(distinct x,y) doesn't work for me
def supports_count_distinct? #:nodoc:
false
end
def concat(*columns)
"(#{columns.join('||')})"
end
end
end
end

View file

@ -0,0 +1,53 @@
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapter < AbstractAdapter
# This mightn't be in Core, but count(distinct x,y) doesn't work for me
def supports_count_distinct? #:nodoc:
false
end
def concat(*columns)
columns = columns.map { |c| "CAST(#{c} AS varchar)" }
"(#{columns.join('||')})"
end
# Executes an INSERT query and returns the new record's ID
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
# Extract the table from the insert sql. Yuck.
table = sql.split(" ", 4)[2].gsub('"', '')
# Try an insert with 'returning id' if available (PG >= 8.2)
if supports_insert_with_returning?
pk, sequence_name = *pk_and_sequence_for(table) unless pk
if pk
quoted_pk = if pk.is_a?(Array)
pk.map { |col| quote_column_name(col) }.join(ID_SEP)
else
quote_column_name(pk)
end
id = select_value("#{sql} RETURNING #{quoted_pk}")
clear_query_cache
return id
end
end
# Otherwise, insert then grab last_insert_id.
if insert_id = super
insert_id
else
# If neither pk nor sequence name is given, look them up.
unless pk || sequence_name
pk, sequence_name = *pk_and_sequence_for(table)
end
# If a pk is given, fallback to default sequence name.
# Don't fetch last insert id for a table without a pk.
if pk && sequence_name ||= default_sequence_name(table, pk)
last_insert_id(table, sequence_name)
end
end
end
end
end
end

View file

@ -0,0 +1,15 @@
require 'active_record/connection_adapters/sqlite_adapter'
module ActiveRecord
module ConnectionAdapters #:nodoc:
class SQLite3Adapter < SQLiteAdapter # :nodoc:
def supports_count_distinct? #:nodoc:
false
end
def concat(*columns)
"(#{columns.join('||')})"
end
end
end
end

View file

@ -0,0 +1,8 @@
class Fixture #:nodoc:
def [](key)
if key.is_a? Array
return key.map { |a_key| self[a_key.to_s] }.to_composite_ids.to_s
end
@fixture[key]
end
end

View file

@ -0,0 +1,20 @@
ActiveRecord::ConnectionAdapters::ColumnDefinition.send(:alias_method, :to_s_without_composite_keys, :to_s)
ActiveRecord::ConnectionAdapters::ColumnDefinition.class_eval <<-'EOF'
def to_s
if name.is_a? Array
"PRIMARY KEY (#{name.join(',')})"
else
to_s_without_composite_keys
end
end
EOF
ActiveRecord::ConnectionAdapters::TableDefinition.class_eval <<-'EOF'
def [](name)
@columns.find { |column|
!column.name.is_a?(Array) && column.name.to_s == name.to_s
}
end
EOF

View file

@ -0,0 +1,19 @@
module ActiveRecord
module Reflection
class AssociationReflection
def primary_key_name
return @primary_key_name if @primary_key_name
case
when macro == :belongs_to
@primary_key_name = options[:foreign_key] || class_name.foreign_key
when options[:as]
@primary_key_name = options[:foreign_key] || "#{options[:as]}_id"
else
@primary_key_name = options[:foreign_key] || active_record.name.foreign_key
end
@primary_key_name = @primary_key_name.to_composite_keys.to_s if @primary_key_name.is_a? Array
@primary_key_name
end
end
end
end

View file

@ -0,0 +1,8 @@
module CompositePrimaryKeys
module VERSION #:nodoc:
MAJOR = 2
MINOR = 2
TINY = 2
STRING = [MAJOR, MINOR, TINY].join('.')
end
end