composite_primary_keys 12.0.8 → 13.0.1

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/History.rdoc +888 -874
  3. data/README.rdoc +181 -180
  4. data/lib/composite_primary_keys/active_model/attribute_assignment.rb +19 -19
  5. data/lib/composite_primary_keys/associations/association_scope.rb +66 -68
  6. data/lib/composite_primary_keys/associations/join_dependency.rb +137 -103
  7. data/lib/composite_primary_keys/attribute_methods/primary_key.rb +0 -2
  8. data/lib/composite_primary_keys/attribute_methods/read.rb +30 -30
  9. data/lib/composite_primary_keys/attribute_methods/write.rb +35 -35
  10. data/lib/composite_primary_keys/attribute_methods.rb +21 -9
  11. data/lib/composite_primary_keys/base.rb +141 -141
  12. data/lib/composite_primary_keys/composite_predicates.rb +2 -1
  13. data/lib/composite_primary_keys/connection_adapters/abstract/database_statements.rb +37 -22
  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/batches.rb +15 -7
  20. data/lib/composite_primary_keys/relation/calculations.rb +104 -81
  21. data/lib/composite_primary_keys/relation/finder_methods.rb +235 -235
  22. data/lib/composite_primary_keys/relation/predicate_builder/association_query_value.rb +39 -20
  23. data/lib/composite_primary_keys/relation/query_methods.rb +42 -42
  24. data/lib/composite_primary_keys/relation/where_clause.rb +18 -23
  25. data/lib/composite_primary_keys/relation.rb +197 -193
  26. data/lib/composite_primary_keys/table_metadata.rb +11 -0
  27. data/lib/composite_primary_keys/version.rb +8 -8
  28. data/lib/composite_primary_keys.rb +119 -119
  29. data/test/abstract_unit.rb +114 -114
  30. data/test/connections/databases.ci.yml +22 -22
  31. data/test/fixtures/db_definitions/db2-create-tables.sql +112 -112
  32. data/test/fixtures/db_definitions/db2-drop-tables.sql +16 -16
  33. data/test/fixtures/db_definitions/mysql.sql +180 -180
  34. data/test/fixtures/db_definitions/oracle.drop.sql +41 -41
  35. data/test/fixtures/db_definitions/oracle.sql +199 -199
  36. data/test/fixtures/db_definitions/postgresql.sql +182 -182
  37. data/test/fixtures/db_definitions/sqlite.sql +169 -169
  38. data/test/fixtures/db_definitions/sqlserver.sql +176 -176
  39. data/test/fixtures/department.rb +16 -16
  40. data/test/fixtures/departments.yml +19 -15
  41. data/test/fixtures/employees.yml +33 -28
  42. data/test/fixtures/membership.rb +8 -6
  43. data/test/fixtures/restaurants_suburbs.yml +10 -10
  44. data/test/fixtures/streets.yml +16 -16
  45. data/test/fixtures/suburbs.yml +14 -14
  46. data/test/fixtures/user.rb +11 -11
  47. data/test/test_associations.rb +372 -358
  48. data/test/test_attributes.rb +75 -60
  49. data/test/test_calculations.rb +49 -42
  50. data/test/test_create.rb +218 -206
  51. data/test/test_delete.rb +188 -179
  52. data/test/test_exists.rb +39 -39
  53. data/test/test_find.rb +170 -164
  54. data/test/test_ids.rb +112 -112
  55. data/test/test_nested_attributes.rb +67 -67
  56. data/test/test_update.rb +102 -96
  57. metadata +6 -6
  58. data/lib/composite_primary_keys/connection_adapters/mysql/database_statements.rb +0 -24
@@ -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? && @klass.connection.visitor.compile(stmt.ast) =~ /['"]#{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? && @klass.connection.visitor.compile(stmt.ast) =~ /['"]#{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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ class TableMetadata # :nodoc:
5
+ def associated_with?(table_name)
6
+ # CPK
7
+ # klass&._reflect_on_association(table_name) || klass&._reflect_on_association(table_name.singularize)
8
+ klass&._reflect_on_association(table_name) || klass&._reflect_on_association(table_name.to_s.singularize)
9
+ end
10
+ end
11
+ end
@@ -1,8 +1,8 @@
1
- module CompositePrimaryKeys
2
- module VERSION
3
- MAJOR = 12
4
- MINOR = 0
5
- TINY = 8
6
- STRING = [MAJOR, MINOR, TINY].join('.')
7
- end
8
- end
1
+ module CompositePrimaryKeys
2
+ module VERSION
3
+ MAJOR = 13
4
+ MINOR = 0
5
+ TINY = 1
6
+ STRING = [MAJOR, MINOR, TINY].join('.')
7
+ end
8
+ end