activerecord 6.0.0.beta1 → 6.0.1.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 (158) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +529 -10
  3. data/README.rdoc +3 -1
  4. data/lib/active_record.rb +7 -1
  5. data/lib/active_record/association_relation.rb +15 -6
  6. data/lib/active_record/associations.rb +4 -3
  7. data/lib/active_record/associations/association.rb +27 -2
  8. data/lib/active_record/associations/builder/association.rb +14 -18
  9. data/lib/active_record/associations/builder/belongs_to.rb +5 -2
  10. data/lib/active_record/associations/builder/collection_association.rb +5 -15
  11. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +1 -1
  12. data/lib/active_record/associations/builder/has_many.rb +2 -0
  13. data/lib/active_record/associations/builder/has_one.rb +35 -1
  14. data/lib/active_record/associations/builder/singular_association.rb +2 -0
  15. data/lib/active_record/associations/collection_association.rb +5 -6
  16. data/lib/active_record/associations/collection_proxy.rb +13 -42
  17. data/lib/active_record/associations/has_many_association.rb +1 -9
  18. data/lib/active_record/associations/has_many_through_association.rb +4 -11
  19. data/lib/active_record/associations/join_dependency.rb +14 -9
  20. data/lib/active_record/associations/join_dependency/join_association.rb +21 -7
  21. data/lib/active_record/associations/preloader.rb +12 -7
  22. data/lib/active_record/associations/preloader/association.rb +37 -34
  23. data/lib/active_record/associations/preloader/through_association.rb +48 -39
  24. data/lib/active_record/attribute_methods.rb +3 -53
  25. data/lib/active_record/attribute_methods/before_type_cast.rb +4 -1
  26. data/lib/active_record/attribute_methods/dirty.rb +47 -14
  27. data/lib/active_record/attribute_methods/primary_key.rb +7 -15
  28. data/lib/active_record/attribute_methods/query.rb +2 -3
  29. data/lib/active_record/attribute_methods/read.rb +3 -9
  30. data/lib/active_record/attribute_methods/write.rb +6 -12
  31. data/lib/active_record/attributes.rb +13 -0
  32. data/lib/active_record/autosave_association.rb +21 -7
  33. data/lib/active_record/base.rb +0 -1
  34. data/lib/active_record/callbacks.rb +3 -3
  35. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +134 -23
  36. data/lib/active_record/connection_adapters/abstract/database_limits.rb +8 -4
  37. data/lib/active_record/connection_adapters/abstract/database_statements.rb +105 -70
  38. data/lib/active_record/connection_adapters/abstract/query_cache.rb +12 -5
  39. data/lib/active_record/connection_adapters/abstract/quoting.rb +63 -6
  40. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +5 -2
  41. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +51 -40
  42. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +1 -1
  43. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +95 -30
  44. data/lib/active_record/connection_adapters/abstract/transaction.rb +17 -6
  45. data/lib/active_record/connection_adapters/abstract_adapter.rb +115 -35
  46. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +106 -138
  47. data/lib/active_record/connection_adapters/column.rb +17 -13
  48. data/lib/active_record/connection_adapters/connection_specification.rb +2 -2
  49. data/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +3 -3
  50. data/lib/active_record/connection_adapters/mysql/database_statements.rb +48 -8
  51. data/lib/active_record/connection_adapters/mysql/quoting.rb +44 -7
  52. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +40 -32
  53. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +14 -6
  54. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +66 -5
  55. data/lib/active_record/connection_adapters/mysql/type_metadata.rb +6 -10
  56. data/lib/active_record/connection_adapters/mysql2_adapter.rb +18 -5
  57. data/lib/active_record/connection_adapters/postgresql/column.rb +17 -30
  58. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +8 -2
  59. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +2 -2
  60. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +1 -1
  61. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +6 -3
  62. data/lib/active_record/connection_adapters/postgresql/quoting.rb +40 -3
  63. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +36 -0
  64. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +98 -89
  65. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +47 -63
  66. data/lib/active_record/connection_adapters/postgresql/type_metadata.rb +23 -27
  67. data/lib/active_record/connection_adapters/postgresql_adapter.rb +95 -24
  68. data/lib/active_record/connection_adapters/schema_cache.rb +32 -14
  69. data/lib/active_record/connection_adapters/sql_type_metadata.rb +11 -8
  70. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +120 -0
  71. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +38 -2
  72. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +28 -2
  73. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +73 -118
  74. data/lib/active_record/connection_handling.rb +40 -17
  75. data/lib/active_record/core.rb +35 -24
  76. data/lib/active_record/database_configurations.rb +99 -50
  77. data/lib/active_record/database_configurations/hash_config.rb +11 -11
  78. data/lib/active_record/database_configurations/url_config.rb +21 -16
  79. data/lib/active_record/dynamic_matchers.rb +1 -1
  80. data/lib/active_record/enum.rb +15 -0
  81. data/lib/active_record/errors.rb +18 -13
  82. data/lib/active_record/fixtures.rb +11 -6
  83. data/lib/active_record/gem_version.rb +2 -2
  84. data/lib/active_record/inheritance.rb +1 -1
  85. data/lib/active_record/insert_all.rb +179 -0
  86. data/lib/active_record/integration.rb +13 -1
  87. data/lib/active_record/internal_metadata.rb +5 -1
  88. data/lib/active_record/locking/optimistic.rb +3 -4
  89. data/lib/active_record/log_subscriber.rb +1 -1
  90. data/lib/active_record/middleware/database_selector.rb +75 -0
  91. data/lib/active_record/middleware/database_selector/resolver.rb +88 -0
  92. data/lib/active_record/middleware/database_selector/resolver/session.rb +45 -0
  93. data/lib/active_record/migration.rb +62 -44
  94. data/lib/active_record/migration/command_recorder.rb +28 -14
  95. data/lib/active_record/migration/compatibility.rb +72 -63
  96. data/lib/active_record/model_schema.rb +3 -0
  97. data/lib/active_record/persistence.rb +212 -19
  98. data/lib/active_record/querying.rb +18 -14
  99. data/lib/active_record/railtie.rb +9 -1
  100. data/lib/active_record/railties/collection_cache_association_loading.rb +3 -3
  101. data/lib/active_record/railties/databases.rake +124 -25
  102. data/lib/active_record/reflection.rb +18 -32
  103. data/lib/active_record/relation.rb +185 -35
  104. data/lib/active_record/relation/calculations.rb +40 -44
  105. data/lib/active_record/relation/delegation.rb +23 -31
  106. data/lib/active_record/relation/finder_methods.rb +23 -14
  107. data/lib/active_record/relation/merger.rb +11 -16
  108. data/lib/active_record/relation/query_attribute.rb +5 -3
  109. data/lib/active_record/relation/query_methods.rb +230 -69
  110. data/lib/active_record/relation/spawn_methods.rb +1 -1
  111. data/lib/active_record/relation/where_clause.rb +10 -10
  112. data/lib/active_record/sanitization.rb +33 -4
  113. data/lib/active_record/schema.rb +1 -1
  114. data/lib/active_record/schema_dumper.rb +10 -1
  115. data/lib/active_record/schema_migration.rb +1 -1
  116. data/lib/active_record/scoping.rb +6 -7
  117. data/lib/active_record/scoping/default.rb +7 -15
  118. data/lib/active_record/scoping/named.rb +10 -2
  119. data/lib/active_record/statement_cache.rb +2 -2
  120. data/lib/active_record/store.rb +48 -0
  121. data/lib/active_record/table_metadata.rb +9 -13
  122. data/lib/active_record/tasks/database_tasks.rb +109 -6
  123. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -1
  124. data/lib/active_record/test_databases.rb +1 -16
  125. data/lib/active_record/test_fixtures.rb +2 -2
  126. data/lib/active_record/timestamp.rb +35 -19
  127. data/lib/active_record/touch_later.rb +4 -2
  128. data/lib/active_record/transactions.rb +56 -46
  129. data/lib/active_record/type_caster/connection.rb +16 -10
  130. data/lib/active_record/validations.rb +1 -0
  131. data/lib/active_record/validations/uniqueness.rb +4 -4
  132. data/lib/arel.rb +18 -4
  133. data/lib/arel/insert_manager.rb +3 -3
  134. data/lib/arel/nodes.rb +2 -1
  135. data/lib/arel/nodes/and.rb +1 -1
  136. data/lib/arel/nodes/case.rb +1 -1
  137. data/lib/arel/nodes/comment.rb +29 -0
  138. data/lib/arel/nodes/select_core.rb +16 -12
  139. data/lib/arel/nodes/unary.rb +1 -0
  140. data/lib/arel/nodes/values_list.rb +2 -17
  141. data/lib/arel/select_manager.rb +10 -10
  142. data/lib/arel/visitors/depth_first.rb +7 -2
  143. data/lib/arel/visitors/dot.rb +7 -2
  144. data/lib/arel/visitors/ibm_db.rb +13 -0
  145. data/lib/arel/visitors/informix.rb +6 -0
  146. data/lib/arel/visitors/mssql.rb +15 -1
  147. data/lib/arel/visitors/oracle12.rb +4 -5
  148. data/lib/arel/visitors/postgresql.rb +4 -10
  149. data/lib/arel/visitors/to_sql.rb +107 -131
  150. data/lib/arel/visitors/visitor.rb +9 -5
  151. data/lib/rails/generators/active_record/migration/migration_generator.rb +1 -1
  152. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +1 -1
  153. data/lib/rails/generators/active_record/migration/templates/migration.rb.tt +4 -2
  154. data/lib/rails/generators/active_record/model/model_generator.rb +1 -1
  155. data/lib/rails/generators/active_record/model/templates/model.rb.tt +10 -1
  156. metadata +19 -12
  157. data/lib/active_record/collection_cache_key.rb +0 -53
  158. data/lib/arel/nodes/values.rb +0 -16
@@ -8,7 +8,7 @@ module ActiveRecord
8
8
  module SpawnMethods
9
9
  # This is overridden by Associations::CollectionProxy
10
10
  def spawn #:nodoc:
11
- @delegate_to_klass ? klass.all : clone
11
+ already_in_scope? ? klass.all : clone
12
12
  end
13
13
 
14
14
  # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation.
@@ -70,7 +70,15 @@ module ActiveRecord
70
70
  predicates == other.predicates
71
71
  end
72
72
 
73
- def invert
73
+ def invert(as = :nand)
74
+ if predicates.size == 1
75
+ inverted_predicates = [ invert_predicate(predicates.first) ]
76
+ elsif as == :nor
77
+ inverted_predicates = predicates.map { |node| invert_predicate(node) }
78
+ else
79
+ inverted_predicates = [ Arel::Nodes::Not.new(ast) ]
80
+ end
81
+
74
82
  WhereClause.new(inverted_predicates)
75
83
  end
76
84
 
@@ -115,10 +123,6 @@ module ActiveRecord
115
123
  node.respond_to?(:operator) && node.operator == :==
116
124
  end
117
125
 
118
- def inverted_predicates
119
- predicates.map { |node| invert_predicate(node) }
120
- end
121
-
122
126
  def invert_predicate(node)
123
127
  case node
124
128
  when NilClass
@@ -140,11 +144,7 @@ module ActiveRecord
140
144
 
141
145
  def except_predicates(columns)
142
146
  predicates.reject do |node|
143
- case node
144
- 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
145
- subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right)
146
- columns.include?(subrelation.name.to_s)
147
- end
147
+ Arel.fetch_attribute(node) { |attr| columns.include?(attr.name.to_s) }
148
148
  end
149
149
  end
150
150
 
@@ -61,8 +61,9 @@ module ActiveRecord
61
61
  # # => "id ASC"
62
62
  def sanitize_sql_for_order(condition)
63
63
  if condition.is_a?(Array) && condition.first.to_s.include?("?")
64
- disallow_raw_sql!([condition.first],
65
- permit: AttributeMethods::ClassMethods::COLUMN_NAME_WITH_ORDER
64
+ disallow_raw_sql!(
65
+ [condition.first],
66
+ permit: connection.column_name_with_order_matcher
66
67
  )
67
68
 
68
69
  # Ensure we aren't dealing with a subclass of String that might
@@ -133,6 +134,33 @@ module ActiveRecord
133
134
  end
134
135
  end
135
136
 
137
+ def disallow_raw_sql!(args, permit: connection.column_name_matcher) # :nodoc:
138
+ unexpected = nil
139
+ args.each do |arg|
140
+ next if arg.is_a?(Symbol) || Arel.arel_node?(arg) || permit.match?(arg.to_s)
141
+ (unexpected ||= []) << arg
142
+ end
143
+
144
+ return unless unexpected
145
+
146
+ if allow_unsafe_raw_sql == :deprecated
147
+ ActiveSupport::Deprecation.warn(
148
+ "Dangerous query method (method whose arguments are used as raw " \
149
+ "SQL) called with non-attribute argument(s): " \
150
+ "#{unexpected.map(&:inspect).join(", ")}. Non-attribute " \
151
+ "arguments will be disallowed in Rails 6.1. This method should " \
152
+ "not be called with user-provided values, such as request " \
153
+ "parameters or model attributes. Known-safe values can be passed " \
154
+ "by wrapping them in Arel.sql()."
155
+ )
156
+ else
157
+ raise(ActiveRecord::UnknownAttributeReference,
158
+ "Query method called with non-attribute argument(s): " +
159
+ unexpected.map(&:inspect).join(", ")
160
+ )
161
+ end
162
+ end
163
+
136
164
  private
137
165
  def replace_bind_variables(statement, values)
138
166
  raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size)
@@ -165,10 +193,11 @@ module ActiveRecord
165
193
 
166
194
  def quote_bound_value(value, c = connection)
167
195
  if value.respond_to?(:map) && !value.acts_like?(:string)
168
- if value.respond_to?(:empty?) && value.empty?
196
+ quoted = value.map { |v| c.quote(v) }
197
+ if quoted.empty?
169
198
  c.quote(nil)
170
199
  else
171
- value.map { |v| c.quote(v) }.join(",")
200
+ quoted.join(",")
172
201
  end
173
202
  else
174
203
  c.quote(value)
@@ -50,7 +50,7 @@ module ActiveRecord
50
50
  instance_eval(&block)
51
51
 
52
52
  if info[:version].present?
53
- ActiveRecord::SchemaMigration.create_table
53
+ connection.schema_migration.create_table
54
54
  connection.assume_migrated_upto_version(info[:version])
55
55
  end
56
56
 
@@ -47,6 +47,7 @@ module ActiveRecord
47
47
  end
48
48
 
49
49
  private
50
+ attr_accessor :table_name
50
51
 
51
52
  def initialize(connection, options = {})
52
53
  @connection = connection
@@ -110,6 +111,8 @@ HEADER
110
111
  def table(table, stream)
111
112
  columns = @connection.columns(table)
112
113
  begin
114
+ self.table_name = table
115
+
113
116
  tbl = StringIO.new
114
117
 
115
118
  # first dump primary key column
@@ -143,7 +146,11 @@ HEADER
143
146
  raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
144
147
  next if column.name == pk
145
148
  type, colspec = column_spec(column)
146
- tbl.print " t.#{type} #{column.name.inspect}"
149
+ if type.is_a?(Symbol)
150
+ tbl.print " t.#{type} #{column.name.inspect}"
151
+ else
152
+ tbl.print " t.column #{column.name.inspect}, #{type.inspect}"
153
+ end
147
154
  tbl.print ", #{format_colspec(colspec)}" if colspec.present?
148
155
  tbl.puts
149
156
  end
@@ -159,6 +166,8 @@ HEADER
159
166
  stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
160
167
  stream.puts "# #{e.message}"
161
168
  stream.puts
169
+ ensure
170
+ self.table_name = nil
162
171
  end
163
172
  end
164
173
 
@@ -19,7 +19,7 @@ module ActiveRecord
19
19
  end
20
20
 
21
21
  def table_name
22
- "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
22
+ "#{table_name_prefix}#{schema_migrations_table_name}#{table_name_suffix}"
23
23
  end
24
24
 
25
25
  def table_exists?
@@ -23,14 +23,13 @@ module ActiveRecord
23
23
  current_scope
24
24
  end
25
25
 
26
- private
27
- def current_scope(skip_inherited_scope = false)
28
- ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope)
29
- end
26
+ def current_scope(skip_inherited_scope = false)
27
+ ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope)
28
+ end
30
29
 
31
- def current_scope=(scope)
32
- ScopeRegistry.set_value_for(:current_scope, self, scope)
33
- end
30
+ def current_scope=(scope)
31
+ ScopeRegistry.set_value_for(:current_scope, self, scope)
32
+ end
34
33
  end
35
34
 
36
35
  def populate_with_current_scope_attributes # :nodoc:
@@ -31,14 +31,7 @@ module ActiveRecord
31
31
  # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
32
32
  # }
33
33
  def unscoped
34
- block_given? ? _scoping(relation) { yield } : relation
35
- end
36
-
37
- def _scoping(relation) # :nodoc:
38
- previous, self.current_scope = current_scope(true), relation
39
- yield
40
- ensure
41
- self.current_scope = previous
34
+ block_given? ? relation.scoping { yield } : relation
42
35
  end
43
36
 
44
37
  # Are there attributes associated with this scope?
@@ -93,8 +86,8 @@ module ActiveRecord
93
86
  # # Should return a scope, you can call 'super' here etc.
94
87
  # end
95
88
  # end
96
- def default_scope(scope = nil) # :doc:
97
- scope = Proc.new if block_given?
89
+ def default_scope(scope = nil, &block) # :doc:
90
+ scope = block if block_given?
98
91
 
99
92
  if scope.is_a?(Relation) || !scope.respond_to?(:call)
100
93
  raise ArgumentError,
@@ -107,7 +100,7 @@ module ActiveRecord
107
100
  self.default_scopes += [scope]
108
101
  end
109
102
 
110
- def build_default_scope(base_rel = nil)
103
+ def build_default_scope(relation = relation())
111
104
  return if abstract_class?
112
105
 
113
106
  if default_scope_override.nil?
@@ -118,15 +111,14 @@ module ActiveRecord
118
111
  # The user has defined their own default scope method, so call that
119
112
  evaluate_default_scope do
120
113
  if scope = default_scope
121
- (base_rel ||= relation).merge!(scope)
114
+ relation.merge!(scope)
122
115
  end
123
116
  end
124
117
  elsif default_scopes.any?
125
- base_rel ||= relation
126
118
  evaluate_default_scope do
127
- default_scopes.inject(base_rel) do |default_scope, scope|
119
+ default_scopes.inject(relation) do |default_scope, scope|
128
120
  scope = scope.respond_to?(:to_proc) ? scope : scope.method(:call)
129
- default_scope.merge!(base_rel.instance_exec(&scope))
121
+ default_scope.instance_exec(&scope) || default_scope
130
122
  end
131
123
  end
132
124
  end
@@ -27,6 +27,14 @@ module ActiveRecord
27
27
  scope = current_scope
28
28
 
29
29
  if scope
30
+ if scope._deprecated_scope_source
31
+ ActiveSupport::Deprecation.warn(<<~MSG.squish)
32
+ Class level methods will no longer inherit scoping from `#{scope._deprecated_scope_source}`
33
+ in Rails 6.1. To continue using the scoped relation, pass it into the block directly.
34
+ To instead access the full set of models, as Rails 6.1 will, use `#{name}.unscoped`.
35
+ MSG
36
+ end
37
+
30
38
  if self == scope.klass
31
39
  scope.clone
32
40
  else
@@ -50,7 +58,7 @@ module ActiveRecord
50
58
  end
51
59
 
52
60
  def default_extensions # :nodoc:
53
- if scope = current_scope || build_default_scope
61
+ if scope = scope_for_association || build_default_scope
54
62
  scope.extensions
55
63
  else
56
64
  []
@@ -180,7 +188,7 @@ module ActiveRecord
180
188
 
181
189
  if body.respond_to?(:to_proc)
182
190
  singleton_class.define_method(name) do |*args|
183
- scope = all._exec_scope(*args, &body)
191
+ scope = all._exec_scope(name, *args, &body)
184
192
  scope = scope.extending(extension) if extension
185
193
  scope
186
194
  end
@@ -113,8 +113,8 @@ module ActiveRecord
113
113
  end
114
114
  end
115
115
 
116
- def self.create(connection, block = Proc.new)
117
- relation = block.call Params.new
116
+ def self.create(connection, callable = nil, &block)
117
+ relation = (callable || block).call Params.new
118
118
  query_builder, binds = connection.cacheable_query(self, relation.arel)
119
119
  bind_map = BindMap.new(binds)
120
120
  new(query_builder, bind_map, relation.klass)
@@ -11,6 +11,12 @@ module ActiveRecord
11
11
  # of the model. This is very helpful for easily exposing store keys to a form or elsewhere that's
12
12
  # already built around just accessing attributes on the model.
13
13
  #
14
+ # Every accessor comes with dirty tracking methods (+key_changed?+, +key_was+ and +key_change+) and
15
+ # methods to access the changes made during the last save (+saved_change_to_key?+, +saved_change_to_key+ and
16
+ # +key_before_last_save+).
17
+ #
18
+ # NOTE: There is no +key_will_change!+ method for accessors, use +store_will_change!+ instead.
19
+ #
14
20
  # Make sure that you declare the database column used for the serialized store as a text, so there's
15
21
  # plenty of room.
16
22
  #
@@ -49,6 +55,12 @@ module ActiveRecord
49
55
  # u.settings[:country] # => 'Denmark'
50
56
  # u.settings['country'] # => 'Denmark'
51
57
  #
58
+ # # Dirty tracking
59
+ # u.color = 'green'
60
+ # u.color_changed? # => true
61
+ # u.color_was # => 'black'
62
+ # u.color_change # => ['black', 'red']
63
+ #
52
64
  # # Add additional accessors to an existing store through store_accessor
53
65
  # class SuperUser < User
54
66
  # store_accessor :settings, :privileges, :servants
@@ -127,6 +139,42 @@ module ActiveRecord
127
139
  define_method(accessor_key) do
128
140
  read_store_attribute(store_attribute, key)
129
141
  end
142
+
143
+ define_method("#{accessor_key}_changed?") do
144
+ return false unless attribute_changed?(store_attribute)
145
+ prev_store, new_store = changes[store_attribute]
146
+ prev_store&.dig(key) != new_store&.dig(key)
147
+ end
148
+
149
+ define_method("#{accessor_key}_change") do
150
+ return unless attribute_changed?(store_attribute)
151
+ prev_store, new_store = changes[store_attribute]
152
+ [prev_store&.dig(key), new_store&.dig(key)]
153
+ end
154
+
155
+ define_method("#{accessor_key}_was") do
156
+ return unless attribute_changed?(store_attribute)
157
+ prev_store, _new_store = changes[store_attribute]
158
+ prev_store&.dig(key)
159
+ end
160
+
161
+ define_method("saved_change_to_#{accessor_key}?") do
162
+ return false unless saved_change_to_attribute?(store_attribute)
163
+ prev_store, new_store = saved_change_to_attribute(store_attribute)
164
+ prev_store&.dig(key) != new_store&.dig(key)
165
+ end
166
+
167
+ define_method("saved_change_to_#{accessor_key}") do
168
+ return unless saved_change_to_attribute?(store_attribute)
169
+ prev_store, new_store = saved_change_to_attribute(store_attribute)
170
+ [prev_store&.dig(key), new_store&.dig(key)]
171
+ end
172
+
173
+ define_method("#{accessor_key}_before_last_save") do
174
+ return unless saved_change_to_attribute?(store_attribute)
175
+ prev_store, _new_store = saved_change_to_attribute(store_attribute)
176
+ prev_store&.dig(key)
177
+ end
130
178
  end
131
179
  end
132
180
 
@@ -4,17 +4,18 @@ module ActiveRecord
4
4
  class TableMetadata # :nodoc:
5
5
  delegate :foreign_type, :foreign_key, :join_primary_key, :join_foreign_key, to: :association, prefix: true
6
6
 
7
- def initialize(klass, arel_table, association = nil)
7
+ def initialize(klass, arel_table, association = nil, types = klass)
8
8
  @klass = klass
9
+ @types = types
9
10
  @arel_table = arel_table
10
11
  @association = association
11
12
  end
12
13
 
13
14
  def resolve_column_aliases(hash)
14
15
  new_hash = hash.dup
15
- hash.each do |key, _|
16
- if (key.is_a?(Symbol)) && klass.attribute_alias?(key)
17
- new_hash[klass.attribute_alias(key)] = new_hash.delete(key)
16
+ hash.each_key do |key|
17
+ if key.is_a?(Symbol) && new_key = klass.attribute_aliases[key.to_s]
18
+ new_hash[new_key] = new_hash.delete(key)
18
19
  end
19
20
  end
20
21
  new_hash
@@ -29,11 +30,7 @@ module ActiveRecord
29
30
  end
30
31
 
31
32
  def type(column_name)
32
- if klass
33
- klass.type_for_attribute(column_name)
34
- else
35
- Type.default_value
36
- end
33
+ types.type_for_attribute(column_name)
37
34
  end
38
35
 
39
36
  def has_column?(column_name)
@@ -52,13 +49,12 @@ module ActiveRecord
52
49
  elsif association && !association.polymorphic?
53
50
  association_klass = association.klass
54
51
  arel_table = association_klass.arel_table.alias(table_name)
52
+ TableMetadata.new(association_klass, arel_table, association)
55
53
  else
56
54
  type_caster = TypeCaster::Connection.new(klass, table_name)
57
- association_klass = nil
58
55
  arel_table = Arel::Table.new(table_name, type_caster: type_caster)
56
+ TableMetadata.new(nil, arel_table, association, type_caster)
59
57
  end
60
-
61
- TableMetadata.new(association_klass, arel_table, association)
62
58
  end
63
59
 
64
60
  def polymorphic_association?
@@ -74,6 +70,6 @@ module ActiveRecord
74
70
  end
75
71
 
76
72
  private
77
- attr_reader :klass, :arel_table, :association
73
+ attr_reader :klass, :types, :arel_table, :association
78
74
  end
79
75
  end
@@ -141,8 +141,21 @@ module ActiveRecord
141
141
  end
142
142
  end
143
143
 
144
- def for_each
145
- databases = Rails.application.config.database_configuration
144
+ def setup_initial_database_yaml
145
+ return {} unless defined?(Rails)
146
+
147
+ begin
148
+ Rails.application.config.load_database_yaml
149
+ rescue
150
+ $stderr.puts "Rails couldn't infer whether you are using multiple databases from your database.yml and can't generate the tasks for the non-primary databases. If you'd like to use this feature, please simplify your ERB."
151
+
152
+ {}
153
+ end
154
+ end
155
+
156
+ def for_each(databases)
157
+ return {} unless defined?(Rails)
158
+
146
159
  database_configs = ActiveRecord::DatabaseConfigurations.new(databases).configs_for(env_name: Rails.env)
147
160
 
148
161
  # if this is a single database application we don't want tasks for each primary database
@@ -153,8 +166,22 @@ module ActiveRecord
153
166
  end
154
167
  end
155
168
 
156
- def create_current(environment = env)
157
- each_current_configuration(environment) { |configuration|
169
+ def raise_for_multi_db(environment = env, command:)
170
+ db_configs = ActiveRecord::Base.configurations.configs_for(env_name: environment)
171
+
172
+ if db_configs.count > 1
173
+ dbs_list = []
174
+
175
+ db_configs.each do |db|
176
+ dbs_list << "#{command}:#{db.spec_name}"
177
+ end
178
+
179
+ raise "You're using a multiple database application. To use `#{command}` you must run the namespaced task with a VERSION. Available tasks are #{dbs_list.to_sentence}."
180
+ end
181
+ end
182
+
183
+ def create_current(environment = env, spec_name = nil)
184
+ each_current_configuration(environment, spec_name) { |configuration|
158
185
  create configuration
159
186
  }
160
187
  ActiveRecord::Base.establish_connection(environment.to_sym)
@@ -182,6 +209,26 @@ module ActiveRecord
182
209
  }
183
210
  end
184
211
 
212
+ def truncate_tables(configuration)
213
+ ActiveRecord::Base.connected_to(database: { truncation: configuration }) do
214
+ conn = ActiveRecord::Base.connection
215
+ table_names = conn.tables
216
+ table_names -= [
217
+ conn.schema_migration.table_name,
218
+ InternalMetadata.table_name
219
+ ]
220
+
221
+ ActiveRecord::Base.connection.truncate_tables(*table_names)
222
+ end
223
+ end
224
+ private :truncate_tables
225
+
226
+ def truncate_all(environment = env)
227
+ ActiveRecord::Base.configurations.configs_for(env_name: environment).each do |db_config|
228
+ truncate_tables db_config.config
229
+ end
230
+ end
231
+
185
232
  def migrate
186
233
  check_target_version
187
234
 
@@ -198,7 +245,7 @@ module ActiveRecord
198
245
  end
199
246
 
200
247
  def migrate_status
201
- unless ActiveRecord::SchemaMigration.table_exists?
248
+ unless ActiveRecord::Base.connection.schema_migration.table_exists?
202
249
  Kernel.abort "Schema migrations table does not exist yet."
203
250
  end
204
251
 
@@ -286,10 +333,60 @@ module ActiveRecord
286
333
  end
287
334
  ActiveRecord::InternalMetadata.create_table
288
335
  ActiveRecord::InternalMetadata[:environment] = environment
336
+ ActiveRecord::InternalMetadata[:schema_sha1] = schema_sha1(file)
289
337
  ensure
290
338
  Migration.verbose = verbose_was
291
339
  end
292
340
 
341
+ def schema_up_to_date?(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = env, spec_name = "primary")
342
+ file ||= dump_filename(spec_name, format)
343
+
344
+ return true unless File.exist?(file)
345
+
346
+ ActiveRecord::Base.establish_connection(configuration)
347
+ return false unless ActiveRecord::InternalMetadata.table_exists?
348
+ ActiveRecord::InternalMetadata[:schema_sha1] == schema_sha1(file)
349
+ end
350
+
351
+ def reconstruct_from_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = env, spec_name = "primary") # :nodoc:
352
+ file ||= dump_filename(spec_name, format)
353
+
354
+ check_schema_file(file)
355
+
356
+ ActiveRecord::Base.establish_connection(configuration)
357
+
358
+ if schema_up_to_date?(configuration, format, file, environment, spec_name)
359
+ truncate_tables(configuration)
360
+ else
361
+ purge(configuration)
362
+ load_schema(configuration, format, file, environment, spec_name)
363
+ end
364
+ rescue ActiveRecord::NoDatabaseError
365
+ create(configuration)
366
+ load_schema(configuration, format, file, environment, spec_name)
367
+ end
368
+
369
+ def dump_schema(configuration, format = ActiveRecord::Base.schema_format, spec_name = "primary") # :nodoc:
370
+ require "active_record/schema_dumper"
371
+ filename = dump_filename(spec_name, format)
372
+ connection = ActiveRecord::Base.connection
373
+
374
+ case format
375
+ when :ruby
376
+ File.open(filename, "w:utf-8") do |file|
377
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
378
+ end
379
+ when :sql
380
+ structure_dump(configuration, filename)
381
+ if connection.schema_migration.table_exists?
382
+ File.open(filename, "a") do |f|
383
+ f.puts connection.dump_schema_information
384
+ f.print "\n"
385
+ end
386
+ end
387
+ end
388
+ end
389
+
293
390
  def schema_file(format = ActiveRecord::Base.schema_format)
294
391
  File.join(db_dir, schema_file_type(format))
295
392
  end
@@ -371,12 +468,14 @@ module ActiveRecord
371
468
  task.is_a?(String) ? task.constantize : task
372
469
  end
373
470
 
374
- def each_current_configuration(environment)
471
+ def each_current_configuration(environment, spec_name = nil)
375
472
  environments = [environment]
376
473
  environments << "test" if environment == "development"
377
474
 
378
475
  environments.each do |env|
379
476
  ActiveRecord::Base.configurations.configs_for(env_name: env).each do |db_config|
477
+ next if spec_name && spec_name != db_config.spec_name
478
+
380
479
  yield db_config.config, db_config.spec_name, env
381
480
  end
382
481
  end
@@ -398,6 +497,10 @@ module ActiveRecord
398
497
  def local_database?(configuration)
399
498
  configuration["host"].blank? || LOCAL_HOSTS.include?(configuration["host"])
400
499
  end
500
+
501
+ def schema_sha1(file)
502
+ Digest::SHA1.hexdigest(File.read(file))
503
+ end
401
504
  end
402
505
  end
403
506
  end