activerecord 6.0.0.beta1 → 6.0.0.beta2

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +99 -2
  3. data/lib/active_record.rb +7 -0
  4. data/lib/active_record/associations/association.rb +17 -0
  5. data/lib/active_record/associations/collection_association.rb +5 -6
  6. data/lib/active_record/associations/collection_proxy.rb +12 -41
  7. data/lib/active_record/associations/has_many_association.rb +1 -9
  8. data/lib/active_record/associations/join_dependency/join_association.rb +11 -6
  9. data/lib/active_record/associations/preloader/association.rb +3 -4
  10. data/lib/active_record/associations/preloader/through_association.rb +9 -20
  11. data/lib/active_record/callbacks.rb +3 -3
  12. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +25 -12
  13. data/lib/active_record/connection_adapters/abstract/database_statements.rb +17 -9
  14. data/lib/active_record/connection_adapters/abstract/query_cache.rb +6 -1
  15. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +5 -2
  16. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +47 -33
  17. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +16 -8
  18. data/lib/active_record/connection_adapters/abstract/transaction.rb +5 -2
  19. data/lib/active_record/connection_adapters/abstract_adapter.rb +6 -4
  20. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +28 -65
  21. data/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +1 -1
  22. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +40 -32
  23. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +8 -2
  24. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +59 -1
  25. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +1 -1
  26. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +6 -3
  27. data/lib/active_record/connection_adapters/postgresql/quoting.rb +1 -1
  28. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +36 -0
  29. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +98 -89
  30. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +15 -27
  31. data/lib/active_record/connection_adapters/postgresql_adapter.rb +30 -0
  32. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +27 -1
  33. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +8 -5
  34. data/lib/active_record/connection_handling.rb +9 -4
  35. data/lib/active_record/core.rb +13 -1
  36. data/lib/active_record/database_configurations.rb +30 -10
  37. data/lib/active_record/database_configurations/hash_config.rb +1 -1
  38. data/lib/active_record/database_configurations/url_config.rb +9 -4
  39. data/lib/active_record/errors.rb +17 -12
  40. data/lib/active_record/gem_version.rb +1 -1
  41. data/lib/active_record/inheritance.rb +1 -1
  42. data/lib/active_record/middleware/database_selector.rb +75 -0
  43. data/lib/active_record/middleware/database_selector/resolver.rb +90 -0
  44. data/lib/active_record/middleware/database_selector/resolver/session.rb +45 -0
  45. data/lib/active_record/migration.rb +1 -1
  46. data/lib/active_record/migration/compatibility.rb +62 -63
  47. data/lib/active_record/persistence.rb +6 -6
  48. data/lib/active_record/querying.rb +2 -3
  49. data/lib/active_record/railtie.rb +9 -0
  50. data/lib/active_record/railties/collection_cache_association_loading.rb +3 -3
  51. data/lib/active_record/reflection.rb +15 -29
  52. data/lib/active_record/relation.rb +86 -15
  53. data/lib/active_record/relation/calculations.rb +2 -4
  54. data/lib/active_record/relation/delegation.rb +1 -1
  55. data/lib/active_record/relation/finder_methods.rb +8 -4
  56. data/lib/active_record/relation/query_attribute.rb +5 -3
  57. data/lib/active_record/relation/query_methods.rb +28 -8
  58. data/lib/active_record/relation/spawn_methods.rb +1 -1
  59. data/lib/active_record/relation/where_clause.rb +1 -5
  60. data/lib/active_record/scoping.rb +6 -7
  61. data/lib/active_record/scoping/default.rb +1 -8
  62. data/lib/active_record/scoping/named.rb +9 -1
  63. data/lib/active_record/test_fixtures.rb +2 -2
  64. data/lib/active_record/timestamp.rb +9 -3
  65. data/lib/active_record/validations/uniqueness.rb +3 -1
  66. data/lib/arel.rb +7 -0
  67. data/lib/arel/nodes/and.rb +1 -1
  68. data/lib/arel/nodes/case.rb +1 -1
  69. metadata +11 -8
@@ -158,10 +158,6 @@ module ActiveRecord
158
158
  end
159
159
 
160
160
  def with_handler(handler_key, &blk) # :nodoc:
161
- unless ActiveRecord::Base.connection_handlers.keys.include?(handler_key)
162
- raise ArgumentError, "The #{handler_key} role does not exist. Add it by establishing a connection with `connects_to` or use an existing role (#{ActiveRecord::Base.connection_handlers.keys.join(", ")})."
163
- end
164
-
165
161
  handler = lookup_connection_handler(handler_key)
166
162
  swap_connection_handler(handler, &blk)
167
163
  end
@@ -180,6 +176,15 @@ module ActiveRecord
180
176
  config_hash
181
177
  end
182
178
 
179
+ # Clears the query cache for all connections associated with the current thread.
180
+ def clear_query_caches_for_current_thread
181
+ ActiveRecord::Base.connection_handlers.each_value do |handler|
182
+ handler.connection_pool_list.each do |pool|
183
+ pool.connection.clear_query_cache if pool.active_connection?
184
+ end
185
+ end
186
+ end
187
+
183
188
  # Returns the connection currently associated with the class. This can
184
189
  # also be used to "borrow" the connection to do database work unrelated
185
190
  # to any of the specific Active Records.
@@ -101,6 +101,7 @@ module ActiveRecord
101
101
  # environment where dumping schema is rarely needed.
102
102
  mattr_accessor :dump_schema_after_migration, instance_writer: false, default: true
103
103
 
104
+ mattr_accessor :database_selector, instance_writer: false
104
105
  ##
105
106
  # :singleton-method:
106
107
  # Specifies which database schemas to dump when calling db:structure:dump.
@@ -124,6 +125,10 @@ module ActiveRecord
124
125
 
125
126
  mattr_accessor :connection_handlers, instance_accessor: false, default: {}
126
127
 
128
+ mattr_accessor :writing_role, instance_accessor: false, default: :writing
129
+
130
+ mattr_accessor :reading_role, instance_accessor: false, default: :reading
131
+
127
132
  class_attribute :default_connection_handler, instance_writer: false
128
133
 
129
134
  self.filter_attributes = []
@@ -137,7 +142,6 @@ module ActiveRecord
137
142
  end
138
143
 
139
144
  self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new
140
- self.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
141
145
  end
142
146
 
143
147
  module ClassMethods
@@ -473,6 +477,14 @@ module ActiveRecord
473
477
  end
474
478
  end
475
479
 
480
+ def present? # :nodoc:
481
+ true
482
+ end
483
+
484
+ def blank? # :nodoc:
485
+ false
486
+ end
487
+
476
488
  # Returns +true+ if the record is read only. Records loaded through joins with piggy-back
477
489
  # attributes will be marked as read only since they cannot be saved.
478
490
  def readonly?
@@ -25,9 +25,9 @@ module ActiveRecord
25
25
  #
26
26
  # Options:
27
27
  #
28
- # <tt>env_name:</tt> The environment name. Defaults to nil which will collect
28
+ # <tt>env_name:</tt> The environment name. Defaults to +nil+ which will collect
29
29
  # configs for all environments.
30
- # <tt>spec_name:</tt> The specification name (ie primary, animals, etc.). Defaults
30
+ # <tt>spec_name:</tt> The specification name (i.e. primary, animals, etc.). Defaults
31
31
  # to +nil+.
32
32
  # <tt>include_replicas:</tt> Determines whether to include replicas in
33
33
  # the returned list. Most of the time we're only iterating over the write
@@ -102,6 +102,7 @@ module ActiveRecord
102
102
 
103
103
  def build_configs(configs)
104
104
  return configs.configurations if configs.is_a?(DatabaseConfigurations)
105
+ return configs if configs.is_a?(Array)
105
106
 
106
107
  build_db_config = configs.each_pair.flat_map do |env_name, config|
107
108
  walk_configs(env_name.to_s, "primary", config)
@@ -134,9 +135,11 @@ module ActiveRecord
134
135
  end
135
136
 
136
137
  def build_db_config_from_hash(env_name, spec_name, config)
137
- if url = config["url"]
138
+ if config.has_key?("url")
139
+ url = config["url"]
138
140
  config_without_url = config.dup
139
141
  config_without_url.delete "url"
142
+
140
143
  ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url, config_without_url)
141
144
  elsif config["database"] || (config.size == 1 && config.values.all? { |v| v.is_a? String })
142
145
  ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config)
@@ -155,7 +158,7 @@ module ActiveRecord
155
158
  configs
156
159
  else
157
160
  configs.map do |config|
158
- ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, config.spec_name, url, config.config)
161
+ ActiveRecord::DatabaseConfigurations::UrlConfig.new(config.env_name, config.spec_name, url, config.config)
159
162
  end
160
163
  end
161
164
  else
@@ -164,21 +167,38 @@ module ActiveRecord
164
167
  end
165
168
 
166
169
  def method_missing(method, *args, &blk)
167
- if Hash.method_defined?(method)
168
- ActiveSupport::Deprecation.warn \
169
- "Returning a hash from ActiveRecord::Base.configurations is deprecated. Therefore calling `#{method}` on the hash is also deprecated. Please switch to using the `configs_for` method instead to collect and iterate over database configurations."
170
- end
171
-
172
170
  case method
173
171
  when :each, :first
172
+ throw_getter_deprecation(method)
174
173
  configurations.send(method, *args, &blk)
175
174
  when :fetch
175
+ throw_getter_deprecation(method)
176
176
  configs_for(env_name: args.first)
177
177
  when :values
178
+ throw_getter_deprecation(method)
178
179
  configurations.map(&:config)
180
+ when :[]=
181
+ throw_setter_deprecation(method)
182
+
183
+ env_name = args[0]
184
+ config = args[1]
185
+
186
+ remaining_configs = configurations.reject { |db_config| db_config.env_name == env_name }
187
+ new_config = build_configs(env_name => config)
188
+ new_configs = remaining_configs + new_config
189
+
190
+ ActiveRecord::Base.configurations = new_configs
179
191
  else
180
- super
192
+ raise NotImplementedError, "`ActiveRecord::Base.configurations` in Rails 6 now returns an object instead of a hash. The `#{method}` method is not supported. Please use `configs_for` or consult the documentation for supported methods."
181
193
  end
182
194
  end
195
+
196
+ def throw_setter_deprecation(method)
197
+ ActiveSupport::Deprecation.warn("Setting `ActiveRecord::Base.configurations` with `#{method}` is deprecated. Use `ActiveRecord::Base.configurations=` directly to set the configurations instead.")
198
+ end
199
+
200
+ def throw_getter_deprecation(method)
201
+ ActiveSupport::Deprecation.warn("`ActiveRecord::Base.configurations` no longer returns a hash. Methods that act on the hash like `#{method}` are deprecated and will be removed in Rails 6.1. Use the `configs_for` method to collect and iterate over the database configurations.")
202
+ end
183
203
  end
184
204
  end
@@ -16,7 +16,7 @@ module ActiveRecord
16
16
  #
17
17
  # Options are:
18
18
  #
19
- # <tt>:env_name</tt> - The Rails environment, ie "development"
19
+ # <tt>:env_name</tt> - The Rails environment, i.e. "development"
20
20
  # <tt>:spec_name</tt> - The specification name. In a standard two-tier
21
21
  # database configuration this will default to "primary". In a multiple
22
22
  # database three-tier database configuration this corresponds to the name
@@ -56,12 +56,17 @@ module ActiveRecord
56
56
  end
57
57
 
58
58
  private
59
- def build_config(original_config, url)
60
- if /^jdbc:/.match?(url)
61
- hash = { "url" => url }
59
+
60
+ def build_url_hash(url)
61
+ if url.nil? || /^jdbc:/.match?(url)
62
+ { "url" => url }
62
63
  else
63
- hash = ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash
64
+ ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash
64
65
  end
66
+ end
67
+
68
+ def build_config(original_config, url)
69
+ hash = build_url_hash(url)
65
70
 
66
71
  if original_config[env_name]
67
72
  original_config[env_name].merge(hash)
@@ -126,16 +126,26 @@ module ActiveRecord
126
126
 
127
127
  # Raised when a foreign key constraint cannot be added because the column type does not match the referenced column type.
128
128
  class MismatchedForeignKey < StatementInvalid
129
- def initialize(adapter = nil, message: nil, sql: nil, binds: nil, table: nil, foreign_key: nil, target_table: nil, primary_key: nil)
130
- @adapter = adapter
129
+ def initialize(
130
+ message: nil,
131
+ sql: nil,
132
+ binds: nil,
133
+ table: nil,
134
+ foreign_key: nil,
135
+ target_table: nil,
136
+ primary_key: nil,
137
+ primary_key_column: nil
138
+ )
131
139
  if table
132
- msg = +<<~EOM
133
- Column `#{foreign_key}` on table `#{table}` has a type of `#{column_type(table, foreign_key)}`.
134
- This does not match column `#{primary_key}` on `#{target_table}`, which has type `#{column_type(target_table, primary_key)}`.
135
- To resolve this issue, change the type of the `#{foreign_key}` column on `#{table}` to be :integer. (For example `t.integer #{foreign_key}`).
140
+ type = primary_key_column.bigint? ? :bigint : primary_key_column.type
141
+ msg = <<~EOM.squish
142
+ Column `#{foreign_key}` on table `#{table}` does not match column `#{primary_key}` on `#{target_table}`,
143
+ which has type `#{primary_key_column.sql_type}`.
144
+ To resolve this issue, change the type of the `#{foreign_key}` column on `#{table}` to be :#{type}.
145
+ (For example `t.#{type} :#{foreign_key}`).
136
146
  EOM
137
147
  else
138
- msg = +<<~EOM
148
+ msg = <<~EOM.squish
139
149
  There is a mismatch between the foreign key and primary key column types.
140
150
  Verify that the foreign key column type and the primary key of the associated table match types.
141
151
  EOM
@@ -145,11 +155,6 @@ module ActiveRecord
145
155
  end
146
156
  super(msg, sql: sql, binds: binds)
147
157
  end
148
-
149
- private
150
- def column_type(table, column)
151
- @adapter.columns(table).detect { |c| c.name == column }.sql_type
152
- end
153
158
  end
154
159
 
155
160
  # Raised when a record cannot be inserted or updated because it would violate a not null constraint.
@@ -10,7 +10,7 @@ module ActiveRecord
10
10
  MAJOR = 6
11
11
  MINOR = 0
12
12
  TINY = 0
13
- PRE = "beta1"
13
+ PRE = "beta2"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -249,7 +249,7 @@ module ActiveRecord
249
249
  sti_column = arel_attribute(inheritance_column, table)
250
250
  sti_names = ([self] + descendants).map(&:sti_name)
251
251
 
252
- sti_column.in(sti_names)
252
+ predicate_builder.build(sti_column, sti_names)
253
253
  end
254
254
 
255
255
  # Detect the subclass from the inheritance column of attrs. If the inheritance column value
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/middleware/database_selector/resolver"
4
+
5
+ module ActiveRecord
6
+ module Middleware
7
+ # The DatabaseSelector Middleware provides a framework for automatically
8
+ # swapping from the primary to the replica database connection. Rails
9
+ # provides a basic framework to determine when to swap and allows for
10
+ # applications to write custom strategy classes to override the default
11
+ # behavior.
12
+ #
13
+ # The resolver class defines when the application should switch (i.e. read
14
+ # from the primary if a write occurred less than 2 seconds ago) and a
15
+ # resolver context class that sets a value that helps the resolver class
16
+ # decide when to switch.
17
+ #
18
+ # Rails default middleware uses the request's session to set a timestamp
19
+ # that informs the application when to read from a primary or read from a
20
+ # replica.
21
+ #
22
+ # To use the DatabaseSelector in your application with default settings add
23
+ # the following options to your environment config:
24
+ #
25
+ # config.active_record.database_selector = { delay: 2.seconds }
26
+ # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
27
+ # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
28
+ #
29
+ # New applications will include these lines commented out in the production.rb.
30
+ #
31
+ # The default behavior can be changed by setting the config options to a
32
+ # custom class:
33
+ #
34
+ # config.active_record.database_selector = { delay: 2.seconds }
35
+ # config.active_record.database_resolver = MyResolver
36
+ # config.active_record.database_resolver_context = MyResolver::MySession
37
+ class DatabaseSelector
38
+ def initialize(app, resolver_klass = Resolver, context_klass = Resolver::Session, options = {})
39
+ @app = app
40
+ @resolver_klass = resolver_klass
41
+ @context_klass = context_klass
42
+ @options = options
43
+ end
44
+
45
+ attr_reader :resolver_klass, :context_klass, :options
46
+
47
+ # Middleware that determines which database connection to use in a multiple
48
+ # database application.
49
+ def call(env)
50
+ request = ActionDispatch::Request.new(env)
51
+
52
+ select_database(request) do
53
+ @app.call(env)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def select_database(request, &blk)
60
+ context = context_klass.call(request)
61
+ resolver = resolver_klass.call(context, options)
62
+
63
+ if reading_request?(request)
64
+ resolver.read(&blk)
65
+ else
66
+ resolver.write(&blk)
67
+ end
68
+ end
69
+
70
+ def reading_request?(request)
71
+ request.get? || request.head?
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/middleware/database_selector/resolver/session"
4
+
5
+ module ActiveRecord
6
+ module Middleware
7
+ class DatabaseSelector
8
+ # The Resolver class is used by the DatabaseSelector middleware to
9
+ # determine which database the request should use.
10
+ #
11
+ # To change the behavior of the Resolver class in your application,
12
+ # create a custom resolver class that inherits from
13
+ # DatabaseSelector::Resolver and implements the methods that need to
14
+ # be changed.
15
+ #
16
+ # By default the Resolver class will send read traffic to the replica
17
+ # if it's been 2 seconds since the last write.
18
+ class Resolver # :nodoc:
19
+ SEND_TO_REPLICA_DELAY = 2.seconds
20
+
21
+ def self.call(context, options = {})
22
+ new(context, options)
23
+ end
24
+
25
+ def initialize(context, options = {})
26
+ @context = context
27
+ @options = options
28
+ @delay = @options && @options[:delay] ? @options[:delay] : SEND_TO_REPLICA_DELAY
29
+ @instrumenter = ActiveSupport::Notifications.instrumenter
30
+ end
31
+
32
+ attr_reader :context, :delay, :instrumenter
33
+
34
+ def read(&blk)
35
+ if read_from_primary?
36
+ read_from_primary(&blk)
37
+ else
38
+ read_from_replica(&blk)
39
+ end
40
+ end
41
+
42
+ def write(&blk)
43
+ write_to_primary(&blk)
44
+ end
45
+
46
+ private
47
+
48
+ def read_from_primary(&blk)
49
+ ActiveRecord::Base.connection.while_preventing_writes do
50
+ ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
51
+ instrumenter.instrument("database_selector.active_record.read_from_primary") do
52
+ yield
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def read_from_replica(&blk)
59
+ ActiveRecord::Base.connected_to(role: ActiveRecord::Base.reading_role) do
60
+ instrumenter.instrument("database_selector.active_record.read_from_replica") do
61
+ yield
62
+ end
63
+ end
64
+ end
65
+
66
+ def write_to_primary(&blk)
67
+ ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
68
+ instrumenter.instrument("database_selector.active_record.wrote_to_primary") do
69
+ yield
70
+ ensure
71
+ context.update_last_write_timestamp
72
+ end
73
+ end
74
+ end
75
+
76
+ def read_from_primary?
77
+ !time_since_last_write_ok?
78
+ end
79
+
80
+ def send_to_replica_delay
81
+ delay
82
+ end
83
+
84
+ def time_since_last_write_ok?
85
+ Time.now - context.last_write_timestamp >= send_to_replica_delay
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Middleware
5
+ class DatabaseSelector
6
+ class Resolver
7
+ # The session class is used by the DatabaseSelector::Resolver to save
8
+ # timestamps of the last write in the session.
9
+ #
10
+ # The last_write is used to determine whether it's safe to read
11
+ # from the replica or the request needs to be sent to the primary.
12
+ class Session # :nodoc:
13
+ def self.call(request)
14
+ new(request.session)
15
+ end
16
+
17
+ # Converts time to a timestamp that represents milliseconds since
18
+ # epoch.
19
+ def self.convert_time_to_timestamp(time)
20
+ time.to_i * 1000 + time.usec / 1000
21
+ end
22
+
23
+ # Converts milliseconds since epoch timestamp into a time object.
24
+ def self.convert_timestamp_to_time(timestamp)
25
+ timestamp ? Time.at(timestamp / 1000, (timestamp % 1000) * 1000) : Time.at(0)
26
+ end
27
+
28
+ def initialize(session)
29
+ @session = session
30
+ end
31
+
32
+ attr_reader :session
33
+
34
+ def last_write_timestamp
35
+ self.class.convert_timestamp_to_time(session[:last_write])
36
+ end
37
+
38
+ def update_last_write_timestamp
39
+ session[:last_write] = self.class.convert_time_to_timestamp(Time.now)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end