composite_primary_keys 14.0.6 → 14.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/History.rdoc +6 -0
  3. data/Rakefile +1 -1
  4. data/lib/composite_primary_keys/associations/association.rb +2 -2
  5. data/lib/composite_primary_keys/associations/association_scope.rb +1 -1
  6. data/lib/composite_primary_keys/associations/has_many_through_association.rb +19 -0
  7. data/lib/composite_primary_keys/connection_adapters/abstract/database_statements.rb +1 -2
  8. data/lib/composite_primary_keys/relation/query_methods.rb +14 -16
  9. data/lib/composite_primary_keys/relation.rb +199 -197
  10. data/lib/composite_primary_keys/version.rb +1 -1
  11. data/lib/composite_primary_keys.rb +117 -119
  12. data/scripts/console.rb +2 -2
  13. data/tasks/databases/trilogy.rake +23 -0
  14. data/test/abstract_unit.rb +124 -118
  15. data/test/connections/databases.ci.yml +10 -0
  16. data/test/fixtures/admin.rb +4 -0
  17. data/test/fixtures/comments.yml +6 -0
  18. data/test/fixtures/db_definitions/db2-create-tables.sql +34 -0
  19. data/test/fixtures/db_definitions/db2-drop-tables.sql +7 -1
  20. data/test/fixtures/db_definitions/mysql.sql +23 -0
  21. data/test/fixtures/db_definitions/oracle.drop.sql +4 -0
  22. data/test/fixtures/db_definitions/oracle.sql +21 -0
  23. data/test/fixtures/db_definitions/postgresql.sql +23 -0
  24. data/test/fixtures/db_definitions/sqlite.sql +21 -0
  25. data/test/fixtures/db_definitions/sqlserver.sql +23 -0
  26. data/test/fixtures/moderator.rb +4 -0
  27. data/test/fixtures/room.rb +4 -1
  28. data/test/fixtures/staff_room.rb +6 -0
  29. data/test/fixtures/staff_room_key.rb +6 -0
  30. data/test/fixtures/user.rb +3 -0
  31. data/test/fixtures/user_with_polymorphic_name.rb +9 -0
  32. data/test/test_associations.rb +403 -403
  33. data/test/test_has_one_through.rb +30 -0
  34. data/test/test_polymorphic.rb +6 -0
  35. metadata +11 -4
  36. data/lib/composite_primary_keys/associations/through_association.rb +0 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76a010358e1d2cacd9b05f6229c1fe4225f20f888fefb57b0bd6a9f62589a720
4
- data.tar.gz: 1b7870c774bbed21d4556cf53518054860903cae4a0987f5978e02fc3883597f
3
+ metadata.gz: de97dfff0f237f22dd32a32189824739087b30c0ea0e14cdd996bd8b69359caf
4
+ data.tar.gz: 722a9e0d02b6fa8ee2010d47e5a37705a43b302df6abf7c7a6d199b0a84b0201
5
5
  SHA512:
6
- metadata.gz: 117d7be0b54619f7adcb05ddb200c3c97571cc74bf2fcf33c99034efaec08924cf6d56e998db1135c92bf71497697b30ca531a882d755329d5b6f9192d606013
7
- data.tar.gz: a9f330398e5b1a9153a67f229a28e83d65c5bc02334e6e9f3456229c4c1e011a3bf03909fa45e37739d4ab47a37663a3ee6ef09c7d7194721e91849e520c0a01
6
+ metadata.gz: e57b5330dc2b0cdb7374f14ed6c7efed22c269cf42298527efc1ec26ee2e2ea8ab9edd97e97a7f4c656662e54a1faa5da28fef365c602060878d375e4caa646a
7
+ data.tar.gz: c2981385774399cdd3e64385d9f7595dd2a3972706b04e825b604f83ed4fe59438df2f114f196ff0fb6ceb389060554259cab7132c2a75073a382101da533bd0
data/History.rdoc CHANGED
@@ -1,3 +1,9 @@
1
+ == 14.0.7 (2023-11-04)
2
+ * Add support for Trilogy Adapter (Zack Mariscal)
3
+ * Add Dockerfile and Docker compose support (Zack Mariscal)
4
+ * Support polymorphic_name (Vladimir Kochnev)
5
+ * Fix HasOneThrough association (kyori19)
6
+
1
7
  == 14.0.6 (2023-02-04)
2
8
  * Port fix for #573 (Charlie Savage)
3
9
  * Port fix for fix #577 (Charlie Savage)
data/Rakefile CHANGED
@@ -23,7 +23,7 @@ Dir.glob('tasks/**/*.rake').each do |rake_file|
23
23
  end
24
24
 
25
25
  # Set up test tasks for each supported connection adapter
26
- %w(mysql sqlite oracle oracle_enhanced postgresql ibm_db sqlserver).each do |adapter|
26
+ %w(mysql sqlite oracle oracle_enhanced postgresql ibm_db sqlserver trilogy).each do |adapter|
27
27
  namespace adapter do
28
28
  desc "Run tests using the #{adapter} adapter"
29
29
  task "test" do
@@ -11,8 +11,8 @@ module ActiveRecord
11
11
  attributes[key1] = owner[key2]
12
12
  end
13
13
 
14
- if reflection.options[:as]
15
- attributes[reflection.type] = owner.class.base_class.name
14
+ if reflection.type
15
+ attributes[reflection.type] = owner.class.polymorphic_name
16
16
  end
17
17
  end
18
18
 
@@ -64,4 +64,4 @@ module ActiveRecord
64
64
  end
65
65
  end
66
66
  end
67
- end
67
+ end
@@ -23,6 +23,25 @@ module ActiveRecord
23
23
  end
24
24
  end
25
25
  end
26
+
27
+ alias :original_construct_join_attributes :construct_join_attributes
28
+
29
+ def construct_join_attributes(*records)
30
+ # CPK
31
+ if !self.source_reflection.polymorphic? && source_reflection.klass.composite?
32
+ ensure_mutable
33
+
34
+ ids = records.map do |record|
35
+ source_reflection.association_primary_key(reflection.klass).map do |key|
36
+ record.send(key)
37
+ end
38
+ end
39
+
40
+ cpk_in_predicate(through_association.scope.klass.arel_table, source_reflection.foreign_key, ids)
41
+ else
42
+ original_construct_join_attributes(*records)
43
+ end
44
+ end
26
45
  end
27
46
  end
28
47
  end
@@ -6,8 +6,7 @@ module ActiveRecord
6
6
  value = exec_insert(sql, name, binds, pk, sequence_name)
7
7
 
8
8
  return id_value if id_value
9
-
10
- if pk.is_a?(Array) && !value.empty?
9
+ if pk.is_a?(Array) && value.respond_to?(:empty?) && !value.empty?
11
10
  # This is a CPK model and the query result is not empty. Thus we can figure out the new ids for each
12
11
  # auto incremented field
13
12
  pk.map {|key| value.first[key]}
@@ -18,23 +18,21 @@ module CompositePrimaryKeys
18
18
  end
19
19
 
20
20
  order_query.flat_map do |o|
21
- order_query.flat_map do |o|
22
- case o
23
- when Arel::Attribute
24
- o.desc
25
- when Arel::Nodes::Ordering
26
- o.reverse
27
- when String
28
- if does_not_support_reverse?(o)
29
- raise IrreversibleOrderError, "Order #{o.inspect} can not be reversed automatically"
30
- end
31
- o.split(",").map! do |s|
32
- s.strip!
33
- s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || (s << " DESC")
34
- end
35
- else
36
- o
21
+ case o
22
+ when Arel::Attribute
23
+ o.desc
24
+ when Arel::Nodes::Ordering
25
+ o.reverse
26
+ when String
27
+ if does_not_support_reverse?(o)
28
+ raise IrreversibleOrderError, "Order #{o.inspect} can not be reversed automatically"
37
29
  end
30
+ o.split(",").map! do |s|
31
+ s.strip!
32
+ s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || (s << " DESC")
33
+ end
34
+ else
35
+ o
38
36
  end
39
37
  end
40
38
  end
@@ -1,197 +1,199 @@
1
- module ActiveRecord
2
- class Relation
3
- alias :initialize_without_cpk :initialize
4
- def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {})
5
- initialize_without_cpk(klass, table: table, predicate_builder: predicate_builder, values: values)
6
- add_cpk_support if klass && klass.composite?
7
- end
8
-
9
- alias :initialize_copy_without_cpk :initialize_copy
10
- def initialize_copy(other)
11
- initialize_copy_without_cpk(other)
12
- add_cpk_support if klass.composite?
13
- end
14
-
15
- def add_cpk_support
16
- extend CompositePrimaryKeys::CompositeRelation
17
- end
18
-
19
- def update_all(updates)
20
- raise ArgumentError, "Empty list of attributes to change" if updates.blank?
21
-
22
- if eager_loading?
23
- relation = apply_join_dependency
24
- return relation.update_all(updates)
25
- end
26
-
27
- stmt = Arel::UpdateManager.new
28
- stmt.table(arel.join_sources.empty? ? table : arel.source)
29
- stmt.key = table[primary_key]
30
-
31
- # CPK
32
- if @klass.composite?
33
- stmt = Arel::UpdateManager.new
34
- stmt.table(arel_table)
35
- cpk_subquery(stmt)
36
- else
37
- stmt.wheres = arel.constraints
38
- end
39
- stmt.take(arel.limit)
40
- stmt.offset(arel.offset)
41
- stmt.order(*arel.orders)
42
-
43
- if updates.is_a?(Hash)
44
- if klass.locking_enabled? &&
45
- !updates.key?(klass.locking_column) &&
46
- !updates.key?(klass.locking_column.to_sym)
47
- attr = table[klass.locking_column]
48
- updates[attr.name] = _increment_attribute(attr)
49
- end
50
- stmt.set _substitute_values(updates)
51
- else
52
- stmt.set Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name))
53
- end
54
-
55
- @klass.connection.update stmt, "#{@klass} Update All"
56
- end
57
-
58
- def delete_all
59
- invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method|
60
- value = @values[method]
61
- method == :distinct ? value : value&.any?
62
- end
63
- if invalid_methods.any?
64
- raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
65
- end
66
-
67
- if eager_loading?
68
- relation = apply_join_dependency
69
- return relation.delete_all
70
- end
71
-
72
- stmt = Arel::DeleteManager.new
73
- stmt.from(arel.join_sources.empty? ? table : arel.source)
74
- stmt.key = table[primary_key]
75
-
76
- # CPK
77
- if @klass.composite?
78
- stmt = Arel::DeleteManager.new
79
- stmt.from(arel_table)
80
- cpk_subquery(stmt)
81
- else
82
- stmt.wheres = arel.constraints
83
- end
84
-
85
- stmt.take(arel.limit)
86
- stmt.offset(arel.offset)
87
- stmt.order(*arel.orders)
88
-
89
- affected = @klass.connection.delete(stmt, "#{@klass} Destroy")
90
-
91
- reset
92
- affected
93
- end
94
-
95
- # CPK
96
- def cpk_subquery(stmt)
97
- # For update and delete statements we need a way to specify which records should
98
- # get updated. By default, Rails creates a nested IN subquery that uses the primary
99
- # key. Postgresql, Sqlite, MariaDb and Oracle support IN subqueries with multiple
100
- # columns but MySQL and SqlServer do not. Instead SQL server supports EXISTS queries
101
- # and MySQL supports obfuscated IN queries. Thus we need to check the type of
102
- # database adapter to decide how to proceed.
103
- if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) && connection.is_a?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
104
- cpk_mysql_subquery(stmt)
105
- elsif defined?(ActiveRecord::ConnectionAdapters::SQLServerAdapter) && connection.is_a?(ActiveRecord::ConnectionAdapters::SQLServerAdapter)
106
- cpk_exists_subquery(stmt)
107
- else
108
- cpk_in_subquery(stmt)
109
- end
110
- end
111
-
112
- # Used by postgresql, sqlite, mariadb and oracle. Example query:
113
- #
114
- # UPDATE reference_codes
115
- # SET ...
116
- # WHERE (reference_codes.reference_type_id, reference_codes.reference_code) IN
117
- # (SELECT reference_codes.reference_type_id, reference_codes.reference_code
118
- # FROM reference_codes)
119
- def cpk_in_subquery(stmt)
120
- # Setup the subquery
121
- subquery = arel.clone
122
- subquery.projections = primary_keys.map do |key|
123
- arel_table[key]
124
- end
125
-
126
- where_fields = primary_keys.map do |key|
127
- arel_table[key]
128
- end
129
- where = Arel::Nodes::Grouping.new(where_fields).in(subquery)
130
- stmt.wheres = [where]
131
- end
132
-
133
- # CPK. This is an alternative to IN subqueries. It is used by sqlserver.
134
- # Example query:
135
- #
136
- # UPDATE reference_codes
137
- # SET ...
138
- # WHERE EXISTS
139
- # (SELECT 1
140
- # FROM reference_codes cpk_child
141
- # WHERE reference_codes.reference_type_id = cpk_child.reference_type_id AND
142
- # reference_codes.reference_code = cpk_child.reference_code)
143
- def cpk_exists_subquery(stmt)
144
- arel_attributes = primary_keys.map do |key|
145
- table[key]
146
- end.to_composite_keys
147
-
148
- # Clone the query
149
- subselect = arel.clone
150
-
151
- # Alias the table - we assume just one table
152
- aliased_table = subselect.froms.first
153
- aliased_table.table_alias = "cpk_child"
154
-
155
- # Project - really we could just set this to "1"
156
- subselect.projections = arel_attributes
157
-
158
- # Setup correlation to the outer query via where clauses
159
- primary_keys.map do |key|
160
- outer_attribute = arel_table[key]
161
- inner_attribute = aliased_table[key]
162
- where = outer_attribute.eq(inner_attribute)
163
- subselect.where(where)
164
- end
165
- stmt.wheres = [Arel::Nodes::Exists.new(subselect)]
166
- end
167
-
168
- # CPK. This is the old way CPK created subqueries and is used by MySql.
169
- # MySQL does not support referencing the same table that is being UPDATEd or
170
- # DELETEd in a subquery so we obfuscate it. The ugly query looks like this:
171
- #
172
- # UPDATE `reference_codes`
173
- # SET ...
174
- # WHERE (reference_codes.reference_type_id, reference_codes.reference_code) IN
175
- # (SELECT reference_type_id,reference_code
176
- # FROM (SELECT DISTINCT `reference_codes`.`reference_type_id`, `reference_codes`.`reference_code`
177
- # FROM `reference_codes`) __active_record_temp)
178
- def cpk_mysql_subquery(stmt)
179
- arel_attributes = primary_keys.map do |key|
180
- table[key]
181
- end.to_composite_keys
182
-
183
- subselect = arel.clone
184
- subselect.projections = arel_attributes
185
-
186
- # Materialize subquery by adding distinct
187
- # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
188
- subselect.distinct unless arel.limit || arel.offset || arel.orders.any?
189
-
190
- key_name = arel_attributes.map(&:name).join(',')
191
-
192
- manager = Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key_name))
193
-
194
- stmt.wheres = [Arel::Nodes::In.new(arel_attributes, manager.ast)]
195
- end
196
- end
197
- end
1
+ module ActiveRecord
2
+ class Relation
3
+ alias :initialize_without_cpk :initialize
4
+ def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {})
5
+ initialize_without_cpk(klass, table: table, predicate_builder: predicate_builder, values: values)
6
+ add_cpk_support if klass && klass.composite?
7
+ end
8
+
9
+ alias :initialize_copy_without_cpk :initialize_copy
10
+ def initialize_copy(other)
11
+ initialize_copy_without_cpk(other)
12
+ add_cpk_support if klass.composite?
13
+ end
14
+
15
+ def add_cpk_support
16
+ extend CompositePrimaryKeys::CompositeRelation
17
+ end
18
+
19
+ def update_all(updates)
20
+ raise ArgumentError, "Empty list of attributes to change" if updates.blank?
21
+
22
+ if eager_loading?
23
+ relation = apply_join_dependency
24
+ return relation.update_all(updates)
25
+ end
26
+
27
+ stmt = Arel::UpdateManager.new
28
+ stmt.table(arel.join_sources.empty? ? table : arel.source)
29
+ stmt.key = table[primary_key]
30
+
31
+ # CPK
32
+ if @klass.composite?
33
+ stmt = Arel::UpdateManager.new
34
+ stmt.table(arel_table)
35
+ cpk_subquery(stmt)
36
+ else
37
+ stmt.wheres = arel.constraints
38
+ end
39
+ stmt.take(arel.limit)
40
+ stmt.offset(arel.offset)
41
+ stmt.order(*arel.orders)
42
+
43
+ if updates.is_a?(Hash)
44
+ if klass.locking_enabled? &&
45
+ !updates.key?(klass.locking_column) &&
46
+ !updates.key?(klass.locking_column.to_sym)
47
+ attr = table[klass.locking_column]
48
+ updates[attr.name] = _increment_attribute(attr)
49
+ end
50
+ stmt.set _substitute_values(updates)
51
+ else
52
+ stmt.set Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name))
53
+ end
54
+
55
+ @klass.connection.update stmt, "#{@klass} Update All"
56
+ end
57
+
58
+ def delete_all
59
+ invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method|
60
+ value = @values[method]
61
+ method == :distinct ? value : value&.any?
62
+ end
63
+ if invalid_methods.any?
64
+ raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
65
+ end
66
+
67
+ if eager_loading?
68
+ relation = apply_join_dependency
69
+ return relation.delete_all
70
+ end
71
+
72
+ stmt = Arel::DeleteManager.new
73
+ stmt.from(arel.join_sources.empty? ? table : arel.source)
74
+ stmt.key = table[primary_key]
75
+
76
+ # CPK
77
+ if @klass.composite?
78
+ stmt = Arel::DeleteManager.new
79
+ stmt.from(arel_table)
80
+ cpk_subquery(stmt)
81
+ else
82
+ stmt.wheres = arel.constraints
83
+ end
84
+
85
+ stmt.take(arel.limit)
86
+ stmt.offset(arel.offset)
87
+ stmt.order(*arel.orders)
88
+
89
+ affected = @klass.connection.delete(stmt, "#{@klass} Destroy")
90
+
91
+ reset
92
+ affected
93
+ end
94
+
95
+ # CPK
96
+ def cpk_subquery(stmt)
97
+ # For update and delete statements we need a way to specify which records should
98
+ # get updated. By default, Rails creates a nested IN subquery that uses the primary
99
+ # key. Postgresql, Sqlite, MariaDb and Oracle support IN subqueries with multiple
100
+ # columns but MySQL and SqlServer do not. Instead SQL server supports EXISTS queries
101
+ # and MySQL supports obfuscated IN queries. Thus we need to check the type of
102
+ # database adapter to decide how to proceed.
103
+ if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) && connection.is_a?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
104
+ cpk_mysql_subquery(stmt)
105
+ elsif defined?(ActiveRecord::ConnectionAdapters::TrilogyAdapter) && connection.is_a?(ActiveRecord::ConnectionAdapters::TrilogyAdapter)
106
+ cpk_mysql_subquery(stmt)
107
+ elsif defined?(ActiveRecord::ConnectionAdapters::SQLServerAdapter) && connection.is_a?(ActiveRecord::ConnectionAdapters::SQLServerAdapter)
108
+ cpk_exists_subquery(stmt)
109
+ else
110
+ cpk_in_subquery(stmt)
111
+ end
112
+ end
113
+
114
+ # Used by postgresql, sqlite, mariadb and oracle. Example query:
115
+ #
116
+ # UPDATE reference_codes
117
+ # SET ...
118
+ # WHERE (reference_codes.reference_type_id, reference_codes.reference_code) IN
119
+ # (SELECT reference_codes.reference_type_id, reference_codes.reference_code
120
+ # FROM reference_codes)
121
+ def cpk_in_subquery(stmt)
122
+ # Setup the subquery
123
+ subquery = arel.clone
124
+ subquery.projections = primary_keys.map do |key|
125
+ arel_table[key]
126
+ end
127
+
128
+ where_fields = primary_keys.map do |key|
129
+ arel_table[key]
130
+ end
131
+ where = Arel::Nodes::Grouping.new(where_fields).in(subquery)
132
+ stmt.wheres = [where]
133
+ end
134
+
135
+ # CPK. This is an alternative to IN subqueries. It is used by sqlserver.
136
+ # Example query:
137
+ #
138
+ # UPDATE reference_codes
139
+ # SET ...
140
+ # WHERE EXISTS
141
+ # (SELECT 1
142
+ # FROM reference_codes cpk_child
143
+ # WHERE reference_codes.reference_type_id = cpk_child.reference_type_id AND
144
+ # reference_codes.reference_code = cpk_child.reference_code)
145
+ def cpk_exists_subquery(stmt)
146
+ arel_attributes = primary_keys.map do |key|
147
+ table[key]
148
+ end.to_composite_keys
149
+
150
+ # Clone the query
151
+ subselect = arel.clone
152
+
153
+ # Alias the table - we assume just one table
154
+ aliased_table = subselect.froms.first
155
+ aliased_table.table_alias = "cpk_child"
156
+
157
+ # Project - really we could just set this to "1"
158
+ subselect.projections = arel_attributes
159
+
160
+ # Setup correlation to the outer query via where clauses
161
+ primary_keys.map do |key|
162
+ outer_attribute = arel_table[key]
163
+ inner_attribute = aliased_table[key]
164
+ where = outer_attribute.eq(inner_attribute)
165
+ subselect.where(where)
166
+ end
167
+ stmt.wheres = [Arel::Nodes::Exists.new(subselect)]
168
+ end
169
+
170
+ # CPK. This is the old way CPK created subqueries and is used by MySql.
171
+ # MySQL does not support referencing the same table that is being UPDATEd or
172
+ # DELETEd in a subquery so we obfuscate it. The ugly query looks like this:
173
+ #
174
+ # UPDATE `reference_codes`
175
+ # SET ...
176
+ # WHERE (reference_codes.reference_type_id, reference_codes.reference_code) IN
177
+ # (SELECT reference_type_id,reference_code
178
+ # FROM (SELECT DISTINCT `reference_codes`.`reference_type_id`, `reference_codes`.`reference_code`
179
+ # FROM `reference_codes`) __active_record_temp)
180
+ def cpk_mysql_subquery(stmt)
181
+ arel_attributes = primary_keys.map do |key|
182
+ table[key]
183
+ end.to_composite_keys
184
+
185
+ subselect = arel.clone
186
+ subselect.projections = arel_attributes
187
+
188
+ # Materialize subquery by adding distinct
189
+ # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
190
+ subselect.distinct unless arel.limit || arel.offset || arel.orders.any?
191
+
192
+ key_name = arel_attributes.map(&:name).join(',')
193
+
194
+ manager = Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key_name))
195
+
196
+ stmt.wheres = [Arel::Nodes::In.new(arel_attributes, manager.ast)]
197
+ end
198
+ end
199
+ end
@@ -2,7 +2,7 @@ module CompositePrimaryKeys
2
2
  module VERSION
3
3
  MAJOR = 14
4
4
  MINOR = 0
5
- TINY = 6
5
+ TINY = 7
6
6
  STRING = [MAJOR, MINOR, TINY].join('.')
7
7
  end
8
8
  end