composite_primary_keys 14.0.1 → 14.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/History.rdoc +900 -894
- data/README.rdoc +182 -182
- data/lib/composite_primary_keys/associations/{join_dependency.rb → join_association.rb} +137 -137
- data/lib/composite_primary_keys/associations/preloader/association.rb +68 -68
- data/lib/composite_primary_keys/composite_predicates.rb +71 -71
- data/lib/composite_primary_keys/persistence.rb +96 -96
- data/lib/composite_primary_keys/relation/calculations.rb +110 -110
- data/lib/composite_primary_keys/version.rb +8 -8
- data/lib/composite_primary_keys.rb +118 -118
- data/test/abstract_unit.rb +118 -118
- data/test/fixtures/department.rb +16 -16
- data/test/fixtures/membership.rb +8 -8
- data/test/test_associations.rb +372 -372
- metadata +2 -2
@@ -1,68 +1,68 @@
|
|
1
|
-
module ActiveRecord
|
2
|
-
module Associations
|
3
|
-
class Preloader
|
4
|
-
class Association
|
5
|
-
|
6
|
-
class LoaderQuery
|
7
|
-
def load_records_for_keys(keys, &block)
|
8
|
-
# CPK
|
9
|
-
if association_key_name.is_a?(Array)
|
10
|
-
predicate = cpk_in_predicate(scope.klass.arel_table, association_key_name, keys)
|
11
|
-
scope.where(predicate).load(&block)
|
12
|
-
else
|
13
|
-
scope.where(association_key_name => keys).load(&block)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
# TODO: is records_for needed anymore? Rails' implementation has changed significantly
|
19
|
-
def records_for(ids)
|
20
|
-
records = if association_key_name.is_a?(Array)
|
21
|
-
predicate = cpk_in_predicate(klass.arel_table, association_key_name, ids)
|
22
|
-
scope.where(predicate)
|
23
|
-
else
|
24
|
-
scope.where(association_key_name => ids)
|
25
|
-
end
|
26
|
-
records.load do |record|
|
27
|
-
# Processing only the first owner
|
28
|
-
# because the record is modified but not an owner
|
29
|
-
owner = owners_by_key[convert_key(record[association_key_name])].first
|
30
|
-
association = owner.association(reflection.name)
|
31
|
-
association.set_inverse_instance(record)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def owners_by_key
|
36
|
-
@owners_by_key ||= owners.each_with_object({}) do |owner, result|
|
37
|
-
# CPK
|
38
|
-
# key = convert_key(owner[owner_key_name])
|
39
|
-
key = if owner_key_name.is_a?(Array)
|
40
|
-
Array(owner_key_name).map do |key_name|
|
41
|
-
convert_key(owner[key_name])
|
42
|
-
end
|
43
|
-
else
|
44
|
-
convert_key(owner[owner_key_name])
|
45
|
-
end
|
46
|
-
(result[key] ||= []) << owner if key
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
# TODO: is records_by_owner needed anymore? Rails' implementation has changed significantly
|
51
|
-
def records_by_owner
|
52
|
-
@records_by_owner ||= preloaded_records.each_with_object({}) do |record, result|
|
53
|
-
key = if association_key_name.is_a?(Array)
|
54
|
-
Array(record[association_key_name]).map do |key|
|
55
|
-
convert_key(key)
|
56
|
-
end
|
57
|
-
else
|
58
|
-
convert_key(record[association_key_name])
|
59
|
-
end
|
60
|
-
owners_by_key[key].each do |owner|
|
61
|
-
(result[owner] ||= []) << record
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class Preloader
|
4
|
+
class Association
|
5
|
+
|
6
|
+
class LoaderQuery
|
7
|
+
def load_records_for_keys(keys, &block)
|
8
|
+
# CPK
|
9
|
+
if association_key_name.is_a?(Array)
|
10
|
+
predicate = cpk_in_predicate(scope.klass.arel_table, association_key_name, keys)
|
11
|
+
scope.where(predicate).load(&block)
|
12
|
+
else
|
13
|
+
scope.where(association_key_name => keys).load(&block)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# TODO: is records_for needed anymore? Rails' implementation has changed significantly
|
19
|
+
def records_for(ids)
|
20
|
+
records = if association_key_name.is_a?(Array)
|
21
|
+
predicate = cpk_in_predicate(klass.arel_table, association_key_name, ids)
|
22
|
+
scope.where(predicate)
|
23
|
+
else
|
24
|
+
scope.where(association_key_name => ids)
|
25
|
+
end
|
26
|
+
records.load do |record|
|
27
|
+
# Processing only the first owner
|
28
|
+
# because the record is modified but not an owner
|
29
|
+
owner = owners_by_key[convert_key(record[association_key_name])].first
|
30
|
+
association = owner.association(reflection.name)
|
31
|
+
association.set_inverse_instance(record)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def owners_by_key
|
36
|
+
@owners_by_key ||= owners.each_with_object({}) do |owner, result|
|
37
|
+
# CPK
|
38
|
+
# key = convert_key(owner[owner_key_name])
|
39
|
+
key = if owner_key_name.is_a?(Array)
|
40
|
+
Array(owner_key_name).map do |key_name|
|
41
|
+
convert_key(owner[key_name])
|
42
|
+
end
|
43
|
+
else
|
44
|
+
convert_key(owner[owner_key_name])
|
45
|
+
end
|
46
|
+
(result[key] ||= []) << owner if key
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# TODO: is records_by_owner needed anymore? Rails' implementation has changed significantly
|
51
|
+
def records_by_owner
|
52
|
+
@records_by_owner ||= preloaded_records.each_with_object({}) do |record, result|
|
53
|
+
key = if association_key_name.is_a?(Array)
|
54
|
+
Array(record[association_key_name]).map do |key|
|
55
|
+
convert_key(key)
|
56
|
+
end
|
57
|
+
else
|
58
|
+
convert_key(record[association_key_name])
|
59
|
+
end
|
60
|
+
owners_by_key[key].each do |owner|
|
61
|
+
(result[owner] ||= []) << record
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -1,71 +1,71 @@
|
|
1
|
-
module CompositePrimaryKeys
|
2
|
-
module Predicates
|
3
|
-
# Similar to module_function, but does not make instance methods private.
|
4
|
-
# https://idiosyncratic-ruby.com/8-self-improvement.html
|
5
|
-
extend self
|
6
|
-
|
7
|
-
def cpk_and_predicate(predicates)
|
8
|
-
if predicates.length == 1
|
9
|
-
predicates.first
|
10
|
-
else
|
11
|
-
Arel::Nodes::And.new(predicates)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
def cpk_or_predicate(predicates, group = true)
|
16
|
-
if predicates.length <= 1
|
17
|
-
predicates.first
|
18
|
-
else
|
19
|
-
split_point = predicates.length / 2
|
20
|
-
predicates_first_half = predicates[0...split_point]
|
21
|
-
predicates_second_half = predicates[split_point..-1]
|
22
|
-
|
23
|
-
or_predicate = ::Arel::Nodes::Or.new(cpk_or_predicate(predicates_first_half, false),
|
24
|
-
cpk_or_predicate(predicates_second_half, false))
|
25
|
-
|
26
|
-
if group
|
27
|
-
::Arel::Nodes::Grouping.new(or_predicate)
|
28
|
-
else
|
29
|
-
or_predicate
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def cpk_id_predicate(table, keys, values)
|
35
|
-
# We zip on values then keys in case values are not provided for each key field
|
36
|
-
eq_predicates = values.zip(keys).map do |value, key|
|
37
|
-
table[key].eq(value)
|
38
|
-
end
|
39
|
-
cpk_and_predicate(eq_predicates)
|
40
|
-
end
|
41
|
-
|
42
|
-
def cpk_join_predicate(table1, key1, table2, key2)
|
43
|
-
key1_fields = Array(key1).map {|key| table1[key]}
|
44
|
-
key2_fields = Array(key2).map {|key| table2[key]}
|
45
|
-
|
46
|
-
eq_predicates = key1_fields.zip(key2_fields).map do |key_field1, key_field2|
|
47
|
-
key_field2 = Arel::Nodes::Quoted.new(key_field2) unless Arel::Attributes::Attribute === key_field2
|
48
|
-
key_field1.eq(key_field2)
|
49
|
-
end
|
50
|
-
cpk_and_predicate(eq_predicates)
|
51
|
-
end
|
52
|
-
|
53
|
-
def cpk_in_predicate(table, primary_keys, ids)
|
54
|
-
and_predicates = ids.map do |id|
|
55
|
-
cpk_id_predicate(table, primary_keys, id)
|
56
|
-
end
|
57
|
-
cpk_or_predicate(and_predicates)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
ActiveRecord::Associations::AssociationScope.send(:include, CompositePrimaryKeys::Predicates)
|
63
|
-
ActiveRecord::Associations::JoinDependency::JoinAssociation.send(:include, CompositePrimaryKeys::Predicates)
|
64
|
-
ActiveRecord::Associations::Preloader::Association.send(:include, CompositePrimaryKeys::Predicates)
|
65
|
-
ActiveRecord::Associations::Preloader::Association::LoaderQuery.send(:include, CompositePrimaryKeys::Predicates)
|
66
|
-
ActiveRecord::Associations::HasManyAssociation.send(:include, CompositePrimaryKeys::Predicates)
|
67
|
-
ActiveRecord::Associations::HasManyThroughAssociation.send(:include, CompositePrimaryKeys::Predicates)
|
68
|
-
ActiveRecord::Base.send(:extend, CompositePrimaryKeys::Predicates)
|
69
|
-
ActiveRecord::Reflection::AbstractReflection.send(:include, CompositePrimaryKeys::Predicates)
|
70
|
-
ActiveRecord::Relation.send(:include, CompositePrimaryKeys::Predicates)
|
71
|
-
ActiveRecord::PredicateBuilder.send(:extend, CompositePrimaryKeys::Predicates)
|
1
|
+
module CompositePrimaryKeys
|
2
|
+
module Predicates
|
3
|
+
# Similar to module_function, but does not make instance methods private.
|
4
|
+
# https://idiosyncratic-ruby.com/8-self-improvement.html
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def cpk_and_predicate(predicates)
|
8
|
+
if predicates.length == 1
|
9
|
+
predicates.first
|
10
|
+
else
|
11
|
+
Arel::Nodes::And.new(predicates)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def cpk_or_predicate(predicates, group = true)
|
16
|
+
if predicates.length <= 1
|
17
|
+
predicates.first
|
18
|
+
else
|
19
|
+
split_point = predicates.length / 2
|
20
|
+
predicates_first_half = predicates[0...split_point]
|
21
|
+
predicates_second_half = predicates[split_point..-1]
|
22
|
+
|
23
|
+
or_predicate = ::Arel::Nodes::Or.new(cpk_or_predicate(predicates_first_half, false),
|
24
|
+
cpk_or_predicate(predicates_second_half, false))
|
25
|
+
|
26
|
+
if group
|
27
|
+
::Arel::Nodes::Grouping.new(or_predicate)
|
28
|
+
else
|
29
|
+
or_predicate
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def cpk_id_predicate(table, keys, values)
|
35
|
+
# We zip on values then keys in case values are not provided for each key field
|
36
|
+
eq_predicates = values.zip(keys).map do |value, key|
|
37
|
+
table[key].eq(value)
|
38
|
+
end
|
39
|
+
cpk_and_predicate(eq_predicates)
|
40
|
+
end
|
41
|
+
|
42
|
+
def cpk_join_predicate(table1, key1, table2, key2)
|
43
|
+
key1_fields = Array(key1).map {|key| table1[key]}
|
44
|
+
key2_fields = Array(key2).map {|key| table2[key]}
|
45
|
+
|
46
|
+
eq_predicates = key1_fields.zip(key2_fields).map do |key_field1, key_field2|
|
47
|
+
key_field2 = Arel::Nodes::Quoted.new(key_field2) unless Arel::Attributes::Attribute === key_field2
|
48
|
+
key_field1.eq(key_field2)
|
49
|
+
end
|
50
|
+
cpk_and_predicate(eq_predicates)
|
51
|
+
end
|
52
|
+
|
53
|
+
def cpk_in_predicate(table, primary_keys, ids)
|
54
|
+
and_predicates = ids.map do |id|
|
55
|
+
cpk_id_predicate(table, primary_keys, id)
|
56
|
+
end
|
57
|
+
cpk_or_predicate(and_predicates)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
ActiveRecord::Associations::AssociationScope.send(:include, CompositePrimaryKeys::Predicates)
|
63
|
+
ActiveRecord::Associations::JoinDependency::JoinAssociation.send(:include, CompositePrimaryKeys::Predicates)
|
64
|
+
ActiveRecord::Associations::Preloader::Association.send(:include, CompositePrimaryKeys::Predicates)
|
65
|
+
ActiveRecord::Associations::Preloader::Association::LoaderQuery.send(:include, CompositePrimaryKeys::Predicates)
|
66
|
+
ActiveRecord::Associations::HasManyAssociation.send(:include, CompositePrimaryKeys::Predicates)
|
67
|
+
ActiveRecord::Associations::HasManyThroughAssociation.send(:include, CompositePrimaryKeys::Predicates)
|
68
|
+
ActiveRecord::Base.send(:extend, CompositePrimaryKeys::Predicates)
|
69
|
+
ActiveRecord::Reflection::AbstractReflection.send(:include, CompositePrimaryKeys::Predicates)
|
70
|
+
ActiveRecord::Relation.send(:include, CompositePrimaryKeys::Predicates)
|
71
|
+
ActiveRecord::PredicateBuilder.send(:extend, CompositePrimaryKeys::Predicates)
|
@@ -1,96 +1,96 @@
|
|
1
|
-
module ActiveRecord
|
2
|
-
module Persistence
|
3
|
-
module ClassMethods
|
4
|
-
def delete(id_or_array)
|
5
|
-
# CPK
|
6
|
-
if self.composite?
|
7
|
-
id_or_array = if id_or_array.is_a?(CompositePrimaryKeys::CompositeKeys)
|
8
|
-
[id_or_array]
|
9
|
-
else
|
10
|
-
Array(id_or_array)
|
11
|
-
end
|
12
|
-
|
13
|
-
# Delete should return the number of deleted records
|
14
|
-
id_or_array.map do |id|
|
15
|
-
# Is the passed in id actually a record?
|
16
|
-
id = id.kind_of?(::ActiveRecord::Base) ? id.id : id
|
17
|
-
delete_by(cpk_id_predicate(self.arel_table, self.primary_key, id))
|
18
|
-
end.sum
|
19
|
-
else
|
20
|
-
delete_by(primary_key => id_or_array)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def _update_record(values, constraints) # :nodoc:
|
25
|
-
# CPK
|
26
|
-
if self.composite? && constraints[primary_key]
|
27
|
-
primary_key_values = constraints.delete(primary_key)
|
28
|
-
primary_key.each_with_index do |key, i|
|
29
|
-
constraints[key] = primary_key_values[i]
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
constraints = constraints.map { |name, value| predicate_builder[name, value] }
|
34
|
-
|
35
|
-
default_constraint = build_default_constraint
|
36
|
-
constraints << default_constraint if default_constraint
|
37
|
-
|
38
|
-
if current_scope = self.global_current_scope
|
39
|
-
constraints << current_scope.where_clause.ast
|
40
|
-
end
|
41
|
-
|
42
|
-
um = Arel::UpdateManager.new(arel_table)
|
43
|
-
um.set(values.transform_keys { |name| arel_table[name] })
|
44
|
-
um.wheres = constraints
|
45
|
-
|
46
|
-
connection.update(um, "#{self} Update")
|
47
|
-
end
|
48
|
-
|
49
|
-
def _delete_record(constraints) # :nodoc:
|
50
|
-
# CPK
|
51
|
-
if self.composite? && constraints[primary_key]
|
52
|
-
primary_key_values = constraints.delete(primary_key)
|
53
|
-
primary_key.each_with_index do |key, i|
|
54
|
-
constraints[key] = primary_key_values[i]
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
constraints = constraints.map { |name, value| predicate_builder[name, value] }
|
59
|
-
|
60
|
-
default_constraint = build_default_constraint
|
61
|
-
constraints << default_constraint if default_constraint
|
62
|
-
|
63
|
-
if current_scope = self.global_current_scope
|
64
|
-
constraints << current_scope.where_clause.ast
|
65
|
-
end
|
66
|
-
|
67
|
-
dm = Arel::DeleteManager.new(arel_table)
|
68
|
-
dm.wheres = constraints
|
69
|
-
|
70
|
-
connection.delete(dm, "#{self} Destroy")
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def _create_record(attribute_names = self.attribute_names)
|
75
|
-
attribute_names = attributes_for_create(attribute_names)
|
76
|
-
|
77
|
-
new_id = self.class._insert_record(
|
78
|
-
attributes_with_values(attribute_names)
|
79
|
-
)
|
80
|
-
|
81
|
-
# CPK
|
82
|
-
if self.composite?
|
83
|
-
self.id = self.id.zip(Array(new_id)).map {|id1, id2| id2.nil? ? id1 : id2}
|
84
|
-
else
|
85
|
-
self.id ||= new_id if self.class.primary_key
|
86
|
-
end
|
87
|
-
|
88
|
-
@new_record = false
|
89
|
-
@previously_new_record = true
|
90
|
-
|
91
|
-
yield(self) if block_given?
|
92
|
-
|
93
|
-
id
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
1
|
+
module ActiveRecord
|
2
|
+
module Persistence
|
3
|
+
module ClassMethods
|
4
|
+
def delete(id_or_array)
|
5
|
+
# CPK
|
6
|
+
if self.composite?
|
7
|
+
id_or_array = if id_or_array.is_a?(CompositePrimaryKeys::CompositeKeys)
|
8
|
+
[id_or_array]
|
9
|
+
else
|
10
|
+
Array(id_or_array)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Delete should return the number of deleted records
|
14
|
+
id_or_array.map do |id|
|
15
|
+
# Is the passed in id actually a record?
|
16
|
+
id = id.kind_of?(::ActiveRecord::Base) ? id.id : id
|
17
|
+
delete_by(cpk_id_predicate(self.arel_table, self.primary_key, id))
|
18
|
+
end.sum
|
19
|
+
else
|
20
|
+
delete_by(primary_key => id_or_array)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def _update_record(values, constraints) # :nodoc:
|
25
|
+
# CPK
|
26
|
+
if self.composite? && constraints[primary_key]
|
27
|
+
primary_key_values = constraints.delete(primary_key)
|
28
|
+
primary_key.each_with_index do |key, i|
|
29
|
+
constraints[key] = primary_key_values[i]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
constraints = constraints.map { |name, value| predicate_builder[name, value] }
|
34
|
+
|
35
|
+
default_constraint = build_default_constraint
|
36
|
+
constraints << default_constraint if default_constraint
|
37
|
+
|
38
|
+
if current_scope = self.global_current_scope
|
39
|
+
constraints << current_scope.where_clause.ast
|
40
|
+
end
|
41
|
+
|
42
|
+
um = Arel::UpdateManager.new(arel_table)
|
43
|
+
um.set(values.transform_keys { |name| arel_table[name] })
|
44
|
+
um.wheres = constraints
|
45
|
+
|
46
|
+
connection.update(um, "#{self} Update")
|
47
|
+
end
|
48
|
+
|
49
|
+
def _delete_record(constraints) # :nodoc:
|
50
|
+
# CPK
|
51
|
+
if self.composite? && constraints[primary_key]
|
52
|
+
primary_key_values = constraints.delete(primary_key)
|
53
|
+
primary_key.each_with_index do |key, i|
|
54
|
+
constraints[key] = primary_key_values[i]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
constraints = constraints.map { |name, value| predicate_builder[name, value] }
|
59
|
+
|
60
|
+
default_constraint = build_default_constraint
|
61
|
+
constraints << default_constraint if default_constraint
|
62
|
+
|
63
|
+
if current_scope = self.global_current_scope
|
64
|
+
constraints << current_scope.where_clause.ast
|
65
|
+
end
|
66
|
+
|
67
|
+
dm = Arel::DeleteManager.new(arel_table)
|
68
|
+
dm.wheres = constraints
|
69
|
+
|
70
|
+
connection.delete(dm, "#{self} Destroy")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def _create_record(attribute_names = self.attribute_names)
|
75
|
+
attribute_names = attributes_for_create(attribute_names)
|
76
|
+
|
77
|
+
new_id = self.class._insert_record(
|
78
|
+
attributes_with_values(attribute_names)
|
79
|
+
)
|
80
|
+
|
81
|
+
# CPK
|
82
|
+
if self.composite?
|
83
|
+
self.id = self.id.zip(Array(new_id)).map {|id1, id2| id2.nil? ? id1 : id2}
|
84
|
+
else
|
85
|
+
self.id ||= new_id if self.class.primary_key
|
86
|
+
end
|
87
|
+
|
88
|
+
@new_record = false
|
89
|
+
@previously_new_record = true
|
90
|
+
|
91
|
+
yield(self) if block_given?
|
92
|
+
|
93
|
+
id
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|