activerecord 6.0.4.8 → 6.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +764 -883
- data/MIT-LICENSE +1 -1
- data/README.rdoc +3 -3
- data/lib/active_record/aggregations.rb +1 -1
- data/lib/active_record/association_relation.rb +22 -14
- data/lib/active_record/associations/alias_tracker.rb +19 -15
- data/lib/active_record/associations/association.rb +39 -27
- data/lib/active_record/associations/association_scope.rb +11 -15
- data/lib/active_record/associations/belongs_to_association.rb +15 -5
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +1 -1
- data/lib/active_record/associations/builder/association.rb +9 -3
- data/lib/active_record/associations/builder/belongs_to.rb +10 -7
- data/lib/active_record/associations/builder/collection_association.rb +5 -4
- data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +0 -1
- data/lib/active_record/associations/builder/has_many.rb +6 -2
- data/lib/active_record/associations/builder/has_one.rb +11 -14
- data/lib/active_record/associations/builder/singular_association.rb +1 -1
- data/lib/active_record/associations/collection_association.rb +19 -13
- data/lib/active_record/associations/collection_proxy.rb +12 -5
- data/lib/active_record/associations/foreign_association.rb +13 -0
- data/lib/active_record/associations/has_many_association.rb +24 -2
- data/lib/active_record/associations/has_many_through_association.rb +10 -4
- data/lib/active_record/associations/has_one_association.rb +15 -1
- data/lib/active_record/associations/join_dependency/join_association.rb +29 -14
- data/lib/active_record/associations/join_dependency/join_part.rb +1 -1
- data/lib/active_record/associations/join_dependency.rb +63 -49
- data/lib/active_record/associations/preloader/association.rb +13 -5
- data/lib/active_record/associations/preloader/through_association.rb +1 -1
- data/lib/active_record/associations/preloader.rb +5 -3
- data/lib/active_record/associations/singular_association.rb +1 -1
- data/lib/active_record/associations.rb +114 -11
- data/lib/active_record/attribute_assignment.rb +10 -8
- data/lib/active_record/attribute_methods/before_type_cast.rb +13 -9
- data/lib/active_record/attribute_methods/dirty.rb +1 -11
- data/lib/active_record/attribute_methods/primary_key.rb +6 -2
- data/lib/active_record/attribute_methods/query.rb +3 -6
- data/lib/active_record/attribute_methods/read.rb +8 -11
- data/lib/active_record/attribute_methods/serialization.rb +4 -4
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +12 -13
- data/lib/active_record/attribute_methods/write.rb +12 -20
- data/lib/active_record/attribute_methods.rb +52 -48
- data/lib/active_record/attributes.rb +27 -7
- data/lib/active_record/autosave_association.rb +47 -30
- data/lib/active_record/base.rb +2 -14
- data/lib/active_record/callbacks.rb +32 -22
- data/lib/active_record/coders/yaml_column.rb +1 -1
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +180 -134
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +2 -44
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +65 -22
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +2 -7
- data/lib/active_record/connection_adapters/abstract/quoting.rb +34 -34
- data/lib/active_record/connection_adapters/abstract/savepoints.rb +3 -3
- data/lib/active_record/connection_adapters/abstract/schema_creation.rb +153 -116
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +110 -30
- data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +3 -3
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +224 -85
- data/lib/active_record/connection_adapters/abstract/transaction.rb +66 -24
- data/lib/active_record/connection_adapters/abstract_adapter.rb +31 -70
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +123 -87
- data/lib/active_record/connection_adapters/column.rb +15 -1
- data/lib/active_record/connection_adapters/deduplicable.rb +29 -0
- data/lib/active_record/connection_adapters/legacy_pool_manager.rb +31 -0
- data/lib/active_record/connection_adapters/mysql/database_statements.rb +22 -24
- data/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb +1 -1
- data/lib/active_record/connection_adapters/mysql/quoting.rb +1 -1
- data/lib/active_record/connection_adapters/mysql/schema_creation.rb +33 -6
- data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +8 -0
- data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +1 -1
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +3 -3
- data/lib/active_record/connection_adapters/mysql/type_metadata.rb +10 -1
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +31 -12
- data/lib/active_record/connection_adapters/pool_config.rb +63 -0
- data/lib/active_record/connection_adapters/pool_manager.rb +43 -0
- data/lib/active_record/connection_adapters/postgresql/column.rb +24 -1
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +12 -53
- data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +3 -5
- data/lib/active_record/connection_adapters/postgresql/oid/date.rb +2 -2
- data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +2 -10
- data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +49 -0
- data/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb +2 -2
- data/lib/active_record/connection_adapters/postgresql/oid/macaddr.rb +25 -0
- data/lib/active_record/connection_adapters/postgresql/oid/money.rb +2 -2
- data/lib/active_record/connection_adapters/postgresql/oid/point.rb +2 -2
- data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +11 -1
- data/lib/active_record/connection_adapters/postgresql/oid.rb +2 -0
- data/lib/active_record/connection_adapters/postgresql/quoting.rb +4 -4
- data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +5 -1
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +61 -29
- data/lib/active_record/connection_adapters/postgresql/type_metadata.rb +8 -0
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +72 -55
- data/lib/active_record/connection_adapters/schema_cache.rb +98 -15
- data/lib/active_record/connection_adapters/sql_type_metadata.rb +10 -0
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +30 -5
- data/lib/active_record/connection_adapters/sqlite3/quoting.rb +1 -1
- data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +5 -1
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +36 -3
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +48 -50
- data/lib/active_record/connection_adapters.rb +50 -0
- data/lib/active_record/connection_handling.rb +210 -71
- data/lib/active_record/core.rb +215 -49
- data/lib/active_record/database_configurations/connection_url_resolver.rb +98 -0
- data/lib/active_record/database_configurations/database_config.rb +52 -9
- data/lib/active_record/database_configurations/hash_config.rb +54 -8
- data/lib/active_record/database_configurations/url_config.rb +15 -40
- data/lib/active_record/database_configurations.rb +124 -85
- data/lib/active_record/delegated_type.rb +209 -0
- data/lib/active_record/destroy_association_async_job.rb +36 -0
- data/lib/active_record/enum.rb +33 -23
- data/lib/active_record/errors.rb +47 -12
- data/lib/active_record/explain.rb +9 -4
- data/lib/active_record/explain_subscriber.rb +1 -1
- data/lib/active_record/fixture_set/file.rb +10 -17
- data/lib/active_record/fixture_set/model_metadata.rb +1 -2
- data/lib/active_record/fixture_set/render_context.rb +1 -1
- data/lib/active_record/fixture_set/table_row.rb +2 -2
- data/lib/active_record/fixtures.rb +54 -8
- data/lib/active_record/gem_version.rb +3 -3
- data/lib/active_record/inheritance.rb +40 -18
- data/lib/active_record/insert_all.rb +32 -5
- data/lib/active_record/integration.rb +3 -5
- data/lib/active_record/internal_metadata.rb +15 -4
- data/lib/active_record/legacy_yaml_adapter.rb +7 -3
- data/lib/active_record/locking/optimistic.rb +13 -16
- data/lib/active_record/locking/pessimistic.rb +6 -2
- data/lib/active_record/log_subscriber.rb +26 -8
- data/lib/active_record/middleware/database_selector/resolver/session.rb +3 -0
- data/lib/active_record/middleware/database_selector/resolver.rb +5 -0
- data/lib/active_record/middleware/database_selector.rb +4 -1
- data/lib/active_record/migration/command_recorder.rb +47 -27
- data/lib/active_record/migration/compatibility.rb +67 -17
- data/lib/active_record/migration.rb +113 -83
- data/lib/active_record/model_schema.rb +88 -42
- data/lib/active_record/nested_attributes.rb +2 -3
- data/lib/active_record/no_touching.rb +1 -1
- data/lib/active_record/persistence.rb +50 -45
- data/lib/active_record/query_cache.rb +15 -5
- data/lib/active_record/querying.rb +11 -6
- data/lib/active_record/railtie.rb +64 -44
- data/lib/active_record/railties/databases.rake +253 -98
- data/lib/active_record/readonly_attributes.rb +4 -0
- data/lib/active_record/reflection.rb +59 -44
- data/lib/active_record/relation/batches/batch_enumerator.rb +25 -9
- data/lib/active_record/relation/batches.rb +38 -31
- data/lib/active_record/relation/calculations.rb +100 -43
- data/lib/active_record/relation/finder_methods.rb +44 -14
- data/lib/active_record/relation/from_clause.rb +1 -1
- data/lib/active_record/relation/merger.rb +20 -23
- data/lib/active_record/relation/predicate_builder/array_handler.rb +8 -9
- data/lib/active_record/relation/predicate_builder/association_query_value.rb +2 -2
- data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +3 -3
- data/lib/active_record/relation/predicate_builder/relation_handler.rb +1 -1
- data/lib/active_record/relation/predicate_builder.rb +57 -33
- data/lib/active_record/relation/query_methods.rb +319 -196
- data/lib/active_record/relation/record_fetch_warning.rb +3 -3
- data/lib/active_record/relation/spawn_methods.rb +6 -5
- data/lib/active_record/relation/where_clause.rb +104 -57
- data/lib/active_record/relation.rb +90 -64
- data/lib/active_record/result.rb +41 -33
- data/lib/active_record/runtime_registry.rb +2 -2
- data/lib/active_record/sanitization.rb +6 -17
- data/lib/active_record/schema_dumper.rb +34 -4
- data/lib/active_record/schema_migration.rb +0 -4
- data/lib/active_record/scoping/named.rb +1 -17
- data/lib/active_record/secure_token.rb +16 -8
- data/lib/active_record/serialization.rb +5 -3
- data/lib/active_record/signed_id.rb +116 -0
- data/lib/active_record/statement_cache.rb +20 -4
- data/lib/active_record/store.rb +2 -2
- data/lib/active_record/suppressor.rb +2 -2
- data/lib/active_record/table_metadata.rb +36 -52
- data/lib/active_record/tasks/database_tasks.rb +139 -113
- data/lib/active_record/tasks/mysql_database_tasks.rb +34 -35
- data/lib/active_record/tasks/postgresql_database_tasks.rb +24 -26
- data/lib/active_record/tasks/sqlite_database_tasks.rb +13 -9
- data/lib/active_record/test_databases.rb +5 -4
- data/lib/active_record/test_fixtures.rb +36 -33
- data/lib/active_record/timestamp.rb +4 -6
- data/lib/active_record/touch_later.rb +21 -21
- data/lib/active_record/transactions.rb +15 -64
- data/lib/active_record/type/serialized.rb +6 -2
- data/lib/active_record/type.rb +8 -1
- data/lib/active_record/type_caster/connection.rb +0 -1
- data/lib/active_record/type_caster/map.rb +8 -5
- data/lib/active_record/validations/associated.rb +1 -1
- data/lib/active_record/validations/numericality.rb +35 -0
- data/lib/active_record/validations/uniqueness.rb +24 -4
- data/lib/active_record/validations.rb +1 -0
- data/lib/active_record.rb +7 -14
- data/lib/arel/attributes/attribute.rb +4 -0
- data/lib/arel/collectors/bind.rb +5 -0
- data/lib/arel/collectors/composite.rb +8 -0
- data/lib/arel/collectors/sql_string.rb +7 -0
- data/lib/arel/collectors/substitute_binds.rb +7 -0
- data/lib/arel/nodes/binary.rb +82 -8
- data/lib/arel/nodes/bind_param.rb +8 -0
- data/lib/arel/nodes/casted.rb +21 -9
- data/lib/arel/nodes/equality.rb +6 -9
- data/lib/arel/nodes/grouping.rb +3 -0
- data/lib/arel/nodes/homogeneous_in.rb +72 -0
- data/lib/arel/nodes/in.rb +8 -1
- data/lib/arel/nodes/infix_operation.rb +13 -1
- data/lib/arel/nodes/join_source.rb +1 -1
- data/lib/arel/nodes/node.rb +7 -6
- data/lib/arel/nodes/ordering.rb +27 -0
- data/lib/arel/nodes/sql_literal.rb +3 -0
- data/lib/arel/nodes/table_alias.rb +7 -3
- data/lib/arel/nodes/unary.rb +0 -1
- data/lib/arel/nodes.rb +3 -1
- data/lib/arel/predications.rb +12 -18
- data/lib/arel/select_manager.rb +1 -2
- data/lib/arel/table.rb +13 -5
- data/lib/arel/visitors/dot.rb +14 -2
- data/lib/arel/visitors/mysql.rb +11 -1
- data/lib/arel/visitors/postgresql.rb +15 -4
- data/lib/arel/visitors/to_sql.rb +89 -78
- data/lib/arel/visitors.rb +0 -7
- data/lib/arel.rb +5 -13
- data/lib/rails/generators/active_record/migration/migration_generator.rb +1 -0
- data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +2 -0
- data/lib/rails/generators/active_record/migration/templates/migration.rb.tt +3 -3
- data/lib/rails/generators/active_record/migration.rb +6 -1
- data/lib/rails/generators/active_record/model/model_generator.rb +39 -2
- data/lib/rails/generators/active_record/model/templates/abstract_base_class.rb.tt +7 -0
- metadata +30 -31
- data/lib/active_record/advisory_lock_base.rb +0 -18
- data/lib/active_record/attribute_decorators.rb +0 -88
- data/lib/active_record/connection_adapters/connection_specification.rb +0 -296
- data/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +0 -29
- data/lib/active_record/define_callbacks.rb +0 -22
- data/lib/active_record/railties/collection_cache_association_loading.rb +0 -34
- data/lib/active_record/relation/predicate_builder/base_handler.rb +0 -18
- data/lib/active_record/relation/where_clause_factory.rb +0 -33
- data/lib/arel/attributes.rb +0 -22
- data/lib/arel/visitors/depth_first.rb +0 -203
- data/lib/arel/visitors/ibm_db.rb +0 -34
- data/lib/arel/visitors/informix.rb +0 -62
- data/lib/arel/visitors/mssql.rb +0 -156
- data/lib/arel/visitors/oracle.rb +0 -158
- data/lib/arel/visitors/oracle12.rb +0 -65
- data/lib/arel/visitors/where_sql.rb +0 -22
@@ -13,14 +13,14 @@ module ActiveRecord
|
|
13
13
|
# Becomes:
|
14
14
|
#
|
15
15
|
# #<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fdc3238f340
|
16
|
-
# @env_name="default_env", @
|
17
|
-
# @config={
|
16
|
+
# @env_name="default_env", @name="primary",
|
17
|
+
# @config={adapter: "postgresql", database: "foo", host: "localhost"},
|
18
18
|
# @url="postgres://localhost/foo">
|
19
19
|
#
|
20
20
|
# ==== Options
|
21
21
|
#
|
22
22
|
# * <tt>:env_name</tt> - The Rails environment, ie "development".
|
23
|
-
# * <tt>:
|
23
|
+
# * <tt>:name</tt> - The db config name. In a standard two-tier
|
24
24
|
# database configuration this will default to "primary". In a multiple
|
25
25
|
# database three-tier database configuration this corresponds to the name
|
26
26
|
# used in the second tier, for example "primary_readonly".
|
@@ -28,49 +28,24 @@ module ActiveRecord
|
|
28
28
|
# * <tt>:config</tt> - The config hash. This is the hash that contains the
|
29
29
|
# database adapter, name, and other important information for database
|
30
30
|
# connections.
|
31
|
-
class UrlConfig <
|
32
|
-
attr_reader :url
|
31
|
+
class UrlConfig < HashConfig
|
32
|
+
attr_reader :url
|
33
33
|
|
34
|
-
def initialize(env_name,
|
35
|
-
super(env_name,
|
36
|
-
@config = build_config(config, url)
|
37
|
-
@url = url
|
38
|
-
end
|
39
|
-
|
40
|
-
def url_config? # :nodoc:
|
41
|
-
true
|
42
|
-
end
|
43
|
-
|
44
|
-
# Determines whether a database configuration is for a replica / readonly
|
45
|
-
# connection. If the +replica+ key is present in the config, +replica?+ will
|
46
|
-
# return +true+.
|
47
|
-
def replica?
|
48
|
-
config["replica"]
|
49
|
-
end
|
34
|
+
def initialize(env_name, name, url, configuration_hash = {})
|
35
|
+
super(env_name, name, configuration_hash)
|
50
36
|
|
51
|
-
|
52
|
-
|
53
|
-
# will return its value.
|
54
|
-
def migrations_paths
|
55
|
-
config["migrations_paths"]
|
37
|
+
@url = url
|
38
|
+
@configuration_hash = @configuration_hash.merge(build_url_hash).freeze
|
56
39
|
end
|
57
40
|
|
58
41
|
private
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def build_config(original_config, url)
|
68
|
-
hash = build_url_hash(url)
|
69
|
-
|
70
|
-
if original_config[env_name]
|
71
|
-
original_config[env_name].merge(hash)
|
42
|
+
# Return a Hash that can be merged into the main config that represents
|
43
|
+
# the passed in url
|
44
|
+
def build_url_hash
|
45
|
+
if url.nil? || url.start_with?("jdbc:")
|
46
|
+
{ url: url }
|
72
47
|
else
|
73
|
-
|
48
|
+
ConnectionUrlResolver.new(url).to_hash
|
74
49
|
end
|
75
50
|
end
|
76
51
|
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require "active_record/database_configurations/database_config"
|
4
4
|
require "active_record/database_configurations/hash_config"
|
5
5
|
require "active_record/database_configurations/url_config"
|
6
|
+
require "active_record/database_configurations/connection_url_resolver"
|
6
7
|
|
7
8
|
module ActiveRecord
|
8
9
|
# ActiveRecord::DatabaseConfigurations returns an array of DatabaseConfig
|
@@ -21,7 +22,7 @@ module ActiveRecord
|
|
21
22
|
# Collects the configs for the environment and optionally the specification
|
22
23
|
# name passed in. To include replica configurations pass <tt>include_replicas: true</tt>.
|
23
24
|
#
|
24
|
-
# If a
|
25
|
+
# If a name is provided a single DatabaseConfig object will be
|
25
26
|
# returned, otherwise an array of DatabaseConfig objects will be
|
26
27
|
# returned that corresponds with the environment and type requested.
|
27
28
|
#
|
@@ -29,13 +30,20 @@ module ActiveRecord
|
|
29
30
|
#
|
30
31
|
# * <tt>env_name:</tt> The environment name. Defaults to +nil+ which will collect
|
31
32
|
# configs for all environments.
|
32
|
-
# * <tt>
|
33
|
-
# to +nil+.
|
33
|
+
# * <tt>name:</tt> The db config name (i.e. primary, animals, etc.). Defaults
|
34
|
+
# to +nil+. If no +env_name+ is specified the config for the default env and the
|
35
|
+
# passed +name+ will be returned.
|
34
36
|
# * <tt>include_replicas:</tt> Determines whether to include replicas in
|
35
37
|
# the returned list. Most of the time we're only iterating over the write
|
36
38
|
# connection (i.e. migrations don't need to run for the write and read connection).
|
37
39
|
# Defaults to +false+.
|
38
|
-
def configs_for(env_name: nil, spec_name: nil, include_replicas: false)
|
40
|
+
def configs_for(env_name: nil, spec_name: nil, name: nil, include_replicas: false)
|
41
|
+
if spec_name
|
42
|
+
name = spec_name
|
43
|
+
ActiveSupport::Deprecation.warn("The kwarg `spec_name` is deprecated in favor of `name`. `spec_name` will be removed in Rails 6.2")
|
44
|
+
end
|
45
|
+
|
46
|
+
env_name ||= default_env if name
|
39
47
|
configs = env_with_configs(env_name)
|
40
48
|
|
41
49
|
unless include_replicas
|
@@ -44,9 +52,9 @@ module ActiveRecord
|
|
44
52
|
end
|
45
53
|
end
|
46
54
|
|
47
|
-
if
|
55
|
+
if name
|
48
56
|
configs.find do |db_config|
|
49
|
-
db_config.
|
57
|
+
db_config.name == name
|
50
58
|
end
|
51
59
|
else
|
52
60
|
configs
|
@@ -59,31 +67,46 @@ module ActiveRecord
|
|
59
67
|
# return the first config hash for the environment.
|
60
68
|
#
|
61
69
|
# { database: "my_db", adapter: "mysql2" }
|
62
|
-
def default_hash(env =
|
70
|
+
def default_hash(env = default_env)
|
63
71
|
default = find_db_config(env)
|
64
|
-
default.
|
72
|
+
default.configuration_hash if default
|
65
73
|
end
|
66
74
|
alias :[] :default_hash
|
75
|
+
deprecate "[]": "Use configs_for", default_hash: "Use configs_for"
|
67
76
|
|
68
77
|
# Returns a single DatabaseConfig object based on the requested environment.
|
69
78
|
#
|
70
79
|
# If the application has multiple databases +find_db_config+ will return
|
71
80
|
# the first DatabaseConfig for the environment.
|
72
81
|
def find_db_config(env)
|
73
|
-
configurations
|
74
|
-
|
75
|
-
|
76
|
-
|
82
|
+
configurations
|
83
|
+
.sort_by.with_index { |db_config, i| db_config.for_current_env? ? [0, i] : [1, i] }
|
84
|
+
.find do |db_config|
|
85
|
+
db_config.env_name == env.to_s ||
|
86
|
+
(db_config.for_current_env? && db_config.name == env.to_s)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# A primary configuration is one that is named primary or if there is
|
91
|
+
# no primary, the first configuration for an environment will be treated
|
92
|
+
# as primary. This is used as the "default" configuration and is used
|
93
|
+
# when the application needs to treat one configuration differently. For
|
94
|
+
# example, when Rails dumps the schema, the primary configuration's schema
|
95
|
+
# file will be named `schema.rb` instead of `primary_schema.rb`.
|
96
|
+
def primary?(name) # :nodoc:
|
97
|
+
return true if name == "primary"
|
98
|
+
|
99
|
+
first_config = find_db_config(default_env)
|
100
|
+
first_config && name == first_config.name
|
77
101
|
end
|
78
102
|
|
79
103
|
# Returns the DatabaseConfigurations object as a Hash.
|
80
104
|
def to_h
|
81
|
-
|
82
|
-
memo.merge(db_config.
|
105
|
+
configurations.inject({}) do |memo, db_config|
|
106
|
+
memo.merge(db_config.env_name => db_config.configuration_hash.stringify_keys)
|
83
107
|
end
|
84
|
-
|
85
|
-
Hash[configs.to_a.reverse]
|
86
108
|
end
|
109
|
+
deprecate to_h: "You can use `ActiveRecord::Base.configurations.configs_for(env_name: 'env', name: 'primary').configuration_hash` to get the configuration hashes."
|
87
110
|
|
88
111
|
# Checks if the application's configurations are empty.
|
89
112
|
#
|
@@ -93,20 +116,43 @@ module ActiveRecord
|
|
93
116
|
end
|
94
117
|
alias :blank? :empty?
|
95
118
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
119
|
+
# Returns fully resolved connection, accepts hash, string or symbol.
|
120
|
+
# Always returns a DatabaseConfiguration::DatabaseConfig
|
121
|
+
#
|
122
|
+
# == Examples
|
123
|
+
#
|
124
|
+
# Symbol representing current environment.
|
125
|
+
#
|
126
|
+
# DatabaseConfigurations.new("production" => {}).resolve(:production)
|
127
|
+
# # => DatabaseConfigurations::HashConfig.new(env_name: "production", config: {})
|
128
|
+
#
|
129
|
+
# One layer deep hash of connection values.
|
130
|
+
#
|
131
|
+
# DatabaseConfigurations.new({}).resolve("adapter" => "sqlite3")
|
132
|
+
# # => DatabaseConfigurations::HashConfig.new(config: {"adapter" => "sqlite3"})
|
133
|
+
#
|
134
|
+
# Connection URL.
|
135
|
+
#
|
136
|
+
# DatabaseConfigurations.new({}).resolve("postgresql://localhost/foo")
|
137
|
+
# # => DatabaseConfigurations::UrlConfig.new(config: {"adapter" => "postgresql", "host" => "localhost", "database" => "foo"})
|
138
|
+
def resolve(config) # :nodoc:
|
139
|
+
return config if DatabaseConfigurations::DatabaseConfig === config
|
140
|
+
|
141
|
+
case config
|
142
|
+
when Symbol
|
143
|
+
resolve_symbol_connection(config)
|
144
|
+
when Hash, String
|
145
|
+
build_db_config_from_raw_config(default_env, "primary", config)
|
146
|
+
else
|
147
|
+
raise TypeError, "Invalid type for configuration. Expected Symbol, String, or Hash. Got #{config.inspect}"
|
148
|
+
end
|
107
149
|
end
|
108
150
|
|
109
151
|
private
|
152
|
+
def default_env
|
153
|
+
ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s
|
154
|
+
end
|
155
|
+
|
110
156
|
def env_with_configs(env = nil)
|
111
157
|
if env
|
112
158
|
configurations.select { |db_config| db_config.env_name == env }
|
@@ -127,107 +173,100 @@ module ActiveRecord
|
|
127
173
|
end
|
128
174
|
end
|
129
175
|
|
130
|
-
current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s
|
131
|
-
|
132
176
|
unless db_configs.find(&:for_current_env?)
|
133
|
-
db_configs << environment_url_config(
|
177
|
+
db_configs << environment_url_config(default_env, "primary", {})
|
134
178
|
end
|
135
179
|
|
136
|
-
merge_db_environment_variables(
|
180
|
+
merge_db_environment_variables(default_env, db_configs.compact)
|
137
181
|
end
|
138
182
|
|
139
183
|
def walk_configs(env_name, config)
|
140
|
-
config.map do |
|
141
|
-
build_db_config_from_raw_config(env_name,
|
184
|
+
config.map do |name, sub_config|
|
185
|
+
build_db_config_from_raw_config(env_name, name.to_s, sub_config)
|
142
186
|
end
|
143
187
|
end
|
144
188
|
|
145
|
-
def
|
189
|
+
def resolve_symbol_connection(name)
|
190
|
+
if db_config = find_db_config(name)
|
191
|
+
db_config
|
192
|
+
else
|
193
|
+
raise AdapterNotSpecified, <<~MSG
|
194
|
+
The `#{name}` database is not configured for the `#{default_env}` environment.
|
195
|
+
|
196
|
+
Available databases configurations are:
|
197
|
+
|
198
|
+
#{build_configuration_sentence}
|
199
|
+
MSG
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def build_configuration_sentence
|
204
|
+
configs = configs_for(include_replicas: true)
|
205
|
+
|
206
|
+
configs.group_by(&:env_name).map do |env, config|
|
207
|
+
names = config.map(&:name)
|
208
|
+
if names.size > 1
|
209
|
+
"#{env}: #{names.join(", ")}"
|
210
|
+
else
|
211
|
+
env
|
212
|
+
end
|
213
|
+
end.join("\n")
|
214
|
+
end
|
215
|
+
|
216
|
+
def build_db_config_from_raw_config(env_name, name, config)
|
146
217
|
case config
|
147
218
|
when String
|
148
|
-
build_db_config_from_string(env_name,
|
219
|
+
build_db_config_from_string(env_name, name, config)
|
149
220
|
when Hash
|
150
|
-
build_db_config_from_hash(env_name,
|
221
|
+
build_db_config_from_hash(env_name, name, config.symbolize_keys)
|
151
222
|
else
|
152
223
|
raise InvalidConfigurationError, "'{ #{env_name} => #{config} }' is not a valid configuration. Expected '#{config}' to be a URL string or a Hash."
|
153
224
|
end
|
154
225
|
end
|
155
226
|
|
156
|
-
def build_db_config_from_string(env_name,
|
227
|
+
def build_db_config_from_string(env_name, name, config)
|
157
228
|
url = config
|
158
229
|
uri = URI.parse(url)
|
159
230
|
if uri.scheme
|
160
|
-
|
231
|
+
UrlConfig.new(env_name, name, url)
|
161
232
|
else
|
162
233
|
raise InvalidConfigurationError, "'{ #{env_name} => #{config} }' is not a valid configuration. Expected '#{config}' to be a URL string or a Hash."
|
163
234
|
end
|
164
235
|
end
|
165
236
|
|
166
|
-
def build_db_config_from_hash(env_name,
|
167
|
-
if config.has_key?(
|
168
|
-
url = config[
|
237
|
+
def build_db_config_from_hash(env_name, name, config)
|
238
|
+
if config.has_key?(:url)
|
239
|
+
url = config[:url]
|
169
240
|
config_without_url = config.dup
|
170
|
-
config_without_url.delete
|
241
|
+
config_without_url.delete :url
|
171
242
|
|
172
|
-
|
243
|
+
UrlConfig.new(env_name, name, url, config_without_url)
|
173
244
|
else
|
174
|
-
|
245
|
+
HashConfig.new(env_name, name, config)
|
175
246
|
end
|
176
247
|
end
|
177
248
|
|
178
249
|
def merge_db_environment_variables(current_env, configs)
|
179
250
|
configs.map do |config|
|
180
|
-
next config if config.
|
251
|
+
next config if config.is_a?(UrlConfig) || config.env_name != current_env
|
181
252
|
|
182
|
-
url_config = environment_url_config(current_env, config.
|
253
|
+
url_config = environment_url_config(current_env, config.name, config.configuration_hash)
|
183
254
|
url_config || config
|
184
255
|
end
|
185
256
|
end
|
186
257
|
|
187
|
-
def environment_url_config(env,
|
188
|
-
url = environment_value_for(
|
258
|
+
def environment_url_config(env, name, config)
|
259
|
+
url = environment_value_for(name)
|
189
260
|
return unless url
|
190
261
|
|
191
|
-
|
262
|
+
UrlConfig.new(env, name, url, config)
|
192
263
|
end
|
193
264
|
|
194
|
-
def environment_value_for(
|
195
|
-
|
196
|
-
url = ENV[
|
197
|
-
url ||= ENV["DATABASE_URL"] if
|
265
|
+
def environment_value_for(name)
|
266
|
+
name_env_key = "#{name.upcase}_DATABASE_URL"
|
267
|
+
url = ENV[name_env_key]
|
268
|
+
url ||= ENV["DATABASE_URL"] if name == "primary"
|
198
269
|
url
|
199
270
|
end
|
200
|
-
|
201
|
-
def method_missing(method, *args, &blk)
|
202
|
-
case method
|
203
|
-
when :fetch
|
204
|
-
throw_getter_deprecation(method)
|
205
|
-
configs_for(env_name: args.first)
|
206
|
-
when :values
|
207
|
-
throw_getter_deprecation(method)
|
208
|
-
configurations.map(&:config)
|
209
|
-
when :[]=
|
210
|
-
throw_setter_deprecation(method)
|
211
|
-
|
212
|
-
env_name = args[0]
|
213
|
-
config = args[1]
|
214
|
-
|
215
|
-
remaining_configs = configurations.reject { |db_config| db_config.env_name == env_name }
|
216
|
-
new_config = build_configs(env_name => config)
|
217
|
-
new_configs = remaining_configs + new_config
|
218
|
-
|
219
|
-
ActiveRecord::Base.configurations = new_configs
|
220
|
-
else
|
221
|
-
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."
|
222
|
-
end
|
223
|
-
end
|
224
|
-
|
225
|
-
def throw_setter_deprecation(method)
|
226
|
-
ActiveSupport::Deprecation.warn("Setting `ActiveRecord::Base.configurations` with `#{method}` is deprecated. Use `ActiveRecord::Base.configurations=` directly to set the configurations instead.")
|
227
|
-
end
|
228
|
-
|
229
|
-
def throw_getter_deprecation(method)
|
230
|
-
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.")
|
231
|
-
end
|
232
271
|
end
|
233
272
|
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/string/inquiry"
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
# == Delegated types
|
7
|
+
#
|
8
|
+
# Class hierarchies can map to relational database tables in many ways. Active Record, for example, offers
|
9
|
+
# purely abstract classes, where the superclass doesn't persist any attributes, and single-table inheritance,
|
10
|
+
# where all attributes from all levels of the hierarchy are represented in a single table. Both have their
|
11
|
+
# places, but neither are without their drawbacks.
|
12
|
+
#
|
13
|
+
# The problem with purely abstract classes is that all concrete subclasses must persist all the shared
|
14
|
+
# attributes themselves in their own tables (also known as class-table inheritance). This makes it hard to
|
15
|
+
# do queries across the hierarchy. For example, imagine you have the following hierarchy:
|
16
|
+
#
|
17
|
+
# Entry < ApplicationRecord
|
18
|
+
# Message < Entry
|
19
|
+
# Comment < Entry
|
20
|
+
#
|
21
|
+
# How do you show a feed that has both +Message+ and +Comment+ records, which can be easily paginated?
|
22
|
+
# Well, you can't! Messages are backed by a messages table and comments by a comments table. You can't
|
23
|
+
# pull from both tables at once and use a consistent OFFSET/LIMIT scheme.
|
24
|
+
#
|
25
|
+
# You can get around the pagination problem by using single-table inheritance, but now you're forced into
|
26
|
+
# a single mega table with all the attributes from all subclasses. No matter how divergent. If a Message
|
27
|
+
# has a subject, but the comment does not, well, now the comment does anyway! So STI works best when there's
|
28
|
+
# little divergence between the subclasses and their attributes.
|
29
|
+
#
|
30
|
+
# But there's a third way: Delegated types. With this approach, the "superclass" is a concrete class
|
31
|
+
# that is represented by its own table, where all the superclass attributes that are shared amongst all the
|
32
|
+
# "subclasses" are stored. And then each of the subclasses have their own individual tables for additional
|
33
|
+
# attributes that are particular to their implementation. This is similar to what's called multi-table
|
34
|
+
# inheritance in Django, but instead of actual inheritance, this approach uses delegation to form the
|
35
|
+
# hierarchy and share responsibilities.
|
36
|
+
#
|
37
|
+
# Let's look at that entry/message/comment example using delegated types:
|
38
|
+
#
|
39
|
+
# # Schema: entries[ id, account_id, creator_id, created_at, updated_at, entryable_type, entryable_id ]
|
40
|
+
# class Entry < ApplicationRecord
|
41
|
+
# belongs_to :account
|
42
|
+
# belongs_to :creator
|
43
|
+
# delegated_type :entryable, types: %w[ Message Comment ]
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# module Entryable
|
47
|
+
# extend ActiveSupport::Concern
|
48
|
+
#
|
49
|
+
# included do
|
50
|
+
# has_one :entry, as: :entryable, touch: true
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# # Schema: messages[ id, subject ]
|
55
|
+
# class Message < ApplicationRecord
|
56
|
+
# include Entryable
|
57
|
+
# has_rich_text :content
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# # Schema: comments[ id, content ]
|
61
|
+
# class Comment < ApplicationRecord
|
62
|
+
# include Entryable
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# As you can see, neither +Message+ nor +Comment+ are meant to stand alone. Crucial metadata for both classes
|
66
|
+
# resides in the +Entry+ "superclass". But the +Entry+ absolutely can stand alone in terms of querying capacity
|
67
|
+
# in particular. You can now easily do things like:
|
68
|
+
#
|
69
|
+
# Account.entries.order(created_at: :desc).limit(50)
|
70
|
+
#
|
71
|
+
# Which is exactly what you want when displaying both comments and messages together. The entry itself can
|
72
|
+
# be rendered as its delegated type easily, like so:
|
73
|
+
#
|
74
|
+
# # entries/_entry.html.erb
|
75
|
+
# <%= render "entries/entryables/#{entry.entryable_name}", entry: entry %>
|
76
|
+
#
|
77
|
+
# # entries/entryables/_message.html.erb
|
78
|
+
# <div class="message">
|
79
|
+
# Posted on <%= entry.created_at %> by <%= entry.creator.name %>: <%= entry.message.content %>
|
80
|
+
# </div>
|
81
|
+
#
|
82
|
+
# # entries/entryables/_comment.html.erb
|
83
|
+
# <div class="comment">
|
84
|
+
# <%= entry.creator.name %> said: <%= entry.comment.content %>
|
85
|
+
# </div>
|
86
|
+
#
|
87
|
+
# == Sharing behavior with concerns and controllers
|
88
|
+
#
|
89
|
+
# The entry "superclass" also serves as a perfect place to put all that shared logic that applies to both
|
90
|
+
# messages and comments, and which acts primarily on the shared attributes. Imagine:
|
91
|
+
#
|
92
|
+
# class Entry < ApplicationRecord
|
93
|
+
# include Eventable, Forwardable, Redeliverable
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# Which allows you to have controllers for things like +ForwardsController+ and +RedeliverableController+
|
97
|
+
# that both act on entries, and thus provide the shared functionality to both messages and comments.
|
98
|
+
#
|
99
|
+
# == Creating new records
|
100
|
+
#
|
101
|
+
# You create a new record that uses delegated typing by creating the delegator and delegatee at the same time,
|
102
|
+
# like so:
|
103
|
+
#
|
104
|
+
# Entry.create! entryable: Comment.new(content: "Hello!"), creator: Current.user
|
105
|
+
#
|
106
|
+
# If you need more complicated composition, or you need to perform dependent validation, you should build a factory
|
107
|
+
# method or class to take care of the complicated needs. This could be as simple as:
|
108
|
+
#
|
109
|
+
# class Entry < ApplicationRecord
|
110
|
+
# def self.create_with_comment(content, creator: Current.user)
|
111
|
+
# create! entryable: Comment.new(content: content), creator: creator
|
112
|
+
# end
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# == Adding further delegation
|
116
|
+
#
|
117
|
+
# The delegated type shouldn't just answer the question of what the underlying class is called. In fact, that's
|
118
|
+
# an anti-pattern most of the time. The reason you're building this hierarchy is to take advantage of polymorphism.
|
119
|
+
# So here's a simple example of that:
|
120
|
+
#
|
121
|
+
# class Entry < ApplicationRecord
|
122
|
+
# delegated_type :entryable, types: %w[ Message Comment ]
|
123
|
+
# delegate :title, to: :entryable
|
124
|
+
# end
|
125
|
+
#
|
126
|
+
# class Message < ApplicationRecord
|
127
|
+
# def title
|
128
|
+
# subject
|
129
|
+
# end
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# class Comment < ApplicationRecord
|
133
|
+
# def title
|
134
|
+
# content.truncate(20)
|
135
|
+
# end
|
136
|
+
# end
|
137
|
+
#
|
138
|
+
# Now you can list a bunch of entries, call +Entry#title+, and polymorphism will provide you with the answer.
|
139
|
+
module DelegatedType
|
140
|
+
# Defines this as a class that'll delegate its type for the passed +role+ to the class references in +types+.
|
141
|
+
# That'll create a polymorphic +belongs_to+ relationship to that +role+, and it'll add all the delegated
|
142
|
+
# type convenience methods:
|
143
|
+
#
|
144
|
+
# class Entry < ApplicationRecord
|
145
|
+
# delegated_type :entryable, types: %w[ Message Comment ], dependent: :destroy
|
146
|
+
# end
|
147
|
+
#
|
148
|
+
# Entry#entryable_class # => +Message+ or +Comment+
|
149
|
+
# Entry#entryable_name # => "message" or "comment"
|
150
|
+
# Entry.messages # => Entry.where(entryable_type: "Message")
|
151
|
+
# Entry#message? # => true when entryable_type == "Message"
|
152
|
+
# Entry#message # => returns the message record, when entryable_type == "Message", otherwise nil
|
153
|
+
# Entry#message_id # => returns entryable_id, when entryable_type == "Message", otherwise nil
|
154
|
+
# Entry.comments # => Entry.where(entryable_type: "Comment")
|
155
|
+
# Entry#comment? # => true when entryable_type == "Comment"
|
156
|
+
# Entry#comment # => returns the comment record, when entryable_type == "Comment", otherwise nil
|
157
|
+
# Entry#comment_id # => returns entryable_id, when entryable_type == "Comment", otherwise nil
|
158
|
+
#
|
159
|
+
# The +options+ are passed directly to the +belongs_to+ call, so this is where you declare +dependent+ etc.
|
160
|
+
#
|
161
|
+
# You can also declare namespaced types:
|
162
|
+
#
|
163
|
+
# class Entry < ApplicationRecord
|
164
|
+
# delegated_type :entryable, types: %w[ Message Comment Access::NoticeMessage ], dependent: :destroy
|
165
|
+
# end
|
166
|
+
#
|
167
|
+
# Entry.access_notice_messages
|
168
|
+
# entry.access_notice_message
|
169
|
+
# entry.access_notice_message?
|
170
|
+
def delegated_type(role, types:, **options)
|
171
|
+
belongs_to role, options.delete(:scope), **options.merge(polymorphic: true)
|
172
|
+
define_delegated_type_methods role, types: types
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
def define_delegated_type_methods(role, types:)
|
177
|
+
role_type = "#{role}_type"
|
178
|
+
role_id = "#{role}_id"
|
179
|
+
|
180
|
+
define_method "#{role}_class" do
|
181
|
+
public_send("#{role}_type").constantize
|
182
|
+
end
|
183
|
+
|
184
|
+
define_method "#{role}_name" do
|
185
|
+
public_send("#{role}_class").model_name.singular.inquiry
|
186
|
+
end
|
187
|
+
|
188
|
+
types.each do |type|
|
189
|
+
scope_name = type.tableize.gsub("/", "_")
|
190
|
+
singular = scope_name.singularize
|
191
|
+
query = "#{singular}?"
|
192
|
+
|
193
|
+
scope scope_name, -> { where(role_type => type) }
|
194
|
+
|
195
|
+
define_method query do
|
196
|
+
public_send(role_type) == type
|
197
|
+
end
|
198
|
+
|
199
|
+
define_method singular do
|
200
|
+
public_send(role) if public_send(query)
|
201
|
+
end
|
202
|
+
|
203
|
+
define_method "#{singular}_id" do
|
204
|
+
public_send(role_id) if public_send(query)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
class DestroyAssociationAsyncError < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
# Job to destroy the records associated with a destroyed record in background.
|
8
|
+
class DestroyAssociationAsyncJob < ActiveJob::Base
|
9
|
+
queue_as { ActiveRecord::Base.queues[:destroy] }
|
10
|
+
|
11
|
+
discard_on ActiveJob::DeserializationError
|
12
|
+
|
13
|
+
def perform(
|
14
|
+
owner_model_name: nil, owner_id: nil,
|
15
|
+
association_class: nil, association_ids: nil, association_primary_key_column: nil,
|
16
|
+
ensuring_owner_was_method: nil
|
17
|
+
)
|
18
|
+
association_model = association_class.constantize
|
19
|
+
owner_class = owner_model_name.constantize
|
20
|
+
owner = owner_class.find_by(owner_class.primary_key.to_sym => owner_id)
|
21
|
+
|
22
|
+
if !owner_destroyed?(owner, ensuring_owner_was_method)
|
23
|
+
raise DestroyAssociationAsyncError, "owner record not destroyed"
|
24
|
+
end
|
25
|
+
|
26
|
+
association_model.where(association_primary_key_column => association_ids).find_each do |r|
|
27
|
+
r.destroy
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def owner_destroyed?(owner, ensuring_owner_was_method)
|
33
|
+
!owner || (ensuring_owner_was_method && owner.public_send(ensuring_owner_was_method))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|