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,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,12 +1,16 @@
1
1
  module CompositePrimaryKeys
2
2
  module ActiveRecord
3
3
  module Batches
4
- def in_batches(of: 1000, start: nil, finish: nil, load: false, error_on_ignore: nil)
4
+ def in_batches(of: 1000, start: nil, finish: nil, load: false, error_on_ignore: nil, order: :asc)
5
5
  relation = self
6
6
  unless block_given?
7
7
  return ::ActiveRecord::Batches::BatchEnumerator.new(of: of, start: start, finish: finish, relation: self)
8
8
  end
9
9
 
10
+ unless [:asc, :desc].include?(order)
11
+ raise ArgumentError, ":order must be :asc or :desc, got #{order.inspect}"
12
+ end
13
+
10
14
  if arel.orders.present?
11
15
  act_on_ignored_order(error_on_ignore)
12
16
  end
@@ -17,8 +21,8 @@ module CompositePrimaryKeys
17
21
  batch_limit = remaining if remaining < batch_limit
18
22
  end
19
23
 
20
- relation = relation.reorder(batch_order).limit(batch_limit)
21
- relation = apply_limits(relation, start, finish)
24
+ relation = relation.reorder(batch_order(order)).limit(batch_limit)
25
+ relation = apply_limits(relation, start, finish, order)
22
26
  relation.skip_query_cache! # Retaining the results in the query cache would undermine the point of batching
23
27
  batch_relation = relation
24
28
 
@@ -61,7 +65,9 @@ module CompositePrimaryKeys
61
65
  end
62
66
 
63
67
  # CPK
64
- # batch_relation = relation.where(arel_attribute(primary_key).gt(primary_key_offset))
68
+ #batch_relation = relation.where(
69
+ # predicate_builder[primary_key, primary_key_offset, order == :desc ? :lt : :gt]
70
+ #)
65
71
  batch_relation = if composite?
66
72
  # CPK
67
73
  # Lexicographically select records
@@ -81,7 +87,9 @@ module CompositePrimaryKeys
81
87
  end.reduce(:or)
82
88
  relation.where(query)
83
89
  else
84
- relation.where(arel_attribute(primary_key).gt(primary_key_offset))
90
+ batch_relation = relation.where(
91
+ predicate_builder[primary_key, primary_key_offset, order == :desc ? :lt : :gt]
92
+ )
85
93
  end
86
94
  end
87
95
  end
@@ -95,9 +103,9 @@ module CompositePrimaryKeys
95
103
  ary.length.times.reduce([]) { |results, i| results << ary[0..i] }
96
104
  end
97
105
 
98
- def batch_order
106
+ def batch_order(order)
99
107
  self.primary_key.map do |key|
100
- arel_attribute(key).asc
108
+ table[key].public_send(order)
101
109
  end
102
110
  end
103
111
  end
@@ -1,81 +1,104 @@
1
- module CompositePrimaryKeys
2
- module ActiveRecord
3
- module Calculations
4
- def aggregate_column(column_name)
5
- # CPK
6
- if column_name.kind_of?(Array)
7
- column_name.map do |column|
8
- @klass.arel_attribute(column_name)
9
- end
10
- elsif @klass.has_attribute?(column_name) || @klass.attribute_alias?(column_name)
11
- @klass.arel_attribute(column_name)
12
- else
13
- Arel.sql(column_name == :all ? "*" : column_name.to_s)
14
- end
15
- end
16
-
17
- def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
18
- column_alias = column_name
19
-
20
- # CPK
21
- # if operation == "count" && (column_name == :all && distinct || has_limit_or_offset?)
22
- # # Shortcut when limit is zero.
23
- # return 0 if limit_value == 0
24
- #
25
- # query_builder = build_count_subquery(spawn, column_name, distinct)
26
- if operation == "count"
27
- relation = unscope(:order)
28
- query_builder = build_count_subquery(spawn, column_name, distinct)
29
- else
30
- # PostgreSQL doesn't like ORDER BY when there are no GROUP BY
31
- relation = unscope(:order).distinct!(false)
32
-
33
- column = aggregate_column(column_name)
34
-
35
- select_value = operation_over_aggregate_column(column, operation, distinct)
36
- if operation == "sum" && distinct
37
- select_value.distinct = true
38
- end
39
-
40
- column_alias = select_value.alias
41
- column_alias ||= @klass.connection.column_name_for_operation(operation, select_value)
42
- relation.select_values = [select_value]
43
-
44
- query_builder = relation.arel
45
- end
46
-
47
- result = skip_query_cache_if_necessary { @klass.connection.select_all(query_builder, nil) }
48
- row = result.first
49
- value = row && row.values.first
50
- type = result.column_types.fetch(column_alias) do
51
- type_for(column_name)
52
- end
53
-
54
- type_cast_calculated_value(value, type, operation)
55
- end
56
-
57
- def build_count_subquery(relation, column_name, distinct)
58
- if column_name == :all
59
- relation.select_values = [ Arel.sql(::ActiveRecord::FinderMethods::ONE_AS_ONE) ] unless distinct
60
- if relation.select_values.first.is_a?(Array)
61
- relation.select_values = relation.select_values.first.map do |column|
62
- Arel::Attribute.new(@klass.unscoped.table, column)
63
- end
64
- end
65
- elsif column_name.is_a?(Array)
66
- relation.select_values = column_name.map do |column|
67
- Arel::Attribute.new(@klass.unscoped.table, column)
68
- end
69
- else
70
- column_alias = Arel.sql("count_column")
71
- relation.select_values = [ aggregate_column(column_name).as(column_alias) ]
72
- end
73
-
74
- subquery = relation.arel.as(Arel.sql("subquery_for_count"))
75
- select_value = operation_over_aggregate_column(column_alias || Arel.star, "count", false)
76
-
77
- Arel::SelectManager.new(subquery).project(select_value)
78
- end
79
- end
80
- end
81
- end
1
+ module CompositePrimaryKeys
2
+ module ActiveRecord
3
+ module Calculations
4
+ def aggregate_column(column_name)
5
+ # CPK
6
+ if column_name.kind_of?(Array)
7
+ # Note: Test don't seem to run this code?
8
+ column_name.map do |column|
9
+ @klass.arel_table[column]
10
+ end
11
+ elsif @klass.has_attribute?(column_name) || @klass.attribute_alias?(column_name)
12
+ @klass.arel_table[column_name]
13
+ else
14
+ Arel.sql(column_name == :all ? "*" : column_name.to_s)
15
+ end
16
+ end
17
+
18
+ def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
19
+ column_alias = column_name
20
+
21
+ # CPK
22
+ # if operation == "count" && (column_name == :all && distinct || has_limit_or_offset?)
23
+ # # Shortcut when limit is zero.
24
+ # return 0 if limit_value == 0
25
+ #
26
+ # query_builder = build_count_subquery(spawn, column_name, distinct)
27
+ if operation == "count"
28
+ relation = unscope(:order)
29
+ query_builder = build_count_subquery(spawn, column_name, distinct)
30
+ else
31
+ # PostgreSQL doesn't like ORDER BY when there are no GROUP BY
32
+ relation = unscope(:order).distinct!(false)
33
+
34
+ column = aggregate_column(column_name)
35
+ select_value = operation_over_aggregate_column(column, operation, distinct)
36
+ select_value.distinct = true if operation == "sum" && distinct
37
+
38
+ relation.select_values = [select_value]
39
+
40
+ query_builder = relation.arel
41
+ end
42
+
43
+ result = skip_query_cache_if_necessary { @klass.connection.select_all(query_builder) }
44
+
45
+ type_cast_calculated_value(result.cast_values.first, operation) do |value|
46
+ type = column.try(:type_caster) ||
47
+ # CPK
48
+ # lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
49
+ lookup_cast_type_from_join_dependencies(column_name.to_s) || ::ActiveRecord::Type.default_value
50
+ type.deserialize(value)
51
+ end
52
+ end
53
+
54
+ def build_count_subquery(relation, column_name, distinct)
55
+ if column_name == :all
56
+ column_alias = Arel.star
57
+ # CPK
58
+ # relation.select_values = [ Arel.sql(FinderMethods::ONE_AS_ONE) ] unless distinct
59
+ relation.select_values = [ Arel.sql(::ActiveRecord::FinderMethods::ONE_AS_ONE) ] unless distinct
60
+ elsif column_name.is_a?(Array)
61
+ column_alias = Arel.star
62
+ relation.select_values = column_name.map do |column|
63
+ Arel::Attribute.new(@klass.unscoped.table, column)
64
+ end
65
+ else
66
+ column_alias = Arel.sql("count_column")
67
+ relation.select_values = [ aggregate_column(column_name).as(column_alias) ]
68
+ end
69
+
70
+ subquery_alias = Arel.sql("subquery_for_count")
71
+ select_value = operation_over_aggregate_column(column_alias, "count", false)
72
+
73
+ relation.build_subquery(subquery_alias, select_value)
74
+ end
75
+
76
+ def calculate(operation, column_name)
77
+ if has_include?(column_name)
78
+ relation = apply_join_dependency
79
+
80
+ if operation.to_s.downcase == "count"
81
+ unless distinct_value || distinct_select?(column_name || select_for_count)
82
+ relation.distinct!
83
+ # CPK
84
+ # relation.select_values = [ klass.primary_key || table[Arel.star] ]
85
+ if klass.primary_key.present? && klass.primary_key.is_a?(Array)
86
+ relation.select_values = klass.primary_key.map do |k|
87
+ "#{connection.quote_table_name(klass.table_name)}.#{connection.quote_column_name(k)}"
88
+ end
89
+ else
90
+ relation.select_values = [ klass.primary_key || table[Arel.star] ]
91
+ end
92
+ end
93
+ # PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT
94
+ relation.order_values = [] if group_values.empty?
95
+ end
96
+
97
+ relation.calculate(operation, column_name)
98
+ else
99
+ perform_calculation(operation, column_name)
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end