composite_primary_keys 12.0.9 → 13.0.0

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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/History.rdoc +883 -877
  3. data/README.rdoc +181 -180
  4. data/lib/composite_primary_keys.rb +119 -117
  5. data/lib/composite_primary_keys/active_model/attribute_assignment.rb +19 -19
  6. data/lib/composite_primary_keys/associations/association_scope.rb +66 -68
  7. data/lib/composite_primary_keys/associations/join_dependency.rb +118 -103
  8. data/lib/composite_primary_keys/attribute_methods.rb +21 -9
  9. data/lib/composite_primary_keys/attribute_methods/primary_key.rb +0 -2
  10. data/lib/composite_primary_keys/attribute_methods/read.rb +30 -30
  11. data/lib/composite_primary_keys/attribute_methods/write.rb +35 -35
  12. data/lib/composite_primary_keys/base.rb +141 -141
  13. data/lib/composite_primary_keys/connection_adapters/abstract/database_statements.rb +37 -37
  14. data/lib/composite_primary_keys/connection_adapters/sqlserver/database_statements.rb +44 -44
  15. data/lib/composite_primary_keys/core.rb +48 -48
  16. data/lib/composite_primary_keys/nested_attributes.rb +1 -1
  17. data/lib/composite_primary_keys/persistence.rb +82 -81
  18. data/lib/composite_primary_keys/reflection.rb +91 -29
  19. data/lib/composite_primary_keys/relation.rb +197 -193
  20. data/lib/composite_primary_keys/relation/batches.rb +15 -7
  21. data/lib/composite_primary_keys/relation/calculations.rb +104 -81
  22. data/lib/composite_primary_keys/relation/finder_methods.rb +235 -235
  23. data/lib/composite_primary_keys/relation/predicate_builder/association_query_value.rb +39 -20
  24. data/lib/composite_primary_keys/relation/query_methods.rb +42 -42
  25. data/lib/composite_primary_keys/relation/where_clause.rb +18 -23
  26. data/lib/composite_primary_keys/table_metadata.rb +11 -0
  27. data/lib/composite_primary_keys/version.rb +8 -8
  28. data/test/abstract_unit.rb +114 -114
  29. data/test/connections/databases.ci.yml +22 -22
  30. data/test/fixtures/db_definitions/db2-create-tables.sql +112 -112
  31. data/test/fixtures/db_definitions/db2-drop-tables.sql +16 -16
  32. data/test/fixtures/db_definitions/mysql.sql +180 -180
  33. data/test/fixtures/db_definitions/oracle.drop.sql +41 -41
  34. data/test/fixtures/db_definitions/oracle.sql +199 -199
  35. data/test/fixtures/db_definitions/postgresql.sql +182 -182
  36. data/test/fixtures/db_definitions/sqlite.sql +169 -169
  37. data/test/fixtures/db_definitions/sqlserver.sql +176 -176
  38. data/test/fixtures/department.rb +16 -16
  39. data/test/fixtures/departments.yml +19 -15
  40. data/test/fixtures/employees.yml +33 -28
  41. data/test/fixtures/restaurants_suburbs.yml +10 -10
  42. data/test/fixtures/streets.yml +16 -16
  43. data/test/fixtures/suburbs.yml +14 -14
  44. data/test/fixtures/user.rb +11 -11
  45. data/test/test_associations.rb +364 -358
  46. data/test/test_attributes.rb +75 -60
  47. data/test/test_calculations.rb +49 -42
  48. data/test/test_create.rb +218 -206
  49. data/test/test_delete.rb +182 -179
  50. data/test/test_exists.rb +39 -39
  51. data/test/test_find.rb +170 -164
  52. data/test/test_ids.rb +112 -112
  53. data/test/test_nested_attributes.rb +67 -67
  54. data/test/test_update.rb +96 -96
  55. metadata +12 -11
@@ -1,49 +1,49 @@
1
- module ActiveRecord
2
- module Core
3
- def initialize_dup(other) # :nodoc:
4
- @attributes = @attributes.deep_dup
5
- # CPK
6
- #@attributes.reset(@primary_key)
7
- Array(self.class.primary_key).each {|key| @attributes.reset(key)}
8
-
9
- _run_initialize_callbacks
10
-
11
- @new_record = true
12
- @destroyed = false
13
- @_start_transaction_state = nil
14
- @transaction_state = nil
15
-
16
- super
17
- end
18
-
19
- module ClassMethods
20
- def find(*ids) # :nodoc:
21
- # We don't have cache keys for this stuff yet
22
- return super unless ids.length == 1
23
- return super if block_given? ||
24
- primary_key.nil? ||
25
- scope_attributes? ||
26
- columns_hash.key?(inheritance_column) && !base_class?
27
-
28
- # CPK
29
- return super if self.composite?
30
-
31
- id = ids.first
32
-
33
- return super if StatementCache.unsupported_value?(id)
34
-
35
- key = primary_key
36
-
37
- statement = cached_find_by_statement(key) { |params|
38
- where(key => params.bind).limit(1)
39
- }
40
-
41
- record = statement.execute([id], connection)&.first
42
- unless record
43
- raise ::ActiveRecord::RecordNotFound.new("Couldn't find #{name} with '#{key}'=#{id}", name, key, id)
44
- end
45
- record
46
- end
47
- end
48
- end
1
+ module ActiveRecord
2
+ module Core
3
+ def initialize_dup(other) # :nodoc:
4
+ @attributes = @attributes.deep_dup
5
+ # CPK
6
+ #@attributes.reset(@primary_key)
7
+ Array(self.class.primary_key).each {|key| @attributes.reset(key)}
8
+
9
+ _run_initialize_callbacks
10
+
11
+ @new_record = true
12
+ @destroyed = false
13
+ @_start_transaction_state = nil
14
+ @transaction_state = nil
15
+
16
+ super
17
+ end
18
+
19
+ module ClassMethods
20
+ def find(*ids) # :nodoc:
21
+ # We don't have cache keys for this stuff yet
22
+ return super unless ids.length == 1
23
+ return super if block_given? ||
24
+ primary_key.nil? ||
25
+ scope_attributes? ||
26
+ columns_hash.key?(inheritance_column) && !base_class?
27
+
28
+ # CPK
29
+ return super if self.composite?
30
+
31
+ id = ids.first
32
+
33
+ return super if StatementCache.unsupported_value?(id)
34
+
35
+ key = primary_key
36
+
37
+ statement = cached_find_by_statement(key) { |params|
38
+ where(key => params.bind).limit(1)
39
+ }
40
+
41
+ record = statement.execute([id], connection)&.first
42
+ unless record
43
+ raise ::ActiveRecord::RecordNotFound.new("Couldn't find #{name} with '#{key}'=#{id}", name, key, id)
44
+ end
45
+ record
46
+ end
47
+ end
48
+ end
49
49
  end
@@ -70,7 +70,7 @@ module ActiveRecord
70
70
  if target_record
71
71
  existing_record = target_record
72
72
  else
73
- association.add_to_target(existing_record, :skip_callbacks)
73
+ association.add_to_target(existing_record, skip_callbacks: true)
74
74
  end
75
75
 
76
76
  assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
@@ -1,81 +1,82 @@
1
- module ActiveRecord
2
- module Persistence
3
- module ClassMethods
4
- def delete(id_or_array)
5
- # CPK
6
- if self.composite?
7
- id_or_array = if id_or_array.is_a?(CompositePrimaryKeys::CompositeKeys)
8
- [id_or_array]
9
- else
10
- Array(id_or_array)
11
- end
12
-
13
- id_or_array.each do |id|
14
- # Is the passed in id actually a record?
15
- id = id.kind_of?(::ActiveRecord::Base) ? id.id : id
16
- delete_by(cpk_id_predicate(self.arel_table, self.primary_key, id))
17
- end
18
- else
19
- delete_by(primary_key => id_or_array)
20
- end
21
- end
22
-
23
- def _update_record(values, constraints) # :nodoc:
24
- # CPK
25
- if self.composite? && constraints[primary_key]
26
- primary_key_values = constraints.delete(primary_key)
27
- primary_key.each_with_index do |key, i|
28
- constraints[key] = primary_key_values[i]
29
- end
30
- end
31
-
32
- constraints = _substitute_values(constraints).map { |attr, bind| attr.eq(bind) }
33
-
34
- um = arel_table.where(
35
- constraints.reduce(&:and)
36
- ).compile_update(_substitute_values(values), primary_key)
37
-
38
- connection.update(um, "#{self} Update")
39
- end
40
-
41
- def _delete_record(constraints) # :nodoc:
42
- # CPK
43
- if self.composite? && constraints[primary_key]
44
- primary_key_values = constraints.delete(primary_key)
45
- primary_key.each_with_index do |key, i|
46
- constraints[key] = primary_key_values[i]
47
- end
48
- end
49
-
50
- constraints = _substitute_values(constraints).map { |attr, bind| attr.eq(bind) }
51
-
52
- dm = Arel::DeleteManager.new
53
- dm.from(arel_table)
54
- dm.wheres = constraints
55
-
56
- connection.delete(dm, "#{self} Destroy")
57
- end
58
- end
59
-
60
- def _create_record(attribute_names = self.attribute_names)
61
- attribute_names = attributes_for_create(attribute_names)
62
-
63
- new_id = self.class._insert_record(
64
- attributes_with_values(attribute_names)
65
- )
66
-
67
- # CPK
68
- if self.composite?
69
- self.id = self.id.zip(Array(new_id)).map {|id1, id2| id2.nil? ? id1 : id2}
70
- else
71
- self.id ||= new_id if self.class.primary_key
72
- end
73
-
74
- @new_record = false
75
-
76
- yield(self) if block_given?
77
-
78
- id
79
- end
80
- end
81
- end
1
+ module ActiveRecord
2
+ module Persistence
3
+ module ClassMethods
4
+ def delete(id_or_array)
5
+ # CPK
6
+ if self.composite?
7
+ id_or_array = if id_or_array.is_a?(CompositePrimaryKeys::CompositeKeys)
8
+ [id_or_array]
9
+ else
10
+ Array(id_or_array)
11
+ end
12
+
13
+ # Delete should return the number of deleted records
14
+ id_or_array.map do |id|
15
+ # Is the passed in id actually a record?
16
+ id = id.kind_of?(::ActiveRecord::Base) ? id.id : id
17
+ delete_by(cpk_id_predicate(self.arel_table, self.primary_key, id))
18
+ end.sum
19
+ else
20
+ delete_by(primary_key => id_or_array)
21
+ end
22
+ end
23
+
24
+ def _update_record(values, constraints) # :nodoc:
25
+ # CPK
26
+ if self.composite? && constraints[primary_key]
27
+ primary_key_values = constraints.delete(primary_key)
28
+ primary_key.each_with_index do |key, i|
29
+ constraints[key] = primary_key_values[i]
30
+ end
31
+ end
32
+
33
+ constraints = _substitute_values(constraints).map { |attr, bind| attr.eq(bind) }
34
+
35
+ um = arel_table.where(
36
+ constraints.reduce(&:and)
37
+ ).compile_update(_substitute_values(values), primary_key)
38
+
39
+ connection.update(um, "#{self} Update")
40
+ end
41
+
42
+ def _delete_record(constraints) # :nodoc:
43
+ # CPK
44
+ if self.composite? && constraints[primary_key]
45
+ primary_key_values = constraints.delete(primary_key)
46
+ primary_key.each_with_index do |key, i|
47
+ constraints[key] = primary_key_values[i]
48
+ end
49
+ end
50
+
51
+ constraints = _substitute_values(constraints).map { |attr, bind| attr.eq(bind) }
52
+
53
+ dm = Arel::DeleteManager.new
54
+ dm.from(arel_table)
55
+ dm.wheres = constraints
56
+
57
+ connection.delete(dm, "#{self} Destroy")
58
+ end
59
+ end
60
+
61
+ def _create_record(attribute_names = self.attribute_names)
62
+ attribute_names = attributes_for_create(attribute_names)
63
+
64
+ new_id = self.class._insert_record(
65
+ attributes_with_values(attribute_names)
66
+ )
67
+
68
+ # CPK
69
+ if self.composite?
70
+ self.id = self.id.zip(Array(new_id)).map {|id1, id2| id2.nil? ? id1 : id2}
71
+ else
72
+ self.id ||= new_id if self.class.primary_key
73
+ end
74
+
75
+ @new_record = false
76
+
77
+ yield(self) if block_given?
78
+
79
+ id
80
+ end
81
+ end
82
+ end
@@ -1,29 +1,91 @@
1
- module ActiveRecord
2
- module Reflection
3
- class AbstractReflection
4
- def join_scope(table, foreign_table, foreign_klass)
5
- predicate_builder = predicate_builder(table)
6
- scope_chain_items = join_scopes(table, predicate_builder)
7
- klass_scope = klass_join_scope(table, predicate_builder)
8
-
9
- key = join_keys.key
10
- foreign_key = join_keys.foreign_key
11
-
12
- # CPK
13
- #klass_scope.where!(table[key].eq(foreign_table[foreign_key]))
14
- constraint = cpk_join_predicate(table, key, foreign_table, foreign_key)
15
- klass_scope.where!(constraint)
16
-
17
- if type
18
- klass_scope.where!(type => foreign_klass.polymorphic_name)
19
- end
20
-
21
- if klass.finder_needs_type_condition?
22
- klass_scope.where!(klass.send(:type_condition, table))
23
- end
24
-
25
- scope_chain_items.inject(klass_scope, &:merge!)
26
- end
27
- end
28
- end
29
- end
1
+ module ActiveRecord
2
+ module Reflection
3
+ class AbstractReflection
4
+ def join_scope(table, foreign_table, foreign_klass)
5
+ predicate_builder = predicate_builder(table)
6
+ scope_chain_items = join_scopes(table, predicate_builder)
7
+ klass_scope = klass_join_scope(table, predicate_builder)
8
+
9
+ key = join_primary_key
10
+ foreign_key = join_foreign_key
11
+
12
+ # CPK
13
+ #klass_scope.where!(table[key].eq(foreign_table[foreign_key]))
14
+ constraint = cpk_join_predicate(table, key, foreign_table, foreign_key)
15
+ klass_scope.where!(constraint)
16
+
17
+ if type
18
+ klass_scope.where!(type => foreign_klass.polymorphic_name)
19
+ end
20
+
21
+ if klass.finder_needs_type_condition?
22
+ klass_scope.where!(klass.send(:type_condition, table))
23
+ end
24
+
25
+ scope_chain_items.inject(klass_scope, &:merge!)
26
+ end
27
+ end
28
+
29
+ class AssociationReflection < MacroReflection
30
+ def foreign_key
31
+ # CPK
32
+ # @foreign_key ||= -(options[:foreign_key]&.to_s || derive_foreign_key)
33
+ @foreign_key ||= extract_keys(options[:foreign_key]) || derive_foreign_key
34
+ end
35
+
36
+ def association_foreign_key
37
+ # CPK
38
+ # @association_foreign_key ||= -(options[:association_foreign_key]&.to_s || class_name.foreign_key)
39
+ @association_foreign_key ||= extract_keys(options[:association_foreign_key]) || class_name.foreign_key
40
+ end
41
+
42
+ def active_record_primary_key
43
+ # CPK (Rails freezes the string returned in the expression that calculates PK here. But Rails uses the `-` method which is not available on Array for CPK, so we calculate it in one line and freeze it on the next)
44
+ # @active_record_primary_key ||= -(options[:primary_key]&.to_s || primary_key(active_record))
45
+ @active_record_primary_key ||= begin
46
+ pk = options[:primary_key] || primary_key(active_record)
47
+ pk.freeze
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def extract_keys(keys)
54
+ case keys
55
+ when Array
56
+ keys.map { |k| k.to_s }
57
+ when NilClass
58
+ nil
59
+ else
60
+ keys.to_s
61
+ end
62
+ end
63
+ end
64
+
65
+ class BelongsToReflection < AssociationReflection
66
+ def association_primary_key(klass = nil)
67
+ if primary_key = options[:primary_key]
68
+ # CPK
69
+ # @association_primary_key ||= -primary_key.to_s
70
+ @association_primary_key ||= primary_key.freeze
71
+ else
72
+ primary_key(klass || self.klass)
73
+ end
74
+ end
75
+ end
76
+
77
+ class ThroughReflection < AbstractReflection #:nodoc:
78
+ def association_primary_key(klass = nil)
79
+ # Get the "actual" source reflection if the immediate source reflection has a
80
+ # source reflection itself
81
+ if primary_key = actual_source_reflection.options[:primary_key]
82
+ # CPK
83
+ # @association_primary_key ||= -primary_key.to_s
84
+ @association_primary_key ||= primary_key.freeze
85
+ else
86
+ primary_key(klass || self.klass)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -1,193 +1,197 @@
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
- # CPK
29
- if @klass.composite?
30
- stmt.table(arel_table)
31
- cpk_subquery(stmt)
32
- else
33
- stmt.table(arel.join_sources.empty? ? table : arel.source)
34
- stmt.key = arel_attribute(primary_key)
35
- stmt.wheres = arel.constraints
36
- end
37
- stmt.take(arel.limit)
38
- stmt.offset(arel.offset)
39
- stmt.order(*arel.orders)
40
-
41
- if updates.is_a?(Hash)
42
- if klass.locking_enabled? &&
43
- !updates.key?(klass.locking_column) &&
44
- !updates.key?(klass.locking_column.to_sym)
45
- attr = arel_attribute(klass.locking_column)
46
- updates[attr.name] = _increment_attribute(attr)
47
- end
48
- stmt.set _substitute_values(updates)
49
- else
50
- stmt.set Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name))
51
- end
52
-
53
- @klass.connection.update stmt, "#{@klass} Update All"
54
- end
55
-
56
- def delete_all
57
- invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method|
58
- value = @values[method]
59
- method == :distinct ? value : value&.any?
60
- end
61
- if invalid_methods.any?
62
- raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
63
- end
64
-
65
- if eager_loading?
66
- relation = apply_join_dependency
67
- return relation.delete_all
68
- end
69
-
70
- stmt = Arel::DeleteManager.new
71
-
72
- if @klass.composite?
73
- stmt.from(arel_table)
74
- cpk_subquery(stmt)
75
- else
76
- stmt.from(arel.join_sources.empty? ? table : arel.source)
77
- stmt.key = arel_attribute(primary_key)
78
- stmt.wheres = arel.constraints
79
- end
80
-
81
- stmt.take(arel.limit)
82
- stmt.offset(arel.offset)
83
- stmt.order(*arel.orders)
84
-
85
- affected = @klass.connection.delete(stmt, "#{@klass} Destroy")
86
-
87
- reset
88
- affected
89
- end
90
-
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)
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
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)]
191
- end
192
- end
193
- 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? && stmt.to_sql =~ /['"]#{primary_key.to_s}['"]/
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? && stmt.to_sql =~ /['"]#{primary_key.to_s}['"]/
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