composite_primary_keys 3.1.7 → 3.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +4 -5
- data/lib/composite_primary_keys.rb +1 -1
- data/lib/composite_primary_keys/associations/association.rb +23 -0
- data/lib/composite_primary_keys/associations/association_scope.rb +67 -0
- data/lib/composite_primary_keys/associations/has_and_belongs_to_many_association.rb +52 -2
- data/lib/composite_primary_keys/associations/join_dependency/join_association.rb +22 -0
- data/lib/composite_primary_keys/associations/join_dependency/join_part.rb +39 -0
- data/lib/composite_primary_keys/associations/preloader/association.rb +61 -0
- data/lib/composite_primary_keys/associations/preloader/belongs_to.rb +13 -0
- data/lib/composite_primary_keys/associations/preloader/has_and_belongs_to_many.rb +46 -0
- data/lib/composite_primary_keys/attribute_methods/primary_key.rb +19 -0
- data/lib/composite_primary_keys/attribute_methods/read.rb +88 -0
- data/lib/composite_primary_keys/attribute_methods/write.rb +40 -0
- data/lib/composite_primary_keys/composite_predicates.rb +44 -0
- data/lib/composite_primary_keys/named_scope.rb +29 -0
- data/lib/composite_primary_keys/relation/finder_methods.rb +119 -0
- data/lib/composite_primary_keys/relation/query_methods.rb +24 -0
- data/lib/composite_primary_keys/version.rb +1 -1
- data/test/abstract_unit.rb +6 -7
- data/test/connections/native_mysql/connection.rb +1 -1
- data/test/test_associations.rb +10 -2
- data/test/test_create.rb +1 -1
- metadata +25 -27
data/History.txt
CHANGED
@@ -1,10 +1,11 @@
|
|
1
|
-
== 3.1.
|
1
|
+
== 3.1.8 2011-05-26
|
2
|
+
* Fix calling clear on a HABTM associate (David Rueck)
|
3
|
+
|
4
|
+
== 3.1.7 2011-05-26
|
2
5
|
* Support regular AR models having one or many composite models (Jacques Fuentes)
|
3
6
|
* Minor test cleanup
|
4
7
|
* Make version requirements more explicit
|
5
8
|
* Remove Arel extensions used for calculations
|
6
|
-
* Fix test that included wrong error constant
|
7
|
-
|
8
9
|
|
9
10
|
== 3.1.6 2011-04-03
|
10
11
|
* Updated belongs_to association to be a bit more flexible with non-CPK
|
@@ -13,14 +14,12 @@
|
|
13
14
|
* Fix write issue when one of they keys in a composite key is
|
14
15
|
called id (Tom Hughes)
|
15
16
|
|
16
|
-
|
17
17
|
== 3.1.5 2011-03-24
|
18
18
|
* Fix simple calculation methods
|
19
19
|
* Fix instantiation of cpk records via associations.
|
20
20
|
* Fix Relation#delete
|
21
21
|
* Fix Relation#destroy
|
22
22
|
|
23
|
-
|
24
23
|
== 3.1.4 2011-03-06
|
25
24
|
* Support ActiveRecord 3.0.5 (interpolate_sql was removed and
|
26
25
|
replaced by interpolate_and_sanitize_sql)
|
@@ -25,7 +25,7 @@ $:.unshift(File.dirname(__FILE__)) unless
|
|
25
25
|
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
26
26
|
|
27
27
|
unless defined?(ActiveRecord)
|
28
|
-
|
28
|
+
require 'rubygems'
|
29
29
|
gem 'activerecord', '>= 3.0.5', '~> 3.0.0'
|
30
30
|
require 'active_record'
|
31
31
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class Association
|
4
|
+
def creation_attributes
|
5
|
+
attributes = {}
|
6
|
+
|
7
|
+
if reflection.macro.in?([:has_one, :has_many]) && !options[:through]
|
8
|
+
# CPK
|
9
|
+
# attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
|
10
|
+
Array(reflection.foreign_key).zip(Array(reflection.active_record_primary_key)).each do |key1, key2|
|
11
|
+
attributes[key1] = owner[key2]
|
12
|
+
end
|
13
|
+
|
14
|
+
if reflection.options[:as]
|
15
|
+
attributes[reflection.type] = owner.class.base_class.name
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
attributes
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class AssociationScope
|
4
|
+
def add_constraints(scope)
|
5
|
+
tables = construct_tables
|
6
|
+
|
7
|
+
chain.each_with_index do |reflection, i|
|
8
|
+
table, foreign_table = tables.shift, tables.first
|
9
|
+
|
10
|
+
if reflection.source_macro == :has_and_belongs_to_many
|
11
|
+
join_table = tables.shift
|
12
|
+
|
13
|
+
# CPK
|
14
|
+
# scope = scope.joins(join(
|
15
|
+
# join_table,
|
16
|
+
# table[reflection.active_record_primary_key].
|
17
|
+
# eq(join_table[reflection.association_foreign_key])
|
18
|
+
#))
|
19
|
+
predicate = cpk_join_predicate(table, reflection.association_primary_key,
|
20
|
+
join_table, reflection.association_foreign_key)
|
21
|
+
scope = scope.joins(join(join_table, predicate))
|
22
|
+
|
23
|
+
table, foreign_table = join_table, tables.first
|
24
|
+
end
|
25
|
+
|
26
|
+
if reflection.source_macro == :belongs_to
|
27
|
+
key = reflection.association_primary_key
|
28
|
+
foreign_key = reflection.foreign_key
|
29
|
+
else
|
30
|
+
key = reflection.foreign_key
|
31
|
+
foreign_key = reflection.active_record_primary_key
|
32
|
+
end
|
33
|
+
|
34
|
+
if reflection == chain.last
|
35
|
+
# CPK
|
36
|
+
# scope = scope.where(table[key].eq(owner[foreign_key]))
|
37
|
+
predicate = cpk_join_predicate(table, key, owner, foreign_key)
|
38
|
+
scope = scope.where(predicate)
|
39
|
+
|
40
|
+
conditions[i].each do |condition|
|
41
|
+
if options[:through] && condition.is_a?(Hash)
|
42
|
+
condition = { table.name => condition }
|
43
|
+
end
|
44
|
+
|
45
|
+
scope = scope.where(interpolate(condition))
|
46
|
+
end
|
47
|
+
else
|
48
|
+
# CPK
|
49
|
+
# constraint = table[key].eq(foreign_table[foreign_key])
|
50
|
+
constraint = cpk_join_predicate(table, key, foreign_table, foreign_key)
|
51
|
+
scope = scope.where(predicate)
|
52
|
+
|
53
|
+
join = join(foreign_table, constraint)
|
54
|
+
|
55
|
+
scope = scope.joins(join)
|
56
|
+
|
57
|
+
unless conditions[i].empty?
|
58
|
+
scope = scope.where(sanitize(conditions[i], table))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
scope
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -88,6 +88,56 @@ module ActiveRecord
|
|
88
88
|
|
89
89
|
return true
|
90
90
|
end
|
91
|
-
|
91
|
+
|
92
|
+
# CPK
|
93
|
+
#def delete_records(records)
|
94
|
+
# if sql = @reflection.options[:delete_sql]
|
95
|
+
# records.each { |record| @owner.connection.delete(interpolate_and_sanitize_sql(sql, record)) }
|
96
|
+
# else
|
97
|
+
# relation = Arel::Table.new(@reflection.options[:join_table])
|
98
|
+
# relation.where(relation[@reflection.primary_key_name].eq(@owner.id).
|
99
|
+
# and(relation[@reflection.association_foreign_key].in(records.map { |x| x.id }.compact))
|
100
|
+
# ).delete
|
101
|
+
# end
|
102
|
+
#end
|
103
|
+
|
104
|
+
# CPK
|
105
|
+
def delete_records(records)
|
106
|
+
if sql = @reflection.options[:delete_sql]
|
107
|
+
records.each { |record| @owner.connection.delete(interpolate_and_sanitize_sql(sql, record)) }
|
108
|
+
else
|
109
|
+
relation = Arel::Table.new(@reflection.options[:join_table])
|
110
|
+
|
111
|
+
if @reflection.cpk_primary_key
|
112
|
+
owner_conditions = []
|
113
|
+
@reflection.cpk_primary_key.each_with_index do |column,i|
|
114
|
+
owner_conditions << relation[column.to_sym].eq(@owner.id[i])
|
115
|
+
end
|
116
|
+
owner_conditions_arel = owner_conditions.inject { |conds, cond| conds.and(cond) }
|
117
|
+
else
|
118
|
+
owner_conditions_arel = relation[@reflection.primary_key_name].eq(@owner.id)
|
119
|
+
end
|
120
|
+
|
121
|
+
if @reflection.association_foreign_key.kind_of?(Array)
|
122
|
+
association_conditions = []
|
123
|
+
records.each do |rec|
|
124
|
+
record_conditions = []
|
125
|
+
@reflection.association_foreign_key.each_with_index do |column,i|
|
126
|
+
record_conditions << relation[column.to_sym].eq(rec.id[i])
|
127
|
+
end
|
128
|
+
association_conditions << record_conditions.inject { |conds, cond| conds.and(cond) }
|
129
|
+
end
|
130
|
+
association_conditions_arel = association_conditions.inject { |conds, cond| conds.or(cond) }
|
131
|
+
else
|
132
|
+
association_conditions_arel = relation[@reflection.association_foreign_key].in(records.map { |x| x.id }.compact)
|
133
|
+
end
|
134
|
+
|
135
|
+
all_conditions_arel = owner_conditions_arel.and(association_conditions_arel)
|
136
|
+
|
137
|
+
relation.where(all_conditions_arel).delete
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
92
142
|
end
|
93
|
-
end
|
143
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class JoinDependency
|
4
|
+
class JoinAssociation
|
5
|
+
def build_constraint(reflection, table, key, foreign_table, foreign_key)
|
6
|
+
# CPK
|
7
|
+
# constraint = table[key].eq(foreign_table[foreign_key])
|
8
|
+
constraint = cpk_join_predicate(table, key, foreign_table, foreign_key)
|
9
|
+
|
10
|
+
if reflection.klass.finder_needs_type_condition?
|
11
|
+
constraint = table.create_and([
|
12
|
+
constraint,
|
13
|
+
reflection.klass.send(:type_condition, table)
|
14
|
+
])
|
15
|
+
end
|
16
|
+
|
17
|
+
constraint
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class JoinDependency
|
4
|
+
class JoinPart
|
5
|
+
def aliased_primary_key
|
6
|
+
# CPK
|
7
|
+
# "#{aliased_prefix}_r0"
|
8
|
+
|
9
|
+
active_record.composite? ?
|
10
|
+
primary_key.inject([]) {|aliased_keys, key| aliased_keys << "#{ aliased_prefix }_r#{aliased_keys.length}"} :
|
11
|
+
"#{ aliased_prefix }_r0"
|
12
|
+
end
|
13
|
+
|
14
|
+
def record_id(row)
|
15
|
+
# CPK
|
16
|
+
# row[aliased_primary_key]
|
17
|
+
active_record.composite? ?
|
18
|
+
aliased_primary_key.map {|key| row[key]}.to_composite_keys :
|
19
|
+
row[aliased_primary_key]
|
20
|
+
end
|
21
|
+
|
22
|
+
def column_names_with_alias
|
23
|
+
unless @column_names_with_alias
|
24
|
+
@column_names_with_alias = []
|
25
|
+
|
26
|
+
# CPK
|
27
|
+
#([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i|
|
28
|
+
keys = active_record.composite? ? primary_key.map(&:to_s) : [primary_key]
|
29
|
+
|
30
|
+
(keys + (column_names - keys)).each_with_index do |column_name, i|
|
31
|
+
@column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
@column_names_with_alias
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class Preloader
|
4
|
+
class Association
|
5
|
+
def records_for(ids)
|
6
|
+
# CPK
|
7
|
+
# scoped.where(association_key.in(ids))
|
8
|
+
predicate = cpk_in_predicate(table, reflection.foreign_key, ids)
|
9
|
+
scoped.where(predicate)
|
10
|
+
end
|
11
|
+
|
12
|
+
def associated_records_by_owner
|
13
|
+
# CPK
|
14
|
+
# owner_keys = owners.map { |owner| owner[owner_key_name] }.compact.uniq
|
15
|
+
owner_keys = owners.map do |owner|
|
16
|
+
Array(owner_key_name).map do |owner_key|
|
17
|
+
owner[owner_key]
|
18
|
+
end
|
19
|
+
end.compact.uniq
|
20
|
+
|
21
|
+
if klass.nil? || owner_keys.empty?
|
22
|
+
records = []
|
23
|
+
else
|
24
|
+
# Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
|
25
|
+
# Make several smaller queries if necessary or make one query if the adapter supports it
|
26
|
+
sliced = owner_keys.each_slice(model.connection.in_clause_length || owner_keys.size)
|
27
|
+
records = sliced.map { |slice| records_for(slice) }.flatten
|
28
|
+
end
|
29
|
+
|
30
|
+
# Each record may have multiple owners, and vice-versa
|
31
|
+
records_by_owner = Hash[owners.map { |owner| [owner, []] }]
|
32
|
+
records.each do |record|
|
33
|
+
# CPK
|
34
|
+
# owner_key = record[association_key_name].to_s
|
35
|
+
owner_key = Array(association_key_name).map do |key_name|
|
36
|
+
record[key_name]
|
37
|
+
end.join(CompositePrimaryKeys::ID_SEP)
|
38
|
+
|
39
|
+
owners_by_key[owner_key].each do |owner|
|
40
|
+
records_by_owner[owner] << record
|
41
|
+
end
|
42
|
+
end
|
43
|
+
records_by_owner
|
44
|
+
end
|
45
|
+
|
46
|
+
def owners_by_key
|
47
|
+
@owners_by_key ||= owners.group_by do |owner|
|
48
|
+
# CPK
|
49
|
+
# key = owner[owner_key_name]
|
50
|
+
key = Array(owner_key_name).map do |key_name|
|
51
|
+
owner[key_name]
|
52
|
+
end
|
53
|
+
# CPK
|
54
|
+
# key && key.to_s
|
55
|
+
key && key.join(CompositePrimaryKeys::ID_SEP)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class Preloader
|
4
|
+
class HasAndBelongsToMany
|
5
|
+
def records_for(ids)
|
6
|
+
# CPK
|
7
|
+
#scope = super
|
8
|
+
predicate = cpk_in_predicate(join_table, reflection.foreign_key, ids)
|
9
|
+
scope = scoped.where(predicate)
|
10
|
+
|
11
|
+
klass.connection.select_all(scope.arel.to_sql, 'SQL', scope.bind_values)
|
12
|
+
end
|
13
|
+
|
14
|
+
def join
|
15
|
+
# CPK
|
16
|
+
#condition = table[reflection.association_primary_key].eq(
|
17
|
+
# join_table[reflection.association_foreign_key])
|
18
|
+
condition = cpk_join_predicate(table, reflection.association_primary_key,
|
19
|
+
join_table, reflection.association_foreign_key)
|
20
|
+
|
21
|
+
table.create_join(join_table, table.create_on(condition))
|
22
|
+
end
|
23
|
+
|
24
|
+
def association_key_alias(field)
|
25
|
+
"ar_association_key_name_#{field.to_s}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def join_select
|
29
|
+
# CPK
|
30
|
+
# association_key.as(Arel.sql(association_key_name))
|
31
|
+
Array(reflection.foreign_key).map do |key|
|
32
|
+
join_table[key].as(Arel.sql(association_key_alias(key)))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def association_key_name
|
37
|
+
# CPK
|
38
|
+
# 'ar_association_key_name'
|
39
|
+
Array(reflection.foreign_key).map do |key|
|
40
|
+
association_key_alias(key)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module AttributeMethods #:nodoc:
|
3
|
+
module PrimaryKey
|
4
|
+
def to_key
|
5
|
+
# CPK
|
6
|
+
#key = send(self.class.primary_key)
|
7
|
+
#[key] if key
|
8
|
+
|
9
|
+
primary_key = self.class.primary_key
|
10
|
+
if primary_key.is_a?(Array)
|
11
|
+
primary_key.collect{|k| send(k)}
|
12
|
+
else
|
13
|
+
key = send(primary_key)
|
14
|
+
[key] if key
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module CompositePrimaryKeys
|
2
|
+
module ActiveRecord
|
3
|
+
module AttributeMethods
|
4
|
+
module Read
|
5
|
+
def self.included(base)
|
6
|
+
base.send(:extend, ClassMethods)
|
7
|
+
alias [] read_attribute
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def define_read_method(method_name, attr_name, column)
|
12
|
+
cast_code = column.type_cast_code('v')
|
13
|
+
# CPK - this is a really horrid hack, needed to get
|
14
|
+
# right class namespace :(
|
15
|
+
if cast_code.match(/^ActiveRecord/)
|
16
|
+
cast_code = "::#{cast_code}"
|
17
|
+
end
|
18
|
+
|
19
|
+
access_code = "(v=@attributes['#{attr_name}']) && #{cast_code}"
|
20
|
+
|
21
|
+
# CPK
|
22
|
+
# unless attr_name.to_s == self.primary_key.to_s
|
23
|
+
# access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
|
24
|
+
# end
|
25
|
+
unless self.primary_keys.include?(attr_name.to_sym)
|
26
|
+
access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
|
27
|
+
end
|
28
|
+
|
29
|
+
if cache_attribute?(attr_name)
|
30
|
+
access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
|
31
|
+
end
|
32
|
+
|
33
|
+
# Where possible, generate the method by evalling a string, as this will result in
|
34
|
+
# faster accesses because it avoids the block eval and then string eval incurred
|
35
|
+
# by the second branch.
|
36
|
+
#
|
37
|
+
# The second, slower, branch is necessary to support instances where the database
|
38
|
+
# returns columns with extra stuff in (like 'my_column(omg)').
|
39
|
+
if method_name =~ ActiveModel::AttributeMethods::COMPILABLE_REGEXP
|
40
|
+
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__
|
41
|
+
def _#{method_name}
|
42
|
+
#{access_code}
|
43
|
+
end
|
44
|
+
|
45
|
+
alias #{method_name} _#{method_name}
|
46
|
+
STR
|
47
|
+
else
|
48
|
+
generated_attribute_methods.module_eval do
|
49
|
+
define_method("_#{method_name}") { eval(access_code) }
|
50
|
+
alias_method(method_name, "_#{method_name}")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def read_attribute(attr_name)
|
57
|
+
# CPK
|
58
|
+
if attr_name.kind_of?(Array)
|
59
|
+
attr_name.map {|name| read_attribute(name)}.to_composite_keys
|
60
|
+
elsif respond_to? "_#{attr_name}"
|
61
|
+
send "_#{attr_name}" if @attributes.has_key?(attr_name.to_s)
|
62
|
+
else
|
63
|
+
_read_attribute attr_name
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def _read_attribute(attr_name)
|
68
|
+
attr_name = attr_name.to_s
|
69
|
+
# CPK
|
70
|
+
# attr_name = self.class.primary_key if attr_name == 'id'
|
71
|
+
attr_name = self.class.primary_key if (attr_name == 'id' and !self.composite?)
|
72
|
+
value = @attributes[attr_name]
|
73
|
+
unless value.nil?
|
74
|
+
if column = column_for_attribute(attr_name)
|
75
|
+
if unserializable_attribute?(attr_name, column)
|
76
|
+
unserialize_attribute(attr_name)
|
77
|
+
else
|
78
|
+
column.type_cast(value)
|
79
|
+
end
|
80
|
+
else
|
81
|
+
value
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module CompositePrimaryKeys
|
2
|
+
module ActiveRecord
|
3
|
+
module AttributeMethods
|
4
|
+
module Write
|
5
|
+
def self.included(base)
|
6
|
+
alias []= write_attribute
|
7
|
+
alias_method :raw_write_attribute, :write_attribute
|
8
|
+
end
|
9
|
+
|
10
|
+
def write_attribute(attr_name, value)
|
11
|
+
if attr_name.kind_of?(Array)
|
12
|
+
unless value.length == attr_name.length
|
13
|
+
raise "Number of attr_names and values do not match"
|
14
|
+
end
|
15
|
+
[attr_name, value].transpose.map {|name,val| _write_attribute(name, val)}
|
16
|
+
value
|
17
|
+
else
|
18
|
+
_write_attribute(attr_name, value)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def _write_attribute(attr_name, value)
|
23
|
+
attr_name = attr_name.to_s
|
24
|
+
# CPK
|
25
|
+
# attr_name = self.class.primary_key if attr_name == 'id'
|
26
|
+
attr_name = self.class.primary_key if (attr_name == 'id' and !self.composite?)
|
27
|
+
@attributes_cache.delete(attr_name)
|
28
|
+
if (column = column_for_attribute(attr_name)) && column.number?
|
29
|
+
@attributes[attr_name] = convert_number_column_value(value)
|
30
|
+
else
|
31
|
+
@attributes[attr_name] = value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module CompositePrimaryKeys
|
2
|
+
module Predicates
|
3
|
+
def cpk_or_predicate(predicates)
|
4
|
+
result = predicates.shift
|
5
|
+
predicates.each do |predicate|
|
6
|
+
result = result.or(predicate)
|
7
|
+
end
|
8
|
+
result
|
9
|
+
end
|
10
|
+
|
11
|
+
def cpk_id_predicate(table, keys, values)
|
12
|
+
eq_predicates = keys.zip(values).map do |key, value|
|
13
|
+
table[key].eq(value)
|
14
|
+
end
|
15
|
+
Arel::Nodes::And.new(eq_predicates)
|
16
|
+
end
|
17
|
+
|
18
|
+
def cpk_join_predicate(table1, key1, table2, key2)
|
19
|
+
key1_fields = Array(key1).map {|key| table1[key]}
|
20
|
+
key2_fields = Array(key2).map {|key| table2[key]}
|
21
|
+
|
22
|
+
predicates = key1_fields.zip(key2_fields).map do |key_field1, key_field2|
|
23
|
+
key_field1.eq(key_field2)
|
24
|
+
end
|
25
|
+
Arel::Nodes::And.new(predicates)
|
26
|
+
end
|
27
|
+
|
28
|
+
def cpk_in_predicate(table, primary_keys, ids)
|
29
|
+
and_predicates = ids.map do |id_set|
|
30
|
+
eq_predicates = Array(primary_keys).zip(id_set).map do |primary_key, value|
|
31
|
+
table[primary_key].eq(value)
|
32
|
+
end
|
33
|
+
Arel::Nodes::And.new(eq_predicates)
|
34
|
+
end
|
35
|
+
|
36
|
+
cpk_or_predicate(and_predicates)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
ActiveRecord::Associations::AssociationScope.send(:include, CompositePrimaryKeys::Predicates)
|
42
|
+
ActiveRecord::Associations::JoinDependency::JoinAssociation.send(:include, CompositePrimaryKeys::Predicates)
|
43
|
+
ActiveRecord::Associations::Preloader::Association.send(:include, CompositePrimaryKeys::Predicates)
|
44
|
+
ActiveRecord::Relation.send(:include, CompositePrimaryKeys::Predicates)
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module CompositePrimaryKeys
|
2
|
+
module ActiveRecord
|
3
|
+
module NamedScope
|
4
|
+
module ClassMethods
|
5
|
+
def scoped(options = nil)
|
6
|
+
result = if options
|
7
|
+
scoped.apply_finder_options(options)
|
8
|
+
else
|
9
|
+
if current_scope
|
10
|
+
current_scope.clone
|
11
|
+
else
|
12
|
+
scope = relation.clone
|
13
|
+
scope.default_scoped = true
|
14
|
+
scope
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# CPK
|
19
|
+
class << result
|
20
|
+
include CompositePrimaryKeys::ActiveRecord::FinderMethods
|
21
|
+
include CompositePrimaryKeys::ActiveRecord::QueryMethods
|
22
|
+
include CompositePrimaryKeys::ActiveRecord::Relation
|
23
|
+
end
|
24
|
+
result
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module CompositePrimaryKeys
|
2
|
+
module ActiveRecord
|
3
|
+
module FinderMethods
|
4
|
+
def construct_limited_ids_condition(relation)
|
5
|
+
orders = relation.order_values
|
6
|
+
# CPK
|
7
|
+
# values = @klass.connection.distinct("#{@klass.connection.quote_table_name table_name}.#{primary_key}", orders)
|
8
|
+
keys = @klass.primary_keys.map do |key|
|
9
|
+
"#{@klass.connection.quote_table_name @klass.table_name}.#{key}"
|
10
|
+
end
|
11
|
+
values = @klass.connection.distinct(keys.join(', '), orders)
|
12
|
+
|
13
|
+
relation = relation.dup
|
14
|
+
|
15
|
+
ids_array = relation.select(values).collect {|row| row[primary_key]}
|
16
|
+
# CPK
|
17
|
+
# ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array)
|
18
|
+
|
19
|
+
# OR together each and expression (key=value and key=value) that matches an id set
|
20
|
+
# since we only need to match 0 or more records
|
21
|
+
or_expressions = ids_array.map do |id_set|
|
22
|
+
# AND together "key=value" exprssios to match each id set
|
23
|
+
and_expressions = [self.primary_keys, id_set].transpose.map do |key, id|
|
24
|
+
table[key].eq(id)
|
25
|
+
end
|
26
|
+
Arel::Nodes::And.new(and_expressions)
|
27
|
+
end
|
28
|
+
|
29
|
+
first = or_expressions.shift
|
30
|
+
Arel::Nodes::Grouping.new(or_expressions.inject(first) do |memo, expr|
|
31
|
+
Arel::Nodes::Or.new(memo, expr)
|
32
|
+
end)
|
33
|
+
end
|
34
|
+
|
35
|
+
def exists?(id = nil)
|
36
|
+
# ID can be:
|
37
|
+
# Array - ['department_id = ? and location_id = ?', 1, 1]
|
38
|
+
# Array -> [1,2]
|
39
|
+
# CompositeKeys -> [1,2]
|
40
|
+
|
41
|
+
id = id.id if ::ActiveRecord::Base === id
|
42
|
+
|
43
|
+
join_dependency = construct_join_dependency_for_association_find
|
44
|
+
relation = construct_relation_for_association_find(join_dependency)
|
45
|
+
relation = relation.except(:select).select("1").limit(1)
|
46
|
+
|
47
|
+
# CPK
|
48
|
+
#case id
|
49
|
+
#when Array, Hash
|
50
|
+
# relation = relation.where(id)
|
51
|
+
#else
|
52
|
+
# relation = relation.where(table[primary_key].eq(id)) if id
|
53
|
+
#end
|
54
|
+
|
55
|
+
case id
|
56
|
+
when CompositePrimaryKeys::CompositeKeys
|
57
|
+
relation = relation.where(cpk_id_predicate(table, primary_key, id))
|
58
|
+
when Array
|
59
|
+
if !id.first.kind_of?(String)
|
60
|
+
return self.exists?(id.to_composite_keys)
|
61
|
+
else
|
62
|
+
relation = relation.where(id)
|
63
|
+
end
|
64
|
+
when Hash
|
65
|
+
relation = relation.where(id)
|
66
|
+
else
|
67
|
+
raise(ArgumentError, "Unsupported id sent to exists?")
|
68
|
+
end
|
69
|
+
connection.select_value(relation.to_sql) ? true : false
|
70
|
+
end
|
71
|
+
|
72
|
+
def find_with_ids(*ids, &block)
|
73
|
+
return to_a.find { |*block_args| yield(*block_args) } if block_given?
|
74
|
+
|
75
|
+
# Supports:
|
76
|
+
# find('1,2') -> ['1,2']
|
77
|
+
# find(1,2) -> [1,2]
|
78
|
+
# find([1,2]) -> [['1,2']]
|
79
|
+
# find([1,2], [3,4]) -> [[1,2],[3,4]]
|
80
|
+
#
|
81
|
+
# Does *not* support:
|
82
|
+
# find('1,2', '3,4') -> ['1,2','3,4']
|
83
|
+
|
84
|
+
# Normalize incoming data. Note the last arg can be nil. Happens
|
85
|
+
# when find is called with nil options like the reload method does.
|
86
|
+
ids.compact!
|
87
|
+
ids = [ids] unless ids.first.kind_of?(Array)
|
88
|
+
|
89
|
+
results = ids.map do |cpk_ids|
|
90
|
+
cpk_ids = if cpk_ids.length == 1
|
91
|
+
cpk_ids.first.split(CompositePrimaryKeys::ID_SEP).to_composite_keys
|
92
|
+
else
|
93
|
+
cpk_ids.to_composite_keys
|
94
|
+
end
|
95
|
+
|
96
|
+
unless cpk_ids.length == @klass.primary_keys.length
|
97
|
+
raise "#{cpk_ids.inspect}: Incorrect number of primary keys for #{@klass.name}: #{@klass.primary_keys.inspect}"
|
98
|
+
end
|
99
|
+
|
100
|
+
new_relation = clone
|
101
|
+
[@klass.primary_keys, cpk_ids].transpose.map do |key, id|
|
102
|
+
new_relation = new_relation.where(key => id)
|
103
|
+
end
|
104
|
+
|
105
|
+
records = new_relation.to_a
|
106
|
+
|
107
|
+
if records.empty?
|
108
|
+
conditions = new_relation.arel.where_sql
|
109
|
+
raise(::ActiveRecord::RecordNotFound,
|
110
|
+
"Couldn't find #{@klass.name} with ID=#{cpk_ids} #{conditions}")
|
111
|
+
end
|
112
|
+
records
|
113
|
+
end.flatten
|
114
|
+
|
115
|
+
ids.length == 1 ? results.first : results
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module CompositePrimaryKeys
|
2
|
+
module ActiveRecord
|
3
|
+
module QueryMethods
|
4
|
+
def reverse_order
|
5
|
+
order_clause = arel.order_clauses
|
6
|
+
|
7
|
+
# CPK
|
8
|
+
# order = order_clause.empty? ?
|
9
|
+
# "#{table_name}.#{primary_key} DESC" :
|
10
|
+
# reverse_sql_order(order_clause).join(', ')
|
11
|
+
|
12
|
+
order = unless order_clause.empty?
|
13
|
+
reverse_sql_order(order_clause).join(', ')
|
14
|
+
else
|
15
|
+
klass.primary_key.map do |key|
|
16
|
+
"#{table_name}.#{key} DESC"
|
17
|
+
end.join(", ")
|
18
|
+
end
|
19
|
+
|
20
|
+
except(:order).order(Arel.sql(order))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/test/abstract_unit.rb
CHANGED
@@ -1,21 +1,20 @@
|
|
1
1
|
dir = File.dirname(__FILE__)
|
2
2
|
PROJECT_ROOT = File.expand_path(File.join(dir, '..'))
|
3
3
|
|
4
|
-
adapter = ENV["ADAPTER"] ||
|
4
|
+
adapter = ENV["ADAPTER"] || "postgresql"
|
5
5
|
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require 'rubygems'
|
6
|
+
require "pp"
|
7
|
+
require "test/unit"
|
8
|
+
require "hash_tricks"
|
10
9
|
|
11
10
|
# To make debugging easier, test within this source tree versus an installed gem
|
12
11
|
#require 'composite_primary_keys'
|
13
12
|
require File.join(PROJECT_ROOT, "lib", "composite_primary_keys")
|
14
13
|
|
15
|
-
require
|
16
|
-
require File.join(PROJECT_ROOT, 'test', 'connections', 'connection_spec')
|
14
|
+
require File.join(PROJECT_ROOT, "test", "connections", "connection_spec")
|
17
15
|
require File.join(PROJECT_ROOT, "test", "connections", "native_#{adapter}", "connection")
|
18
16
|
|
17
|
+
|
19
18
|
# Tell ActiveRecord where to find models
|
20
19
|
ActiveSupport::Dependencies.autoload_paths << File.join(PROJECT_ROOT, 'test', 'fixtures')
|
21
20
|
|
@@ -7,7 +7,7 @@ def connection_string
|
|
7
7
|
options['u'] = SPEC['username'] if SPEC['username']
|
8
8
|
options['p'] = SPEC['password'] if SPEC['password']
|
9
9
|
options['S'] = SPEC['sock'] if SPEC['sock']
|
10
|
-
options.map { |key, value| "-#{key}#{value}" }.join(" ")
|
10
|
+
options.map { |key, value| "-#{key} #{value}" }.join(" ")
|
11
11
|
end
|
12
12
|
|
13
13
|
# Adapter config setup in locals/database_connections.rb
|
data/test/test_associations.rb
CHANGED
@@ -160,7 +160,15 @@ class TestAssociations < ActiveSupport::TestCase
|
|
160
160
|
assert_equal 2, @restaurant.suburbs.size
|
161
161
|
end
|
162
162
|
|
163
|
-
def
|
163
|
+
def test_habtm_clear
|
164
|
+
@restaurant = Restaurant.find([1,1])
|
165
|
+
assert_equal 2, @restaurant.suburbs.size
|
166
|
+
@restaurant.suburbs.clear
|
167
|
+
@restaurant = Restaurant.find([1,1])
|
168
|
+
assert_equal 0, @restaurant.suburbs.size
|
169
|
+
end
|
170
|
+
|
171
|
+
def test_has_many_with_primary_key
|
164
172
|
@membership = Membership.find([1, 1])
|
165
173
|
assert_equal 2, @membership.reading.id
|
166
174
|
end
|
@@ -235,4 +243,4 @@ class TestAssociations < ActiveSupport::TestCase
|
|
235
243
|
assert_equal(1, memberships.length)
|
236
244
|
assert_equal([1,1], memberships[0].id)
|
237
245
|
end
|
238
|
-
end
|
246
|
+
end
|
data/test/test_create.rb
CHANGED
@@ -38,7 +38,7 @@ class TestCreate < ActiveSupport::TestCase
|
|
38
38
|
begin
|
39
39
|
@obj = @klass.create(@klass_info[:create].block(@klass.primary_key))
|
40
40
|
@successful = !composite?
|
41
|
-
rescue ActiveRecord::CompositeKeyError
|
41
|
+
rescue CompositePrimaryKeys::ActiveRecord::CompositeKeyError
|
42
42
|
@successful = false
|
43
43
|
rescue
|
44
44
|
flunk "Incorrect exception raised: #{$!}, #{$!.class}"
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: composite_primary_keys
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 3
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 3.1.
|
9
|
+
- 8
|
10
|
+
version: 3.1.8
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Dr Nic Williams
|
@@ -16,30 +16,22 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date: 2011-05-
|
19
|
+
date: 2011-05-26 00:00:00 Z
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
22
|
-
name:
|
22
|
+
name: activerecord
|
23
23
|
prerelease: false
|
24
24
|
requirement: &id001 !ruby/object:Gem::Requirement
|
25
25
|
none: false
|
26
26
|
requirements:
|
27
27
|
- - ~>
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
hash:
|
29
|
+
hash: 7
|
30
30
|
segments:
|
31
|
-
-
|
31
|
+
- 3
|
32
32
|
- 0
|
33
33
|
- 0
|
34
|
-
version:
|
35
|
-
type: :runtime
|
36
|
-
version_requirements: *id001
|
37
|
-
- !ruby/object:Gem::Dependency
|
38
|
-
name: activerecord
|
39
|
-
prerelease: false
|
40
|
-
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
|
-
requirements:
|
34
|
+
version: 3.0.0
|
43
35
|
- - ">="
|
44
36
|
- !ruby/object:Gem::Version
|
45
37
|
hash: 13
|
@@ -48,20 +40,12 @@ dependencies:
|
|
48
40
|
- 0
|
49
41
|
- 5
|
50
42
|
version: 3.0.5
|
51
|
-
- - ~>
|
52
|
-
- !ruby/object:Gem::Version
|
53
|
-
hash: 7
|
54
|
-
segments:
|
55
|
-
- 3
|
56
|
-
- 0
|
57
|
-
- 0
|
58
|
-
version: 3.0.0
|
59
43
|
type: :runtime
|
60
|
-
version_requirements: *
|
44
|
+
version_requirements: *id001
|
61
45
|
- !ruby/object:Gem::Dependency
|
62
46
|
name: rspec
|
63
47
|
prerelease: false
|
64
|
-
requirement: &
|
48
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
65
49
|
none: false
|
66
50
|
requirements:
|
67
51
|
- - ">="
|
@@ -71,7 +55,7 @@ dependencies:
|
|
71
55
|
- 0
|
72
56
|
version: "0"
|
73
57
|
type: :development
|
74
|
-
version_requirements: *
|
58
|
+
version_requirements: *id002
|
75
59
|
description: Composite key support for ActiveRecord 3
|
76
60
|
email:
|
77
61
|
- drnicwilliams@gmail.com
|
@@ -89,17 +73,28 @@ files:
|
|
89
73
|
- init.rb
|
90
74
|
- install.rb
|
91
75
|
- loader.rb
|
76
|
+
- lib/composite_primary_keys/associations/association.rb
|
92
77
|
- lib/composite_primary_keys/associations/association_proxy.rb
|
78
|
+
- lib/composite_primary_keys/associations/association_scope.rb
|
93
79
|
- lib/composite_primary_keys/associations/has_and_belongs_to_many_association.rb
|
94
80
|
- lib/composite_primary_keys/associations/has_many_association.rb
|
95
81
|
- lib/composite_primary_keys/associations/has_one_association.rb
|
82
|
+
- lib/composite_primary_keys/associations/join_dependency/join_association.rb
|
83
|
+
- lib/composite_primary_keys/associations/join_dependency/join_part.rb
|
84
|
+
- lib/composite_primary_keys/associations/preloader/association.rb
|
85
|
+
- lib/composite_primary_keys/associations/preloader/belongs_to.rb
|
86
|
+
- lib/composite_primary_keys/associations/preloader/has_and_belongs_to_many.rb
|
96
87
|
- lib/composite_primary_keys/associations/through_association_scope.rb
|
97
88
|
- lib/composite_primary_keys/associations.rb
|
98
89
|
- lib/composite_primary_keys/association_preload.rb
|
90
|
+
- lib/composite_primary_keys/attribute_methods/primary_key.rb
|
91
|
+
- lib/composite_primary_keys/attribute_methods/read.rb
|
92
|
+
- lib/composite_primary_keys/attribute_methods/write.rb
|
99
93
|
- lib/composite_primary_keys/attribute_methods.rb
|
100
94
|
- lib/composite_primary_keys/base.rb
|
101
95
|
- lib/composite_primary_keys/calculations.rb
|
102
96
|
- lib/composite_primary_keys/composite_arrays.rb
|
97
|
+
- lib/composite_primary_keys/composite_predicates.rb
|
103
98
|
- lib/composite_primary_keys/connection_adapters/abstract_adapter.rb
|
104
99
|
- lib/composite_primary_keys/connection_adapters/ibm_db_adapter.rb
|
105
100
|
- lib/composite_primary_keys/connection_adapters/oracle_adapter.rb
|
@@ -108,11 +103,14 @@ files:
|
|
108
103
|
- lib/composite_primary_keys/connection_adapters/sqlite3_adapter.rb
|
109
104
|
- lib/composite_primary_keys/finder_methods.rb
|
110
105
|
- lib/composite_primary_keys/fixtures.rb
|
106
|
+
- lib/composite_primary_keys/named_scope.rb
|
111
107
|
- lib/composite_primary_keys/persistence.rb
|
112
108
|
- lib/composite_primary_keys/primary_key.rb
|
113
109
|
- lib/composite_primary_keys/query_methods.rb
|
114
110
|
- lib/composite_primary_keys/read.rb
|
115
111
|
- lib/composite_primary_keys/reflection.rb
|
112
|
+
- lib/composite_primary_keys/relation/finder_methods.rb
|
113
|
+
- lib/composite_primary_keys/relation/query_methods.rb
|
116
114
|
- lib/composite_primary_keys/relation.rb
|
117
115
|
- lib/composite_primary_keys/validations/uniqueness.rb
|
118
116
|
- lib/composite_primary_keys/version.rb
|