composite_primary_keys 14.0.9 → 14.0.10

Sign up to get free protection for your applications and to get access to all the features.
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