composite_primary_keys 12.0.3 → 12.0.8

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: 8349636fa2a670d60ef8f8cf288d31f805aafebb8c3f0d121c23aafb4f34dc08
4
+ data.tar.gz: 7932c338e6e3fad7831715dff3b879a9364356e19b93eeba8e5db55e74f6ad25
5
5
  SHA512:
6
- metadata.gz: e9a6b4eb511b992f4ed4aa971fb6eb8a19af49cff1b06998f4d7093a155f2b6c39674c444d70595833ed887bbe2406d665f5c48751316c743597ae442c0d44fd
7
- data.tar.gz: dbdb54e9141e4d8dd959712f9f7f52f43ce22e7b184f0cc274769ca241418af4fd9775c7532fa492f1988c61fcb6a2b69143fe94889ebbf1b9a76c3b30cb0bdd
6
+ metadata.gz: 9659459ef7f9d4a0331e778904d7c3fe869c18c638a9b852bd1961cd8892699e6ef6e7cbea3127510926277cc6ad21a1ecd5d0e722f229b883361be02dc7aff9
7
+ data.tar.gz: 7dd7ce6eac76e6728f00337f325d55014b04e209458d826e439454a53eee9668f4e5cb165a2dfa07d4c9821d75f88666b156a66a875c78e87ce97dec35f7d262
data/History.rdoc CHANGED
@@ -1,3 +1,23 @@
1
+ == 12.0.8 (2021-02-16)
2
+ * Revert to previous MySQL/MariaDB auto increment fix
3
+
4
+ == 12.0.7 (2021-02-15)
5
+ * Switch to GitHub actions (ta1kt0me)
6
+ * Fix MySQL/MariaDB query cache issue bug introduced in version 12.0.3 - see #539 (Charlie Savage)
7
+ * Do a better job of supporting composite keys with an auto incrementing field for
8
+ MySQL and MariaDB (Charlie Savage)
9
+
10
+ == 12.0.6 (2021-01-04)
11
+ * Fix issue when calling in_batches without a block (Charlie Savage)
12
+
13
+ == 12.0.5 (2020-12-31)
14
+ * Finally issue with SQLServer when tables are marked as exclude_output_inserted. See #535. (Charlie Savage)
15
+
16
+ == 12.0.4 (2020-12-30)
17
+ * Fix compatibility with Ruby Ruby 2.6 and below (ta1kt0me)
18
+ * Finally get SQLServer mass updates and deletes working (Charlie Savage)
19
+ * Fix MySQL mass updates and deletes that were broken by 12.0.3 (Charlie Savage)
20
+
1
21
  == 12.0.3 (2020-11-11)
2
22
  * Prevents infinite loops with gems which modify the 'attributes' method (Nicholas Guarino)
3
23
  * Improve delete_all and update_all queries (Charlie Savage)
@@ -62,6 +62,7 @@ require 'active_record/nested_attributes'
62
62
  require 'active_record/connection_adapters/abstract/database_statements'
63
63
  require 'active_record/connection_adapters/abstract_adapter'
64
64
  require 'active_record/connection_adapters/postgresql/database_statements'
65
+ require 'active_record/connection_adapters/mysql/database_statements'
65
66
 
66
67
  require 'active_record/relation/where_clause'
67
68
 
@@ -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
@@ -15,15 +15,18 @@ module ActiveRecord
15
15
 
16
16
  table_name ||= get_table_name(sql)
17
17
  exclude_output_inserted = exclude_output_inserted_table_name?(table_name, sql)
18
-
19
18
  if exclude_output_inserted
20
19
  id_sql_type = exclude_output_inserted.is_a?(TrueClass) ? "bigint" : exclude_output_inserted
20
+ # CPK
21
+ # <<~SQL.squish
22
+ # DECLARE @ssaIdInsertTable table (#{quoted_pk} #{id_sql_type});
23
+ # #{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT INSERTED.#{quoted_pk} INTO @ssaIdInsertTable"}
24
+ # SELECT CAST(#{quoted_pk.join(',')} AS #{id_sql_type}) FROM @ssaIdInsertTable
25
+ # SQL
21
26
  <<~SQL.squish
22
- DECLARE @ssaIdInsertTable table (#{quoted_pk} #{id_sql_type});
23
- # CPK
24
- # #{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT INSERTED.#{quoted_pk} INTO @ssaIdInsertTable"}
27
+ DECLARE @ssaIdInsertTable table (#{quoted_pk.map {|subkey| "#{subkey} #{id_sql_type}"}.join(", ")});
25
28
  #{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT INSERTED.#{quoted_pk.join(', INSERTED.')} INTO @ssaIdInsertTable"}
26
- SELECT CAST(#{quoted_pk.join(',')} AS #{id_sql_type}) FROM @ssaIdInsertTable
29
+ SELECT #{quoted_pk.map {|subkey| "CAST(#{subkey} AS #{id_sql_type}) #{subkey}"}.join(", ")} FROM @ssaIdInsertTable
27
30
  SQL
28
31
  else
29
32
  # CPK
@@ -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
@@ -4,7 +4,7 @@ module CompositePrimaryKeys
4
4
  def in_batches(of: 1000, start: nil, finish: nil, load: false, error_on_ignore: nil)
5
5
  relation = self
6
6
  unless block_given?
7
- return BatchEnumerator.new(of: of, start: start, finish: finish, relation: self)
7
+ return ::ActiveRecord::Batches::BatchEnumerator.new(of: of, start: start, finish: finish, relation: self)
8
8
  end
9
9
 
10
10
  if arel.orders.present?
@@ -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 = 8
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
 
@@ -1,7 +1,9 @@
1
1
  mysql:
2
2
  adapter: mysql2
3
- username: travis
4
- password: ""
3
+ username: github
4
+ password: github
5
+ host: 127.0.0.1
6
+ port: 3306
5
7
  encoding: utf8mb4
6
8
  charset: utf8mb4
7
9
  collation: utf8mb4_bin
@@ -11,6 +13,7 @@ postgresql:
11
13
  adapter: postgresql
12
14
  database: composite_primary_keys_unittest
13
15
  username: postgres
16
+ password: postgres
14
17
  host: localhost
15
18
 
16
19
  sqlite:
@@ -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)
data/test/test_create.rb CHANGED
@@ -48,12 +48,21 @@ class TestCreate < ActiveSupport::TestCase
48
48
  end
49
49
  end
50
50
 
51
+ def test_create_with_array
52
+ date = Date.new(2027, 01, 27)
53
+ tariff = Tariff.create!(id: [10, date], amount: 27)
54
+ refute_nil(tariff)
55
+ assert_equal([10, date], tariff.id)
56
+ assert_equal(date, tariff.start_date)
57
+ assert_equal(27, tariff.amount)
58
+ end
59
+
51
60
  def test_create_with_partial_serial
52
61
  attributes = {:location_id => 100}
53
62
 
54
63
  # SQLite does not support an autoincrementing field in a composite key
55
64
  if Department.connection.class.name == "ActiveRecord::ConnectionAdapters::SQLite3Adapter"
56
- attributes[:id] = 100
65
+ attributes[:id] = 200
57
66
  end
58
67
 
59
68
  department = Department.new(attributes)
@@ -177,4 +186,21 @@ class TestCreate < ActiveSupport::TestCase
177
186
  suburb = Suburb.find_or_create_by!(:name => 'New Suburb', :city_id => 3, :suburb_id => 1)
178
187
  refute_nil(suburb)
179
188
  end
189
+
190
+ def test_cache
191
+ Suburb.cache do
192
+ # Suburb does not exist
193
+ suburb = Suburb.find_by(:city_id => 10, :suburb_id => 10)
194
+ assert_nil(suburb)
195
+
196
+ # Create it
197
+ suburb = Suburb.create!(:name => 'New Suburb', :city_id => 10, :suburb_id => 10)
198
+
199
+ # Should be able to find it
200
+ suburb = Suburb.find_by(:city_id => 10)
201
+ refute_nil(suburb)
202
+ refute_nil(suburb.city_id)
203
+ refute_nil(suburb.suburb_id)
204
+ end
205
+ end
180
206
  end
data/test/test_find.rb CHANGED
@@ -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)
@@ -91,6 +103,13 @@ class TestFind < ActiveSupport::TestCase
91
103
  end
92
104
  end
93
105
 
106
+ def test_in_batches_enumerator
107
+ enumerator = Department.in_batches
108
+ enumerator.each do |batch|
109
+ assert_equal(Department.count, batch.size)
110
+ end
111
+ end
112
+
94
113
  def test_in_batches_of_1
95
114
  num_found = 0
96
115
  Department.in_batches(of: 1) do |batch|
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.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Charlie Savage
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-12 00:00:00.000000000 Z
11
+ date: 2021-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -39,7 +39,7 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  description: Composite key support for ActiveRecord
42
- email:
42
+ email:
43
43
  executables: []
44
44
  extensions: []
45
45
  extra_rdoc_files: []
@@ -201,7 +201,7 @@ homepage: https://github.com/composite-primary-keys/composite_primary_keys
201
201
  licenses:
202
202
  - MIT
203
203
  metadata: {}
204
- post_install_message:
204
+ post_install_message:
205
205
  rdoc_options: []
206
206
  require_paths:
207
207
  - lib
@@ -216,16 +216,16 @@ required_rubygems_version: !ruby/object:Gem::Requirement
216
216
  - !ruby/object:Gem::Version
217
217
  version: '0'
218
218
  requirements: []
219
- rubygems_version: 3.1.4
220
- signing_key:
219
+ rubygems_version: 3.2.8
220
+ signing_key:
221
221
  specification_version: 4
222
222
  summary: Composite key support for ActiveRecord
223
223
  test_files:
224
- - test/abstract_unit.rb
225
224
  - test/README_tests.rdoc
225
+ - test/abstract_unit.rb
226
226
  - test/test_associations.rb
227
- - test/test_attributes.rb
228
227
  - test/test_attribute_methods.rb
228
+ - test/test_attributes.rb
229
229
  - test/test_calculations.rb
230
230
  - test/test_callbacks.rb
231
231
  - test/test_composite_arrays.rb