composite_primary_keys 12.0.3 → 12.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c7ee90af81fc2755ecd8b49d91808047736a1ec2d99fea0961bdc6813e9c6c1
4
- data.tar.gz: e0ff0a387dbd7ff3b7d3c0777ecf1fd54d4a5637128179c6a0788f71481079c5
3
+ metadata.gz: 2465cca0fa83a628e24f3221e88095690295c99b2bf46818bd2ebd76df8300b5
4
+ data.tar.gz: a22c2b61575c26e23e51b314c05c158663c26ce7a9c64fb56019d596edc6a844
5
5
  SHA512:
6
- metadata.gz: e9a6b4eb511b992f4ed4aa971fb6eb8a19af49cff1b06998f4d7093a155f2b6c39674c444d70595833ed887bbe2406d665f5c48751316c743597ae442c0d44fd
7
- data.tar.gz: dbdb54e9141e4d8dd959712f9f7f52f43ce22e7b184f0cc274769ca241418af4fd9775c7532fa492f1988c61fcb6a2b69143fe94889ebbf1b9a76c3b30cb0bdd
6
+ metadata.gz: 3147006b49f98c3d1e88dbec56590fad63c13506634427361e14e06d611e75666c89c48c3e9151d174dc075d850a238285be6bc62739d8971d7bf190bc6e9db9
7
+ data.tar.gz: 34d811c5ab6afb7d5b4b9dc871b2df660f42abaf146ffc7faa60c9d4d2edfb8c1a8fa6e49b40488a6090a7bb64218fafb6b0e965744b4daa0d11c0fc34605abf
data/History.rdoc CHANGED
@@ -1,3 +1,26 @@
1
+ == 12.0.9 (2021-02-22)
2
+ * Third time is hopefully the charm on MySQL/MariaDB auto increment fix
3
+
4
+ == 12.0.8 (2021-02-16)
5
+ * Revert to previous MySQL/MariaDB auto increment fix
6
+
7
+ == 12.0.7 (2021-02-15)
8
+ * Switch to GitHub actions (ta1kt0me)
9
+ * Fix MySQL/MariaDB query cache issue bug introduced in version 12.0.3 - see #539 (Charlie Savage)
10
+ * Do a better job of supporting composite keys with an auto incrementing field for
11
+ MySQL and MariaDB (Charlie Savage)
12
+
13
+ == 12.0.6 (2021-01-04)
14
+ * Fix issue when calling in_batches without a block (Charlie Savage)
15
+
16
+ == 12.0.5 (2020-12-31)
17
+ * Finally issue with SQLServer when tables are marked as exclude_output_inserted. See #535. (Charlie Savage)
18
+
19
+ == 12.0.4 (2020-12-30)
20
+ * Fix compatibility with Ruby Ruby 2.6 and below (ta1kt0me)
21
+ * Finally get SQLServer mass updates and deletes working (Charlie Savage)
22
+ * Fix MySQL mass updates and deletes that were broken by 12.0.3 (Charlie Savage)
23
+
1
24
  == 12.0.3 (2020-11-11)
2
25
  * Prevents infinite loops with gems which modify the 'attributes' method (Nicholas Guarino)
3
26
  * Improve delete_all and update_all queries (Charlie Savage)
@@ -99,7 +99,6 @@ require_relative 'composite_primary_keys/nested_attributes'
99
99
 
100
100
  require_relative 'composite_primary_keys/connection_adapters/abstract/database_statements'
101
101
  require_relative 'composite_primary_keys/connection_adapters/abstract_adapter'
102
- require_relative 'composite_primary_keys/connection_adapters/mysql/database_statements'
103
102
  require_relative 'composite_primary_keys/connection_adapters/postgresql/database_statements'
104
103
  require_relative 'composite_primary_keys/connection_adapters/sqlserver/database_statements'
105
104
 
@@ -5,16 +5,31 @@ module ActiveRecord
5
5
  sql, binds = to_sql_and_binds(arel, binds)
6
6
  value = exec_insert(sql, name, binds, pk, sequence_name)
7
7
 
8
+ return id_value if id_value
9
+
8
10
  if pk.is_a?(Array) && !value.empty?
9
11
  # This is a CPK model and the query result is not empty. Thus we can figure out the new ids for each
10
12
  # auto incremented field
11
- id_value || pk.map {|key| value.first[key]}
13
+ pk.map {|key| value.first[key]}
12
14
  elsif pk.is_a?(Array)
13
- # This is CPK, but we don't know what autoincremented fields were updated. So return nil, which means
14
- # the existing id_value of the model will be used.
15
- id_value || Array.new(pk.size)
15
+ # This is CPK, but we don't know what autoincremented fields were updated.
16
+ result = Array.new(pk.size)
17
+
18
+ # Is there an autoincrementing field?
19
+ auto_key = pk.find do |key|
20
+ attribute = arel.ast.relation[key]
21
+ column = column_for_attribute(attribute)
22
+ if column.respond_to?(:auto_increment?)
23
+ column.auto_increment?
24
+ end
25
+ end
26
+
27
+ if auto_key
28
+ result[pk.index(auto_key)] = last_inserted_id(value)
29
+ end
30
+ result
16
31
  else
17
- id_value || last_inserted_id(value)
32
+ last_inserted_id(value)
18
33
  end
19
34
  end
20
35
  end
@@ -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 = 9
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.9
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-22 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: []
@@ -71,7 +71,6 @@ files:
71
71
  - lib/composite_primary_keys/composite_relation.rb
72
72
  - lib/composite_primary_keys/connection_adapters/abstract/database_statements.rb
73
73
  - lib/composite_primary_keys/connection_adapters/abstract_adapter.rb
74
- - lib/composite_primary_keys/connection_adapters/mysql/database_statements.rb
75
74
  - lib/composite_primary_keys/connection_adapters/postgresql/database_statements.rb
76
75
  - lib/composite_primary_keys/connection_adapters/sqlserver/database_statements.rb
77
76
  - lib/composite_primary_keys/core.rb
@@ -201,7 +200,7 @@ homepage: https://github.com/composite-primary-keys/composite_primary_keys
201
200
  licenses:
202
201
  - MIT
203
202
  metadata: {}
204
- post_install_message:
203
+ post_install_message:
205
204
  rdoc_options: []
206
205
  require_paths:
207
206
  - lib
@@ -216,16 +215,16 @@ required_rubygems_version: !ruby/object:Gem::Requirement
216
215
  - !ruby/object:Gem::Version
217
216
  version: '0'
218
217
  requirements: []
219
- rubygems_version: 3.1.4
220
- signing_key:
218
+ rubygems_version: 3.2.8
219
+ signing_key:
221
220
  specification_version: 4
222
221
  summary: Composite key support for ActiveRecord
223
222
  test_files:
224
- - test/abstract_unit.rb
225
223
  - test/README_tests.rdoc
224
+ - test/abstract_unit.rb
226
225
  - test/test_associations.rb
227
- - test/test_attributes.rb
228
226
  - test/test_attribute_methods.rb
227
+ - test/test_attributes.rb
229
228
  - test/test_calculations.rb
230
229
  - test/test_callbacks.rb
231
230
  - test/test_composite_arrays.rb
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveRecord
4
- module ConnectionAdapters
5
- module MySQL
6
- module DatabaseStatements
7
- def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
8
- sql, binds = to_sql_and_binds(arel, binds)
9
- value = exec_insert(sql, name, binds, pk, sequence_name)
10
-
11
- # CPK
12
- if pk.is_a?(Array)
13
- pk.map do |key|
14
- column = self.column_for(arel.ast.relation.name, key)
15
- column.auto_increment? ? last_inserted_id(value) : nil
16
- end
17
- else
18
- id_value || last_inserted_id(value)
19
- end
20
- end
21
- end
22
- end
23
- end
24
- end