activerecord 5.2.0.beta2 → 5.2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (80) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +231 -2
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/lib/active_record.rb +1 -1
  6. data/lib/active_record/aggregations.rb +4 -5
  7. data/lib/active_record/association_relation.rb +2 -2
  8. data/lib/active_record/associations.rb +18 -12
  9. data/lib/active_record/associations/alias_tracker.rb +2 -10
  10. data/lib/active_record/associations/association.rb +1 -1
  11. data/lib/active_record/associations/belongs_to_association.rb +9 -9
  12. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +1 -6
  13. data/lib/active_record/associations/builder/association.rb +2 -2
  14. data/lib/active_record/associations/builder/belongs_to.rb +7 -3
  15. data/lib/active_record/associations/collection_association.rb +2 -2
  16. data/lib/active_record/associations/collection_proxy.rb +1 -1
  17. data/lib/active_record/associations/has_many_association.rb +1 -1
  18. data/lib/active_record/associations/has_many_through_association.rb +4 -17
  19. data/lib/active_record/associations/has_one_through_association.rb +5 -6
  20. data/lib/active_record/associations/preloader.rb +1 -1
  21. data/lib/active_record/associations/preloader/association.rb +2 -2
  22. data/lib/active_record/associations/through_association.rb +22 -9
  23. data/lib/active_record/attribute_methods.rb +1 -5
  24. data/lib/active_record/attribute_methods/dirty.rb +2 -4
  25. data/lib/active_record/attributes.rb +1 -1
  26. data/lib/active_record/autosave_association.rb +3 -0
  27. data/lib/active_record/callbacks.rb +2 -2
  28. data/lib/active_record/collection_cache_key.rb +5 -6
  29. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +1 -3
  30. data/lib/active_record/connection_adapters/abstract/database_statements.rb +57 -21
  31. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +1 -0
  32. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +20 -3
  33. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +55 -15
  34. data/lib/active_record/connection_adapters/abstract_adapter.rb +19 -6
  35. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +55 -64
  36. data/lib/active_record/connection_adapters/mysql/database_statements.rb +8 -1
  37. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +0 -4
  38. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +21 -6
  39. data/lib/active_record/connection_adapters/postgresql/oid/decimal.rb +1 -1
  40. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +1 -1
  41. data/lib/active_record/connection_adapters/postgresql/quoting.rb +9 -1
  42. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +12 -0
  43. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +13 -4
  44. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +170 -48
  45. data/lib/active_record/connection_adapters/postgresql_adapter.rb +15 -5
  46. data/lib/active_record/connection_adapters/schema_cache.rb +2 -2
  47. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +63 -18
  48. data/lib/active_record/core.rb +12 -3
  49. data/lib/active_record/enum.rb +2 -0
  50. data/lib/active_record/fixtures.rb +28 -37
  51. data/lib/active_record/gem_version.rb +1 -1
  52. data/lib/active_record/inheritance.rb +3 -4
  53. data/lib/active_record/log_subscriber.rb +41 -0
  54. data/lib/active_record/migration.rb +138 -120
  55. data/lib/active_record/migration/compatibility.rb +20 -0
  56. data/lib/active_record/model_schema.rb +19 -16
  57. data/lib/active_record/persistence.rb +8 -11
  58. data/lib/active_record/railtie.rb +7 -2
  59. data/lib/active_record/railties/databases.rake +8 -11
  60. data/lib/active_record/reflection.rb +10 -13
  61. data/lib/active_record/relation.rb +27 -17
  62. data/lib/active_record/relation/calculations.rb +17 -12
  63. data/lib/active_record/relation/finder_methods.rb +30 -37
  64. data/lib/active_record/relation/merger.rb +30 -2
  65. data/lib/active_record/relation/predicate_builder.rb +12 -0
  66. data/lib/active_record/relation/predicate_builder/association_query_value.rb +1 -1
  67. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  68. data/lib/active_record/relation/query_methods.rb +14 -24
  69. data/lib/active_record/relation/spawn_methods.rb +1 -1
  70. data/lib/active_record/relation/where_clause.rb +16 -2
  71. data/lib/active_record/relation/where_clause_factory.rb +1 -2
  72. data/lib/active_record/sanitization.rb +130 -128
  73. data/lib/active_record/schema.rb +1 -1
  74. data/lib/active_record/schema_dumper.rb +12 -3
  75. data/lib/active_record/scoping/named.rb +6 -0
  76. data/lib/active_record/store.rb +1 -1
  77. data/lib/active_record/table_metadata.rb +10 -3
  78. data/lib/active_record/tasks/database_tasks.rb +4 -4
  79. data/lib/active_record/type_caster/map.rb +1 -1
  80. metadata +9 -9
@@ -23,7 +23,11 @@ module ActiveRecord
23
23
  # build a relation to merge in rather than directly merging
24
24
  # the values.
25
25
  def other
26
- other = Relation.create(relation.klass, relation.table, relation.predicate_builder)
26
+ other = Relation.create(
27
+ relation.klass,
28
+ table: relation.table,
29
+ predicate_builder: relation.predicate_builder
30
+ )
27
31
  hash.each { |k, v|
28
32
  if k == :joins
29
33
  if Hash === v
@@ -52,7 +56,7 @@ module ActiveRecord
52
56
 
53
57
  NORMAL_VALUES = Relation::VALUE_METHODS -
54
58
  Relation::CLAUSE_METHODS -
55
- [:includes, :preload, :joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc:
59
+ [:includes, :preload, :joins, :left_outer_joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc:
56
60
 
57
61
  def normal_values
58
62
  NORMAL_VALUES
@@ -79,6 +83,7 @@ module ActiveRecord
79
83
  merge_clauses
80
84
  merge_preloads
81
85
  merge_joins
86
+ merge_outer_joins
82
87
 
83
88
  relation
84
89
  end
@@ -129,6 +134,29 @@ module ActiveRecord
129
134
  end
130
135
  end
131
136
 
137
+ def merge_outer_joins
138
+ return if other.left_outer_joins_values.blank?
139
+
140
+ if other.klass == relation.klass
141
+ relation.left_outer_joins!(*other.left_outer_joins_values)
142
+ else
143
+ alias_tracker = nil
144
+ joins_dependency = other.left_outer_joins_values.map do |join|
145
+ case join
146
+ when Hash, Symbol, Array
147
+ alias_tracker ||= other.alias_tracker
148
+ ActiveRecord::Associations::JoinDependency.new(
149
+ other.klass, other.table, join, alias_tracker
150
+ )
151
+ else
152
+ join
153
+ end
154
+ end
155
+
156
+ relation.left_outer_joins!(*joins_dependency)
157
+ end
158
+ end
159
+
132
160
  def merge_multi_values
133
161
  if other.reordering_value
134
162
  # override any order specified in the original relation
@@ -86,6 +86,18 @@ module ActiveRecord
86
86
  expand_from_hash(query).reduce(&:and)
87
87
  end
88
88
  queries.reduce(&:or)
89
+ elsif table.aggregated_with?(key)
90
+ mapping = table.reflect_on_aggregation(key).mapping
91
+ queries = Array.wrap(value).map do |object|
92
+ mapping.map do |field_attr, aggregate_attr|
93
+ if mapping.size == 1 && !object.respond_to?(aggregate_attr)
94
+ build(table.arel_attribute(field_attr), object)
95
+ else
96
+ build(table.arel_attribute(field_attr), object.send(aggregate_attr))
97
+ end
98
+ end.reduce(&:and)
99
+ end
100
+ queries.reduce(&:or)
89
101
  # FIXME: Deprecate this and provide a public API to force equality
90
102
  elsif (value.is_a?(Range) || value.is_a?(Array)) &&
91
103
  table.type(key.to_s).respond_to?(:subtype)
@@ -30,7 +30,7 @@ module ActiveRecord
30
30
  end
31
31
 
32
32
  def primary_key
33
- associated_table.association_join_keys.key
33
+ associated_table.association_join_primary_key
34
34
  end
35
35
 
36
36
  def convert_to_id(value)
@@ -29,7 +29,7 @@ module ActiveRecord
29
29
  end
30
30
 
31
31
  def primary_key(value)
32
- associated_table.association_primary_key(base_class(value))
32
+ associated_table.association_join_primary_key(base_class(value))
33
33
  end
34
34
 
35
35
  def base_class(value)
@@ -327,8 +327,8 @@ module ActiveRecord
327
327
  end
328
328
 
329
329
  VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
330
- :limit, :offset, :joins, :includes, :from,
331
- :readonly, :having])
330
+ :limit, :offset, :joins, :left_outer_joins,
331
+ :includes, :from, :readonly, :having])
332
332
 
333
333
  # Removes an unwanted relation that is already defined on a chain of relations.
334
334
  # This is useful when passing around chains of relations and would like to
@@ -375,10 +375,11 @@ module ActiveRecord
375
375
  args.each do |scope|
376
376
  case scope
377
377
  when Symbol
378
+ scope = :left_outer_joins if scope == :left_joins
378
379
  if !VALID_UNSCOPING_VALUES.include?(scope)
379
380
  raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
380
381
  end
381
- set_value(scope, nil)
382
+ set_value(scope, DEFAULT_VALUES[scope])
382
383
  when Hash
383
384
  scope.each do |key, target_value|
384
385
  if key != :where
@@ -444,15 +445,13 @@ module ActiveRecord
444
445
  #
445
446
  def left_outer_joins(*args)
446
447
  check_if_method_has_arguments!(__callee__, args)
447
-
448
- args.compact!
449
- args.flatten!
450
-
451
448
  spawn.left_outer_joins!(*args)
452
449
  end
453
450
  alias :left_joins :left_outer_joins
454
451
 
455
452
  def left_outer_joins!(*args) # :nodoc:
453
+ args.compact!
454
+ args.flatten!
456
455
  self.left_outer_joins_values += args
457
456
  self
458
457
  end
@@ -907,7 +906,7 @@ module ActiveRecord
907
906
  protected
908
907
  # Returns a relation value with a given name
909
908
  def get_value(name) # :nodoc:
910
- @values[name] || default_value_for(name)
909
+ @values.fetch(name, DEFAULT_VALUES[name])
911
910
  end
912
911
 
913
912
  # Sets the relation value with the given name
@@ -980,6 +979,8 @@ module ActiveRecord
980
979
  case join
981
980
  when Hash, Symbol, Array
982
981
  :association_join
982
+ when ActiveRecord::Associations::JoinDependency
983
+ :stashed_join
983
984
  else
984
985
  raise ArgumentError, "only Hash, Symbol and Array are allowed"
985
986
  end
@@ -1015,7 +1016,7 @@ module ActiveRecord
1015
1016
  join_nodes = buckets[:join_node].uniq
1016
1017
  string_joins = buckets[:string_join].map(&:strip).uniq
1017
1018
 
1018
- join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins)
1019
+ join_list = join_nodes + convert_join_strings_to_ast(string_joins)
1019
1020
  alias_tracker = alias_tracker(join_list, aliases)
1020
1021
 
1021
1022
  join_dependency = ActiveRecord::Associations::JoinDependency.new(
@@ -1030,7 +1031,7 @@ module ActiveRecord
1030
1031
  alias_tracker.aliases
1031
1032
  end
1032
1033
 
1033
- def convert_join_strings_to_ast(table, joins)
1034
+ def convert_join_strings_to_ast(joins)
1034
1035
  joins
1035
1036
  .flatten
1036
1037
  .reject(&:blank?)
@@ -1040,8 +1041,8 @@ module ActiveRecord
1040
1041
  def build_select(arel)
1041
1042
  if select_values.any?
1042
1043
  arel.project(*arel_columns(select_values.uniq))
1043
- elsif @klass.ignored_columns.any?
1044
- arel.project(*arel_columns(@klass.column_names))
1044
+ elsif klass.ignored_columns.any?
1045
+ arel.project(*klass.column_names.map { |field| arel_attribute(field) })
1045
1046
  else
1046
1047
  arel.project(table[Arel.star])
1047
1048
  end
@@ -1121,7 +1122,7 @@ module ActiveRecord
1121
1122
 
1122
1123
  def preprocess_order_args(order_args)
1123
1124
  order_args.map! do |arg|
1124
- klass.send(:sanitize_sql_for_order, arg)
1125
+ klass.sanitize_sql_for_order(arg)
1125
1126
  end
1126
1127
  order_args.flatten!
1127
1128
 
@@ -1192,7 +1193,6 @@ module ActiveRecord
1192
1193
 
1193
1194
  DEFAULT_VALUES = {
1194
1195
  create_with: FROZEN_EMPTY_HASH,
1195
- readonly: false,
1196
1196
  where: Relation::WhereClause.empty,
1197
1197
  having: Relation::WhereClause.empty,
1198
1198
  from: Relation::FromClause.empty
@@ -1201,15 +1201,5 @@ module ActiveRecord
1201
1201
  Relation::MULTI_VALUE_METHODS.each do |value|
1202
1202
  DEFAULT_VALUES[value] ||= FROZEN_EMPTY_ARRAY
1203
1203
  end
1204
-
1205
- Relation::SINGLE_VALUE_METHODS.each do |value|
1206
- DEFAULT_VALUES[value] = nil if DEFAULT_VALUES[value].nil?
1207
- end
1208
-
1209
- def default_value_for(name)
1210
- DEFAULT_VALUES.fetch(name) do
1211
- raise ArgumentError, "unknown relation value #{name.inspect}"
1212
- end
1213
- end
1214
1204
  end
1215
1205
  end
@@ -69,7 +69,7 @@ module ActiveRecord
69
69
  private
70
70
 
71
71
  def relation_with(values)
72
- result = Relation.create(klass, table, predicate_builder, values)
72
+ result = Relation.create(klass, values: values)
73
73
  result.extend(*extending_values) if extending_values.any?
74
74
  result
75
75
  end
@@ -47,7 +47,7 @@ module ActiveRecord
47
47
  end
48
48
 
49
49
  def to_h(table_name = nil)
50
- equalities = predicates.grep(Arel::Nodes::Equality)
50
+ equalities = equalities(predicates)
51
51
  if table_name
52
52
  equalities = equalities.select do |node|
53
53
  node.left.relation.name == table_name
@@ -90,6 +90,20 @@ module ActiveRecord
90
90
  end
91
91
 
92
92
  private
93
+ def equalities(predicates)
94
+ equalities = []
95
+
96
+ predicates.each do |node|
97
+ case node
98
+ when Arel::Nodes::Equality
99
+ equalities << node
100
+ when Arel::Nodes::And
101
+ equalities.concat equalities(node.children)
102
+ end
103
+ end
104
+
105
+ equalities
106
+ end
93
107
 
94
108
  def predicates_unreferenced_by(other)
95
109
  predicates.reject do |n|
@@ -121,7 +135,7 @@ module ActiveRecord
121
135
  end
122
136
 
123
137
  def except_predicates(columns)
124
- self.predicates.reject do |node|
138
+ predicates.reject do |node|
125
139
  case node
126
140
  when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual
127
141
  subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right)
@@ -11,10 +11,9 @@ module ActiveRecord
11
11
  def build(opts, other)
12
12
  case opts
13
13
  when String, Array
14
- parts = [klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
14
+ parts = [klass.sanitize_sql(other.empty? ? opts : ([opts] + other))]
15
15
  when Hash
16
16
  attributes = predicate_builder.resolve_column_aliases(opts)
17
- attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes)
18
17
  attributes.stringify_keys!
19
18
 
20
19
  parts = predicate_builder.build_from_hash(attributes)
@@ -5,80 +5,135 @@ module ActiveRecord
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  module ClassMethods
8
- private
9
-
10
- # Accepts an array or string of SQL conditions and sanitizes
11
- # them into a valid SQL fragment for a WHERE clause.
12
- #
13
- # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
14
- # # => "name='foo''bar' and group_id=4"
15
- #
16
- # sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
17
- # # => "name='foo''bar' and group_id='4'"
18
- #
19
- # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4])
20
- # # => "name='foo''bar' and group_id='4'"
21
- #
22
- # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'")
23
- # # => "name='foo''bar' and group_id='4'"
24
- def sanitize_sql_for_conditions(condition) # :doc:
25
- return nil if condition.blank?
26
-
27
- case condition
28
- when Array; sanitize_sql_array(condition)
29
- else condition
30
- end
8
+ # Accepts an array or string of SQL conditions and sanitizes
9
+ # them into a valid SQL fragment for a WHERE clause.
10
+ #
11
+ # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
12
+ # # => "name='foo''bar' and group_id=4"
13
+ #
14
+ # sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
15
+ # # => "name='foo''bar' and group_id='4'"
16
+ #
17
+ # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4])
18
+ # # => "name='foo''bar' and group_id='4'"
19
+ #
20
+ # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'")
21
+ # # => "name='foo''bar' and group_id='4'"
22
+ def sanitize_sql_for_conditions(condition)
23
+ return nil if condition.blank?
24
+
25
+ case condition
26
+ when Array; sanitize_sql_array(condition)
27
+ else condition
31
28
  end
32
- alias :sanitize_sql :sanitize_sql_for_conditions
33
-
34
- # Accepts an array, hash, or string of SQL conditions and sanitizes
35
- # them into a valid SQL fragment for a SET clause.
36
- #
37
- # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4])
38
- # # => "name=NULL and group_id=4"
39
- #
40
- # sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4])
41
- # # => "name=NULL and group_id=4"
42
- #
43
- # Post.send(:sanitize_sql_for_assignment, { name: nil, group_id: 4 })
44
- # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4"
45
- #
46
- # sanitize_sql_for_assignment("name=NULL and group_id='4'")
47
- # # => "name=NULL and group_id='4'"
48
- def sanitize_sql_for_assignment(assignments, default_table_name = table_name) # :doc:
49
- case assignments
50
- when Array; sanitize_sql_array(assignments)
51
- when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name)
52
- else assignments
53
- end
29
+ end
30
+ alias :sanitize_sql :sanitize_sql_for_conditions
31
+
32
+ # Accepts an array, hash, or string of SQL conditions and sanitizes
33
+ # them into a valid SQL fragment for a SET clause.
34
+ #
35
+ # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4])
36
+ # # => "name=NULL and group_id=4"
37
+ #
38
+ # sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4])
39
+ # # => "name=NULL and group_id=4"
40
+ #
41
+ # Post.sanitize_sql_for_assignment({ name: nil, group_id: 4 })
42
+ # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4"
43
+ #
44
+ # sanitize_sql_for_assignment("name=NULL and group_id='4'")
45
+ # # => "name=NULL and group_id='4'"
46
+ def sanitize_sql_for_assignment(assignments, default_table_name = table_name)
47
+ case assignments
48
+ when Array; sanitize_sql_array(assignments)
49
+ when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name)
50
+ else assignments
54
51
  end
55
-
56
- # Accepts an array, or string of SQL conditions and sanitizes
57
- # them into a valid SQL fragment for an ORDER clause.
58
- #
59
- # sanitize_sql_for_order(["field(id, ?)", [1,3,2]])
60
- # # => "field(id, 1,3,2)"
61
- #
62
- # sanitize_sql_for_order("id ASC")
63
- # # => "id ASC"
64
- def sanitize_sql_for_order(condition) # :doc:
65
- if condition.is_a?(Array) && condition.first.to_s.include?("?")
66
- enforce_raw_sql_whitelist([condition.first],
67
- whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST
68
- )
69
-
70
- # Ensure we aren't dealing with a subclass of String that might
71
- # override methods we use (eg. Arel::Nodes::SqlLiteral).
72
- if condition.first.kind_of?(String) && !condition.first.instance_of?(String)
73
- condition = [String.new(condition.first), *condition[1..-1]]
74
- end
75
-
76
- Arel.sql(sanitize_sql_array(condition))
77
- else
78
- condition
52
+ end
53
+
54
+ # Accepts an array, or string of SQL conditions and sanitizes
55
+ # them into a valid SQL fragment for an ORDER clause.
56
+ #
57
+ # sanitize_sql_for_order(["field(id, ?)", [1,3,2]])
58
+ # # => "field(id, 1,3,2)"
59
+ #
60
+ # sanitize_sql_for_order("id ASC")
61
+ # # => "id ASC"
62
+ def sanitize_sql_for_order(condition)
63
+ if condition.is_a?(Array) && condition.first.to_s.include?("?")
64
+ enforce_raw_sql_whitelist([condition.first],
65
+ whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST
66
+ )
67
+
68
+ # Ensure we aren't dealing with a subclass of String that might
69
+ # override methods we use (eg. Arel::Nodes::SqlLiteral).
70
+ if condition.first.kind_of?(String) && !condition.first.instance_of?(String)
71
+ condition = [String.new(condition.first), *condition[1..-1]]
79
72
  end
73
+
74
+ Arel.sql(sanitize_sql_array(condition))
75
+ else
76
+ condition
80
77
  end
78
+ end
79
+
80
+ # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
81
+ #
82
+ # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts")
83
+ # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1"
84
+ def sanitize_sql_hash_for_assignment(attrs, table)
85
+ c = connection
86
+ attrs.map do |attr, value|
87
+ type = type_for_attribute(attr)
88
+ value = type.serialize(type.cast(value))
89
+ "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}"
90
+ end.join(", ")
91
+ end
92
+
93
+ # Sanitizes a +string+ so that it is safe to use within an SQL
94
+ # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%".
95
+ #
96
+ # sanitize_sql_like("100%")
97
+ # # => "100\\%"
98
+ #
99
+ # sanitize_sql_like("snake_cased_string")
100
+ # # => "snake\\_cased\\_string"
101
+ #
102
+ # sanitize_sql_like("100%", "!")
103
+ # # => "100!%"
104
+ #
105
+ # sanitize_sql_like("snake_cased_string", "!")
106
+ # # => "snake!_cased!_string"
107
+ def sanitize_sql_like(string, escape_character = "\\")
108
+ pattern = Regexp.union(escape_character, "%", "_")
109
+ string.gsub(pattern) { |x| [escape_character, x].join }
110
+ end
111
+
112
+ # Accepts an array of conditions. The array has each value
113
+ # sanitized and interpolated into the SQL statement.
114
+ #
115
+ # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
116
+ # # => "name='foo''bar' and group_id=4"
117
+ #
118
+ # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
119
+ # # => "name='foo''bar' and group_id=4"
120
+ #
121
+ # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
122
+ # # => "name='foo''bar' and group_id='4'"
123
+ def sanitize_sql_array(ary)
124
+ statement, *values = ary
125
+ if values.first.is_a?(Hash) && /:\w+/.match?(statement)
126
+ replace_named_bind_variables(statement, values.first)
127
+ elsif statement.include?("?")
128
+ replace_bind_variables(statement, values)
129
+ elsif statement.blank?
130
+ statement
131
+ else
132
+ statement % values.collect { |value| connection.quote_string(value.to_s) }
133
+ end
134
+ end
81
135
 
136
+ private
82
137
  # Accepts a hash of SQL conditions and replaces those attributes
83
138
  # that correspond to a {#composed_of}[rdoc-ref:Aggregations::ClassMethods#composed_of]
84
139
  # relationship with their expanded aggregate attribute values.
@@ -100,10 +155,12 @@ module ActiveRecord
100
155
  if aggregation = reflect_on_aggregation(attr.to_sym)
101
156
  mapping = aggregation.mapping
102
157
  mapping.each do |field_attr, aggregate_attr|
103
- if mapping.size == 1 && !value.respond_to?(aggregate_attr)
104
- expanded_attrs[field_attr] = value
158
+ expanded_attrs[field_attr] = if value.is_a?(Array)
159
+ value.map { |it| it.send(aggregate_attr) }
160
+ elsif mapping.size == 1 && !value.respond_to?(aggregate_attr)
161
+ value
105
162
  else
106
- expanded_attrs[field_attr] = value.send(aggregate_attr)
163
+ value.send(aggregate_attr)
107
164
  end
108
165
  end
109
166
  else
@@ -112,62 +169,7 @@ module ActiveRecord
112
169
  end
113
170
  expanded_attrs
114
171
  end
115
-
116
- # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
117
- #
118
- # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts")
119
- # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1"
120
- def sanitize_sql_hash_for_assignment(attrs, table) # :doc:
121
- c = connection
122
- attrs.map do |attr, value|
123
- type = type_for_attribute(attr.to_s)
124
- value = type.serialize(type.cast(value))
125
- "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}"
126
- end.join(", ")
127
- end
128
-
129
- # Sanitizes a +string+ so that it is safe to use within an SQL
130
- # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%".
131
- #
132
- # sanitize_sql_like("100%")
133
- # # => "100\\%"
134
- #
135
- # sanitize_sql_like("snake_cased_string")
136
- # # => "snake\\_cased\\_string"
137
- #
138
- # sanitize_sql_like("100%", "!")
139
- # # => "100!%"
140
- #
141
- # sanitize_sql_like("snake_cased_string", "!")
142
- # # => "snake!_cased!_string"
143
- def sanitize_sql_like(string, escape_character = "\\") # :doc:
144
- pattern = Regexp.union(escape_character, "%", "_")
145
- string.gsub(pattern) { |x| [escape_character, x].join }
146
- end
147
-
148
- # Accepts an array of conditions. The array has each value
149
- # sanitized and interpolated into the SQL statement.
150
- #
151
- # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
152
- # # => "name='foo''bar' and group_id=4"
153
- #
154
- # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
155
- # # => "name='foo''bar' and group_id=4"
156
- #
157
- # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
158
- # # => "name='foo''bar' and group_id='4'"
159
- def sanitize_sql_array(ary) # :doc:
160
- statement, *values = ary
161
- if values.first.is_a?(Hash) && /:\w+/.match?(statement)
162
- replace_named_bind_variables(statement, values.first)
163
- elsif statement.include?("?")
164
- replace_bind_variables(statement, values)
165
- elsif statement.blank?
166
- statement
167
- else
168
- statement % values.collect { |value| connection.quote_string(value.to_s) }
169
- end
170
- end
172
+ deprecate :expand_hash_conditions_for_aggregates
171
173
 
172
174
  def replace_bind_variables(statement, values)
173
175
  raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size)