composite_primary_keys 12.0.3 → 12.0.4

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: 1c7ee90af81fc2755ecd8b49d91808047736a1ec2d99fea0961bdc6813e9c6c1
4
- data.tar.gz: e0ff0a387dbd7ff3b7d3c0777ecf1fd54d4a5637128179c6a0788f71481079c5
3
+ metadata.gz: 7659741f962a45ac7c557ae8dcb77a01bb361bd46ac8032f9dd4c05a89b14962
4
+ data.tar.gz: e88d4c5c7e8dee12e00b8d339ec5397d28f104bfb8ea526e979ad45101f0225d
5
5
  SHA512:
6
- metadata.gz: e9a6b4eb511b992f4ed4aa971fb6eb8a19af49cff1b06998f4d7093a155f2b6c39674c444d70595833ed887bbe2406d665f5c48751316c743597ae442c0d44fd
7
- data.tar.gz: dbdb54e9141e4d8dd959712f9f7f52f43ce22e7b184f0cc274769ca241418af4fd9775c7532fa492f1988c61fcb6a2b69143fe94889ebbf1b9a76c3b30cb0bdd
6
+ metadata.gz: f99e8ab455cf07e5030a94cf87566143486027e359ca97709aca115350abe0f2e263b4d459134c1f032fd14e046a598e144e7107b5ba2d41d5ad182591cb205e
7
+ data.tar.gz: 0ace5f54f8e1338393cf1e86e24ebe7fa7e414215fe7fdcfb2f4c7059ae6ebf40f38e4fc6aca3a20357e5794f4dbdb93d668da24c3aa8eb947b0ea35f9505f63
@@ -1,3 +1,8 @@
1
+ == 12.0.4 (2020-12-30)
2
+ * Fix compatibility with Ruby Ruby 2.6 and below (ta1kt0me)
3
+ * Finally get SQLServer mass updates and deletes working (Charlie Savage)
4
+ * Fix MySQL mass updates and deletes that were broken by 12.0.3 (Charlie Savage)
5
+
1
6
  == 12.0.3 (2020-11-11)
2
7
  * Prevents infinite loops with gems which modify the 'attributes' method (Nicholas Guarino)
3
8
  * Improve delete_all and update_all queries (Charlie Savage)
@@ -11,7 +11,7 @@ module ActiveRecord
11
11
  # CPK
12
12
  if pk.is_a?(Array)
13
13
  pk.map do |key|
14
- column = self.column_for(arel.ast.relation.name, key)
14
+ column = column_for(arel.ast.relation.name, key)
15
15
  column.auto_increment? ? last_inserted_id(value) : nil
16
16
  end
17
17
  else
@@ -40,7 +40,7 @@ module ActiveRecord
40
40
 
41
41
  record = statement.execute([id], connection)&.first
42
42
  unless record
43
- raise RecordNotFound.new("Couldn't find #{name} with '#{key}'=#{id}", name, key, id)
43
+ raise ::ActiveRecord::RecordNotFound.new("Couldn't find #{name} with '#{key}'=#{id}", name, key, id)
44
44
  end
45
45
  record
46
46
  end
@@ -28,7 +28,7 @@ module ActiveRecord
28
28
  # CPK
29
29
  if @klass.composite?
30
30
  stmt.table(arel_table)
31
- cpk_in_subquery(stmt)
31
+ cpk_subquery(stmt)
32
32
  else
33
33
  stmt.table(arel.join_sources.empty? ? table : arel.source)
34
34
  stmt.key = arel_attribute(primary_key)
@@ -71,7 +71,7 @@ module ActiveRecord
71
71
 
72
72
  if @klass.composite?
73
73
  stmt.from(arel_table)
74
- cpk_in_subquery(stmt)
74
+ cpk_subquery(stmt)
75
75
  else
76
76
  stmt.from(arel.join_sources.empty? ? table : arel.source)
77
77
  stmt.key = arel_attribute(primary_key)
@@ -89,6 +89,29 @@ module ActiveRecord
89
89
  end
90
90
 
91
91
  # CPK
92
+ def cpk_subquery(stmt)
93
+ # For update and delete statements we need a way to specify which records should
94
+ # get updated. By default, Rails creates a nested IN subquery that uses the primary
95
+ # key. Postgresql, Sqlite, MariaDb and Oracle support IN subqueries with multiple
96
+ # columns but MySQL and SqlServer do not. Instead SQL server supports EXISTS queries
97
+ # and MySQL supports obfuscated IN queries. Thus we need to check the type of
98
+ # database adapter to decide how to proceed.
99
+ if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) && connection.is_a?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
100
+ cpk_mysql_subquery(stmt)
101
+ elsif defined?(ActiveRecord::ConnectionAdapters::SQLServerAdapter) && connection.is_a?(ActiveRecord::ConnectionAdapters::SQLServerAdapter)
102
+ cpk_exists_subquery(stmt)
103
+ else
104
+ cpk_in_subquery(stmt)
105
+ end
106
+ end
107
+
108
+ # Used by postgresql, sqlite, mariadb and oracle. Example query:
109
+ #
110
+ # UPDATE reference_codes
111
+ # SET ...
112
+ # WHERE (reference_codes.reference_type_id, reference_codes.reference_code) IN
113
+ # (SELECT reference_codes.reference_type_id, reference_codes.reference_code
114
+ # FROM reference_codes)
92
115
  def cpk_in_subquery(stmt)
93
116
  # Setup the subquery
94
117
  subquery = arel.clone
@@ -103,25 +126,68 @@ module ActiveRecord
103
126
  stmt.wheres = [where]
104
127
  end
105
128
 
129
+ # CPK. This is an alternative to IN subqueries. It is used by sqlserver.
130
+ # Example query:
131
+ #
132
+ # UPDATE reference_codes
133
+ # SET ...
134
+ # WHERE EXISTS
135
+ # (SELECT 1
136
+ # FROM reference_codes cpk_child
137
+ # WHERE reference_codes.reference_type_id = cpk_child.reference_type_id AND
138
+ # reference_codes.reference_code = cpk_child.reference_code)
106
139
  def cpk_exists_subquery(stmt)
107
- # Alias the outer table so we can join to in from the subquery
108
- aliased_table = arel_table.alias("cpk_outer_relation")
109
- stmt.table(aliased_table)
140
+ arel_attributes = primary_keys.map do |key|
141
+ arel_attribute(key)
142
+ end.to_composite_keys
110
143
 
111
- # Setup the subquery
112
- subquery = arel.clone
113
- subquery.projections = primary_keys.map do |key|
114
- arel_table[key]
115
- end
144
+ # Clone the query
145
+ subselect = arel.clone
146
+
147
+ # Alias the table - we assume just one table
148
+ aliased_table = subselect.froms.first
149
+ aliased_table.table_alias = "cpk_child"
150
+
151
+ # Project - really we could just set this to "1"
152
+ subselect.projections = arel_attributes
116
153
 
117
154
  # Setup correlation to the outer query via where clauses
118
155
  primary_keys.map do |key|
119
- outer_attribute = aliased_table[key]
120
- inner_attribute = arel_table[key]
156
+ outer_attribute = arel_table[key]
157
+ inner_attribute = aliased_table[key]
121
158
  where = outer_attribute.eq(inner_attribute)
122
- subquery.where(where)
159
+ subselect.where(where)
123
160
  end
124
- stmt.wheres = [Arel::Nodes::Exists.new(subquery)]
161
+ stmt.wheres = [Arel::Nodes::Exists.new(subselect)]
162
+ end
163
+
164
+ # CPK. This is the old way CPK created subqueries and is used by MySql.
165
+ # MySQL does not support referencing the same table that is being UPDATEd or
166
+ # DELETEd in a subquery so we obfuscate it. The ugly query looks like this:
167
+ #
168
+ # UPDATE `reference_codes`
169
+ # SET ...
170
+ # WHERE (reference_codes.reference_type_id, reference_codes.reference_code) IN
171
+ # (SELECT reference_type_id,reference_code
172
+ # FROM (SELECT DISTINCT `reference_codes`.`reference_type_id`, `reference_codes`.`reference_code`
173
+ # FROM `reference_codes`) __active_record_temp)
174
+ def cpk_mysql_subquery(stmt)
175
+ arel_attributes = primary_keys.map do |key|
176
+ arel_attribute(key)
177
+ end.to_composite_keys
178
+
179
+ subselect = arel.clone
180
+ subselect.projections = arel_attributes
181
+
182
+ # Materialize subquery by adding distinct
183
+ # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
184
+ subselect.distinct unless arel.limit || arel.offset || arel.orders.any?
185
+
186
+ key_name = arel_attributes.map(&:name).join(',')
187
+
188
+ manager = Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key_name))
189
+
190
+ stmt.wheres = [Arel::Nodes::In.new(arel_attributes, manager.ast)]
125
191
  end
126
192
  end
127
193
  end
@@ -96,7 +96,7 @@ module CompositePrimaryKeys
96
96
  case ids.size
97
97
  when 0
98
98
  error_message = "Couldn't find #{model_name} without an ID"
99
- raise RecordNotFound.new(error_message, model_name, primary_key)
99
+ raise ::ActiveRecord::RecordNotFound.new(error_message, model_name, primary_key)
100
100
  when 1
101
101
  result = find_one(ids.first)
102
102
  expects_array ? [ result ] : result
@@ -2,7 +2,7 @@ module CompositePrimaryKeys
2
2
  module VERSION
3
3
  MAJOR = 12
4
4
  MINOR = 0
5
- TINY = 3
5
+ TINY = 4
6
6
  STRING = [MAJOR, MINOR, TINY].join('.')
7
7
  end
8
8
  end
@@ -1,4 +1,4 @@
1
- spec_name = ENV['ADAPTER'] || 'mysql'
1
+ spec_name = ENV['ADAPTER'] || 'sqlite'
2
2
  require 'bundler'
3
3
  require 'minitest/autorun'
4
4
 
@@ -72,7 +72,7 @@ create table readings (
72
72
  primary key (id)
73
73
  );
74
74
 
75
- create table groups (
75
+ create table `groups` (
76
76
  id int not null auto_increment,
77
77
  name varchar(50) not null,
78
78
  primary key (id)
@@ -74,6 +74,18 @@ class TestFind < ActiveSupport::TestCase
74
74
  assert_equal(with_quoted_identifiers(expected), error.message)
75
75
  end
76
76
 
77
+ def test_find_with_invalid_ids
78
+ assert_raise(::ActiveRecord::RecordNotFound) do
79
+ Suburb.find([-1, -1])
80
+ end
81
+ end
82
+
83
+ def test_find_with_no_ids
84
+ assert_raise(::ActiveRecord::RecordNotFound) do
85
+ Suburb.find
86
+ end
87
+ end
88
+
77
89
  def test_find_last_suburb
78
90
  suburb = Suburb.last
79
91
  assert_equal([2,2], suburb.id)
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: 12.0.3
4
+ version: 12.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Charlie Savage
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-12 00:00:00.000000000 Z
11
+ date: 2020-12-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord