composite_primary_keys 12.0.2 → 12.0.7

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of composite_primary_keys might be problematic. Click here for more details.

Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/History.rdoc +30 -0
  3. data/README.rdoc +3 -2
  4. data/lib/composite_primary_keys.rb +57 -56
  5. data/lib/composite_primary_keys/active_model/attribute_assignment.rb +19 -0
  6. data/lib/composite_primary_keys/arel/sqlserver.rb +1 -3
  7. data/lib/composite_primary_keys/associations/through_association.rb +2 -1
  8. data/lib/composite_primary_keys/attribute_methods.rb +1 -1
  9. data/lib/composite_primary_keys/attribute_methods/primary_key.rb +13 -0
  10. data/lib/composite_primary_keys/base.rb +11 -0
  11. data/lib/composite_primary_keys/composite_arrays.rb +0 -8
  12. data/lib/composite_primary_keys/connection_adapters/abstract/database_statements.rb +24 -4
  13. data/lib/composite_primary_keys/connection_adapters/mysql/database_statements.rb +24 -0
  14. data/lib/composite_primary_keys/connection_adapters/sqlserver/database_statements.rb +32 -11
  15. data/lib/composite_primary_keys/core.rb +1 -1
  16. data/lib/composite_primary_keys/persistence.rb +2 -2
  17. data/lib/composite_primary_keys/relation.rb +100 -25
  18. data/lib/composite_primary_keys/relation/batches.rb +1 -1
  19. data/lib/composite_primary_keys/relation/finder_methods.rb +1 -1
  20. data/lib/composite_primary_keys/relation/predicate_builder/association_query_value.rb +1 -1
  21. data/lib/composite_primary_keys/version.rb +1 -1
  22. data/test/abstract_unit.rb +2 -1
  23. data/test/connections/databases.ci.yml +5 -2
  24. data/test/fixtures/article.rb +4 -0
  25. data/test/fixtures/articles.yml +4 -3
  26. data/test/fixtures/comment.rb +1 -3
  27. data/test/fixtures/comments.yml +10 -9
  28. data/test/fixtures/db_definitions/db2-create-tables.sql +0 -14
  29. data/test/fixtures/db_definitions/db2-drop-tables.sql +1 -3
  30. data/test/fixtures/db_definitions/mysql.sql +7 -44
  31. data/test/fixtures/db_definitions/oracle.drop.sql +3 -9
  32. data/test/fixtures/db_definitions/oracle.sql +12 -48
  33. data/test/fixtures/db_definitions/postgresql.sql +7 -44
  34. data/test/fixtures/db_definitions/sqlite.sql +6 -42
  35. data/test/fixtures/db_definitions/sqlserver.sql +5 -41
  36. data/test/fixtures/department.rb +8 -3
  37. data/test/fixtures/departments.yml +4 -4
  38. data/test/fixtures/readings.yml +2 -2
  39. data/test/fixtures/restaurants_suburbs.yml +1 -1
  40. data/test/fixtures/streets.yml +2 -2
  41. data/test/fixtures/suburbs.yml +2 -2
  42. data/test/fixtures/user.rb +3 -2
  43. data/test/test_associations.rb +30 -23
  44. data/test/test_create.rb +41 -18
  45. data/test/test_delete.rb +3 -3
  46. data/test/test_exists.rb +5 -5
  47. data/test/test_find.rb +21 -2
  48. data/test/test_habtm.rb +2 -2
  49. data/test/test_ids.rb +2 -6
  50. data/test/test_nested_attributes.rb +0 -57
  51. data/test/test_polymorphic.rb +29 -13
  52. data/test/test_preload.rb +4 -3
  53. data/test/test_serialize.rb +2 -2
  54. data/test/test_update.rb +19 -1
  55. metadata +11 -25
  56. data/test/fixtures/hack.rb +0 -5
  57. data/test/fixtures/hacks.yml +0 -3
  58. data/test/fixtures/pk_called_id.rb +0 -5
  59. data/test/fixtures/pk_called_ids.yml +0 -11
  60. data/test/fixtures/reference_code_using_composite_key_alias.rb +0 -8
  61. data/test/fixtures/reference_code_using_simple_key_alias.rb +0 -8
  62. data/test/fixtures/seat.rb +0 -5
  63. data/test/fixtures/seats.yml +0 -9
  64. data/test/fixtures/topic.rb +0 -6
  65. data/test/fixtures/topic_source.rb +0 -7
  66. data/test/test_aliases.rb +0 -18
  67. data/test/test_enum.rb +0 -21
  68. data/test/test_suite.rb +0 -35
@@ -27,15 +27,8 @@ module ActiveRecord
27
27
  stmt = Arel::UpdateManager.new
28
28
  # CPK
29
29
  if @klass.composite?
30
- stmt.table(table)
31
-
32
- arel_attributes = primary_keys.map do |key|
33
- arel_attribute(key)
34
- end.to_composite_keys
35
-
36
- subselect = subquery_for(arel_attributes, arel)
37
-
38
- stmt.wheres = [arel_attributes.in(subselect)]
30
+ stmt.table(arel_table)
31
+ cpk_subquery(stmt)
39
32
  else
40
33
  stmt.table(arel.join_sources.empty? ? table : arel.source)
41
34
  stmt.key = arel_attribute(primary_key)
@@ -75,22 +68,16 @@ module ActiveRecord
75
68
  end
76
69
 
77
70
  stmt = Arel::DeleteManager.new
78
- # CPK
79
- if @klass.composite?
80
- stmt.from(table)
81
-
82
- arel_attributes = primary_keys.map do |key|
83
- arel_attribute(key)
84
- end.to_composite_keys
85
71
 
86
- subselect = subquery_for(arel_attributes, arel)
87
-
88
- stmt.wheres = [arel_attributes.in(subselect)]
72
+ if @klass.composite?
73
+ stmt.from(arel_table)
74
+ cpk_subquery(stmt)
89
75
  else
90
76
  stmt.from(arel.join_sources.empty? ? table : arel.source)
91
77
  stmt.key = arel_attribute(primary_key)
92
78
  stmt.wheres = arel.constraints
93
79
  end
80
+
94
81
  stmt.take(arel.limit)
95
82
  stmt.offset(arel.offset)
96
83
  stmt.order(*arel.orders)
@@ -102,17 +89,105 @@ module ActiveRecord
102
89
  end
103
90
 
104
91
  # CPK
105
- def subquery_for(key, select)
106
- subselect = select.clone
107
- subselect.projections = key
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)
115
+ def cpk_in_subquery(stmt)
116
+ # Setup the subquery
117
+ subquery = arel.clone
118
+ subquery.projections = primary_keys.map do |key|
119
+ arel_table[key]
120
+ end
121
+
122
+ where_fields = primary_keys.map do |key|
123
+ arel_table[key]
124
+ end
125
+ where = Arel::Nodes::Grouping.new(where_fields).in(subquery)
126
+ stmt.wheres = [where]
127
+ end
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)
139
+ def cpk_exists_subquery(stmt)
140
+ arel_attributes = primary_keys.map do |key|
141
+ arel_attribute(key)
142
+ end.to_composite_keys
143
+
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
153
+
154
+ # Setup correlation to the outer query via where clauses
155
+ primary_keys.map do |key|
156
+ outer_attribute = arel_table[key]
157
+ inner_attribute = aliased_table[key]
158
+ where = outer_attribute.eq(inner_attribute)
159
+ subselect.where(where)
160
+ end
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
108
181
 
109
182
  # Materialize subquery by adding distinct
110
183
  # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
111
- subselect.distinct unless select.limit || select.offset || select.orders.any?
184
+ subselect.distinct unless arel.limit || arel.offset || arel.orders.any?
185
+
186
+ key_name = arel_attributes.map(&:name).join(',')
112
187
 
113
- key_name = Array(key).map {|a_key| a_key.name }.join(',')
188
+ manager = Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key_name))
114
189
 
115
- Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key_name))
190
+ stmt.wheres = [Arel::Nodes::In.new(arel_attributes, manager.ast)]
116
191
  end
117
192
  end
118
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
@@ -6,7 +6,7 @@ module ActiveRecord
6
6
  if associated_table.association_join_foreign_key.is_a?(Array)
7
7
  if ids.is_a?(ActiveRecord::Relation)
8
8
  ids.map do |id|
9
- id.ids_hash
9
+ associated_table.association_join_foreign_key.zip(id.id).to_h
10
10
  end
11
11
  else
12
12
  [associated_table.association_join_foreign_key.zip(ids).to_h]
@@ -2,7 +2,7 @@ module CompositePrimaryKeys
2
2
  module VERSION
3
3
  MAJOR = 12
4
4
  MINOR = 0
5
- TINY = 2
5
+ TINY = 7
6
6
  STRING = [MAJOR, MINOR, TINY].join('.')
7
7
  end
8
8
  end
@@ -1,7 +1,8 @@
1
1
  spec_name = ENV['ADAPTER'] || 'sqlite'
2
- require 'bundler/setup'
2
+ require 'bundler'
3
3
  require 'minitest/autorun'
4
4
 
5
+ Bundler.setup(:default, spec_name.to_sym)
5
6
  Bundler.require(:default, spec_name.to_sym)
6
7
  require 'composite_primary_keys'
7
8
 
@@ -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:
@@ -2,5 +2,9 @@ class Article < ActiveRecord::Base
2
2
  validates :id, uniqueness: true, numericality: true, allow_nil: true, allow_blank: true, on: :create
3
3
  has_many :readings, :dependent => :delete_all
4
4
  has_many :users, :through => :readings
5
+
6
+ has_many :comments, :dependent => :delete_all
7
+ has_many :employee_commentators, :through => :comments, :source => :person, :source_type => :employee
8
+ has_many :user_commentators, :through => :comments, :source => :person, :source_type => "User"
5
9
  end
6
10
 
@@ -1,7 +1,8 @@
1
1
  first:
2
- id: 1
3
2
  name: Article One
4
3
 
5
4
  second:
6
- id: 2
7
- name: Article Two
5
+ name: Article Two
6
+
7
+ third:
8
+ name: Article Three
@@ -1,7 +1,5 @@
1
1
  class Comment < ActiveRecord::Base
2
2
  belongs_to :person, :polymorphic => true
3
- belongs_to :hack
4
-
5
- enum :shown => [ :true, :false ]
3
+ belongs_to :article
6
4
  end
7
5
 
@@ -1,16 +1,17 @@
1
- comment1:
1
+ employee_comment:
2
2
  id: 1
3
+ article: first
3
4
  person_id: 1
4
5
  person_type: Employee
5
-
6
- comment2:
6
+
7
+ user_comment_1:
7
8
  id: 2
9
+ article: second
8
10
  person_id: 1
9
11
  person_type: User
10
- hack_id: 7
11
-
12
- comment3:
12
+
13
+ user_comment_2:
13
14
  id: 3
14
- person_id: 7
15
- person_type: Hack
16
-
15
+ article: second
16
+ person_id: 2
17
+ person_type: User
@@ -1,17 +1,3 @@
1
- CREATE TABLE topics (
2
- id integer NOT NULL,
3
- name varchar(50) default NULL,
4
- feed_size integer default NULL,
5
- PRIMARY KEY (id)
6
- );
7
-
8
- CREATE TABLE topic_sources (
9
- topic_id integer NOT NULL,
10
- platform varchar(50) NOT NULL,
11
- keywords varchar(50) default NULL,
12
- PRIMARY KEY (topic_id,platform)
13
- );
14
-
15
1
  CREATE TABLE reference_types (
16
2
  reference_type_id integer NOT NULL generated by default as identity (start with 100, increment by 1, no cache),
17
3
  type_label varchar(50) default NULL,
@@ -14,6 +14,4 @@ drop table PRODUCT_TARIFFS;
14
14
  drop table KITCHEN_SINK;
15
15
  drop table RESTAURANTS;
16
16
  drop table RESTAURANTS_SUBURBS;
17
- drop table PRODUCTS_RESTAURANTS;
18
- drop table TOPICS;
19
- drop table TOPIC_SOURCES;
17
+ drop table PRODUCTS_RESTAURANTS;
@@ -1,17 +1,3 @@
1
- create table topics (
2
- id int not null auto_increment,
3
- name varchar(50) default null,
4
- feed_size int default null,
5
- primary key (id)
6
- );
7
-
8
- create table topic_sources (
9
- topic_id int not null,
10
- platform varchar(50) not null,
11
- keywords varchar(50) default null,
12
- primary key (topic_id,platform)
13
- );
14
-
15
1
  create table reference_types (
16
2
  reference_type_id int not null auto_increment,
17
3
  type_label varchar(50) default null,
@@ -52,7 +38,7 @@ create table product_tariffs (
52
38
  );
53
39
 
54
40
  create table suburbs (
55
- city_id int not null,
41
+ city_id int not null auto_increment,
56
42
  suburb_id int not null,
57
43
  name varchar(50) not null,
58
44
  primary key (city_id, suburb_id)
@@ -86,7 +72,7 @@ create table readings (
86
72
  primary key (id)
87
73
  );
88
74
 
89
- create table groups (
75
+ create table `groups` (
90
76
  id int not null auto_increment,
91
77
  name varchar(50) not null,
92
78
  primary key (id)
@@ -107,9 +93,9 @@ create table membership_statuses (
107
93
  );
108
94
 
109
95
  create table departments (
110
- department_id int not null,
96
+ id int not null auto_increment,
111
97
  location_id int not null,
112
- primary key (department_id, location_id)
98
+ primary key (id, location_id)
113
99
  );
114
100
 
115
101
  create table employees (
@@ -122,16 +108,9 @@ create table employees (
122
108
 
123
109
  create table comments (
124
110
  id int not null auto_increment,
125
- person_id int default null,
126
- shown int default null,
127
- person_type varchar(100) default null,
128
- hack_id int default null,
129
- primary key (id)
130
- );
131
-
132
- create table hacks (
133
- id int not null auto_increment,
134
- name varchar(50) not null,
111
+ article_id int not null,
112
+ person_id int not null,
113
+ person_type varchar(100) not null,
135
114
  primary key (id)
136
115
  );
137
116
 
@@ -184,13 +163,6 @@ create table room_assignments (
184
163
  room_id int not null
185
164
  );
186
165
 
187
- create table seats (
188
- flight_number int not null,
189
- seat int not null,
190
- customer int,
191
- primary key (flight_number, seat)
192
- );
193
-
194
166
  create table capitols (
195
167
  country varchar(100) not null,
196
168
  city varchar(100) not null,
@@ -206,13 +178,4 @@ create table products_restaurants (
206
178
  create table employees_groups (
207
179
  employee_id int not null,
208
180
  group_id int not null
209
- );
210
-
211
- create table pk_called_ids (
212
- id integer not null,
213
- reference_code int not null,
214
- code_label varchar(50) default null,
215
- abbreviation varchar(50) default null,
216
- description varchar(50) default null,
217
- primary key (id, reference_code)
218
181
  );
@@ -1,6 +1,3 @@
1
- drop table topics;
2
- drop sequence topics_seq;
3
- drop table topic_sources;
4
1
  drop table reference_types;
5
2
  drop sequence reference_types_seq;
6
3
  drop table reference_codes;
@@ -9,6 +6,7 @@ drop sequence products_seq;
9
6
  drop table tariffs;
10
7
  drop table product_tariffs;
11
8
  drop table suburbs;
9
+ drop sequence suburbs_city_id_seq;
12
10
  drop table streets;
13
11
  drop sequence streets_seq;
14
12
  drop table users;
@@ -23,12 +21,11 @@ drop table memberships;
23
21
  drop table membership_statuses;
24
22
  drop sequence membership_statuses_seq;
25
23
  drop table departments;
24
+ drop sequence departments_seq;
26
25
  drop table employees;
27
26
  drop sequence employees_seq;
28
27
  drop table comments;
29
28
  drop sequence comments_seq;
30
- drop table hacks;
31
- drop sequence hacks_seq;
32
29
  drop table restaurants;
33
30
  drop table restaurants_suburbs;
34
31
  drop table dorms;
@@ -40,9 +37,6 @@ drop table room_attribute_assignments;
40
37
  drop table room_assignments;
41
38
  drop table students;
42
39
  drop sequence students_seq;
43
- drop table seats;
44
40
  drop table capitols;
45
41
  drop table products_restaurants;
46
- drop table employees_groups;
47
- drop table pk_called_ids;
48
- drop sequence pk_called_ids_seq;
42
+ drop table employees_groups;