composite_primary_keys 13.0.4 → 13.0.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 875a5dc3886b5b887bbcb77bdcb0bfa40ad377e5084fd1bcdf3a71b5c226dbc3
4
- data.tar.gz: 9cad15004b91f80917564f623855836217c2ce8e745eb5e6fc47464f10c7cf0c
3
+ metadata.gz: e705c71e8e7a1f2d4aee39d46ba133177960963c2d126ee41caea675ad743dfe
4
+ data.tar.gz: 266bba1595212c20101e3052cc2a24de8654ce817412b82cfbacee6cf3044c56
5
5
  SHA512:
6
- metadata.gz: f771c6c4266eb770f685c7fc0960c344c10b7666a24240c59b456afa2c4c52c058e530e8e3d1ddfb4575468eb5eaf30a77b141216820eb028d774fc9f9593326
7
- data.tar.gz: 33d2e087cdfc10ea336038a5ffabdb5dd878133ba11ed874ee2d6bd7cf5e572f9acf8b3a996bf04b402c161ffe49d2237ae7284266b58e6eed98b53a890ca7e5
6
+ metadata.gz: 80a8ad394e2393f3fbae332a9ba5d8db8bea4484b09834e5ef0749395b188e796e13cab5625688530e4f299464400e2cf0dc67a37f6a27160890cbe08ae8857d
7
+ data.tar.gz: 48a459a2296938f41a40e98e8f533ae0d7ee8bcd427406dd05af4a1a697466c19e1ba4d4c2212b7d4518e1308ef528ec83f1d52b5b59808016c65d8314c3f074
data/History.rdoc CHANGED
@@ -1,4 +1,15 @@
1
- == 13.0.4
1
+ == 13.0.7 (2023-02-04)
2
+ * Fix #573 (Charlie Savage)
3
+
4
+ == 13.0.6 (2023-02-04)
5
+ * Fix #577 (Charlie Savage)
6
+
7
+ == 13.0.5 (2023-02-04)
8
+ * Improve query generation for cpk_in_predicate. This reduces the length of
9
+ queries when loading many keys and enables Postgres to use index scans
10
+ more frequently. (Andrew Kiellor)
11
+
12
+ == 13.0.4 (2022-12-05)
2
13
  * Fix previously_new_record? not being set to true after create
3
14
 
4
15
  == 13.0.3 (2022-01-09)
@@ -33,20 +33,28 @@ module ActiveRecord
33
33
  end
34
34
  end
35
35
 
36
- def records_by_owner
37
- @records_by_owner ||= preloaded_records.each_with_object({}) do |record, result|
38
- key = if association_key_name.is_a?(Array)
39
- Array(record[association_key_name]).map do |key|
40
- convert_key(key)
41
- end
42
- else
43
- convert_key(record[association_key_name])
44
- end
45
- owners_by_key[key].each do |owner|
46
- (result[owner] ||= []) << record
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
47
52
  end
53
+
54
+ assignments
48
55
  end
49
56
  end
57
+
50
58
  end
51
59
  end
52
60
  end
@@ -51,9 +51,59 @@ module CompositePrimaryKeys
51
51
  end
52
52
 
53
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)
54
62
  and_predicates = ids.map do |id|
55
63
  cpk_id_predicate(table, primary_keys, id)
56
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
+
57
107
  cpk_or_predicate(and_predicates)
58
108
  end
59
109
  end
@@ -29,7 +29,7 @@ module ActiveRecord
29
29
  stmt.key = table[primary_key]
30
30
 
31
31
  # CPK
32
- if @klass.composite? && @klass.connection.visitor.compile(stmt.ast) =~ /['"]#{primary_key.to_s}['"]/
32
+ if @klass.composite?
33
33
  stmt = Arel::UpdateManager.new
34
34
  stmt.table(arel_table)
35
35
  cpk_subquery(stmt)
@@ -74,7 +74,7 @@ module ActiveRecord
74
74
  stmt.key = table[primary_key]
75
75
 
76
76
  # CPK
77
- if @klass.composite? && @klass.connection.visitor.compile(stmt.ast) =~ /['"]#{primary_key.to_s}['"]/
77
+ if @klass.composite?
78
78
  stmt = Arel::DeleteManager.new
79
79
  stmt.from(arel_table)
80
80
  cpk_subquery(stmt)
@@ -2,7 +2,7 @@ module CompositePrimaryKeys
2
2
  module VERSION
3
3
  MAJOR = 13
4
4
  MINOR = 0
5
- TINY = 4
5
+ TINY = 7
6
6
  STRING = [MAJOR, MINOR, TINY].join('.')
7
7
  end
8
8
  end
@@ -1,4 +1,4 @@
1
- spec_name = ENV['ADAPTER'] || 'sqlite'
1
+ spec_name = ENV['ADAPTER'] || 'postgresql'
2
2
  require 'bundler'
3
3
  require 'minitest/autorun'
4
4
 
@@ -1,60 +1,130 @@
1
- require File.expand_path('../abstract_unit', __FILE__)
2
-
3
- class TestPredicates < ActiveSupport::TestCase
4
- fixtures :departments
5
-
6
- include CompositePrimaryKeys::Predicates
7
-
8
- def test_or
9
- dep = Department.arel_table
10
-
11
- predicates = Array.new
12
-
13
- 3.times do |i|
14
- predicates << dep[:id].eq(i)
15
- end
16
-
17
- connection = ActiveRecord::Base.connection
18
- quoted = "#{connection.quote_table_name('departments')}.#{connection.quote_column_name('id')}"
19
- expected = "(#{quoted} = 0 OR #{quoted} = 1 OR #{quoted} = 2)"
20
-
21
- pred = cpk_or_predicate(predicates)
22
- assert_equal(with_quoted_identifiers(expected), pred.to_sql)
23
- end
24
-
25
- def test_or_with_many_values
26
- dep = Arel::Table.new(:departments)
27
-
28
- predicates = Array.new
29
-
30
- number_of_predicates = 3000 # This should really be big
31
- number_of_predicates.times do |i|
32
- predicates << dep[:id].eq(i)
33
- end
34
-
35
- connection = ActiveRecord::Base.connection
36
- quoted = "#{connection.quote_table_name('departments')}.#{connection.quote_column_name('id')}"
37
- expected_ungrouped = ((0...number_of_predicates).map { |i| "#{quoted} = #{i}" }).join(' OR ')
38
- expected = "(#{expected_ungrouped})"
39
-
40
- pred = cpk_or_predicate(predicates)
41
- assert_equal(with_quoted_identifiers(expected), pred.to_sql)
42
- end
43
-
44
- def test_and
45
- dep = Department.arel_table
46
-
47
- predicates = Array.new
48
-
49
- 3.times do |i|
50
- predicates << dep[:id].eq(i)
51
- end
52
-
53
- connection = ActiveRecord::Base.connection
54
- quoted = "#{connection.quote_table_name('departments')}.#{connection.quote_column_name('id')}"
55
- expected = "#{quoted} = 0 AND #{quoted} = 1 AND #{quoted} = 2"
56
-
57
- pred = cpk_and_predicate(predicates)
58
- assert_equal(with_quoted_identifiers(expected), pred.to_sql)
59
- end
60
- end
1
+ require File.expand_path('../abstract_unit', __FILE__)
2
+
3
+ class TestPredicates < ActiveSupport::TestCase
4
+ fixtures :departments
5
+
6
+ include CompositePrimaryKeys::Predicates
7
+
8
+ def test_or
9
+ dep = Department.arel_table
10
+
11
+ predicates = Array.new
12
+
13
+ 3.times do |i|
14
+ predicates << dep[:id].eq(i)
15
+ end
16
+
17
+ connection = ActiveRecord::Base.connection
18
+ quoted = "#{connection.quote_table_name('departments')}.#{connection.quote_column_name('id')}"
19
+ expected = "(#{quoted} = 0 OR #{quoted} = 1 OR #{quoted} = 2)"
20
+
21
+ pred = cpk_or_predicate(predicates)
22
+ assert_equal(with_quoted_identifiers(expected), pred.to_sql)
23
+ end
24
+
25
+ def test_or_with_many_values
26
+ dep = Arel::Table.new(:departments)
27
+
28
+ predicates = Array.new
29
+
30
+ number_of_predicates = 3000 # This should really be big
31
+ number_of_predicates.times do |i|
32
+ predicates << dep[:id].eq(i)
33
+ end
34
+
35
+ connection = ActiveRecord::Base.connection
36
+ quoted = "#{connection.quote_table_name('departments')}.#{connection.quote_column_name('id')}"
37
+ expected_ungrouped = ((0...number_of_predicates).map { |i| "#{quoted} = #{i}" }).join(' OR ')
38
+ expected = "(#{expected_ungrouped})"
39
+
40
+ pred = cpk_or_predicate(predicates)
41
+ assert_equal(with_quoted_identifiers(expected), pred.to_sql)
42
+ end
43
+
44
+ def test_and
45
+ dep = Department.arel_table
46
+
47
+ predicates = Array.new
48
+
49
+ 3.times do |i|
50
+ predicates << dep[:id].eq(i)
51
+ end
52
+
53
+ connection = ActiveRecord::Base.connection
54
+ quoted = "#{connection.quote_table_name('departments')}.#{connection.quote_column_name('id')}"
55
+ expected = "#{quoted} = 0 AND #{quoted} = 1 AND #{quoted} = 2"
56
+
57
+ pred = cpk_and_predicate(predicates)
58
+ assert_equal(with_quoted_identifiers(expected), pred.to_sql)
59
+ end
60
+
61
+ def test_in
62
+ dep = Department.arel_table
63
+
64
+ primary_keys = [[1, 1], [1, 2]]
65
+
66
+ connection = ActiveRecord::Base.connection
67
+ quoted_id_column = "#{connection.quote_table_name('departments')}.#{connection.quote_column_name('id')}"
68
+ quoted_location_id_column = "#{connection.quote_table_name('departments')}.#{connection.quote_column_name('location_id')}"
69
+ expected = "#{quoted_id_column} = 1 AND #{quoted_location_id_column} IN (1, 2)"
70
+
71
+ pred = cpk_in_predicate(dep, [:id, :location_id], primary_keys)
72
+ assert_equal(with_quoted_identifiers(expected), pred.to_sql)
73
+ end
74
+
75
+ def test_in_with_low_cardinality_second_key_part
76
+ dep = Department.arel_table
77
+
78
+ primary_keys = [[1, 1], [2, 1]]
79
+
80
+ connection = ActiveRecord::Base.connection
81
+ quoted_id_column = "#{connection.quote_table_name('departments')}.#{connection.quote_column_name('id')}"
82
+ quoted_location_id_column = "#{connection.quote_table_name('departments')}.#{connection.quote_column_name('location_id')}"
83
+ expected = "#{quoted_location_id_column} = 1 AND #{quoted_id_column} IN (1, 2)"
84
+
85
+ pred = cpk_in_predicate(dep, [:id, :location_id], primary_keys)
86
+ assert_equal(with_quoted_identifiers(expected), pred.to_sql)
87
+ end
88
+
89
+ def test_in_with_nil_primary_key_part
90
+ dep = Department.arel_table
91
+
92
+ primary_keys = [[nil, 1], [nil, 2]]
93
+
94
+ connection = ActiveRecord::Base.connection
95
+ quoted_id_column = "#{connection.quote_table_name('departments')}.#{connection.quote_column_name('id')}"
96
+ quoted_location_id_column = "#{connection.quote_table_name('departments')}.#{connection.quote_column_name('location_id')}"
97
+ expected = "#{quoted_id_column} IS NULL AND #{quoted_location_id_column} IN (1, 2)"
98
+
99
+ pred = cpk_in_predicate(dep, [:id, :location_id], primary_keys)
100
+ assert_equal(with_quoted_identifiers(expected), pred.to_sql)
101
+ end
102
+
103
+ def test_in_with_nil_secondary_key_part
104
+ dep = Department.arel_table
105
+
106
+ primary_keys = [[1, 1], [1, nil]]
107
+
108
+ connection = ActiveRecord::Base.connection
109
+ quoted_id_column = "#{connection.quote_table_name('departments')}.#{connection.quote_column_name('id')}"
110
+ quoted_location_id_column = "#{connection.quote_table_name('departments')}.#{connection.quote_column_name('location_id')}"
111
+ expected = "#{quoted_id_column} = 1 AND (#{quoted_location_id_column} IN (1) OR #{quoted_location_id_column} IS NULL)"
112
+
113
+ pred = cpk_in_predicate(dep, [:id, :location_id], primary_keys)
114
+ assert_equal(with_quoted_identifiers(expected), pred.to_sql)
115
+ end
116
+
117
+ def test_in_with_multiple_primary_key_parts
118
+ dep = Department.arel_table
119
+
120
+ primary_keys = [[1, 1], [1, 2], [2, 3], [2, 4]]
121
+
122
+ connection = ActiveRecord::Base.connection
123
+ quoted_id_column = "#{connection.quote_table_name('departments')}.#{connection.quote_column_name('id')}"
124
+ quoted_location_id_column = "#{connection.quote_table_name('departments')}.#{connection.quote_column_name('location_id')}"
125
+ expected = "(#{quoted_id_column} = 1 AND #{quoted_location_id_column} IN (1, 2) OR #{quoted_id_column} = 2 AND #{quoted_location_id_column} IN (3, 4))"
126
+
127
+ pred = cpk_in_predicate(dep, [:id, :location_id], primary_keys)
128
+ assert_equal(with_quoted_identifiers(expected), pred.to_sql)
129
+ end
130
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: composite_primary_keys
3
3
  version: !ruby/object:Gem::Version
4
- version: 13.0.4
4
+ version: 13.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Charlie Savage
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-05 00:00:00.000000000 Z
11
+ date: 2023-02-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -216,7 +216,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
216
216
  - !ruby/object:Gem::Version
217
217
  version: '0'
218
218
  requirements: []
219
- rubygems_version: 3.3.14
219
+ rubygems_version: 3.4.6
220
220
  signing_key:
221
221
  specification_version: 4
222
222
  summary: Composite key support for ActiveRecord