activerecord 5.2.3
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 +7 -0
- data/CHANGELOG.md +937 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +217 -0
- data/examples/performance.rb +185 -0
- data/examples/simple.rb +15 -0
- data/lib/active_record.rb +188 -0
- data/lib/active_record/aggregations.rb +283 -0
- data/lib/active_record/association_relation.rb +40 -0
- data/lib/active_record/associations.rb +1860 -0
- data/lib/active_record/associations/alias_tracker.rb +81 -0
- data/lib/active_record/associations/association.rb +299 -0
- data/lib/active_record/associations/association_scope.rb +168 -0
- data/lib/active_record/associations/belongs_to_association.rb +130 -0
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +40 -0
- data/lib/active_record/associations/builder/association.rb +140 -0
- data/lib/active_record/associations/builder/belongs_to.rb +163 -0
- data/lib/active_record/associations/builder/collection_association.rb +82 -0
- data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +135 -0
- data/lib/active_record/associations/builder/has_many.rb +17 -0
- data/lib/active_record/associations/builder/has_one.rb +30 -0
- data/lib/active_record/associations/builder/singular_association.rb +42 -0
- data/lib/active_record/associations/collection_association.rb +513 -0
- data/lib/active_record/associations/collection_proxy.rb +1131 -0
- data/lib/active_record/associations/foreign_association.rb +13 -0
- data/lib/active_record/associations/has_many_association.rb +144 -0
- data/lib/active_record/associations/has_many_through_association.rb +227 -0
- data/lib/active_record/associations/has_one_association.rb +120 -0
- data/lib/active_record/associations/has_one_through_association.rb +45 -0
- data/lib/active_record/associations/join_dependency.rb +262 -0
- data/lib/active_record/associations/join_dependency/join_association.rb +60 -0
- data/lib/active_record/associations/join_dependency/join_base.rb +23 -0
- data/lib/active_record/associations/join_dependency/join_part.rb +71 -0
- data/lib/active_record/associations/preloader.rb +193 -0
- data/lib/active_record/associations/preloader/association.rb +131 -0
- data/lib/active_record/associations/preloader/through_association.rb +107 -0
- data/lib/active_record/associations/singular_association.rb +73 -0
- data/lib/active_record/associations/through_association.rb +121 -0
- data/lib/active_record/attribute_assignment.rb +88 -0
- data/lib/active_record/attribute_decorators.rb +90 -0
- data/lib/active_record/attribute_methods.rb +492 -0
- data/lib/active_record/attribute_methods/before_type_cast.rb +78 -0
- data/lib/active_record/attribute_methods/dirty.rb +150 -0
- data/lib/active_record/attribute_methods/primary_key.rb +143 -0
- data/lib/active_record/attribute_methods/query.rb +42 -0
- data/lib/active_record/attribute_methods/read.rb +85 -0
- data/lib/active_record/attribute_methods/serialization.rb +90 -0
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +91 -0
- data/lib/active_record/attribute_methods/write.rb +68 -0
- data/lib/active_record/attributes.rb +266 -0
- data/lib/active_record/autosave_association.rb +498 -0
- data/lib/active_record/base.rb +329 -0
- data/lib/active_record/callbacks.rb +353 -0
- data/lib/active_record/coders/json.rb +15 -0
- data/lib/active_record/coders/yaml_column.rb +50 -0
- data/lib/active_record/collection_cache_key.rb +53 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +1068 -0
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +72 -0
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +540 -0
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +145 -0
- data/lib/active_record/connection_adapters/abstract/quoting.rb +200 -0
- data/lib/active_record/connection_adapters/abstract/savepoints.rb +23 -0
- data/lib/active_record/connection_adapters/abstract/schema_creation.rb +146 -0
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +685 -0
- data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +95 -0
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +1396 -0
- data/lib/active_record/connection_adapters/abstract/transaction.rb +283 -0
- data/lib/active_record/connection_adapters/abstract_adapter.rb +628 -0
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +887 -0
- data/lib/active_record/connection_adapters/column.rb +91 -0
- data/lib/active_record/connection_adapters/connection_specification.rb +287 -0
- data/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +33 -0
- data/lib/active_record/connection_adapters/mysql/column.rb +27 -0
- data/lib/active_record/connection_adapters/mysql/database_statements.rb +140 -0
- data/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb +72 -0
- data/lib/active_record/connection_adapters/mysql/quoting.rb +44 -0
- data/lib/active_record/connection_adapters/mysql/schema_creation.rb +73 -0
- data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +87 -0
- data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +80 -0
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +148 -0
- data/lib/active_record/connection_adapters/mysql/type_metadata.rb +35 -0
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +129 -0
- data/lib/active_record/connection_adapters/postgresql/column.rb +44 -0
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +163 -0
- data/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb +44 -0
- data/lib/active_record/connection_adapters/postgresql/oid.rb +34 -0
- data/lib/active_record/connection_adapters/postgresql/oid/array.rb +92 -0
- data/lib/active_record/connection_adapters/postgresql/oid/bit.rb +56 -0
- data/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb +15 -0
- data/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +17 -0
- data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +50 -0
- data/lib/active_record/connection_adapters/postgresql/oid/date.rb +23 -0
- data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +23 -0
- data/lib/active_record/connection_adapters/postgresql/oid/decimal.rb +15 -0
- data/lib/active_record/connection_adapters/postgresql/oid/enum.rb +21 -0
- data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +71 -0
- data/lib/active_record/connection_adapters/postgresql/oid/inet.rb +15 -0
- data/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +15 -0
- data/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb +45 -0
- data/lib/active_record/connection_adapters/postgresql/oid/money.rb +41 -0
- data/lib/active_record/connection_adapters/postgresql/oid/oid.rb +15 -0
- data/lib/active_record/connection_adapters/postgresql/oid/point.rb +65 -0
- data/lib/active_record/connection_adapters/postgresql/oid/range.rb +97 -0
- data/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +18 -0
- data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +111 -0
- data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +23 -0
- data/lib/active_record/connection_adapters/postgresql/oid/vector.rb +28 -0
- data/lib/active_record/connection_adapters/postgresql/oid/xml.rb +30 -0
- data/lib/active_record/connection_adapters/postgresql/quoting.rb +168 -0
- data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +43 -0
- data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +65 -0
- data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +206 -0
- data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +50 -0
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +774 -0
- data/lib/active_record/connection_adapters/postgresql/type_metadata.rb +39 -0
- data/lib/active_record/connection_adapters/postgresql/utils.rb +81 -0
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +863 -0
- data/lib/active_record/connection_adapters/schema_cache.rb +118 -0
- data/lib/active_record/connection_adapters/sql_type_metadata.rb +34 -0
- data/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb +21 -0
- data/lib/active_record/connection_adapters/sqlite3/quoting.rb +67 -0
- data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +17 -0
- data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +19 -0
- data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +18 -0
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +106 -0
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +573 -0
- data/lib/active_record/connection_adapters/statement_pool.rb +61 -0
- data/lib/active_record/connection_handling.rb +145 -0
- data/lib/active_record/core.rb +559 -0
- data/lib/active_record/counter_cache.rb +218 -0
- data/lib/active_record/define_callbacks.rb +22 -0
- data/lib/active_record/dynamic_matchers.rb +122 -0
- data/lib/active_record/enum.rb +244 -0
- data/lib/active_record/errors.rb +380 -0
- data/lib/active_record/explain.rb +50 -0
- data/lib/active_record/explain_registry.rb +32 -0
- data/lib/active_record/explain_subscriber.rb +34 -0
- data/lib/active_record/fixture_set/file.rb +82 -0
- data/lib/active_record/fixtures.rb +1065 -0
- data/lib/active_record/gem_version.rb +17 -0
- data/lib/active_record/inheritance.rb +283 -0
- data/lib/active_record/integration.rb +155 -0
- data/lib/active_record/internal_metadata.rb +45 -0
- data/lib/active_record/legacy_yaml_adapter.rb +48 -0
- data/lib/active_record/locale/en.yml +48 -0
- data/lib/active_record/locking/optimistic.rb +198 -0
- data/lib/active_record/locking/pessimistic.rb +89 -0
- data/lib/active_record/log_subscriber.rb +137 -0
- data/lib/active_record/migration.rb +1378 -0
- data/lib/active_record/migration/command_recorder.rb +240 -0
- data/lib/active_record/migration/compatibility.rb +217 -0
- data/lib/active_record/migration/join_table.rb +17 -0
- data/lib/active_record/model_schema.rb +521 -0
- data/lib/active_record/nested_attributes.rb +600 -0
- data/lib/active_record/no_touching.rb +58 -0
- data/lib/active_record/null_relation.rb +68 -0
- data/lib/active_record/persistence.rb +763 -0
- data/lib/active_record/query_cache.rb +45 -0
- data/lib/active_record/querying.rb +70 -0
- data/lib/active_record/railtie.rb +226 -0
- data/lib/active_record/railties/console_sandbox.rb +7 -0
- data/lib/active_record/railties/controller_runtime.rb +56 -0
- data/lib/active_record/railties/databases.rake +377 -0
- data/lib/active_record/readonly_attributes.rb +24 -0
- data/lib/active_record/reflection.rb +1044 -0
- data/lib/active_record/relation.rb +629 -0
- data/lib/active_record/relation/batches.rb +287 -0
- data/lib/active_record/relation/batches/batch_enumerator.rb +69 -0
- data/lib/active_record/relation/calculations.rb +417 -0
- data/lib/active_record/relation/delegation.rb +147 -0
- data/lib/active_record/relation/finder_methods.rb +565 -0
- data/lib/active_record/relation/from_clause.rb +26 -0
- data/lib/active_record/relation/merger.rb +193 -0
- data/lib/active_record/relation/predicate_builder.rb +152 -0
- data/lib/active_record/relation/predicate_builder/array_handler.rb +48 -0
- data/lib/active_record/relation/predicate_builder/association_query_value.rb +46 -0
- data/lib/active_record/relation/predicate_builder/base_handler.rb +19 -0
- data/lib/active_record/relation/predicate_builder/basic_object_handler.rb +20 -0
- data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +56 -0
- data/lib/active_record/relation/predicate_builder/range_handler.rb +42 -0
- data/lib/active_record/relation/predicate_builder/relation_handler.rb +19 -0
- data/lib/active_record/relation/query_attribute.rb +45 -0
- data/lib/active_record/relation/query_methods.rb +1231 -0
- data/lib/active_record/relation/record_fetch_warning.rb +51 -0
- data/lib/active_record/relation/spawn_methods.rb +77 -0
- data/lib/active_record/relation/where_clause.rb +186 -0
- data/lib/active_record/relation/where_clause_factory.rb +34 -0
- data/lib/active_record/result.rb +149 -0
- data/lib/active_record/runtime_registry.rb +24 -0
- data/lib/active_record/sanitization.rb +222 -0
- data/lib/active_record/schema.rb +70 -0
- data/lib/active_record/schema_dumper.rb +255 -0
- data/lib/active_record/schema_migration.rb +56 -0
- data/lib/active_record/scoping.rb +106 -0
- data/lib/active_record/scoping/default.rb +152 -0
- data/lib/active_record/scoping/named.rb +213 -0
- data/lib/active_record/secure_token.rb +40 -0
- data/lib/active_record/serialization.rb +22 -0
- data/lib/active_record/statement_cache.rb +121 -0
- data/lib/active_record/store.rb +211 -0
- data/lib/active_record/suppressor.rb +61 -0
- data/lib/active_record/table_metadata.rb +82 -0
- data/lib/active_record/tasks/database_tasks.rb +337 -0
- data/lib/active_record/tasks/mysql_database_tasks.rb +115 -0
- data/lib/active_record/tasks/postgresql_database_tasks.rb +143 -0
- data/lib/active_record/tasks/sqlite_database_tasks.rb +83 -0
- data/lib/active_record/timestamp.rb +153 -0
- data/lib/active_record/touch_later.rb +64 -0
- data/lib/active_record/transactions.rb +502 -0
- data/lib/active_record/translation.rb +24 -0
- data/lib/active_record/type.rb +79 -0
- data/lib/active_record/type/adapter_specific_registry.rb +136 -0
- data/lib/active_record/type/date.rb +9 -0
- data/lib/active_record/type/date_time.rb +9 -0
- data/lib/active_record/type/decimal_without_scale.rb +15 -0
- data/lib/active_record/type/hash_lookup_type_map.rb +25 -0
- data/lib/active_record/type/internal/timezone.rb +17 -0
- data/lib/active_record/type/json.rb +30 -0
- data/lib/active_record/type/serialized.rb +71 -0
- data/lib/active_record/type/text.rb +11 -0
- data/lib/active_record/type/time.rb +21 -0
- data/lib/active_record/type/type_map.rb +62 -0
- data/lib/active_record/type/unsigned_integer.rb +17 -0
- data/lib/active_record/type_caster.rb +9 -0
- data/lib/active_record/type_caster/connection.rb +33 -0
- data/lib/active_record/type_caster/map.rb +23 -0
- data/lib/active_record/validations.rb +93 -0
- data/lib/active_record/validations/absence.rb +25 -0
- data/lib/active_record/validations/associated.rb +60 -0
- data/lib/active_record/validations/length.rb +26 -0
- data/lib/active_record/validations/presence.rb +68 -0
- data/lib/active_record/validations/uniqueness.rb +238 -0
- data/lib/active_record/version.rb +10 -0
- data/lib/rails/generators/active_record.rb +19 -0
- data/lib/rails/generators/active_record/application_record/application_record_generator.rb +27 -0
- data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +5 -0
- data/lib/rails/generators/active_record/migration.rb +35 -0
- data/lib/rails/generators/active_record/migration/migration_generator.rb +78 -0
- data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +24 -0
- data/lib/rails/generators/active_record/migration/templates/migration.rb.tt +46 -0
- data/lib/rails/generators/active_record/model/model_generator.rb +48 -0
- data/lib/rails/generators/active_record/model/templates/model.rb.tt +13 -0
- data/lib/rails/generators/active_record/model/templates/module.rb.tt +7 -0
- metadata +333 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record/scoping/default"
|
4
|
+
require "active_record/scoping/named"
|
5
|
+
|
6
|
+
module ActiveRecord
|
7
|
+
# This class is used to create a table that keeps track of values and keys such
|
8
|
+
# as which environment migrations were run in.
|
9
|
+
class InternalMetadata < ActiveRecord::Base # :nodoc:
|
10
|
+
class << self
|
11
|
+
def primary_key
|
12
|
+
"key"
|
13
|
+
end
|
14
|
+
|
15
|
+
def table_name
|
16
|
+
"#{table_name_prefix}#{ActiveRecord::Base.internal_metadata_table_name}#{table_name_suffix}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def []=(key, value)
|
20
|
+
find_or_initialize_by(key: key).update_attributes!(value: value)
|
21
|
+
end
|
22
|
+
|
23
|
+
def [](key)
|
24
|
+
where(key: key).pluck(:value).first
|
25
|
+
end
|
26
|
+
|
27
|
+
def table_exists?
|
28
|
+
connection.table_exists?(table_name)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Creates an internal metadata table with columns +key+ and +value+
|
32
|
+
def create_table
|
33
|
+
unless table_exists?
|
34
|
+
key_options = connection.internal_string_options_for_primary_key
|
35
|
+
|
36
|
+
connection.create_table(table_name, id: false) do |t|
|
37
|
+
t.string :key, key_options
|
38
|
+
t.string :value
|
39
|
+
t.timestamps
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module LegacyYamlAdapter
|
5
|
+
def self.convert(klass, coder)
|
6
|
+
return coder unless coder.is_a?(Psych::Coder)
|
7
|
+
|
8
|
+
case coder["active_record_yaml_version"]
|
9
|
+
when 1, 2 then coder
|
10
|
+
else
|
11
|
+
if coder["attributes"].is_a?(ActiveModel::AttributeSet)
|
12
|
+
Rails420.convert(klass, coder)
|
13
|
+
else
|
14
|
+
Rails41.convert(klass, coder)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module Rails420
|
20
|
+
def self.convert(klass, coder)
|
21
|
+
attribute_set = coder["attributes"]
|
22
|
+
|
23
|
+
klass.attribute_names.each do |attr_name|
|
24
|
+
attribute = attribute_set[attr_name]
|
25
|
+
if attribute.type.is_a?(Delegator)
|
26
|
+
type_from_klass = klass.type_for_attribute(attr_name)
|
27
|
+
attribute_set[attr_name] = attribute.with_type(type_from_klass)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
coder
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module Rails41
|
36
|
+
def self.convert(klass, coder)
|
37
|
+
attributes = klass.attributes_builder
|
38
|
+
.build_from_database(coder["attributes"])
|
39
|
+
new_record = coder["attributes"][klass.primary_key].blank?
|
40
|
+
|
41
|
+
{
|
42
|
+
"attributes" => attributes,
|
43
|
+
"new_record" => new_record,
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
en:
|
2
|
+
# Attributes names common to most models
|
3
|
+
#attributes:
|
4
|
+
#created_at: "Created at"
|
5
|
+
#updated_at: "Updated at"
|
6
|
+
|
7
|
+
# Default error messages
|
8
|
+
errors:
|
9
|
+
messages:
|
10
|
+
required: "must exist"
|
11
|
+
taken: "has already been taken"
|
12
|
+
|
13
|
+
# Active Record models configuration
|
14
|
+
activerecord:
|
15
|
+
errors:
|
16
|
+
messages:
|
17
|
+
record_invalid: "Validation failed: %{errors}"
|
18
|
+
restrict_dependent_destroy:
|
19
|
+
has_one: "Cannot delete record because a dependent %{record} exists"
|
20
|
+
has_many: "Cannot delete record because dependent %{record} exist"
|
21
|
+
# Append your own errors here or at the model/attributes scope.
|
22
|
+
|
23
|
+
# You can define own errors for models or model attributes.
|
24
|
+
# The values :model, :attribute and :value are always available for interpolation.
|
25
|
+
#
|
26
|
+
# For example,
|
27
|
+
# models:
|
28
|
+
# user:
|
29
|
+
# blank: "This is a custom blank message for %{model}: %{attribute}"
|
30
|
+
# attributes:
|
31
|
+
# login:
|
32
|
+
# blank: "This is a custom blank message for User login"
|
33
|
+
# Will define custom blank validation message for User model and
|
34
|
+
# custom blank validation message for login attribute of User model.
|
35
|
+
#models:
|
36
|
+
|
37
|
+
# Translate model names. Used in Model.human_name().
|
38
|
+
#models:
|
39
|
+
# For example,
|
40
|
+
# user: "Dude"
|
41
|
+
# will translate User model name to "Dude"
|
42
|
+
|
43
|
+
# Translate model attribute names. Used in Model.human_attribute_name(attribute).
|
44
|
+
#attributes:
|
45
|
+
# For example,
|
46
|
+
# user:
|
47
|
+
# login: "Handle"
|
48
|
+
# will translate User attribute "login" as "Handle"
|
@@ -0,0 +1,198 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Locking
|
5
|
+
# == What is Optimistic Locking
|
6
|
+
#
|
7
|
+
# Optimistic locking allows multiple users to access the same record for edits, and assumes a minimum of
|
8
|
+
# conflicts with the data. It does this by checking whether another process has made changes to a record since
|
9
|
+
# it was opened, an <tt>ActiveRecord::StaleObjectError</tt> exception is thrown if that has occurred
|
10
|
+
# and the update is ignored.
|
11
|
+
#
|
12
|
+
# Check out <tt>ActiveRecord::Locking::Pessimistic</tt> for an alternative.
|
13
|
+
#
|
14
|
+
# == Usage
|
15
|
+
#
|
16
|
+
# Active Record supports optimistic locking if the +lock_version+ field is present. Each update to the
|
17
|
+
# record increments the +lock_version+ column and the locking facilities ensure that records instantiated twice
|
18
|
+
# will let the last one saved raise a +StaleObjectError+ if the first was also updated. Example:
|
19
|
+
#
|
20
|
+
# p1 = Person.find(1)
|
21
|
+
# p2 = Person.find(1)
|
22
|
+
#
|
23
|
+
# p1.first_name = "Michael"
|
24
|
+
# p1.save
|
25
|
+
#
|
26
|
+
# p2.first_name = "should fail"
|
27
|
+
# p2.save # Raises an ActiveRecord::StaleObjectError
|
28
|
+
#
|
29
|
+
# Optimistic locking will also check for stale data when objects are destroyed. Example:
|
30
|
+
#
|
31
|
+
# p1 = Person.find(1)
|
32
|
+
# p2 = Person.find(1)
|
33
|
+
#
|
34
|
+
# p1.first_name = "Michael"
|
35
|
+
# p1.save
|
36
|
+
#
|
37
|
+
# p2.destroy # Raises an ActiveRecord::StaleObjectError
|
38
|
+
#
|
39
|
+
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
|
40
|
+
# or otherwise apply the business logic needed to resolve the conflict.
|
41
|
+
#
|
42
|
+
# This locking mechanism will function inside a single Ruby process. To make it work across all
|
43
|
+
# web requests, the recommended approach is to add +lock_version+ as a hidden field to your form.
|
44
|
+
#
|
45
|
+
# This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>.
|
46
|
+
# To override the name of the +lock_version+ column, set the <tt>locking_column</tt> class attribute:
|
47
|
+
#
|
48
|
+
# class Person < ActiveRecord::Base
|
49
|
+
# self.locking_column = :lock_person
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
module Optimistic
|
53
|
+
extend ActiveSupport::Concern
|
54
|
+
|
55
|
+
included do
|
56
|
+
class_attribute :lock_optimistically, instance_writer: false, default: true
|
57
|
+
end
|
58
|
+
|
59
|
+
def locking_enabled? #:nodoc:
|
60
|
+
self.class.locking_enabled?
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
def _create_record(attribute_names = self.attribute_names, *)
|
65
|
+
if locking_enabled?
|
66
|
+
# We always want to persist the locking version, even if we don't detect
|
67
|
+
# a change from the default, since the database might have no default
|
68
|
+
attribute_names |= [self.class.locking_column]
|
69
|
+
end
|
70
|
+
super
|
71
|
+
end
|
72
|
+
|
73
|
+
def _touch_row(attribute_names, time)
|
74
|
+
super
|
75
|
+
ensure
|
76
|
+
clear_attribute_change(self.class.locking_column) if locking_enabled?
|
77
|
+
end
|
78
|
+
|
79
|
+
def _update_row(attribute_names, attempted_action = "update")
|
80
|
+
return super unless locking_enabled?
|
81
|
+
|
82
|
+
begin
|
83
|
+
locking_column = self.class.locking_column
|
84
|
+
previous_lock_value = read_attribute_before_type_cast(locking_column)
|
85
|
+
attribute_names << locking_column
|
86
|
+
|
87
|
+
self[locking_column] += 1
|
88
|
+
|
89
|
+
affected_rows = self.class._update_record(
|
90
|
+
attributes_with_values(attribute_names),
|
91
|
+
self.class.primary_key => id_in_database,
|
92
|
+
locking_column => previous_lock_value
|
93
|
+
)
|
94
|
+
|
95
|
+
if affected_rows != 1
|
96
|
+
raise ActiveRecord::StaleObjectError.new(self, attempted_action)
|
97
|
+
end
|
98
|
+
|
99
|
+
affected_rows
|
100
|
+
|
101
|
+
# If something went wrong, revert the locking_column value.
|
102
|
+
rescue Exception
|
103
|
+
self[locking_column] = previous_lock_value.to_i
|
104
|
+
raise
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def destroy_row
|
109
|
+
return super unless locking_enabled?
|
110
|
+
|
111
|
+
locking_column = self.class.locking_column
|
112
|
+
|
113
|
+
affected_rows = self.class._delete_record(
|
114
|
+
self.class.primary_key => id_in_database,
|
115
|
+
locking_column => read_attribute_before_type_cast(locking_column)
|
116
|
+
)
|
117
|
+
|
118
|
+
if affected_rows != 1
|
119
|
+
raise ActiveRecord::StaleObjectError.new(self, "destroy")
|
120
|
+
end
|
121
|
+
|
122
|
+
affected_rows
|
123
|
+
end
|
124
|
+
|
125
|
+
module ClassMethods
|
126
|
+
DEFAULT_LOCKING_COLUMN = "lock_version"
|
127
|
+
|
128
|
+
# Returns true if the +lock_optimistically+ flag is set to true
|
129
|
+
# (which it is, by default) and the table includes the
|
130
|
+
# +locking_column+ column (defaults to +lock_version+).
|
131
|
+
def locking_enabled?
|
132
|
+
lock_optimistically && columns_hash[locking_column]
|
133
|
+
end
|
134
|
+
|
135
|
+
# Set the column to use for optimistic locking. Defaults to +lock_version+.
|
136
|
+
def locking_column=(value)
|
137
|
+
reload_schema_from_cache
|
138
|
+
@locking_column = value.to_s
|
139
|
+
end
|
140
|
+
|
141
|
+
# The version column used for optimistic locking. Defaults to +lock_version+.
|
142
|
+
def locking_column
|
143
|
+
@locking_column = DEFAULT_LOCKING_COLUMN unless defined?(@locking_column)
|
144
|
+
@locking_column
|
145
|
+
end
|
146
|
+
|
147
|
+
# Reset the column used for optimistic locking back to the +lock_version+ default.
|
148
|
+
def reset_locking_column
|
149
|
+
self.locking_column = DEFAULT_LOCKING_COLUMN
|
150
|
+
end
|
151
|
+
|
152
|
+
# Make sure the lock version column gets updated when counters are
|
153
|
+
# updated.
|
154
|
+
def update_counters(id, counters)
|
155
|
+
counters = counters.merge(locking_column => 1) if locking_enabled?
|
156
|
+
super
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
# We need to apply this decorator here, rather than on module inclusion. The closure
|
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
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# In de/serialize we change `nil` to 0, so that we can allow passing
|
178
|
+
# `nil` values to `lock_version`, and not result in `ActiveRecord::StaleObjectError`
|
179
|
+
# during update record.
|
180
|
+
class LockingType < DelegateClass(Type::Value) # :nodoc:
|
181
|
+
def deserialize(value)
|
182
|
+
super.to_i
|
183
|
+
end
|
184
|
+
|
185
|
+
def serialize(value)
|
186
|
+
super.to_i
|
187
|
+
end
|
188
|
+
|
189
|
+
def init_with(coder)
|
190
|
+
__setobj__(coder["subtype"])
|
191
|
+
end
|
192
|
+
|
193
|
+
def encode_with(coder)
|
194
|
+
coder["subtype"] = __getobj__
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Locking
|
5
|
+
# Locking::Pessimistic provides support for row-level locking using
|
6
|
+
# SELECT ... FOR UPDATE and other lock types.
|
7
|
+
#
|
8
|
+
# Chain <tt>ActiveRecord::Base#find</tt> to <tt>ActiveRecord::QueryMethods#lock</tt> to obtain an exclusive
|
9
|
+
# lock on the selected rows:
|
10
|
+
# # select * from accounts where id=1 for update
|
11
|
+
# Account.lock.find(1)
|
12
|
+
#
|
13
|
+
# Call <tt>lock('some locking clause')</tt> to use a database-specific locking clause
|
14
|
+
# of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example:
|
15
|
+
#
|
16
|
+
# Account.transaction do
|
17
|
+
# # select * from accounts where name = 'shugo' limit 1 for update
|
18
|
+
# shugo = Account.where("name = 'shugo'").lock(true).first
|
19
|
+
# yuko = Account.where("name = 'yuko'").lock(true).first
|
20
|
+
# shugo.balance -= 100
|
21
|
+
# shugo.save!
|
22
|
+
# yuko.balance += 100
|
23
|
+
# yuko.save!
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# You can also use <tt>ActiveRecord::Base#lock!</tt> method to lock one record by id.
|
27
|
+
# This may be better if you don't need to lock every row. Example:
|
28
|
+
#
|
29
|
+
# Account.transaction do
|
30
|
+
# # select * from accounts where ...
|
31
|
+
# accounts = Account.where(...)
|
32
|
+
# account1 = accounts.detect { |account| ... }
|
33
|
+
# account2 = accounts.detect { |account| ... }
|
34
|
+
# # select * from accounts where id=? for update
|
35
|
+
# account1.lock!
|
36
|
+
# account2.lock!
|
37
|
+
# account1.balance -= 100
|
38
|
+
# account1.save!
|
39
|
+
# account2.balance += 100
|
40
|
+
# account2.save!
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# You can start a transaction and acquire the lock in one go by calling
|
44
|
+
# <tt>with_lock</tt> with a block. The block is called from within
|
45
|
+
# a transaction, the object is already locked. Example:
|
46
|
+
#
|
47
|
+
# account = Account.first
|
48
|
+
# account.with_lock do
|
49
|
+
# # This block is called within a transaction,
|
50
|
+
# # account is already locked.
|
51
|
+
# account.balance -= 100
|
52
|
+
# account.save!
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# Database-specific information on row locking:
|
56
|
+
# MySQL: https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html
|
57
|
+
# PostgreSQL: https://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE
|
58
|
+
module Pessimistic
|
59
|
+
# Obtain a row lock on this record. Reloads the record to obtain the requested
|
60
|
+
# lock. Pass an SQL locking clause to append the end of the SELECT statement
|
61
|
+
# or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns
|
62
|
+
# the locked record.
|
63
|
+
def lock!(lock = true)
|
64
|
+
if persisted?
|
65
|
+
if has_changes_to_save?
|
66
|
+
raise(<<-MSG.squish)
|
67
|
+
Locking a record with unpersisted changes is not supported. Use
|
68
|
+
`save` to persist the changes, or `reload` to discard them
|
69
|
+
explicitly.
|
70
|
+
MSG
|
71
|
+
end
|
72
|
+
|
73
|
+
reload(lock: lock)
|
74
|
+
end
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
# Wraps the passed block in a transaction, locking the object
|
79
|
+
# before yielding. You can pass the SQL locking clause
|
80
|
+
# as argument (see <tt>lock!</tt>).
|
81
|
+
def with_lock(lock = true)
|
82
|
+
transaction do
|
83
|
+
lock!(lock)
|
84
|
+
yield
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
5
|
+
IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"]
|
6
|
+
|
7
|
+
def self.runtime=(value)
|
8
|
+
ActiveRecord::RuntimeRegistry.sql_runtime = value
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.runtime
|
12
|
+
ActiveRecord::RuntimeRegistry.sql_runtime ||= 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.reset_runtime
|
16
|
+
rt, self.runtime = runtime, 0
|
17
|
+
rt
|
18
|
+
end
|
19
|
+
|
20
|
+
def sql(event)
|
21
|
+
self.class.runtime += event.duration
|
22
|
+
return unless logger.debug?
|
23
|
+
|
24
|
+
payload = event.payload
|
25
|
+
|
26
|
+
return if IGNORE_PAYLOAD_NAMES.include?(payload[:name])
|
27
|
+
|
28
|
+
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
|
29
|
+
name = "CACHE #{name}" if payload[:cached]
|
30
|
+
sql = payload[:sql]
|
31
|
+
binds = nil
|
32
|
+
|
33
|
+
unless (payload[:binds] || []).empty?
|
34
|
+
casted_params = type_casted_binds(payload[:type_casted_binds])
|
35
|
+
binds = " " + payload[:binds].zip(casted_params).map { |attr, value|
|
36
|
+
render_bind(attr, value)
|
37
|
+
}.inspect
|
38
|
+
end
|
39
|
+
|
40
|
+
name = colorize_payload_name(name, payload[:name])
|
41
|
+
sql = color(sql, sql_color(sql), true)
|
42
|
+
|
43
|
+
debug " #{name} #{sql}#{binds}"
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def type_casted_binds(casted_binds)
|
48
|
+
casted_binds.respond_to?(:call) ? casted_binds.call : casted_binds
|
49
|
+
end
|
50
|
+
|
51
|
+
def render_bind(attr, value)
|
52
|
+
if attr.is_a?(Array)
|
53
|
+
attr = attr.first
|
54
|
+
elsif attr.type.binary? && attr.value
|
55
|
+
value = "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>"
|
56
|
+
end
|
57
|
+
|
58
|
+
[attr && attr.name, value]
|
59
|
+
end
|
60
|
+
|
61
|
+
def colorize_payload_name(name, payload_name)
|
62
|
+
if payload_name.blank? || payload_name == "SQL" # SQL vs Model Load/Exists
|
63
|
+
color(name, MAGENTA, true)
|
64
|
+
else
|
65
|
+
color(name, CYAN, true)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def sql_color(sql)
|
70
|
+
case sql
|
71
|
+
when /\A\s*rollback/mi
|
72
|
+
RED
|
73
|
+
when /select .*for update/mi, /\A\s*lock/mi
|
74
|
+
WHITE
|
75
|
+
when /\A\s*select/i
|
76
|
+
BLUE
|
77
|
+
when /\A\s*insert/i
|
78
|
+
GREEN
|
79
|
+
when /\A\s*update/i
|
80
|
+
YELLOW
|
81
|
+
when /\A\s*delete/i
|
82
|
+
RED
|
83
|
+
when /transaction\s*\Z/i
|
84
|
+
CYAN
|
85
|
+
else
|
86
|
+
MAGENTA
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def logger
|
91
|
+
ActiveRecord::Base.logger
|
92
|
+
end
|
93
|
+
|
94
|
+
def debug(progname = nil, &block)
|
95
|
+
return unless super
|
96
|
+
|
97
|
+
if ActiveRecord::Base.verbose_query_logs
|
98
|
+
log_query_source
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def log_query_source
|
103
|
+
source_line, line_number = extract_callstack(caller_locations)
|
104
|
+
|
105
|
+
if source_line
|
106
|
+
if defined?(::Rails.root)
|
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)
|
118
|
+
end
|
119
|
+
|
120
|
+
offending_line = line || callstack.first
|
121
|
+
|
122
|
+
[
|
123
|
+
offending_line.path,
|
124
|
+
offending_line.lineno
|
125
|
+
]
|
126
|
+
end
|
127
|
+
|
128
|
+
RAILS_GEM_ROOT = File.expand_path("../../..", __dir__) + "/"
|
129
|
+
|
130
|
+
def ignored_callstack(path)
|
131
|
+
path.start_with?(RAILS_GEM_ROOT) ||
|
132
|
+
path.start_with?(RbConfig::CONFIG["rubylibdir"])
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
ActiveRecord::LogSubscriber.attach_to :active_record
|