composite_primary_keys 13.0.7 → 14.0.9

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/History.rdoc +50 -1
  3. data/README.rdoc +182 -181
  4. data/Rakefile +1 -1
  5. data/lib/composite_primary_keys/associations/association.rb +2 -2
  6. data/lib/composite_primary_keys/associations/association_scope.rb +1 -1
  7. data/lib/composite_primary_keys/associations/collection_association.rb +38 -31
  8. data/lib/composite_primary_keys/associations/has_many_through_association.rb +19 -0
  9. data/lib/composite_primary_keys/associations/preloader/association.rb +52 -61
  10. data/lib/composite_primary_keys/autosave_association.rb +60 -60
  11. data/lib/composite_primary_keys/composite_arrays.rb +88 -86
  12. data/lib/composite_primary_keys/composite_predicates.rb +121 -120
  13. data/lib/composite_primary_keys/connection_adapters/abstract/database_statements.rb +1 -2
  14. data/lib/composite_primary_keys/nested_attributes.rb +2 -2
  15. data/lib/composite_primary_keys/persistence.rb +96 -83
  16. data/lib/composite_primary_keys/relation/calculations.rb +110 -104
  17. data/lib/composite_primary_keys/relation/query_methods.rb +14 -16
  18. data/lib/composite_primary_keys/relation.rb +2 -0
  19. data/lib/composite_primary_keys/validations/uniqueness.rb +40 -32
  20. data/lib/composite_primary_keys/version.rb +2 -2
  21. data/lib/composite_primary_keys.rb +117 -119
  22. data/scripts/console.rb +2 -2
  23. data/tasks/databases/trilogy.rake +23 -0
  24. data/test/abstract_unit.rb +124 -114
  25. data/test/connections/databases.ci.yml +10 -0
  26. data/test/fixtures/admin.rb +4 -0
  27. data/test/fixtures/comments.yml +6 -0
  28. data/test/fixtures/db_definitions/db2-create-tables.sql +34 -0
  29. data/test/fixtures/db_definitions/db2-drop-tables.sql +7 -1
  30. data/test/fixtures/db_definitions/mysql.sql +23 -0
  31. data/test/fixtures/db_definitions/oracle.drop.sql +4 -0
  32. data/test/fixtures/db_definitions/oracle.sql +21 -0
  33. data/test/fixtures/db_definitions/postgresql.sql +23 -0
  34. data/test/fixtures/db_definitions/sqlite.sql +21 -0
  35. data/test/fixtures/db_definitions/sqlserver.sql +23 -0
  36. data/test/fixtures/department.rb +20 -16
  37. data/test/fixtures/moderator.rb +4 -0
  38. data/test/fixtures/room.rb +4 -1
  39. data/test/fixtures/room_assignment.rb +18 -14
  40. data/test/fixtures/staff_room.rb +6 -0
  41. data/test/fixtures/staff_room_key.rb +6 -0
  42. data/test/fixtures/user.rb +3 -0
  43. data/test/fixtures/user_with_polymorphic_name.rb +9 -0
  44. data/test/test_associations.rb +403 -372
  45. data/test/test_composite_arrays.rb +44 -38
  46. data/test/test_has_one_through.rb +30 -0
  47. data/test/test_nested_attributes.rb +23 -0
  48. data/test/test_polymorphic.rb +6 -0
  49. metadata +14 -7
  50. data/lib/composite_primary_keys/associations/through_association.rb +0 -24
@@ -1,61 +1,52 @@
1
- module ActiveRecord
2
- module Associations
3
- class Preloader
4
- class Association
5
- def records_for(ids)
6
- records = if association_key_name.is_a?(Array)
7
- predicate = cpk_in_predicate(klass.arel_table, association_key_name, ids)
8
- scope.where(predicate)
9
- else
10
- scope.where(association_key_name => ids)
11
- end
12
- records.load do |record|
13
- # Processing only the first owner
14
- # because the record is modified but not an owner
15
- owner = owners_by_key[convert_key(record[association_key_name])].first
16
- association = owner.association(reflection.name)
17
- association.set_inverse_instance(record)
18
- end
19
- end
20
-
21
- def owners_by_key
22
- @owners_by_key ||= owners.each_with_object({}) do |owner, result|
23
- # CPK
24
- # key = convert_key(owner[owner_key_name])
25
- key = if owner_key_name.is_a?(Array)
26
- Array(owner_key_name).map do |key_name|
27
- convert_key(owner[key_name])
28
- end
29
- else
30
- convert_key(owner[owner_key_name])
31
- end
32
- (result[key] ||= []) << owner if key
33
- end
34
- end
35
-
36
- def load_records
37
- # owners can be duplicated when a relation has a collection association join
38
- # #compare_by_identity makes such owners different hash keys
39
- @records_by_owner = {}.compare_by_identity
40
- raw_records = owner_keys.empty? ? [] : records_for(owner_keys)
41
-
42
- @preloaded_records = raw_records.select do |record|
43
- assignments = false
44
-
45
- owners_by_key[convert_key(record[association_key_name])].each do |owner|
46
- entries = (@records_by_owner[owner] ||= [])
47
-
48
- if reflection.collection? || entries.empty?
49
- entries << record
50
- assignments = true
51
- end
52
- end
53
-
54
- assignments
55
- end
56
- end
57
-
58
- end
59
- end
60
- end
61
- 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
+ end
50
+ end
51
+ end
52
+ end
@@ -1,60 +1,60 @@
1
- module ActiveRecord
2
- module AutosaveAssociation
3
- def save_has_one_association(reflection)
4
- association = association_instance_get(reflection.name)
5
- record = association && association.load_target
6
-
7
- if record && !record.destroyed?
8
- autosave = reflection.options[:autosave]
9
-
10
- if autosave && record.marked_for_destruction?
11
- record.destroy
12
- elsif autosave != false
13
- # CPK
14
- #key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
15
- key = reflection.options[:primary_key] ? self[reflection.options[:primary_key]] : id
16
-
17
- if (autosave && record.changed_for_autosave?) || new_record? || record_changed?(reflection, record, key)
18
- unless reflection.through_reflection
19
- record[reflection.foreign_key] = key
20
- if inverse_reflection = reflection.inverse_of
21
- record.association(inverse_reflection.name).loaded!
22
- end
23
- end
24
-
25
- saved = record.save(validate: !autosave)
26
- raise ActiveRecord::Rollback if !saved && autosave
27
- saved
28
- end
29
- end
30
- end
31
- end
32
-
33
- def save_belongs_to_association(reflection)
34
- association = association_instance_get(reflection.name)
35
- return unless association && association.loaded? && !association.stale_target?
36
-
37
- record = association.load_target
38
- if record && !record.destroyed?
39
- autosave = reflection.options[:autosave]
40
-
41
- if autosave && record.marked_for_destruction?
42
- self[reflection.foreign_key] = nil
43
- record.destroy
44
- elsif autosave != false
45
- saved = record.save(validate: !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
46
-
47
- if association.updated?
48
- # CPK
49
- # association_id = record.send(reflection.options[:primary_key] || :id)
50
- association_id = reflection.options[:primary_key] ? record[reflection.options[:primary_key]] : record.id
51
- self[reflection.foreign_key] = association_id
52
- association.loaded!
53
- end
54
-
55
- saved if autosave
56
- end
57
- end
58
- end
59
- end
60
- end
1
+ module ActiveRecord
2
+ module AutosaveAssociation
3
+ def save_has_one_association(reflection)
4
+ association = association_instance_get(reflection.name)
5
+ record = association && association.load_target
6
+
7
+ if record && !record.destroyed?
8
+ autosave = reflection.options[:autosave]
9
+
10
+ if autosave && record.marked_for_destruction?
11
+ record.destroy
12
+ elsif autosave != false
13
+ # CPK
14
+ #key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
15
+ key = reflection.options[:primary_key] ? self[reflection.options[:primary_key]] : id
16
+
17
+ if (autosave && record.changed_for_autosave?) || new_record? || _record_changed?(reflection, record, key)
18
+ unless reflection.through_reflection
19
+ record[reflection.foreign_key] = key
20
+ if inverse_reflection = reflection.inverse_of
21
+ record.association(inverse_reflection.name).loaded!
22
+ end
23
+ end
24
+
25
+ saved = record.save(validate: !autosave)
26
+ raise ActiveRecord::Rollback if !saved && autosave
27
+ saved
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ def save_belongs_to_association(reflection)
34
+ association = association_instance_get(reflection.name)
35
+ return unless association && association.loaded? && !association.stale_target?
36
+
37
+ record = association.load_target
38
+ if record && !record.destroyed?
39
+ autosave = reflection.options[:autosave]
40
+
41
+ if autosave && record.marked_for_destruction?
42
+ self[reflection.foreign_key] = nil
43
+ record.destroy
44
+ elsif autosave != false
45
+ saved = record.save(validate: !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
46
+
47
+ if association.updated?
48
+ # CPK
49
+ # association_id = record.send(reflection.options[:primary_key] || :id)
50
+ association_id = reflection.options[:primary_key] ? record[reflection.options[:primary_key]] : record.id
51
+ self[reflection.foreign_key] = association_id
52
+ association.loaded!
53
+ end
54
+
55
+ saved if autosave
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -1,86 +1,88 @@
1
- module CompositePrimaryKeys
2
- ID_SEP = ','
3
- ID_SET_SEP = ';'
4
- ESCAPE_CHAR = '^'
5
-
6
- module ArrayExtension
7
- def to_composite_keys
8
- CompositeKeys.new(self)
9
- end
10
- end
11
-
12
- # Convert mixed representation of CPKs (by strings or arrays) to normalized
13
- # representation (just by arrays).
14
- #
15
- # `ids` is Array that may contain:
16
- # 1. A CPK represented by an array or a string.
17
- # 2. An array of CPKs represented by arrays or strings.
18
- #
19
- # There is an issue. Let `ids` contain an array with serveral strings. We can't distinguish case 1
20
- # from case 2 there in general. E.g. the item can be an array containing appropriate number of strings,
21
- # and each string can contain appropriate number of commas. We consider case 2 to win there.
22
- def self.normalize(ids, cpk_size)
23
- ids.map do |id|
24
- if Utils.cpk_as_array?(id, cpk_size) && id.any? { |item| !Utils.cpk_as_string?(item, cpk_size) }
25
- # CPK as an array - case 1
26
- id
27
- elsif id.is_a?(Array)
28
- # An array of CPKs - case 2
29
- normalize(id, cpk_size)
30
- elsif id.is_a?(String)
31
- # CPK as a string - case 1
32
- CompositeKeys.parse(id)
33
- else
34
- id
35
- end
36
- end
37
- end
38
-
39
- class CompositeKeys < Array
40
-
41
- def self.parse(value)
42
- case value
43
- when Array
44
- value.to_composite_keys
45
- when String
46
- value.split(ID_SEP).map { |key| Utils.unescape_string_key(key) }.to_composite_keys
47
- else
48
- raise(ArgumentError, "Unsupported type: #{value}")
49
- end
50
- end
51
-
52
- def to_s
53
- # Doing this makes it easier to parse Base#[](attr_name)
54
- map { |key| Utils.escape_string_key(key.to_s) }.join(ID_SEP)
55
- end
56
- end
57
-
58
- module Utils
59
- class << self
60
- def escape_string_key(key)
61
- key.gsub(Regexp.union(ESCAPE_CHAR, ID_SEP)) do |unsafe|
62
- "#{ESCAPE_CHAR}#{unsafe.ord.to_s(16).upcase}"
63
- end
64
- end
65
-
66
- def unescape_string_key(key)
67
- key.gsub(/#{Regexp.escape(ESCAPE_CHAR)}[0-9a-fA-F]{2}/) do |escaped|
68
- char = escaped.slice(1, 2).hex.chr
69
- (char == ESCAPE_CHAR || char == ID_SEP) ? char : escaped
70
- end
71
- end
72
-
73
- def cpk_as_array?(value, pk_size)
74
- # We don't permit Array to be an element of CPK.
75
- value.is_a?(Array) && value.size == pk_size && value.none? { |item| item.is_a?(Array) }
76
- end
77
-
78
- def cpk_as_string?(value, pk_size)
79
- value.is_a?(String) && value.count(ID_SEP) == pk_size - 1
80
- end
81
- end
82
- end
83
- private_constant :Utils
84
- end
85
-
86
- Array.send(:include, CompositePrimaryKeys::ArrayExtension)
1
+ module CompositePrimaryKeys
2
+ ID_SEP = ','
3
+ ID_SET_SEP = ';'
4
+ ESCAPE_CHAR = '^'
5
+
6
+ module ArrayExtension
7
+ def to_composite_keys
8
+ CompositeKeys.new(self)
9
+ end
10
+ end
11
+
12
+ # Convert mixed representation of CPKs (by strings or arrays) to normalized
13
+ # representation (just by arrays).
14
+ #
15
+ # `ids` is Array that may contain:
16
+ # 1. A CPK represented by an array or a string.
17
+ # 2. An array of CPKs represented by arrays or strings.
18
+ #
19
+ # There is an issue. Let `ids` contain an array with serveral strings. We can't distinguish case 1
20
+ # from case 2 there in general. E.g. the item can be an array containing appropriate number of strings,
21
+ # and each string can contain appropriate number of commas. We consider case 2 to win there.
22
+ def self.normalize(ids, cpk_size)
23
+ ids.map do |id|
24
+ if Utils.cpk_as_array?(id, cpk_size) && id.any? { |item| !Utils.cpk_as_string?(item, cpk_size) }
25
+ # CPK as an array - case 1
26
+ id
27
+ elsif id.is_a?(Array)
28
+ # An array of CPKs - case 2
29
+ normalize(id, cpk_size)
30
+ elsif id.is_a?(String)
31
+ # CPK as a string - case 1
32
+ CompositeKeys.parse(id)
33
+ else
34
+ id
35
+ end
36
+ end
37
+ end
38
+
39
+ class CompositeKeys < Array
40
+
41
+ def self.parse(value)
42
+ case value
43
+ when Array
44
+ value.to_composite_keys
45
+ when String
46
+ value.split(ID_SEP).map { |key| Utils.unescape_string_key(key) }.to_composite_keys
47
+ else
48
+ raise(ArgumentError, "Unsupported type: #{value}")
49
+ end
50
+ end
51
+
52
+ def to_s
53
+ # Doing this makes it easier to parse Base#[](attr_name)
54
+ map { |key| Utils.escape_string_key(key.to_s) }.join(ID_SEP)
55
+ end
56
+
57
+ alias_method :to_param, :to_s
58
+ end
59
+
60
+ module Utils
61
+ class << self
62
+ def escape_string_key(key)
63
+ key.gsub(Regexp.union(ESCAPE_CHAR, ID_SEP)) do |unsafe|
64
+ "#{ESCAPE_CHAR}#{unsafe.ord.to_s(16).upcase}"
65
+ end
66
+ end
67
+
68
+ def unescape_string_key(key)
69
+ key.gsub(/#{Regexp.escape(ESCAPE_CHAR)}[0-9a-fA-F]{2}/) do |escaped|
70
+ char = escaped.slice(1, 2).hex.chr
71
+ (char == ESCAPE_CHAR || char == ID_SEP) ? char : escaped
72
+ end
73
+ end
74
+
75
+ def cpk_as_array?(value, pk_size)
76
+ # We don't permit Array to be an element of CPK.
77
+ value.is_a?(Array) && value.size == pk_size && value.none? { |item| item.is_a?(Array) }
78
+ end
79
+
80
+ def cpk_as_string?(value, pk_size)
81
+ value.is_a?(String) && value.count(ID_SEP) == pk_size - 1
82
+ end
83
+ end
84
+ end
85
+ private_constant :Utils
86
+ end
87
+
88
+ Array.send(:include, CompositePrimaryKeys::ArrayExtension)
@@ -1,120 +1,121 @@
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
- if primary_keys.length == 2
55
- cpk_in_predicate_with_grouped_keys(table, primary_keys, ids)
56
- else
57
- cpk_in_predicate_with_non_grouped_keys(table, primary_keys, ids)
58
- end
59
- end
60
-
61
- def cpk_in_predicate_with_non_grouped_keys(table, primary_keys, ids)
62
- and_predicates = ids.map do |id|
63
- cpk_id_predicate(table, primary_keys, id)
64
- end
65
-
66
- cpk_or_predicate(and_predicates)
67
- end
68
-
69
- def cpk_in_predicate_with_grouped_keys(table, primary_keys, ids)
70
- keys_by_first_column_name = Hash.new { |hash, key| hash[key] = [] }
71
- keys_by_second_column_name = Hash.new { |hash, key| hash[key] = [] }
72
-
73
- ids.map.each do |first_key_part, second_key_part|
74
- keys_by_first_column_name[first_key_part] << second_key_part
75
- keys_by_second_column_name[second_key_part] << first_key_part
76
- end
77
-
78
- low_cardinality_column_name, high_cardinality_column_name, groups = \
79
- if keys_by_first_column_name.size <= keys_by_second_column_name.size
80
- [primary_keys.first, primary_keys.second, keys_by_first_column_name]
81
- else
82
- [primary_keys.second, primary_keys.first, keys_by_second_column_name]
83
- end
84
-
85
- and_predicates = groups.map do |low_cardinality_value, high_cardinality_values|
86
- non_nil_high_cardinality_values = high_cardinality_values.compact
87
- in_clause = table[high_cardinality_column_name].in(non_nil_high_cardinality_values)
88
- inclusion_clauses = if non_nil_high_cardinality_values.size != high_cardinality_values.size
89
- Arel::Nodes::Grouping.new(
90
- Arel::Nodes::Or.new(
91
- in_clause,
92
- table[high_cardinality_column_name].eq(nil)
93
- )
94
- )
95
- else
96
- in_clause
97
- end
98
-
99
- Arel::Nodes::And.new(
100
- [
101
- table[low_cardinality_column_name].eq(low_cardinality_value),
102
- inclusion_clauses
103
- ]
104
- )
105
- end
106
-
107
- cpk_or_predicate(and_predicates)
108
- end
109
- end
110
- end
111
-
112
- ActiveRecord::Associations::AssociationScope.send(:include, CompositePrimaryKeys::Predicates)
113
- ActiveRecord::Associations::JoinDependency::JoinAssociation.send(:include, CompositePrimaryKeys::Predicates)
114
- ActiveRecord::Associations::Preloader::Association.send(:include, CompositePrimaryKeys::Predicates)
115
- ActiveRecord::Associations::HasManyAssociation.send(:include, CompositePrimaryKeys::Predicates)
116
- ActiveRecord::Associations::HasManyThroughAssociation.send(:include, CompositePrimaryKeys::Predicates)
117
- ActiveRecord::Base.send(:extend, CompositePrimaryKeys::Predicates)
118
- ActiveRecord::Reflection::AbstractReflection.send(:include, CompositePrimaryKeys::Predicates)
119
- ActiveRecord::Relation.send(:include, CompositePrimaryKeys::Predicates)
120
- 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
+ if primary_keys.length == 2
55
+ cpk_in_predicate_with_grouped_keys(table, primary_keys, ids)
56
+ else
57
+ cpk_in_predicate_with_non_grouped_keys(table, primary_keys, ids)
58
+ end
59
+ end
60
+
61
+ def cpk_in_predicate_with_non_grouped_keys(table, primary_keys, ids)
62
+ and_predicates = ids.map do |id|
63
+ cpk_id_predicate(table, primary_keys, id)
64
+ end
65
+
66
+ cpk_or_predicate(and_predicates)
67
+ end
68
+
69
+ def cpk_in_predicate_with_grouped_keys(table, primary_keys, ids)
70
+ keys_by_first_column_name = Hash.new { |hash, key| hash[key] = [] }
71
+ keys_by_second_column_name = Hash.new { |hash, key| hash[key] = [] }
72
+
73
+ ids.map.each do |first_key_part, second_key_part|
74
+ keys_by_first_column_name[first_key_part] << second_key_part
75
+ keys_by_second_column_name[second_key_part] << first_key_part
76
+ end
77
+
78
+ low_cardinality_column_name, high_cardinality_column_name, groups = \
79
+ if keys_by_first_column_name.size <= keys_by_second_column_name.size
80
+ [primary_keys.first, primary_keys.second, keys_by_first_column_name]
81
+ else
82
+ [primary_keys.second, primary_keys.first, keys_by_second_column_name]
83
+ end
84
+
85
+ and_predicates = groups.map do |low_cardinality_value, high_cardinality_values|
86
+ non_nil_high_cardinality_values = high_cardinality_values.compact
87
+ in_clause = table[high_cardinality_column_name].in(non_nil_high_cardinality_values)
88
+ inclusion_clauses = if non_nil_high_cardinality_values.size != high_cardinality_values.size
89
+ Arel::Nodes::Grouping.new(
90
+ Arel::Nodes::Or.new(
91
+ in_clause,
92
+ table[high_cardinality_column_name].eq(nil)
93
+ )
94
+ )
95
+ else
96
+ in_clause
97
+ end
98
+
99
+ Arel::Nodes::And.new(
100
+ [
101
+ table[low_cardinality_column_name].eq(low_cardinality_value),
102
+ inclusion_clauses
103
+ ]
104
+ )
105
+ end
106
+
107
+ cpk_or_predicate(and_predicates)
108
+ end
109
+ end
110
+ end
111
+
112
+ ActiveRecord::Associations::AssociationScope.send(:include, CompositePrimaryKeys::Predicates)
113
+ ActiveRecord::Associations::JoinDependency::JoinAssociation.send(:include, CompositePrimaryKeys::Predicates)
114
+ ActiveRecord::Associations::Preloader::Association.send(:include, CompositePrimaryKeys::Predicates)
115
+ ActiveRecord::Associations::Preloader::Association::LoaderQuery.send(:include, CompositePrimaryKeys::Predicates)
116
+ ActiveRecord::Associations::HasManyAssociation.send(:include, CompositePrimaryKeys::Predicates)
117
+ ActiveRecord::Associations::HasManyThroughAssociation.send(:include, CompositePrimaryKeys::Predicates)
118
+ ActiveRecord::Base.send(:extend, CompositePrimaryKeys::Predicates)
119
+ ActiveRecord::Reflection::AbstractReflection.send(:include, CompositePrimaryKeys::Predicates)
120
+ ActiveRecord::Relation.send(:include, CompositePrimaryKeys::Predicates)
121
+ ActiveRecord::PredicateBuilder.send(:extend, CompositePrimaryKeys::Predicates)
@@ -6,8 +6,7 @@ module ActiveRecord
6
6
  value = exec_insert(sql, name, binds, pk, sequence_name)
7
7
 
8
8
  return id_value if id_value
9
-
10
- if pk.is_a?(Array) && !value.empty?
9
+ if pk.is_a?(Array) && value.respond_to?(:empty?) && !value.empty?
11
10
  # This is a CPK model and the query result is not empty. Thus we can figure out the new ids for each
12
11
  # auto incremented field
13
12
  pk.map {|key| value.first[key]}