activerecord 5.2.8.1 → 6.1.6.1
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 +1255 -596
- data/MIT-LICENSE +3 -1
- data/README.rdoc +7 -5
- data/examples/performance.rb +1 -1
- data/lib/active_record/aggregations.rb +9 -8
- data/lib/active_record/association_relation.rb +30 -10
- data/lib/active_record/associations/alias_tracker.rb +19 -16
- data/lib/active_record/associations/association.rb +100 -41
- data/lib/active_record/associations/association_scope.rb +23 -21
- data/lib/active_record/associations/belongs_to_association.rb +55 -48
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +7 -6
- data/lib/active_record/associations/builder/association.rb +45 -22
- data/lib/active_record/associations/builder/belongs_to.rb +29 -59
- data/lib/active_record/associations/builder/collection_association.rb +8 -17
- data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +17 -41
- data/lib/active_record/associations/builder/has_many.rb +8 -2
- data/lib/active_record/associations/builder/has_one.rb +33 -2
- data/lib/active_record/associations/builder/singular_association.rb +3 -1
- data/lib/active_record/associations/collection_association.rb +44 -34
- data/lib/active_record/associations/collection_proxy.rb +25 -21
- data/lib/active_record/associations/foreign_association.rb +20 -0
- data/lib/active_record/associations/has_many_association.rb +26 -13
- data/lib/active_record/associations/has_many_through_association.rb +24 -18
- data/lib/active_record/associations/has_one_association.rb +43 -31
- data/lib/active_record/associations/has_one_through_association.rb +5 -5
- data/lib/active_record/associations/join_dependency/join_association.rb +44 -22
- data/lib/active_record/associations/join_dependency/join_part.rb +5 -5
- data/lib/active_record/associations/join_dependency.rb +91 -60
- data/lib/active_record/associations/preloader/association.rb +69 -43
- data/lib/active_record/associations/preloader/through_association.rb +49 -40
- data/lib/active_record/associations/preloader.rb +47 -34
- data/lib/active_record/associations/singular_association.rb +3 -17
- data/lib/active_record/associations/through_association.rb +1 -1
- data/lib/active_record/associations.rb +137 -25
- data/lib/active_record/attribute_assignment.rb +17 -19
- data/lib/active_record/attribute_methods/before_type_cast.rb +13 -7
- data/lib/active_record/attribute_methods/dirty.rb +101 -40
- data/lib/active_record/attribute_methods/primary_key.rb +20 -25
- data/lib/active_record/attribute_methods/query.rb +4 -8
- data/lib/active_record/attribute_methods/read.rb +14 -56
- data/lib/active_record/attribute_methods/serialization.rb +12 -7
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +12 -15
- data/lib/active_record/attribute_methods/write.rb +18 -34
- data/lib/active_record/attribute_methods.rb +81 -143
- data/lib/active_record/attributes.rb +46 -9
- data/lib/active_record/autosave_association.rb +57 -42
- data/lib/active_record/base.rb +4 -17
- data/lib/active_record/callbacks.rb +158 -43
- data/lib/active_record/coders/yaml_column.rb +1 -2
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +272 -130
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +7 -36
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +167 -146
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +18 -14
- data/lib/active_record/connection_adapters/abstract/quoting.rb +98 -47
- data/lib/active_record/connection_adapters/abstract/savepoints.rb +3 -3
- data/lib/active_record/connection_adapters/abstract/schema_creation.rb +153 -110
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +211 -90
- data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -4
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +385 -144
- data/lib/active_record/connection_adapters/abstract/transaction.rb +167 -69
- data/lib/active_record/connection_adapters/abstract_adapter.rb +229 -99
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +243 -275
- data/lib/active_record/connection_adapters/column.rb +30 -12
- data/lib/active_record/connection_adapters/deduplicable.rb +29 -0
- data/lib/active_record/connection_adapters/legacy_pool_manager.rb +35 -0
- data/lib/active_record/connection_adapters/mysql/column.rb +1 -1
- data/lib/active_record/connection_adapters/mysql/database_statements.rb +88 -32
- data/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb +1 -2
- data/lib/active_record/connection_adapters/mysql/quoting.rb +59 -7
- data/lib/active_record/connection_adapters/mysql/schema_creation.rb +34 -10
- data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +48 -32
- data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +18 -7
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +142 -19
- data/lib/active_record/connection_adapters/mysql/type_metadata.rb +14 -9
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +53 -18
- data/lib/active_record/connection_adapters/pool_config.rb +73 -0
- data/lib/active_record/connection_adapters/pool_manager.rb +47 -0
- data/lib/active_record/connection_adapters/postgresql/column.rb +37 -28
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +40 -54
- data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -2
- data/lib/active_record/connection_adapters/postgresql/oid/bit.rb +1 -4
- 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 +10 -2
- data/lib/active_record/connection_adapters/postgresql/oid/enum.rb +0 -1
- data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +1 -2
- data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +49 -0
- data/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb +3 -4
- data/lib/active_record/connection_adapters/postgresql/oid/macaddr.rb +25 -0
- data/lib/active_record/connection_adapters/postgresql/oid/oid.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/oid/point.rb +3 -4
- data/lib/active_record/connection_adapters/postgresql/oid/range.rb +25 -7
- data/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +9 -7
- data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +15 -3
- data/lib/active_record/connection_adapters/postgresql/oid.rb +2 -0
- data/lib/active_record/connection_adapters/postgresql/quoting.rb +47 -10
- data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -2
- data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +19 -4
- data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +107 -91
- data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +0 -1
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +120 -100
- data/lib/active_record/connection_adapters/postgresql/type_metadata.rb +31 -26
- data/lib/active_record/connection_adapters/postgresql/utils.rb +0 -1
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +224 -120
- data/lib/active_record/connection_adapters/schema_cache.rb +159 -21
- data/lib/active_record/connection_adapters/sql_type_metadata.rb +17 -6
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +146 -0
- data/lib/active_record/connection_adapters/sqlite3/quoting.rb +42 -7
- data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +5 -1
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +77 -13
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +174 -186
- data/lib/active_record/connection_adapters/statement_pool.rb +0 -1
- data/lib/active_record/connection_adapters.rb +52 -0
- data/lib/active_record/connection_handling.rb +293 -33
- data/lib/active_record/core.rb +333 -98
- data/lib/active_record/counter_cache.rb +8 -30
- data/lib/active_record/database_configurations/connection_url_resolver.rb +99 -0
- data/lib/active_record/database_configurations/database_config.rb +80 -0
- data/lib/active_record/database_configurations/hash_config.rb +96 -0
- data/lib/active_record/database_configurations/url_config.rb +53 -0
- data/lib/active_record/database_configurations.rb +273 -0
- data/lib/active_record/delegated_type.rb +209 -0
- data/lib/active_record/destroy_association_async_job.rb +36 -0
- data/lib/active_record/dynamic_matchers.rb +3 -4
- data/lib/active_record/enum.rb +108 -36
- data/lib/active_record/errors.rb +62 -19
- data/lib/active_record/explain.rb +10 -6
- 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 +32 -0
- data/lib/active_record/fixture_set/render_context.rb +17 -0
- data/lib/active_record/fixture_set/table_row.rb +152 -0
- data/lib/active_record/fixture_set/table_rows.rb +46 -0
- data/lib/active_record/fixtures.rb +200 -481
- data/lib/active_record/gem_version.rb +3 -3
- data/lib/active_record/inheritance.rb +53 -24
- data/lib/active_record/insert_all.rb +212 -0
- data/lib/active_record/integration.rb +67 -17
- data/lib/active_record/internal_metadata.rb +28 -9
- data/lib/active_record/legacy_yaml_adapter.rb +7 -3
- data/lib/active_record/locking/optimistic.rb +37 -23
- data/lib/active_record/locking/pessimistic.rb +9 -5
- data/lib/active_record/log_subscriber.rb +35 -35
- data/lib/active_record/middleware/database_selector/resolver/session.rb +48 -0
- data/lib/active_record/middleware/database_selector/resolver.rb +92 -0
- data/lib/active_record/middleware/database_selector.rb +77 -0
- data/lib/active_record/migration/command_recorder.rb +96 -44
- data/lib/active_record/migration/compatibility.rb +145 -64
- data/lib/active_record/migration/join_table.rb +0 -1
- data/lib/active_record/migration.rb +206 -157
- data/lib/active_record/model_schema.rb +148 -22
- data/lib/active_record/nested_attributes.rb +4 -7
- data/lib/active_record/no_touching.rb +8 -1
- data/lib/active_record/null_relation.rb +0 -1
- data/lib/active_record/persistence.rb +267 -59
- data/lib/active_record/query_cache.rb +21 -4
- data/lib/active_record/querying.rb +40 -23
- data/lib/active_record/railtie.rb +116 -59
- data/lib/active_record/railties/console_sandbox.rb +2 -4
- data/lib/active_record/railties/controller_runtime.rb +30 -35
- data/lib/active_record/railties/databases.rake +411 -80
- data/lib/active_record/readonly_attributes.rb +4 -0
- data/lib/active_record/reflection.rb +109 -93
- data/lib/active_record/relation/batches/batch_enumerator.rb +25 -9
- data/lib/active_record/relation/batches.rb +44 -35
- data/lib/active_record/relation/calculations.rb +157 -90
- data/lib/active_record/relation/delegation.rb +35 -50
- data/lib/active_record/relation/finder_methods.rb +64 -39
- data/lib/active_record/relation/from_clause.rb +5 -1
- data/lib/active_record/relation/merger.rb +32 -40
- data/lib/active_record/relation/predicate_builder/array_handler.rb +13 -13
- data/lib/active_record/relation/predicate_builder/association_query_value.rb +5 -9
- data/lib/active_record/relation/predicate_builder/basic_object_handler.rb +1 -2
- data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +11 -10
- data/lib/active_record/relation/predicate_builder/range_handler.rb +3 -23
- data/lib/active_record/relation/predicate_builder/relation_handler.rb +1 -1
- data/lib/active_record/relation/predicate_builder.rb +62 -45
- data/lib/active_record/relation/query_attribute.rb +13 -8
- data/lib/active_record/relation/query_methods.rb +476 -187
- data/lib/active_record/relation/record_fetch_warning.rb +3 -3
- data/lib/active_record/relation/spawn_methods.rb +9 -9
- data/lib/active_record/relation/where_clause.rb +115 -62
- data/lib/active_record/relation.rb +379 -115
- data/lib/active_record/result.rb +64 -38
- data/lib/active_record/runtime_registry.rb +2 -2
- data/lib/active_record/sanitization.rb +22 -41
- data/lib/active_record/schema.rb +2 -11
- data/lib/active_record/schema_dumper.rb +54 -9
- data/lib/active_record/schema_migration.rb +7 -9
- data/lib/active_record/scoping/default.rb +4 -8
- data/lib/active_record/scoping/named.rb +17 -24
- data/lib/active_record/scoping.rb +8 -9
- 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 +49 -6
- data/lib/active_record/store.rb +88 -9
- data/lib/active_record/suppressor.rb +2 -2
- data/lib/active_record/table_metadata.rb +42 -43
- data/lib/active_record/tasks/database_tasks.rb +277 -81
- data/lib/active_record/tasks/mysql_database_tasks.rb +37 -39
- data/lib/active_record/tasks/postgresql_database_tasks.rb +27 -32
- data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -17
- data/lib/active_record/test_databases.rb +24 -0
- data/lib/active_record/test_fixtures.rb +287 -0
- data/lib/active_record/timestamp.rb +43 -32
- data/lib/active_record/touch_later.rb +23 -22
- data/lib/active_record/transactions.rb +62 -118
- data/lib/active_record/translation.rb +1 -1
- data/lib/active_record/type/adapter_specific_registry.rb +3 -13
- data/lib/active_record/type/hash_lookup_type_map.rb +0 -1
- data/lib/active_record/type/serialized.rb +6 -3
- data/lib/active_record/type/time.rb +10 -0
- data/lib/active_record/type/type_map.rb +0 -1
- data/lib/active_record/type/unsigned_integer.rb +0 -1
- data/lib/active_record/type.rb +10 -5
- data/lib/active_record/type_caster/connection.rb +15 -15
- data/lib/active_record/type_caster/map.rb +8 -8
- data/lib/active_record/validations/associated.rb +1 -2
- data/lib/active_record/validations/numericality.rb +35 -0
- data/lib/active_record/validations/uniqueness.rb +38 -30
- data/lib/active_record/validations.rb +4 -3
- data/lib/active_record.rb +13 -12
- data/lib/arel/alias_predication.rb +9 -0
- data/lib/arel/attributes/attribute.rb +41 -0
- data/lib/arel/collectors/bind.rb +29 -0
- data/lib/arel/collectors/composite.rb +39 -0
- data/lib/arel/collectors/plain_string.rb +20 -0
- data/lib/arel/collectors/sql_string.rb +27 -0
- data/lib/arel/collectors/substitute_binds.rb +35 -0
- data/lib/arel/crud.rb +42 -0
- data/lib/arel/delete_manager.rb +18 -0
- data/lib/arel/errors.rb +9 -0
- data/lib/arel/expressions.rb +29 -0
- data/lib/arel/factory_methods.rb +49 -0
- data/lib/arel/insert_manager.rb +49 -0
- data/lib/arel/math.rb +45 -0
- data/lib/arel/nodes/and.rb +32 -0
- data/lib/arel/nodes/ascending.rb +23 -0
- data/lib/arel/nodes/binary.rb +126 -0
- data/lib/arel/nodes/bind_param.rb +44 -0
- data/lib/arel/nodes/case.rb +55 -0
- data/lib/arel/nodes/casted.rb +62 -0
- data/lib/arel/nodes/comment.rb +29 -0
- data/lib/arel/nodes/count.rb +12 -0
- data/lib/arel/nodes/delete_statement.rb +45 -0
- data/lib/arel/nodes/descending.rb +23 -0
- data/lib/arel/nodes/equality.rb +15 -0
- data/lib/arel/nodes/extract.rb +24 -0
- data/lib/arel/nodes/false.rb +16 -0
- data/lib/arel/nodes/full_outer_join.rb +8 -0
- data/lib/arel/nodes/function.rb +44 -0
- data/lib/arel/nodes/grouping.rb +11 -0
- data/lib/arel/nodes/homogeneous_in.rb +76 -0
- data/lib/arel/nodes/in.rb +15 -0
- data/lib/arel/nodes/infix_operation.rb +92 -0
- data/lib/arel/nodes/inner_join.rb +8 -0
- data/lib/arel/nodes/insert_statement.rb +37 -0
- data/lib/arel/nodes/join_source.rb +20 -0
- data/lib/arel/nodes/matches.rb +18 -0
- data/lib/arel/nodes/named_function.rb +23 -0
- data/lib/arel/nodes/node.rb +51 -0
- data/lib/arel/nodes/node_expression.rb +13 -0
- data/lib/arel/nodes/ordering.rb +27 -0
- data/lib/arel/nodes/outer_join.rb +8 -0
- data/lib/arel/nodes/over.rb +15 -0
- data/lib/arel/nodes/regexp.rb +16 -0
- data/lib/arel/nodes/right_outer_join.rb +8 -0
- data/lib/arel/nodes/select_core.rb +67 -0
- data/lib/arel/nodes/select_statement.rb +41 -0
- data/lib/arel/nodes/sql_literal.rb +19 -0
- data/lib/arel/nodes/string_join.rb +11 -0
- data/lib/arel/nodes/table_alias.rb +31 -0
- data/lib/arel/nodes/terminal.rb +16 -0
- data/lib/arel/nodes/true.rb +16 -0
- data/lib/arel/nodes/unary.rb +44 -0
- data/lib/arel/nodes/unary_operation.rb +20 -0
- data/lib/arel/nodes/unqualified_column.rb +22 -0
- data/lib/arel/nodes/update_statement.rb +41 -0
- data/lib/arel/nodes/values_list.rb +9 -0
- data/lib/arel/nodes/window.rb +126 -0
- data/lib/arel/nodes/with.rb +11 -0
- data/lib/arel/nodes.rb +70 -0
- data/lib/arel/order_predications.rb +13 -0
- data/lib/arel/predications.rb +250 -0
- data/lib/arel/select_manager.rb +270 -0
- data/lib/arel/table.rb +118 -0
- data/lib/arel/tree_manager.rb +72 -0
- data/lib/arel/update_manager.rb +34 -0
- data/lib/arel/visitors/dot.rb +308 -0
- data/lib/arel/visitors/mysql.rb +93 -0
- data/lib/arel/visitors/postgresql.rb +120 -0
- data/lib/arel/visitors/sqlite.rb +38 -0
- data/lib/arel/visitors/to_sql.rb +899 -0
- data/lib/arel/visitors/visitor.rb +45 -0
- data/lib/arel/visitors.rb +13 -0
- data/lib/arel/window_predications.rb +9 -0
- data/lib/arel.rb +54 -0
- data/lib/rails/generators/active_record/application_record/application_record_generator.rb +0 -1
- data/lib/rails/generators/active_record/migration/migration_generator.rb +3 -5
- data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +3 -1
- data/lib/rails/generators/active_record/migration/templates/migration.rb.tt +7 -5
- data/lib/rails/generators/active_record/migration.rb +19 -2
- 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
- data/lib/rails/generators/active_record/model/templates/model.rb.tt +10 -1
- metadata +116 -30
- data/lib/active_record/attribute_decorators.rb +0 -90
- data/lib/active_record/collection_cache_key.rb +0 -53
- data/lib/active_record/connection_adapters/connection_specification.rb +0 -287
- data/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +0 -33
- data/lib/active_record/define_callbacks.rb +0 -22
- data/lib/active_record/relation/predicate_builder/base_handler.rb +0 -19
- data/lib/active_record/relation/where_clause_factory.rb +0 -34
@@ -60,8 +60,17 @@ module ActiveRecord
|
|
60
60
|
self.class.locking_enabled?
|
61
61
|
end
|
62
62
|
|
63
|
+
def increment!(*, **) #:nodoc:
|
64
|
+
super.tap do
|
65
|
+
if locking_enabled?
|
66
|
+
self[self.class.locking_column] += 1
|
67
|
+
clear_attribute_change(self.class.locking_column)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
63
72
|
private
|
64
|
-
def _create_record(attribute_names = self.attribute_names
|
73
|
+
def _create_record(attribute_names = self.attribute_names)
|
65
74
|
if locking_enabled?
|
66
75
|
# We always want to persist the locking version, even if we don't detect
|
67
76
|
# a change from the default, since the database might have no default
|
@@ -71,9 +80,8 @@ module ActiveRecord
|
|
71
80
|
end
|
72
81
|
|
73
82
|
def _touch_row(attribute_names, time)
|
83
|
+
@_touch_attr_names << self.class.locking_column if locking_enabled?
|
74
84
|
super
|
75
|
-
ensure
|
76
|
-
clear_attribute_change(self.class.locking_column) if locking_enabled?
|
77
85
|
end
|
78
86
|
|
79
87
|
def _update_row(attribute_names, attempted_action = "update")
|
@@ -81,15 +89,18 @@ module ActiveRecord
|
|
81
89
|
|
82
90
|
begin
|
83
91
|
locking_column = self.class.locking_column
|
84
|
-
|
92
|
+
lock_attribute_was = @attributes[locking_column]
|
93
|
+
lock_value_for_database = _lock_value_for_database(locking_column)
|
94
|
+
|
95
|
+
attribute_names = attribute_names.dup if attribute_names.frozen?
|
85
96
|
attribute_names << locking_column
|
86
97
|
|
87
98
|
self[locking_column] += 1
|
88
99
|
|
89
100
|
affected_rows = self.class._update_record(
|
90
101
|
attributes_with_values(attribute_names),
|
91
|
-
|
92
|
-
locking_column =>
|
102
|
+
@primary_key => id_in_database,
|
103
|
+
locking_column => lock_value_for_database
|
93
104
|
)
|
94
105
|
|
95
106
|
if affected_rows != 1
|
@@ -100,7 +111,7 @@ module ActiveRecord
|
|
100
111
|
|
101
112
|
# If something went wrong, revert the locking_column value.
|
102
113
|
rescue Exception
|
103
|
-
|
114
|
+
@attributes[locking_column] = lock_attribute_was
|
104
115
|
raise
|
105
116
|
end
|
106
117
|
end
|
@@ -111,8 +122,8 @@ module ActiveRecord
|
|
111
122
|
locking_column = self.class.locking_column
|
112
123
|
|
113
124
|
affected_rows = self.class._delete_record(
|
114
|
-
|
115
|
-
locking_column =>
|
125
|
+
@primary_key => id_in_database,
|
126
|
+
locking_column => _lock_value_for_database(locking_column)
|
116
127
|
)
|
117
128
|
|
118
129
|
if affected_rows != 1
|
@@ -122,6 +133,14 @@ module ActiveRecord
|
|
122
133
|
affected_rows
|
123
134
|
end
|
124
135
|
|
136
|
+
def _lock_value_for_database(locking_column)
|
137
|
+
if will_save_change_to_attribute?(locking_column)
|
138
|
+
@attributes[locking_column].value_for_database
|
139
|
+
else
|
140
|
+
@attributes[locking_column].original_value_for_database
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
125
144
|
module ClassMethods
|
126
145
|
DEFAULT_LOCKING_COLUMN = "lock_version"
|
127
146
|
|
@@ -156,21 +175,12 @@ module ActiveRecord
|
|
156
175
|
super
|
157
176
|
end
|
158
177
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
# created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
|
163
|
-
# sub class being decorated. As such, changes to `lock_optimistically`, or
|
164
|
-
# `locking_column` would not be picked up.
|
165
|
-
def inherited(subclass)
|
166
|
-
subclass.class_eval do
|
167
|
-
is_lock_column = ->(name, _) { lock_optimistically && name == locking_column }
|
168
|
-
decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type|
|
169
|
-
LockingType.new(type)
|
170
|
-
end
|
171
|
-
end
|
172
|
-
super
|
178
|
+
def define_attribute(name, cast_type, **) # :nodoc:
|
179
|
+
if lock_optimistically && name == locking_column
|
180
|
+
cast_type = LockingType.new(cast_type)
|
173
181
|
end
|
182
|
+
super
|
183
|
+
end
|
174
184
|
end
|
175
185
|
end
|
176
186
|
|
@@ -178,6 +188,10 @@ module ActiveRecord
|
|
178
188
|
# `nil` values to `lock_version`, and not result in `ActiveRecord::StaleObjectError`
|
179
189
|
# during update record.
|
180
190
|
class LockingType < DelegateClass(Type::Value) # :nodoc:
|
191
|
+
def self.new(subtype)
|
192
|
+
self === subtype ? subtype : super
|
193
|
+
end
|
194
|
+
|
181
195
|
def deserialize(value)
|
182
196
|
super.to_i
|
183
197
|
end
|
@@ -14,9 +14,9 @@ module ActiveRecord
|
|
14
14
|
# of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example:
|
15
15
|
#
|
16
16
|
# Account.transaction do
|
17
|
-
# # select * from accounts where name = 'shugo' limit 1 for update
|
18
|
-
# shugo = Account.
|
19
|
-
# yuko = Account.
|
17
|
+
# # select * from accounts where name = 'shugo' limit 1 for update nowait
|
18
|
+
# shugo = Account.lock("FOR UPDATE NOWAIT").find_by(name: "shugo")
|
19
|
+
# yuko = Account.lock("FOR UPDATE NOWAIT").find_by(name: "yuko")
|
20
20
|
# shugo.balance -= 100
|
21
21
|
# shugo.save!
|
22
22
|
# yuko.balance += 100
|
@@ -53,8 +53,12 @@ module ActiveRecord
|
|
53
53
|
# end
|
54
54
|
#
|
55
55
|
# Database-specific information on row locking:
|
56
|
-
#
|
57
|
-
#
|
56
|
+
#
|
57
|
+
# [MySQL]
|
58
|
+
# https://dev.mysql.com/doc/refman/en/innodb-locking-reads.html
|
59
|
+
#
|
60
|
+
# [PostgreSQL]
|
61
|
+
# https://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE
|
58
62
|
module Pessimistic
|
59
63
|
# Obtain a row lock on this record. Reloads the record to obtain the requested
|
60
64
|
# lock. Pass an SQL locking clause to append the end of the SELECT statement
|
@@ -4,6 +4,8 @@ module ActiveRecord
|
|
4
4
|
class LogSubscriber < ActiveSupport::LogSubscriber
|
5
5
|
IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"]
|
6
6
|
|
7
|
+
class_attribute :backtrace_cleaner, default: ActiveSupport::BacktraceCleaner.new
|
8
|
+
|
7
9
|
def self.runtime=(value)
|
8
10
|
ActiveRecord::RuntimeRegistry.sql_runtime = value
|
9
11
|
end
|
@@ -17,6 +19,16 @@ module ActiveRecord
|
|
17
19
|
rt
|
18
20
|
end
|
19
21
|
|
22
|
+
def strict_loading_violation(event)
|
23
|
+
debug do
|
24
|
+
owner = event.payload[:owner]
|
25
|
+
association = event.payload[:reflection].klass
|
26
|
+
name = event.payload[:reflection].name
|
27
|
+
|
28
|
+
color("Strict loading violation: #{owner} is marked for strict loading. The #{association} association named :#{name} cannot be lazily loaded.", RED)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
20
32
|
def sql(event)
|
21
33
|
self.class.runtime += event.duration
|
22
34
|
return unless logger.debug?
|
@@ -30,15 +42,19 @@ module ActiveRecord
|
|
30
42
|
sql = payload[:sql]
|
31
43
|
binds = nil
|
32
44
|
|
33
|
-
|
45
|
+
if payload[:binds]&.any?
|
34
46
|
casted_params = type_casted_binds(payload[:type_casted_binds])
|
35
|
-
|
36
|
-
|
37
|
-
|
47
|
+
|
48
|
+
binds = []
|
49
|
+
payload[:binds].each_with_index do |attr, i|
|
50
|
+
binds << render_bind(attr, casted_params[i])
|
51
|
+
end
|
52
|
+
binds = binds.inspect
|
53
|
+
binds.prepend(" ")
|
38
54
|
end
|
39
55
|
|
40
56
|
name = colorize_payload_name(name, payload[:name])
|
41
|
-
sql = color(sql, sql_color(sql), true)
|
57
|
+
sql = color(sql, sql_color(sql), true) if colorize_logging
|
42
58
|
|
43
59
|
debug " #{name} #{sql}#{binds}"
|
44
60
|
end
|
@@ -49,13 +65,18 @@ module ActiveRecord
|
|
49
65
|
end
|
50
66
|
|
51
67
|
def render_bind(attr, value)
|
52
|
-
|
68
|
+
case attr
|
69
|
+
when ActiveModel::Attribute
|
70
|
+
if attr.type.binary? && attr.value
|
71
|
+
value = "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>"
|
72
|
+
end
|
73
|
+
when Array
|
53
74
|
attr = attr.first
|
54
|
-
|
55
|
-
|
75
|
+
else
|
76
|
+
attr = nil
|
56
77
|
end
|
57
78
|
|
58
|
-
[attr
|
79
|
+
[attr&.name, value]
|
59
80
|
end
|
60
81
|
|
61
82
|
def colorize_payload_name(name, payload_name)
|
@@ -100,36 +121,15 @@ module ActiveRecord
|
|
100
121
|
end
|
101
122
|
|
102
123
|
def log_query_source
|
103
|
-
|
124
|
+
source = extract_query_source_location(caller)
|
104
125
|
|
105
|
-
if
|
106
|
-
|
107
|
-
app_root = "#{::Rails.root.to_s}/".freeze
|
108
|
-
source_line = source_line.sub(app_root, "")
|
109
|
-
end
|
110
|
-
|
111
|
-
logger.debug(" ↳ #{ source_line }:#{ line_number }")
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
def extract_callstack(callstack)
|
116
|
-
line = callstack.find do |frame|
|
117
|
-
frame.absolute_path && !ignored_callstack(frame.absolute_path)
|
126
|
+
if source
|
127
|
+
logger.debug(" ↳ #{source}")
|
118
128
|
end
|
119
|
-
|
120
|
-
offending_line = line || callstack.first
|
121
|
-
|
122
|
-
[
|
123
|
-
offending_line.path,
|
124
|
-
offending_line.lineno
|
125
|
-
]
|
126
129
|
end
|
127
130
|
|
128
|
-
|
129
|
-
|
130
|
-
def ignored_callstack(path)
|
131
|
-
path.start_with?(RAILS_GEM_ROOT) ||
|
132
|
-
path.start_with?(RbConfig::CONFIG["rubylibdir"])
|
131
|
+
def extract_query_source_location(locations)
|
132
|
+
backtrace_cleaner.clean(locations.lazy).first
|
133
133
|
end
|
134
134
|
end
|
135
135
|
end
|
@@ -0,0 +1,48 @@
|
|
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
|
+
|
42
|
+
def save(response)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record/middleware/database_selector/resolver/session"
|
4
|
+
require "active_support/core_ext/numeric/time"
|
5
|
+
|
6
|
+
module ActiveRecord
|
7
|
+
module Middleware
|
8
|
+
class DatabaseSelector
|
9
|
+
# The Resolver class is used by the DatabaseSelector middleware to
|
10
|
+
# determine which database the request should use.
|
11
|
+
#
|
12
|
+
# To change the behavior of the Resolver class in your application,
|
13
|
+
# create a custom resolver class that inherits from
|
14
|
+
# DatabaseSelector::Resolver and implements the methods that need to
|
15
|
+
# be changed.
|
16
|
+
#
|
17
|
+
# By default the Resolver class will send read traffic to the replica
|
18
|
+
# if it's been 2 seconds since the last write.
|
19
|
+
class Resolver # :nodoc:
|
20
|
+
SEND_TO_REPLICA_DELAY = 2.seconds
|
21
|
+
|
22
|
+
def self.call(context, options = {})
|
23
|
+
new(context, options)
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(context, options = {})
|
27
|
+
@context = context
|
28
|
+
@options = options
|
29
|
+
@delay = @options && @options[:delay] ? @options[:delay] : SEND_TO_REPLICA_DELAY
|
30
|
+
@instrumenter = ActiveSupport::Notifications.instrumenter
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :context, :delay, :instrumenter
|
34
|
+
|
35
|
+
def read(&blk)
|
36
|
+
if read_from_primary?
|
37
|
+
read_from_primary(&blk)
|
38
|
+
else
|
39
|
+
read_from_replica(&blk)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def write(&blk)
|
44
|
+
write_to_primary(&blk)
|
45
|
+
end
|
46
|
+
|
47
|
+
def update_context(response)
|
48
|
+
context.save(response)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
def read_from_primary(&blk)
|
53
|
+
ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role, prevent_writes: true) do
|
54
|
+
instrumenter.instrument("database_selector.active_record.read_from_primary") do
|
55
|
+
yield
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def read_from_replica(&blk)
|
61
|
+
ActiveRecord::Base.connected_to(role: ActiveRecord::Base.reading_role, prevent_writes: true) do
|
62
|
+
instrumenter.instrument("database_selector.active_record.read_from_replica") do
|
63
|
+
yield
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def write_to_primary(&blk)
|
69
|
+
ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role, prevent_writes: false) do
|
70
|
+
instrumenter.instrument("database_selector.active_record.wrote_to_primary") do
|
71
|
+
yield
|
72
|
+
ensure
|
73
|
+
context.update_last_write_timestamp
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def read_from_primary?
|
79
|
+
!time_since_last_write_ok?
|
80
|
+
end
|
81
|
+
|
82
|
+
def send_to_replica_delay
|
83
|
+
delay
|
84
|
+
end
|
85
|
+
|
86
|
+
def time_since_last_write_ok?
|
87
|
+
Time.now - context.last_write_timestamp >= send_to_replica_delay
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,77 @@
|
|
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 = nil, context_klass = nil, options = {})
|
39
|
+
@app = app
|
40
|
+
@resolver_klass = resolver_klass || Resolver
|
41
|
+
@context_klass = context_klass || Resolver::Session
|
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
|
+
def select_database(request, &blk)
|
59
|
+
context = context_klass.call(request)
|
60
|
+
resolver = resolver_klass.call(context, options)
|
61
|
+
|
62
|
+
response = if reading_request?(request)
|
63
|
+
resolver.read(&blk)
|
64
|
+
else
|
65
|
+
resolver.write(&blk)
|
66
|
+
end
|
67
|
+
|
68
|
+
resolver.update_context(response)
|
69
|
+
response
|
70
|
+
end
|
71
|
+
|
72
|
+
def reading_request?(request)
|
73
|
+
request.get? || request.head?
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|