composite_primary_keys 14.0.9 → 14.0.10

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/History.rdoc +10 -0
  3. data/README.rdoc +182 -182
  4. data/Rakefile +37 -37
  5. data/lib/composite_primary_keys/associations/collection_association.rb +38 -38
  6. data/lib/composite_primary_keys/associations/preloader/association.rb +52 -52
  7. data/lib/composite_primary_keys/autosave_association.rb +60 -60
  8. data/lib/composite_primary_keys/composite_arrays.rb +88 -88
  9. data/lib/composite_primary_keys/composite_predicates.rb +121 -121
  10. data/lib/composite_primary_keys/connection_adapters/abstract/database_statements.rb +36 -36
  11. data/lib/composite_primary_keys/core.rb +71 -48
  12. data/lib/composite_primary_keys/persistence.rb +96 -96
  13. data/lib/composite_primary_keys/reflection.rb +93 -91
  14. data/lib/composite_primary_keys/relation/calculations.rb +110 -110
  15. data/lib/composite_primary_keys/relation/query_methods.rb +40 -40
  16. data/lib/composite_primary_keys/relation.rb +199 -199
  17. data/lib/composite_primary_keys/validations/uniqueness.rb +40 -40
  18. data/lib/composite_primary_keys/version.rb +1 -1
  19. data/lib/composite_primary_keys.rb +117 -117
  20. data/scripts/console.rb +48 -48
  21. data/tasks/databases/trilogy.rake +23 -23
  22. data/test/abstract_unit.rb +124 -124
  23. data/test/connections/databases.ci.yml +32 -32
  24. data/test/fixtures/admin.rb +4 -4
  25. data/test/fixtures/db_definitions/db2-create-tables.sql +146 -146
  26. data/test/fixtures/db_definitions/db2-drop-tables.sql +23 -23
  27. data/test/fixtures/db_definitions/mysql.sql +203 -203
  28. data/test/fixtures/db_definitions/oracle.drop.sql +45 -45
  29. data/test/fixtures/db_definitions/oracle.sql +220 -220
  30. data/test/fixtures/db_definitions/postgresql.sql +205 -205
  31. data/test/fixtures/db_definitions/sqlite.sql +190 -190
  32. data/test/fixtures/db_definitions/sqlserver.sql +199 -199
  33. data/test/fixtures/department.rb +20 -20
  34. data/test/fixtures/moderator.rb +4 -4
  35. data/test/fixtures/room.rb +14 -14
  36. data/test/fixtures/room_assignment.rb +18 -18
  37. data/test/fixtures/staff_room.rb +6 -6
  38. data/test/fixtures/staff_room_key.rb +6 -6
  39. data/test/fixtures/user.rb +14 -14
  40. data/test/test_associations.rb +403 -403
  41. data/test/test_composite_arrays.rb +44 -44
  42. data/test/test_equal.rb +55 -26
  43. data/test/test_has_one_through.rb +30 -30
  44. data/test/test_hash.rb +73 -0
  45. data/test/test_nested_attributes.rb +90 -90
  46. metadata +7 -8
@@ -1,199 +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::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
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
@@ -1,40 +1,40 @@
1
- module ActiveRecord
2
- module Validations
3
- class UniquenessValidator
4
- def validate_each(record, attribute, value)
5
- finder_class = find_finder_class_for(record)
6
- value = map_enum_attribute(finder_class, attribute, value)
7
-
8
- relation = build_relation(finder_class, attribute, value)
9
- if record.persisted?
10
- # CPK
11
- if finder_class.primary_key.is_a?(Array)
12
- predicate = finder_class.cpk_id_predicate(finder_class.arel_table, finder_class.primary_key, record.id_in_database || record.id)
13
- relation = relation.where.not(predicate)
14
- elsif finder_class.primary_key
15
- relation = relation.where.not(finder_class.primary_key => record.id_in_database)
16
- else
17
- raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.")
18
- end
19
- end
20
- relation = scope_relation(record, relation)
21
- if options[:conditions]
22
- conditions = options[:conditions]
23
-
24
- relation = if conditions.arity.zero?
25
- relation.instance_exec(&conditions)
26
- else
27
- relation.instance_exec(record, &conditions)
28
- end
29
- end
30
-
31
- if relation.exists?
32
- error_options = options.except(:case_sensitive, :scope, :conditions)
33
- error_options[:value] = value
34
-
35
- record.errors.add(attribute, :taken, **error_options)
36
- end
37
- end
38
- end
39
- end
40
- end
1
+ module ActiveRecord
2
+ module Validations
3
+ class UniquenessValidator
4
+ def validate_each(record, attribute, value)
5
+ finder_class = find_finder_class_for(record)
6
+ value = map_enum_attribute(finder_class, attribute, value)
7
+
8
+ relation = build_relation(finder_class, attribute, value)
9
+ if record.persisted?
10
+ # CPK
11
+ if finder_class.primary_key.is_a?(Array)
12
+ predicate = finder_class.cpk_id_predicate(finder_class.arel_table, finder_class.primary_key, record.id_in_database || record.id)
13
+ relation = relation.where.not(predicate)
14
+ elsif finder_class.primary_key
15
+ relation = relation.where.not(finder_class.primary_key => record.id_in_database)
16
+ else
17
+ raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.")
18
+ end
19
+ end
20
+ relation = scope_relation(record, relation)
21
+ if options[:conditions]
22
+ conditions = options[:conditions]
23
+
24
+ relation = if conditions.arity.zero?
25
+ relation.instance_exec(&conditions)
26
+ else
27
+ relation.instance_exec(record, &conditions)
28
+ end
29
+ end
30
+
31
+ if relation.exists?
32
+ error_options = options.except(:case_sensitive, :scope, :conditions)
33
+ error_options[:value] = value
34
+
35
+ record.errors.add(attribute, :taken, **error_options)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -2,7 +2,7 @@ module CompositePrimaryKeys
2
2
  module VERSION
3
3
  MAJOR = 14
4
4
  MINOR = 0
5
- TINY = 9
5
+ TINY = 10
6
6
  STRING = [MAJOR, MINOR, TINY].join('.')
7
7
  end
8
8
  end